/*!
|
|
* Copyright 2015 Drifty Co.
|
|
* http://drifty.com/
|
|
*
|
|
* Ionic, v1.3.1
|
|
* A powerful HTML5 mobile app framework.
|
|
* http://ionicframework.com/
|
|
*
|
|
* By @maxlynch, @benjsperry, @adamdbradley <3
|
|
*
|
|
* Licensed under the MIT license. Please see LICENSE for more information.
|
|
*
|
|
*/
|
|
|
|
(function() {
|
|
|
|
// Create global ionic obj and its namespaces
|
|
// build processes may have already created an ionic obj
|
|
window.ionic = window.ionic || {};
|
|
window.ionic.views = {};
|
|
window.ionic.version = '1.3.1';
|
|
|
|
(function (ionic) {
|
|
|
|
ionic.DelegateService = function(methodNames) {
|
|
|
|
if (methodNames.indexOf('$getByHandle') > -1) {
|
|
throw new Error("Method '$getByHandle' is implicitly added to each delegate service. Do not list it as a method.");
|
|
}
|
|
|
|
function trueFn() { return true; }
|
|
|
|
return ['$log', function($log) {
|
|
|
|
/*
|
|
* Creates a new object that will have all the methodNames given,
|
|
* and call them on the given the controller instance matching given
|
|
* handle.
|
|
* The reason we don't just let $getByHandle return the controller instance
|
|
* itself is that the controller instance might not exist yet.
|
|
*
|
|
* We want people to be able to do
|
|
* `var instance = $ionicScrollDelegate.$getByHandle('foo')` on controller
|
|
* instantiation, but on controller instantiation a child directive
|
|
* may not have been compiled yet!
|
|
*
|
|
* So this is our way of solving this problem: we create an object
|
|
* that will only try to fetch the controller with given handle
|
|
* once the methods are actually called.
|
|
*/
|
|
function DelegateInstance(instances, handle) {
|
|
this._instances = instances;
|
|
this.handle = handle;
|
|
}
|
|
methodNames.forEach(function(methodName) {
|
|
DelegateInstance.prototype[methodName] = instanceMethodCaller(methodName);
|
|
});
|
|
|
|
|
|
/**
|
|
* The delegate service (eg $ionicNavBarDelegate) is just an instance
|
|
* with a non-defined handle, a couple extra methods for registering
|
|
* and narrowing down to a specific handle.
|
|
*/
|
|
function DelegateService() {
|
|
this._instances = [];
|
|
}
|
|
DelegateService.prototype = DelegateInstance.prototype;
|
|
DelegateService.prototype._registerInstance = function(instance, handle, filterFn) {
|
|
var instances = this._instances;
|
|
instance.$$delegateHandle = handle;
|
|
instance.$$filterFn = filterFn || trueFn;
|
|
instances.push(instance);
|
|
|
|
return function deregister() {
|
|
var index = instances.indexOf(instance);
|
|
if (index !== -1) {
|
|
instances.splice(index, 1);
|
|
}
|
|
};
|
|
};
|
|
DelegateService.prototype.$getByHandle = function(handle) {
|
|
return new DelegateInstance(this._instances, handle);
|
|
};
|
|
|
|
return new DelegateService();
|
|
|
|
function instanceMethodCaller(methodName) {
|
|
return function caller() {
|
|
var handle = this.handle;
|
|
var args = arguments;
|
|
var foundInstancesCount = 0;
|
|
var returnValue;
|
|
|
|
this._instances.forEach(function(instance) {
|
|
if ((!handle || handle == instance.$$delegateHandle) && instance.$$filterFn(instance)) {
|
|
foundInstancesCount++;
|
|
var ret = instance[methodName].apply(instance, args);
|
|
//Only return the value from the first call
|
|
if (foundInstancesCount === 1) {
|
|
returnValue = ret;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!foundInstancesCount && handle) {
|
|
return $log.warn(
|
|
'Delegate for handle "' + handle + '" could not find a ' +
|
|
'corresponding element with delegate-handle="' + handle + '"! ' +
|
|
methodName + '() was not called!\n' +
|
|
'Possible cause: If you are calling ' + methodName + '() immediately, and ' +
|
|
'your element with delegate-handle="' + handle + '" is a child of your ' +
|
|
'controller, then your element may not be compiled yet. Put a $timeout ' +
|
|
'around your call to ' + methodName + '() and try again.'
|
|
);
|
|
}
|
|
return returnValue;
|
|
};
|
|
}
|
|
|
|
}];
|
|
};
|
|
|
|
})(window.ionic);
|
|
|
|
(function(window, document, ionic) {
|
|
|
|
var readyCallbacks = [];
|
|
var isDomReady = document.readyState === 'complete' || document.readyState === 'interactive';
|
|
|
|
function domReady() {
|
|
isDomReady = true;
|
|
for (var x = 0; x < readyCallbacks.length; x++) {
|
|
ionic.requestAnimationFrame(readyCallbacks[x]);
|
|
}
|
|
readyCallbacks = [];
|
|
document.removeEventListener('DOMContentLoaded', domReady);
|
|
}
|
|
if (!isDomReady) {
|
|
document.addEventListener('DOMContentLoaded', domReady);
|
|
}
|
|
|
|
|
|
// From the man himself, Mr. Paul Irish.
|
|
// The requestAnimationFrame polyfill
|
|
// Put it on window just to preserve its context
|
|
// without having to use .call
|
|
window._rAF = (function() {
|
|
return window.requestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame ||
|
|
window.mozRequestAnimationFrame ||
|
|
function(callback) {
|
|
window.setTimeout(callback, 16);
|
|
};
|
|
})();
|
|
|
|
var cancelAnimationFrame = window.cancelAnimationFrame ||
|
|
window.webkitCancelAnimationFrame ||
|
|
window.mozCancelAnimationFrame ||
|
|
window.webkitCancelRequestAnimationFrame;
|
|
|
|
/**
|
|
* @ngdoc utility
|
|
* @name ionic.DomUtil
|
|
* @module ionic
|
|
*/
|
|
ionic.DomUtil = {
|
|
//Call with proper context
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#requestAnimationFrame
|
|
* @alias ionic.requestAnimationFrame
|
|
* @description Calls [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame), or a polyfill if not available.
|
|
* @param {function} callback The function to call when the next frame
|
|
* happens.
|
|
*/
|
|
requestAnimationFrame: function(cb) {
|
|
return window._rAF(cb);
|
|
},
|
|
|
|
cancelAnimationFrame: function(requestId) {
|
|
cancelAnimationFrame(requestId);
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#animationFrameThrottle
|
|
* @alias ionic.animationFrameThrottle
|
|
* @description
|
|
* When given a callback, if that callback is called 100 times between
|
|
* animation frames, adding Throttle will make it only run the last of
|
|
* the 100 calls.
|
|
*
|
|
* @param {function} callback a function which will be throttled to
|
|
* requestAnimationFrame
|
|
* @returns {function} A function which will then call the passed in callback.
|
|
* The passed in callback will receive the context the returned function is
|
|
* called with.
|
|
*/
|
|
animationFrameThrottle: function(cb) {
|
|
var args, isQueued, context;
|
|
return function() {
|
|
args = arguments;
|
|
context = this;
|
|
if (!isQueued) {
|
|
isQueued = true;
|
|
ionic.requestAnimationFrame(function() {
|
|
cb.apply(context, args);
|
|
isQueued = false;
|
|
});
|
|
}
|
|
};
|
|
},
|
|
|
|
contains: function(parentNode, otherNode) {
|
|
var current = otherNode;
|
|
while (current) {
|
|
if (current === parentNode) return true;
|
|
current = current.parentNode;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#getPositionInParent
|
|
* @description
|
|
* Find an element's scroll offset within its container.
|
|
* @param {DOMElement} element The element to find the offset of.
|
|
* @returns {object} A position object with the following properties:
|
|
* - `{number}` `left` The left offset of the element.
|
|
* - `{number}` `top` The top offset of the element.
|
|
*/
|
|
getPositionInParent: function(el) {
|
|
return {
|
|
left: el.offsetLeft,
|
|
top: el.offsetTop
|
|
};
|
|
},
|
|
|
|
getOffsetTop: function(el) {
|
|
var curtop = 0;
|
|
if (el.offsetParent) {
|
|
do {
|
|
curtop += el.offsetTop;
|
|
el = el.offsetParent;
|
|
} while (el)
|
|
return curtop;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#ready
|
|
* @description
|
|
* Call a function when the DOM is ready, or if it is already ready
|
|
* call the function immediately.
|
|
* @param {function} callback The function to be called.
|
|
*/
|
|
ready: function(cb) {
|
|
if (isDomReady) {
|
|
ionic.requestAnimationFrame(cb);
|
|
} else {
|
|
readyCallbacks.push(cb);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#getTextBounds
|
|
* @description
|
|
* Get a rect representing the bounds of the given textNode.
|
|
* @param {DOMElement} textNode The textNode to find the bounds of.
|
|
* @returns {object} An object representing the bounds of the node. Properties:
|
|
* - `{number}` `left` The left position of the textNode.
|
|
* - `{number}` `right` The right position of the textNode.
|
|
* - `{number}` `top` The top position of the textNode.
|
|
* - `{number}` `bottom` The bottom position of the textNode.
|
|
* - `{number}` `width` The width of the textNode.
|
|
* - `{number}` `height` The height of the textNode.
|
|
*/
|
|
getTextBounds: function(textNode) {
|
|
if (document.createRange) {
|
|
var range = document.createRange();
|
|
range.selectNodeContents(textNode);
|
|
if (range.getBoundingClientRect) {
|
|
var rect = range.getBoundingClientRect();
|
|
if (rect) {
|
|
var sx = window.scrollX;
|
|
var sy = window.scrollY;
|
|
|
|
return {
|
|
top: rect.top + sy,
|
|
left: rect.left + sx,
|
|
right: rect.left + sx + rect.width,
|
|
bottom: rect.top + sy + rect.height,
|
|
width: rect.width,
|
|
height: rect.height
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#getChildIndex
|
|
* @description
|
|
* Get the first index of a child node within the given element of the
|
|
* specified type.
|
|
* @param {DOMElement} element The element to find the index of.
|
|
* @param {string} type The nodeName to match children of element against.
|
|
* @returns {number} The index, or -1, of a child with nodeName matching type.
|
|
*/
|
|
getChildIndex: function(element, type) {
|
|
if (type) {
|
|
var ch = element.parentNode.children;
|
|
var c;
|
|
for (var i = 0, k = 0, j = ch.length; i < j; i++) {
|
|
c = ch[i];
|
|
if (c.nodeName && c.nodeName.toLowerCase() == type) {
|
|
if (c == element) {
|
|
return k;
|
|
}
|
|
k++;
|
|
}
|
|
}
|
|
}
|
|
return Array.prototype.slice.call(element.parentNode.children).indexOf(element);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
swapNodes: function(src, dest) {
|
|
dest.parentNode.insertBefore(src, dest);
|
|
},
|
|
|
|
elementIsDescendant: function(el, parent, stopAt) {
|
|
var current = el;
|
|
do {
|
|
if (current === parent) return true;
|
|
current = current.parentNode;
|
|
} while (current && current !== stopAt);
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#getParentWithClass
|
|
* @param {DOMElement} element
|
|
* @param {string} className
|
|
* @returns {DOMElement} The closest parent of element matching the
|
|
* className, or null.
|
|
*/
|
|
getParentWithClass: function(e, className, depth) {
|
|
depth = depth || 10;
|
|
while (e.parentNode && depth--) {
|
|
if (e.parentNode.classList && e.parentNode.classList.contains(className)) {
|
|
return e.parentNode;
|
|
}
|
|
e = e.parentNode;
|
|
}
|
|
return null;
|
|
},
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#getParentOrSelfWithClass
|
|
* @param {DOMElement} element
|
|
* @param {string} className
|
|
* @returns {DOMElement} The closest parent or self matching the
|
|
* className, or null.
|
|
*/
|
|
getParentOrSelfWithClass: function(e, className, depth) {
|
|
depth = depth || 10;
|
|
while (e && depth--) {
|
|
if (e.classList && e.classList.contains(className)) {
|
|
return e;
|
|
}
|
|
e = e.parentNode;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#rectContains
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {number} x1
|
|
* @param {number} y1
|
|
* @param {number} x2
|
|
* @param {number} y2
|
|
* @returns {boolean} Whether {x,y} fits within the rectangle defined by
|
|
* {x1,y1,x2,y2}.
|
|
*/
|
|
rectContains: function(x, y, x1, y1, x2, y2) {
|
|
if (x < x1 || x > x2) return false;
|
|
if (y < y1 || y > y2) return false;
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.DomUtil#blurAll
|
|
* @description
|
|
* Blurs any currently focused input element
|
|
* @returns {DOMElement} The element blurred or null
|
|
*/
|
|
blurAll: function() {
|
|
if (document.activeElement && document.activeElement != document.body) {
|
|
document.activeElement.blur();
|
|
return document.activeElement;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
cachedAttr: function(ele, key, value) {
|
|
ele = ele && ele.length && ele[0] || ele;
|
|
if (ele && ele.setAttribute) {
|
|
var dataKey = '$attr-' + key;
|
|
if (arguments.length > 2) {
|
|
if (ele[dataKey] !== value) {
|
|
ele.setAttribute(key, value);
|
|
ele[dataKey] = value;
|
|
}
|
|
} else if (typeof ele[dataKey] == 'undefined') {
|
|
ele[dataKey] = ele.getAttribute(key);
|
|
}
|
|
return ele[dataKey];
|
|
}
|
|
},
|
|
|
|
cachedStyles: function(ele, styles) {
|
|
ele = ele && ele.length && ele[0] || ele;
|
|
if (ele && ele.style) {
|
|
for (var prop in styles) {
|
|
if (ele['$style-' + prop] !== styles[prop]) {
|
|
ele.style[prop] = ele['$style-' + prop] = styles[prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
//Shortcuts
|
|
ionic.requestAnimationFrame = ionic.DomUtil.requestAnimationFrame;
|
|
ionic.cancelAnimationFrame = ionic.DomUtil.cancelAnimationFrame;
|
|
ionic.animationFrameThrottle = ionic.DomUtil.animationFrameThrottle;
|
|
|
|
})(window, document, ionic);
|
|
|
|
/**
|
|
* ion-events.js
|
|
*
|
|
* Author: Max Lynch <max@drifty.com>
|
|
*
|
|
* Framework events handles various mobile browser events, and
|
|
* detects special events like tap/swipe/etc. and emits them
|
|
* as custom events that can be used in an app.
|
|
*
|
|
* Portions lovingly adapted from github.com/maker/ratchet and github.com/alexgibson/tap.js - thanks guys!
|
|
*/
|
|
|
|
(function(ionic) {
|
|
|
|
// Custom event polyfill
|
|
ionic.CustomEvent = (function() {
|
|
if( typeof window.CustomEvent === 'function' ) return CustomEvent;
|
|
|
|
var customEvent = function(event, params) {
|
|
var evt;
|
|
params = params || {
|
|
bubbles: false,
|
|
cancelable: false,
|
|
detail: undefined
|
|
};
|
|
try {
|
|
evt = document.createEvent("CustomEvent");
|
|
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
|
|
} catch (error) {
|
|
// fallback for browsers that don't support createEvent('CustomEvent')
|
|
evt = document.createEvent("Event");
|
|
for (var param in params) {
|
|
evt[param] = params[param];
|
|
}
|
|
evt.initEvent(event, params.bubbles, params.cancelable);
|
|
}
|
|
return evt;
|
|
};
|
|
customEvent.prototype = window.Event.prototype;
|
|
return customEvent;
|
|
})();
|
|
|
|
|
|
/**
|
|
* @ngdoc utility
|
|
* @name ionic.EventController
|
|
* @module ionic
|
|
*/
|
|
ionic.EventController = {
|
|
VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'],
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.EventController#trigger
|
|
* @alias ionic.trigger
|
|
* @param {string} eventType The event to trigger.
|
|
* @param {object} data The data for the event. Hint: pass in
|
|
* `{target: targetElement}`
|
|
* @param {boolean=} bubbles Whether the event should bubble up the DOM.
|
|
* @param {boolean=} cancelable Whether the event should be cancelable.
|
|
*/
|
|
// Trigger a new event
|
|
trigger: function(eventType, data, bubbles, cancelable) {
|
|
var event = new ionic.CustomEvent(eventType, {
|
|
detail: data,
|
|
bubbles: !!bubbles,
|
|
cancelable: !!cancelable
|
|
});
|
|
|
|
// Make sure to trigger the event on the given target, or dispatch it from
|
|
// the window if we don't have an event target
|
|
data && data.target && data.target.dispatchEvent && data.target.dispatchEvent(event) || window.dispatchEvent(event);
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.EventController#on
|
|
* @alias ionic.on
|
|
* @description Listen to an event on an element.
|
|
* @param {string} type The event to listen for.
|
|
* @param {function} callback The listener to be called.
|
|
* @param {DOMElement} element The element to listen for the event on.
|
|
*/
|
|
on: function(type, callback, element) {
|
|
var e = element || window;
|
|
|
|
// Bind a gesture if it's a virtual event
|
|
for(var i = 0, j = this.VIRTUALIZED_EVENTS.length; i < j; i++) {
|
|
if(type == this.VIRTUALIZED_EVENTS[i]) {
|
|
var gesture = new ionic.Gesture(element);
|
|
gesture.on(type, callback);
|
|
return gesture;
|
|
}
|
|
}
|
|
|
|
// Otherwise bind a normal event
|
|
e.addEventListener(type, callback);
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.EventController#off
|
|
* @alias ionic.off
|
|
* @description Remove an event listener.
|
|
* @param {string} type
|
|
* @param {function} callback
|
|
* @param {DOMElement} element
|
|
*/
|
|
off: function(type, callback, element) {
|
|
element.removeEventListener(type, callback);
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.EventController#onGesture
|
|
* @alias ionic.onGesture
|
|
* @description Add an event listener for a gesture on an element.
|
|
*
|
|
* Available eventTypes (from [hammer.js](http://eightmedia.github.io/hammer.js/)):
|
|
*
|
|
* `hold`, `tap`, `doubletap`, `drag`, `dragstart`, `dragend`, `dragup`, `dragdown`, <br/>
|
|
* `dragleft`, `dragright`, `swipe`, `swipeup`, `swipedown`, `swipeleft`, `swiperight`, <br/>
|
|
* `transform`, `transformstart`, `transformend`, `rotate`, `pinch`, `pinchin`, `pinchout`, <br/>
|
|
* `touch`, `release`
|
|
*
|
|
* @param {string} eventType The gesture event to listen for.
|
|
* @param {function(e)} callback The function to call when the gesture
|
|
* happens.
|
|
* @param {DOMElement} element The angular element to listen for the event on.
|
|
* @param {object} options object.
|
|
* @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on).
|
|
*/
|
|
onGesture: function(type, callback, element, options) {
|
|
var gesture = new ionic.Gesture(element, options);
|
|
gesture.on(type, callback);
|
|
return gesture;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.EventController#offGesture
|
|
* @alias ionic.offGesture
|
|
* @description Remove an event listener for a gesture created on an element.
|
|
* @param {ionic.Gesture} gesture The gesture that should be removed.
|
|
* @param {string} eventType The gesture event to remove the listener for.
|
|
* @param {function(e)} callback The listener to remove.
|
|
|
|
*/
|
|
offGesture: function(gesture, type, callback) {
|
|
gesture && gesture.off(type, callback);
|
|
},
|
|
|
|
handlePopState: function() {}
|
|
};
|
|
|
|
|
|
// Map some convenient top-level functions for event handling
|
|
ionic.on = function() { ionic.EventController.on.apply(ionic.EventController, arguments); };
|
|
ionic.off = function() { ionic.EventController.off.apply(ionic.EventController, arguments); };
|
|
ionic.trigger = ionic.EventController.trigger;//function() { ionic.EventController.trigger.apply(ionic.EventController.trigger, arguments); };
|
|
ionic.onGesture = function() { return ionic.EventController.onGesture.apply(ionic.EventController.onGesture, arguments); };
|
|
ionic.offGesture = function() { return ionic.EventController.offGesture.apply(ionic.EventController.offGesture, arguments); };
|
|
|
|
})(window.ionic);
|
|
|
|
/* eslint camelcase:0 */
|
|
/**
|
|
* Simple gesture controllers with some common gestures that emit
|
|
* gesture events.
|
|
*
|
|
* Ported from github.com/EightMedia/hammer.js Gestures - thanks!
|
|
*/
|
|
(function(ionic) {
|
|
|
|
/**
|
|
* ionic.Gestures
|
|
* use this to create instances
|
|
* @param {HTMLElement} element
|
|
* @param {Object} options
|
|
* @returns {ionic.Gestures.Instance}
|
|
* @constructor
|
|
*/
|
|
ionic.Gesture = function(element, options) {
|
|
return new ionic.Gestures.Instance(element, options || {});
|
|
};
|
|
|
|
ionic.Gestures = {};
|
|
|
|
// default settings
|
|
ionic.Gestures.defaults = {
|
|
// add css to the element to prevent the browser from doing
|
|
// its native behavior. this doesnt prevent the scrolling,
|
|
// but cancels the contextmenu, tap highlighting etc
|
|
// set to false to disable this
|
|
stop_browser_behavior: 'disable-user-behavior'
|
|
};
|
|
|
|
// detect touchevents
|
|
ionic.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
|
|
ionic.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window);
|
|
|
|
// dont use mouseevents on mobile devices
|
|
ionic.Gestures.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i;
|
|
ionic.Gestures.NO_MOUSEEVENTS = ionic.Gestures.HAS_TOUCHEVENTS && window.navigator.userAgent.match(ionic.Gestures.MOBILE_REGEX);
|
|
|
|
// eventtypes per touchevent (start, move, end)
|
|
// are filled by ionic.Gestures.event.determineEventTypes on setup
|
|
ionic.Gestures.EVENT_TYPES = {};
|
|
|
|
// direction defines
|
|
ionic.Gestures.DIRECTION_DOWN = 'down';
|
|
ionic.Gestures.DIRECTION_LEFT = 'left';
|
|
ionic.Gestures.DIRECTION_UP = 'up';
|
|
ionic.Gestures.DIRECTION_RIGHT = 'right';
|
|
|
|
// pointer type
|
|
ionic.Gestures.POINTER_MOUSE = 'mouse';
|
|
ionic.Gestures.POINTER_TOUCH = 'touch';
|
|
ionic.Gestures.POINTER_PEN = 'pen';
|
|
|
|
// touch event defines
|
|
ionic.Gestures.EVENT_START = 'start';
|
|
ionic.Gestures.EVENT_MOVE = 'move';
|
|
ionic.Gestures.EVENT_END = 'end';
|
|
|
|
// hammer document where the base events are added at
|
|
ionic.Gestures.DOCUMENT = window.document;
|
|
|
|
// plugins namespace
|
|
ionic.Gestures.plugins = {};
|
|
|
|
// if the window events are set...
|
|
ionic.Gestures.READY = false;
|
|
|
|
/**
|
|
* setup events to detect gestures on the document
|
|
*/
|
|
function setup() {
|
|
if(ionic.Gestures.READY) {
|
|
return;
|
|
}
|
|
|
|
// find what eventtypes we add listeners to
|
|
ionic.Gestures.event.determineEventTypes();
|
|
|
|
// Register all gestures inside ionic.Gestures.gestures
|
|
for(var name in ionic.Gestures.gestures) {
|
|
if(ionic.Gestures.gestures.hasOwnProperty(name)) {
|
|
ionic.Gestures.detection.register(ionic.Gestures.gestures[name]);
|
|
}
|
|
}
|
|
|
|
// Add touch events on the document
|
|
ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_MOVE, ionic.Gestures.detection.detect);
|
|
ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_END, ionic.Gestures.detection.detect);
|
|
|
|
// ionic.Gestures is ready...!
|
|
ionic.Gestures.READY = true;
|
|
}
|
|
|
|
/**
|
|
* create new hammer instance
|
|
* all methods should return the instance itself, so it is chainable.
|
|
* @param {HTMLElement} element
|
|
* @param {Object} [options={}]
|
|
* @returns {ionic.Gestures.Instance}
|
|
* @name Gesture.Instance
|
|
* @constructor
|
|
*/
|
|
ionic.Gestures.Instance = function(element, options) {
|
|
var self = this;
|
|
|
|
// A null element was passed into the instance, which means
|
|
// whatever lookup was done to find this element failed to find it
|
|
// so we can't listen for events on it.
|
|
if(element === null) {
|
|
void 0;
|
|
return this;
|
|
}
|
|
|
|
// setup ionic.GesturesJS window events and register all gestures
|
|
// this also sets up the default options
|
|
setup();
|
|
|
|
this.element = element;
|
|
|
|
// start/stop detection option
|
|
this.enabled = true;
|
|
|
|
// merge options
|
|
this.options = ionic.Gestures.utils.extend(
|
|
ionic.Gestures.utils.extend({}, ionic.Gestures.defaults),
|
|
options || {});
|
|
|
|
// add some css to the element to prevent the browser from doing its native behavoir
|
|
if(this.options.stop_browser_behavior) {
|
|
ionic.Gestures.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
|
|
}
|
|
|
|
// start detection on touchstart
|
|
ionic.Gestures.event.onTouch(element, ionic.Gestures.EVENT_START, function(ev) {
|
|
if(self.enabled) {
|
|
ionic.Gestures.detection.startDetect(self, ev);
|
|
}
|
|
});
|
|
|
|
// return instance
|
|
return this;
|
|
};
|
|
|
|
|
|
ionic.Gestures.Instance.prototype = {
|
|
/**
|
|
* bind events to the instance
|
|
* @param {String} gesture
|
|
* @param {Function} handler
|
|
* @returns {ionic.Gestures.Instance}
|
|
*/
|
|
on: function onEvent(gesture, handler){
|
|
var gestures = gesture.split(' ');
|
|
for(var t = 0; t < gestures.length; t++) {
|
|
this.element.addEventListener(gestures[t], handler, false);
|
|
}
|
|
return this;
|
|
},
|
|
|
|
|
|
/**
|
|
* unbind events to the instance
|
|
* @param {String} gesture
|
|
* @param {Function} handler
|
|
* @returns {ionic.Gestures.Instance}
|
|
*/
|
|
off: function offEvent(gesture, handler){
|
|
var gestures = gesture.split(' ');
|
|
for(var t = 0; t < gestures.length; t++) {
|
|
this.element.removeEventListener(gestures[t], handler, false);
|
|
}
|
|
return this;
|
|
},
|
|
|
|
|
|
/**
|
|
* trigger gesture event
|
|
* @param {String} gesture
|
|
* @param {Object} eventData
|
|
* @returns {ionic.Gestures.Instance}
|
|
*/
|
|
trigger: function triggerEvent(gesture, eventData){
|
|
// create DOM event
|
|
var event = ionic.Gestures.DOCUMENT.createEvent('Event');
|
|
event.initEvent(gesture, true, true);
|
|
event.gesture = eventData;
|
|
|
|
// trigger on the target if it is in the instance element,
|
|
// this is for event delegation tricks
|
|
var element = this.element;
|
|
if(ionic.Gestures.utils.hasParent(eventData.target, element)) {
|
|
element = eventData.target;
|
|
}
|
|
|
|
element.dispatchEvent(event);
|
|
return this;
|
|
},
|
|
|
|
|
|
/**
|
|
* enable of disable hammer.js detection
|
|
* @param {Boolean} state
|
|
* @returns {ionic.Gestures.Instance}
|
|
*/
|
|
enable: function enable(state) {
|
|
this.enabled = state;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* this holds the last move event,
|
|
* used to fix empty touchend issue
|
|
* see the onTouch event for an explanation
|
|
* type {Object}
|
|
*/
|
|
var last_move_event = null;
|
|
|
|
|
|
/**
|
|
* when the mouse is hold down, this is true
|
|
* type {Boolean}
|
|
*/
|
|
var enable_detect = false;
|
|
|
|
|
|
/**
|
|
* when touch events have been fired, this is true
|
|
* type {Boolean}
|
|
*/
|
|
var touch_triggered = false;
|
|
|
|
|
|
ionic.Gestures.event = {
|
|
/**
|
|
* simple addEventListener
|
|
* @param {HTMLElement} element
|
|
* @param {String} type
|
|
* @param {Function} handler
|
|
*/
|
|
bindDom: function(element, type, handler) {
|
|
var types = type.split(' ');
|
|
for(var t = 0; t < types.length; t++) {
|
|
element.addEventListener(types[t], handler, false);
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* touch events with mouse fallback
|
|
* @param {HTMLElement} element
|
|
* @param {String} eventType like ionic.Gestures.EVENT_MOVE
|
|
* @param {Function} handler
|
|
*/
|
|
onTouch: function onTouch(element, eventType, handler) {
|
|
var self = this;
|
|
|
|
this.bindDom(element, ionic.Gestures.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
|
|
var sourceEventType = ev.type.toLowerCase();
|
|
|
|
// onmouseup, but when touchend has been fired we do nothing.
|
|
// this is for touchdevices which also fire a mouseup on touchend
|
|
if(sourceEventType.match(/mouse/) && touch_triggered) {
|
|
return;
|
|
}
|
|
|
|
// mousebutton must be down or a touch event
|
|
else if( sourceEventType.match(/touch/) || // touch events are always on screen
|
|
sourceEventType.match(/pointerdown/) || // pointerevents touch
|
|
(sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
|
|
){
|
|
enable_detect = true;
|
|
}
|
|
|
|
// mouse isn't pressed
|
|
else if(sourceEventType.match(/mouse/) && ev.which !== 1) {
|
|
enable_detect = false;
|
|
}
|
|
|
|
|
|
// we are in a touch event, set the touch triggered bool to true,
|
|
// this for the conflicts that may occur on ios and android
|
|
if(sourceEventType.match(/touch|pointer/)) {
|
|
touch_triggered = true;
|
|
}
|
|
|
|
// count the total touches on the screen
|
|
var count_touches = 0;
|
|
|
|
// when touch has been triggered in this detection session
|
|
// and we are now handling a mouse event, we stop that to prevent conflicts
|
|
if(enable_detect) {
|
|
// update pointerevent
|
|
if(ionic.Gestures.HAS_POINTEREVENTS && eventType != ionic.Gestures.EVENT_END) {
|
|
count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
|
|
}
|
|
// touch
|
|
else if(sourceEventType.match(/touch/)) {
|
|
count_touches = ev.touches.length;
|
|
}
|
|
// mouse
|
|
else if(!touch_triggered) {
|
|
count_touches = sourceEventType.match(/up/) ? 0 : 1;
|
|
}
|
|
|
|
// if we are in a end event, but when we remove one touch and
|
|
// we still have enough, set eventType to move
|
|
if(count_touches > 0 && eventType == ionic.Gestures.EVENT_END) {
|
|
eventType = ionic.Gestures.EVENT_MOVE;
|
|
}
|
|
// no touches, force the end event
|
|
else if(!count_touches) {
|
|
eventType = ionic.Gestures.EVENT_END;
|
|
}
|
|
|
|
// store the last move event
|
|
if(count_touches || last_move_event === null) {
|
|
last_move_event = ev;
|
|
}
|
|
|
|
// trigger the handler
|
|
handler.call(ionic.Gestures.detection, self.collectEventData(element, eventType, self.getTouchList(last_move_event, eventType), ev));
|
|
|
|
// remove pointerevent from list
|
|
if(ionic.Gestures.HAS_POINTEREVENTS && eventType == ionic.Gestures.EVENT_END) {
|
|
count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
|
|
}
|
|
}
|
|
|
|
//debug(sourceEventType +" "+ eventType);
|
|
|
|
// on the end we reset everything
|
|
if(!count_touches) {
|
|
last_move_event = null;
|
|
enable_detect = false;
|
|
touch_triggered = false;
|
|
ionic.Gestures.PointerEvent.reset();
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
/**
|
|
* we have different events for each device/browser
|
|
* determine what we need and set them in the ionic.Gestures.EVENT_TYPES constant
|
|
*/
|
|
determineEventTypes: function determineEventTypes() {
|
|
// determine the eventtype we want to set
|
|
var types;
|
|
|
|
// pointerEvents magic
|
|
if(ionic.Gestures.HAS_POINTEREVENTS) {
|
|
types = ionic.Gestures.PointerEvent.getEvents();
|
|
}
|
|
// on Android, iOS, blackberry, windows mobile we dont want any mouseevents
|
|
else if(ionic.Gestures.NO_MOUSEEVENTS) {
|
|
types = [
|
|
'touchstart',
|
|
'touchmove',
|
|
'touchend touchcancel'];
|
|
}
|
|
// for non pointer events browsers and mixed browsers,
|
|
// like chrome on windows8 touch laptop
|
|
else {
|
|
types = [
|
|
'touchstart mousedown',
|
|
'touchmove mousemove',
|
|
'touchend touchcancel mouseup'];
|
|
}
|
|
|
|
ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_START] = types[0];
|
|
ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_MOVE] = types[1];
|
|
ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_END] = types[2];
|
|
},
|
|
|
|
|
|
/**
|
|
* create touchlist depending on the event
|
|
* @param {Object} ev
|
|
* @param {String} eventType used by the fakemultitouch plugin
|
|
*/
|
|
getTouchList: function getTouchList(ev/*, eventType*/) {
|
|
// get the fake pointerEvent touchlist
|
|
if(ionic.Gestures.HAS_POINTEREVENTS) {
|
|
return ionic.Gestures.PointerEvent.getTouchList();
|
|
}
|
|
// get the touchlist
|
|
else if(ev.touches) {
|
|
return ev.touches;
|
|
}
|
|
// make fake touchlist from mouse position
|
|
else {
|
|
ev.identifier = 1;
|
|
return [ev];
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* collect event data for ionic.Gestures js
|
|
* @param {HTMLElement} element
|
|
* @param {String} eventType like ionic.Gestures.EVENT_MOVE
|
|
* @param {Object} eventData
|
|
*/
|
|
collectEventData: function collectEventData(element, eventType, touches, ev) {
|
|
|
|
// find out pointerType
|
|
var pointerType = ionic.Gestures.POINTER_TOUCH;
|
|
if(ev.type.match(/mouse/) || ionic.Gestures.PointerEvent.matchType(ionic.Gestures.POINTER_MOUSE, ev)) {
|
|
pointerType = ionic.Gestures.POINTER_MOUSE;
|
|
}
|
|
|
|
return {
|
|
center: ionic.Gestures.utils.getCenter(touches),
|
|
timeStamp: new Date().getTime(),
|
|
target: ev.target,
|
|
touches: touches,
|
|
eventType: eventType,
|
|
pointerType: pointerType,
|
|
srcEvent: ev,
|
|
|
|
/**
|
|
* prevent the browser default actions
|
|
* mostly used to disable scrolling of the browser
|
|
*/
|
|
preventDefault: function() {
|
|
if(this.srcEvent.preventManipulation) {
|
|
this.srcEvent.preventManipulation();
|
|
}
|
|
|
|
if(this.srcEvent.preventDefault) {
|
|
// this.srcEvent.preventDefault();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* stop bubbling the event up to its parents
|
|
*/
|
|
stopPropagation: function() {
|
|
this.srcEvent.stopPropagation();
|
|
},
|
|
|
|
/**
|
|
* immediately stop gesture detection
|
|
* might be useful after a swipe was detected
|
|
* @return {*}
|
|
*/
|
|
stopDetect: function() {
|
|
return ionic.Gestures.detection.stopDetect();
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
ionic.Gestures.PointerEvent = {
|
|
/**
|
|
* holds all pointers
|
|
* type {Object}
|
|
*/
|
|
pointers: {},
|
|
|
|
/**
|
|
* get a list of pointers
|
|
* @returns {Array} touchlist
|
|
*/
|
|
getTouchList: function() {
|
|
var self = this;
|
|
var touchlist = [];
|
|
|
|
// we can use forEach since pointerEvents only is in IE10
|
|
Object.keys(self.pointers).sort().forEach(function(id) {
|
|
touchlist.push(self.pointers[id]);
|
|
});
|
|
return touchlist;
|
|
},
|
|
|
|
/**
|
|
* update the position of a pointer
|
|
* @param {String} type ionic.Gestures.EVENT_END
|
|
* @param {Object} pointerEvent
|
|
*/
|
|
updatePointer: function(type, pointerEvent) {
|
|
if(type == ionic.Gestures.EVENT_END) {
|
|
this.pointers = {};
|
|
}
|
|
else {
|
|
pointerEvent.identifier = pointerEvent.pointerId;
|
|
this.pointers[pointerEvent.pointerId] = pointerEvent;
|
|
}
|
|
|
|
return Object.keys(this.pointers).length;
|
|
},
|
|
|
|
/**
|
|
* check if ev matches pointertype
|
|
* @param {String} pointerType ionic.Gestures.POINTER_MOUSE
|
|
* @param {PointerEvent} ev
|
|
*/
|
|
matchType: function(pointerType, ev) {
|
|
if(!ev.pointerType) {
|
|
return false;
|
|
}
|
|
|
|
var types = {};
|
|
types[ionic.Gestures.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == ionic.Gestures.POINTER_MOUSE);
|
|
types[ionic.Gestures.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == ionic.Gestures.POINTER_TOUCH);
|
|
types[ionic.Gestures.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == ionic.Gestures.POINTER_PEN);
|
|
return types[pointerType];
|
|
},
|
|
|
|
|
|
/**
|
|
* get events
|
|
*/
|
|
getEvents: function() {
|
|
return [
|
|
'pointerdown MSPointerDown',
|
|
'pointermove MSPointerMove',
|
|
'pointerup pointercancel MSPointerUp MSPointerCancel'
|
|
];
|
|
},
|
|
|
|
/**
|
|
* reset the list
|
|
*/
|
|
reset: function() {
|
|
this.pointers = {};
|
|
}
|
|
};
|
|
|
|
|
|
ionic.Gestures.utils = {
|
|
/**
|
|
* extend method,
|
|
* also used for cloning when dest is an empty object
|
|
* @param {Object} dest
|
|
* @param {Object} src
|
|
* @param {Boolean} merge do a merge
|
|
* @returns {Object} dest
|
|
*/
|
|
extend: function extend(dest, src, merge) {
|
|
for (var key in src) {
|
|
if(dest[key] !== undefined && merge) {
|
|
continue;
|
|
}
|
|
dest[key] = src[key];
|
|
}
|
|
return dest;
|
|
},
|
|
|
|
|
|
/**
|
|
* find if a node is in the given parent
|
|
* used for event delegation tricks
|
|
* @param {HTMLElement} node
|
|
* @param {HTMLElement} parent
|
|
* @returns {boolean} has_parent
|
|
*/
|
|
hasParent: function(node, parent) {
|
|
while(node){
|
|
if(node == parent) {
|
|
return true;
|
|
}
|
|
node = node.parentNode;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
|
|
/**
|
|
* get the center of all the touches
|
|
* @param {Array} touches
|
|
* @returns {Object} center
|
|
*/
|
|
getCenter: function getCenter(touches) {
|
|
var valuesX = [], valuesY = [];
|
|
|
|
for(var t = 0, len = touches.length; t < len; t++) {
|
|
valuesX.push(touches[t].pageX);
|
|
valuesY.push(touches[t].pageY);
|
|
}
|
|
|
|
return {
|
|
pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
|
|
pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* calculate the velocity between two points
|
|
* @param {Number} delta_time
|
|
* @param {Number} delta_x
|
|
* @param {Number} delta_y
|
|
* @returns {Object} velocity
|
|
*/
|
|
getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
|
|
return {
|
|
x: Math.abs(delta_x / delta_time) || 0,
|
|
y: Math.abs(delta_y / delta_time) || 0
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* calculate the angle between two coordinates
|
|
* @param {Touch} touch1
|
|
* @param {Touch} touch2
|
|
* @returns {Number} angle
|
|
*/
|
|
getAngle: function getAngle(touch1, touch2) {
|
|
var y = touch2.pageY - touch1.pageY,
|
|
x = touch2.pageX - touch1.pageX;
|
|
return Math.atan2(y, x) * 180 / Math.PI;
|
|
},
|
|
|
|
|
|
/**
|
|
* angle to direction define
|
|
* @param {Touch} touch1
|
|
* @param {Touch} touch2
|
|
* @returns {String} direction constant, like ionic.Gestures.DIRECTION_LEFT
|
|
*/
|
|
getDirection: function getDirection(touch1, touch2) {
|
|
var x = Math.abs(touch1.pageX - touch2.pageX),
|
|
y = Math.abs(touch1.pageY - touch2.pageY);
|
|
|
|
if(x >= y) {
|
|
return touch1.pageX - touch2.pageX > 0 ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
|
|
}
|
|
else {
|
|
return touch1.pageY - touch2.pageY > 0 ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* calculate the distance between two touches
|
|
* @param {Touch} touch1
|
|
* @param {Touch} touch2
|
|
* @returns {Number} distance
|
|
*/
|
|
getDistance: function getDistance(touch1, touch2) {
|
|
var x = touch2.pageX - touch1.pageX,
|
|
y = touch2.pageY - touch1.pageY;
|
|
return Math.sqrt((x * x) + (y * y));
|
|
},
|
|
|
|
|
|
/**
|
|
* calculate the scale factor between two touchLists (fingers)
|
|
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
|
|
* @param {Array} start
|
|
* @param {Array} end
|
|
* @returns {Number} scale
|
|
*/
|
|
getScale: function getScale(start, end) {
|
|
// need two fingers...
|
|
if(start.length >= 2 && end.length >= 2) {
|
|
return this.getDistance(end[0], end[1]) /
|
|
this.getDistance(start[0], start[1]);
|
|
}
|
|
return 1;
|
|
},
|
|
|
|
|
|
/**
|
|
* calculate the rotation degrees between two touchLists (fingers)
|
|
* @param {Array} start
|
|
* @param {Array} end
|
|
* @returns {Number} rotation
|
|
*/
|
|
getRotation: function getRotation(start, end) {
|
|
// need two fingers
|
|
if(start.length >= 2 && end.length >= 2) {
|
|
return this.getAngle(end[1], end[0]) -
|
|
this.getAngle(start[1], start[0]);
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
|
|
/**
|
|
* boolean if the direction is vertical
|
|
* @param {String} direction
|
|
* @returns {Boolean} is_vertical
|
|
*/
|
|
isVertical: function isVertical(direction) {
|
|
return (direction == ionic.Gestures.DIRECTION_UP || direction == ionic.Gestures.DIRECTION_DOWN);
|
|
},
|
|
|
|
|
|
/**
|
|
* stop browser default behavior with css class
|
|
* @param {HtmlElement} element
|
|
* @param {Object} css_class
|
|
*/
|
|
stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_class) {
|
|
// changed from making many style changes to just adding a preset classname
|
|
// less DOM manipulations, less code, and easier to control in the CSS side of things
|
|
// hammer.js doesn't come with CSS, but ionic does, which is why we prefer this method
|
|
if(element && element.classList) {
|
|
element.classList.add(css_class);
|
|
element.onselectstart = function() {
|
|
return false;
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
ionic.Gestures.detection = {
|
|
// contains all registred ionic.Gestures.gestures in the correct order
|
|
gestures: [],
|
|
|
|
// data of the current ionic.Gestures.gesture detection session
|
|
current: null,
|
|
|
|
// the previous ionic.Gestures.gesture session data
|
|
// is a full clone of the previous gesture.current object
|
|
previous: null,
|
|
|
|
// when this becomes true, no gestures are fired
|
|
stopped: false,
|
|
|
|
|
|
/**
|
|
* start ionic.Gestures.gesture detection
|
|
* @param {ionic.Gestures.Instance} inst
|
|
* @param {Object} eventData
|
|
*/
|
|
startDetect: function startDetect(inst, eventData) {
|
|
// already busy with a ionic.Gestures.gesture detection on an element
|
|
if(this.current) {
|
|
return;
|
|
}
|
|
|
|
this.stopped = false;
|
|
|
|
this.current = {
|
|
inst: inst, // reference to ionic.GesturesInstance we're working for
|
|
startEvent: ionic.Gestures.utils.extend({}, eventData), // start eventData for distances, timing etc
|
|
lastEvent: false, // last eventData
|
|
name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc
|
|
};
|
|
|
|
this.detect(eventData);
|
|
},
|
|
|
|
|
|
/**
|
|
* ionic.Gestures.gesture detection
|
|
* @param {Object} eventData
|
|
*/
|
|
detect: function detect(eventData) {
|
|
if(!this.current || this.stopped) {
|
|
return null;
|
|
}
|
|
|
|
// extend event data with calculations about scale, distance etc
|
|
eventData = this.extendEventData(eventData);
|
|
|
|
// instance options
|
|
var inst_options = this.current.inst.options;
|
|
|
|
// call ionic.Gestures.gesture handlers
|
|
for(var g = 0, len = this.gestures.length; g < len; g++) {
|
|
var gesture = this.gestures[g];
|
|
|
|
// only when the instance options have enabled this gesture
|
|
if(!this.stopped && inst_options[gesture.name] !== false) {
|
|
// if a handler returns false, we stop with the detection
|
|
if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
|
|
this.stopDetect();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// store as previous event event
|
|
if(this.current) {
|
|
this.current.lastEvent = eventData;
|
|
}
|
|
|
|
// endevent, but not the last touch, so dont stop
|
|
if(eventData.eventType == ionic.Gestures.EVENT_END && !eventData.touches.length - 1) {
|
|
this.stopDetect();
|
|
}
|
|
|
|
return eventData;
|
|
},
|
|
|
|
|
|
/**
|
|
* clear the ionic.Gestures.gesture vars
|
|
* this is called on endDetect, but can also be used when a final ionic.Gestures.gesture has been detected
|
|
* to stop other ionic.Gestures.gestures from being fired
|
|
*/
|
|
stopDetect: function stopDetect() {
|
|
// clone current data to the store as the previous gesture
|
|
// used for the double tap gesture, since this is an other gesture detect session
|
|
this.previous = ionic.Gestures.utils.extend({}, this.current);
|
|
|
|
// reset the current
|
|
this.current = null;
|
|
|
|
// stopped!
|
|
this.stopped = true;
|
|
},
|
|
|
|
|
|
/**
|
|
* extend eventData for ionic.Gestures.gestures
|
|
* @param {Object} ev
|
|
* @returns {Object} ev
|
|
*/
|
|
extendEventData: function extendEventData(ev) {
|
|
var startEv = this.current.startEvent;
|
|
|
|
// if the touches change, set the new touches over the startEvent touches
|
|
// this because touchevents don't have all the touches on touchstart, or the
|
|
// user must place his fingers at the EXACT same time on the screen, which is not realistic
|
|
// but, sometimes it happens that both fingers are touching at the EXACT same time
|
|
if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
|
|
// extend 1 level deep to get the touchlist with the touch objects
|
|
startEv.touches = [];
|
|
for(var i = 0, len = ev.touches.length; i < len; i++) {
|
|
startEv.touches.push(ionic.Gestures.utils.extend({}, ev.touches[i]));
|
|
}
|
|
}
|
|
|
|
var delta_time = ev.timeStamp - startEv.timeStamp,
|
|
delta_x = ev.center.pageX - startEv.center.pageX,
|
|
delta_y = ev.center.pageY - startEv.center.pageY,
|
|
velocity = ionic.Gestures.utils.getVelocity(delta_time, delta_x, delta_y);
|
|
|
|
ionic.Gestures.utils.extend(ev, {
|
|
deltaTime: delta_time,
|
|
deltaX: delta_x,
|
|
deltaY: delta_y,
|
|
|
|
velocityX: velocity.x,
|
|
velocityY: velocity.y,
|
|
|
|
distance: ionic.Gestures.utils.getDistance(startEv.center, ev.center),
|
|
angle: ionic.Gestures.utils.getAngle(startEv.center, ev.center),
|
|
direction: ionic.Gestures.utils.getDirection(startEv.center, ev.center),
|
|
|
|
scale: ionic.Gestures.utils.getScale(startEv.touches, ev.touches),
|
|
rotation: ionic.Gestures.utils.getRotation(startEv.touches, ev.touches),
|
|
|
|
startEvent: startEv
|
|
});
|
|
|
|
return ev;
|
|
},
|
|
|
|
|
|
/**
|
|
* register new gesture
|
|
* @param {Object} gesture object, see gestures.js for documentation
|
|
* @returns {Array} gestures
|
|
*/
|
|
register: function register(gesture) {
|
|
// add an enable gesture options if there is no given
|
|
var options = gesture.defaults || {};
|
|
if(options[gesture.name] === undefined) {
|
|
options[gesture.name] = true;
|
|
}
|
|
|
|
// extend ionic.Gestures default options with the ionic.Gestures.gesture options
|
|
ionic.Gestures.utils.extend(ionic.Gestures.defaults, options, true);
|
|
|
|
// set its index
|
|
gesture.index = gesture.index || 1000;
|
|
|
|
// add ionic.Gestures.gesture to the list
|
|
this.gestures.push(gesture);
|
|
|
|
// sort the list by index
|
|
this.gestures.sort(function(a, b) {
|
|
if (a.index < b.index) {
|
|
return -1;
|
|
}
|
|
if (a.index > b.index) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
return this.gestures;
|
|
}
|
|
};
|
|
|
|
|
|
ionic.Gestures.gestures = ionic.Gestures.gestures || {};
|
|
|
|
/**
|
|
* Custom gestures
|
|
* ==============================
|
|
*
|
|
* Gesture object
|
|
* --------------------
|
|
* The object structure of a gesture:
|
|
*
|
|
* { name: 'mygesture',
|
|
* index: 1337,
|
|
* defaults: {
|
|
* mygesture_option: true
|
|
* }
|
|
* handler: function(type, ev, inst) {
|
|
* // trigger gesture event
|
|
* inst.trigger(this.name, ev);
|
|
* }
|
|
* }
|
|
|
|
* @param {String} name
|
|
* this should be the name of the gesture, lowercase
|
|
* it is also being used to disable/enable the gesture per instance config.
|
|
*
|
|
* @param {Number} [index=1000]
|
|
* the index of the gesture, where it is going to be in the stack of gestures detection
|
|
* like when you build an gesture that depends on the drag gesture, it is a good
|
|
* idea to place it after the index of the drag gesture.
|
|
*
|
|
* @param {Object} [defaults={}]
|
|
* the default settings of the gesture. these are added to the instance settings,
|
|
* and can be overruled per instance. you can also add the name of the gesture,
|
|
* but this is also added by default (and set to true).
|
|
*
|
|
* @param {Function} handler
|
|
* this handles the gesture detection of your custom gesture and receives the
|
|
* following arguments:
|
|
*
|
|
* @param {Object} eventData
|
|
* event data containing the following properties:
|
|
* timeStamp {Number} time the event occurred
|
|
* target {HTMLElement} target element
|
|
* touches {Array} touches (fingers, pointers, mouse) on the screen
|
|
* pointerType {String} kind of pointer that was used. matches ionic.Gestures.POINTER_MOUSE|TOUCH
|
|
* center {Object} center position of the touches. contains pageX and pageY
|
|
* deltaTime {Number} the total time of the touches in the screen
|
|
* deltaX {Number} the delta on x axis we haved moved
|
|
* deltaY {Number} the delta on y axis we haved moved
|
|
* velocityX {Number} the velocity on the x
|
|
* velocityY {Number} the velocity on y
|
|
* angle {Number} the angle we are moving
|
|
* direction {String} the direction we are moving. matches ionic.Gestures.DIRECTION_UP|DOWN|LEFT|RIGHT
|
|
* distance {Number} the distance we haved moved
|
|
* scale {Number} scaling of the touches, needs 2 touches
|
|
* rotation {Number} rotation of the touches, needs 2 touches *
|
|
* eventType {String} matches ionic.Gestures.EVENT_START|MOVE|END
|
|
* srcEvent {Object} the source event, like TouchStart or MouseDown *
|
|
* startEvent {Object} contains the same properties as above,
|
|
* but from the first touch. this is used to calculate
|
|
* distances, deltaTime, scaling etc
|
|
*
|
|
* @param {ionic.Gestures.Instance} inst
|
|
* the instance we are doing the detection for. you can get the options from
|
|
* the inst.options object and trigger the gesture event by calling inst.trigger
|
|
*
|
|
*
|
|
* Handle gestures
|
|
* --------------------
|
|
* inside the handler you can get/set ionic.Gestures.detectionic.current. This is the current
|
|
* detection sessionic. It has the following properties
|
|
* @param {String} name
|
|
* contains the name of the gesture we have detected. it has not a real function,
|
|
* only to check in other gestures if something is detected.
|
|
* like in the drag gesture we set it to 'drag' and in the swipe gesture we can
|
|
* check if the current gesture is 'drag' by accessing ionic.Gestures.detectionic.current.name
|
|
*
|
|
* readonly
|
|
* @param {ionic.Gestures.Instance} inst
|
|
* the instance we do the detection for
|
|
*
|
|
* readonly
|
|
* @param {Object} startEvent
|
|
* contains the properties of the first gesture detection in this sessionic.
|
|
* Used for calculations about timing, distance, etc.
|
|
*
|
|
* readonly
|
|
* @param {Object} lastEvent
|
|
* contains all the properties of the last gesture detect in this sessionic.
|
|
*
|
|
* after the gesture detection session has been completed (user has released the screen)
|
|
* the ionic.Gestures.detectionic.current object is copied into ionic.Gestures.detectionic.previous,
|
|
* this is usefull for gestures like doubletap, where you need to know if the
|
|
* previous gesture was a tap
|
|
*
|
|
* options that have been set by the instance can be received by calling inst.options
|
|
*
|
|
* You can trigger a gesture event by calling inst.trigger("mygesture", event).
|
|
* The first param is the name of your gesture, the second the event argument
|
|
*
|
|
*
|
|
* Register gestures
|
|
* --------------------
|
|
* When an gesture is added to the ionic.Gestures.gestures object, it is auto registered
|
|
* at the setup of the first ionic.Gestures instance. You can also call ionic.Gestures.detectionic.register
|
|
* manually and pass your gesture object as a param
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Hold
|
|
* Touch stays at the same place for x time
|
|
* events hold
|
|
*/
|
|
ionic.Gestures.gestures.Hold = {
|
|
name: 'hold',
|
|
index: 10,
|
|
defaults: {
|
|
hold_timeout: 500,
|
|
hold_threshold: 9
|
|
},
|
|
timer: null,
|
|
handler: function holdGesture(ev, inst) {
|
|
switch(ev.eventType) {
|
|
case ionic.Gestures.EVENT_START:
|
|
// clear any running timers
|
|
clearTimeout(this.timer);
|
|
|
|
// set the gesture so we can check in the timeout if it still is
|
|
ionic.Gestures.detection.current.name = this.name;
|
|
|
|
// set timer and if after the timeout it still is hold,
|
|
// we trigger the hold event
|
|
this.timer = setTimeout(function() {
|
|
if(ionic.Gestures.detection.current.name == 'hold') {
|
|
ionic.tap.cancelClick();
|
|
inst.trigger('hold', ev);
|
|
}
|
|
}, inst.options.hold_timeout);
|
|
break;
|
|
|
|
// when you move or end we clear the timer
|
|
case ionic.Gestures.EVENT_MOVE:
|
|
if(ev.distance > inst.options.hold_threshold) {
|
|
clearTimeout(this.timer);
|
|
}
|
|
break;
|
|
|
|
case ionic.Gestures.EVENT_END:
|
|
clearTimeout(this.timer);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Tap/DoubleTap
|
|
* Quick touch at a place or double at the same place
|
|
* events tap, doubletap
|
|
*/
|
|
ionic.Gestures.gestures.Tap = {
|
|
name: 'tap',
|
|
index: 100,
|
|
defaults: {
|
|
tap_max_touchtime: 250,
|
|
tap_max_distance: 10,
|
|
tap_always: true,
|
|
doubletap_distance: 20,
|
|
doubletap_interval: 300
|
|
},
|
|
handler: function tapGesture(ev, inst) {
|
|
if(ev.eventType == ionic.Gestures.EVENT_END && ev.srcEvent.type != 'touchcancel') {
|
|
// previous gesture, for the double tap since these are two different gesture detections
|
|
var prev = ionic.Gestures.detection.previous,
|
|
did_doubletap = false;
|
|
|
|
// when the touchtime is higher then the max touch time
|
|
// or when the moving distance is too much
|
|
if(ev.deltaTime > inst.options.tap_max_touchtime ||
|
|
ev.distance > inst.options.tap_max_distance) {
|
|
return;
|
|
}
|
|
|
|
// check if double tap
|
|
if(prev && prev.name == 'tap' &&
|
|
(ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
|
|
ev.distance < inst.options.doubletap_distance) {
|
|
inst.trigger('doubletap', ev);
|
|
did_doubletap = true;
|
|
}
|
|
|
|
// do a single tap
|
|
if(!did_doubletap || inst.options.tap_always) {
|
|
ionic.Gestures.detection.current.name = 'tap';
|
|
inst.trigger('tap', ev);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Swipe
|
|
* triggers swipe events when the end velocity is above the threshold
|
|
* events swipe, swipeleft, swiperight, swipeup, swipedown
|
|
*/
|
|
ionic.Gestures.gestures.Swipe = {
|
|
name: 'swipe',
|
|
index: 40,
|
|
defaults: {
|
|
// set 0 for unlimited, but this can conflict with transform
|
|
swipe_max_touches: 1,
|
|
swipe_velocity: 0.4
|
|
},
|
|
handler: function swipeGesture(ev, inst) {
|
|
if(ev.eventType == ionic.Gestures.EVENT_END) {
|
|
// max touches
|
|
if(inst.options.swipe_max_touches > 0 &&
|
|
ev.touches.length > inst.options.swipe_max_touches) {
|
|
return;
|
|
}
|
|
|
|
// when the distance we moved is too small we skip this gesture
|
|
// or we can be already in dragging
|
|
if(ev.velocityX > inst.options.swipe_velocity ||
|
|
ev.velocityY > inst.options.swipe_velocity) {
|
|
// trigger swipe events
|
|
inst.trigger(this.name, ev);
|
|
inst.trigger(this.name + ev.direction, ev);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Drag
|
|
* Move with x fingers (default 1) around on the page. Blocking the scrolling when
|
|
* moving left and right is a good practice. When all the drag events are blocking
|
|
* you disable scrolling on that area.
|
|
* events drag, drapleft, dragright, dragup, dragdown
|
|
*/
|
|
ionic.Gestures.gestures.Drag = {
|
|
name: 'drag',
|
|
index: 50,
|
|
defaults: {
|
|
drag_min_distance: 10,
|
|
// Set correct_for_drag_min_distance to true to make the starting point of the drag
|
|
// be calculated from where the drag was triggered, not from where the touch started.
|
|
// Useful to avoid a jerk-starting drag, which can make fine-adjustments
|
|
// through dragging difficult, and be visually unappealing.
|
|
correct_for_drag_min_distance: true,
|
|
// set 0 for unlimited, but this can conflict with transform
|
|
drag_max_touches: 1,
|
|
// prevent default browser behavior when dragging occurs
|
|
// be careful with it, it makes the element a blocking element
|
|
// when you are using the drag gesture, it is a good practice to set this true
|
|
drag_block_horizontal: true,
|
|
drag_block_vertical: true,
|
|
// drag_lock_to_axis keeps the drag gesture on the axis that it started on,
|
|
// It disallows vertical directions if the initial direction was horizontal, and vice versa.
|
|
drag_lock_to_axis: false,
|
|
// drag lock only kicks in when distance > drag_lock_min_distance
|
|
// This way, locking occurs only when the distance has become large enough to reliably determine the direction
|
|
drag_lock_min_distance: 25,
|
|
// prevent default if the gesture is going the given direction
|
|
prevent_default_directions: []
|
|
},
|
|
triggered: false,
|
|
handler: function dragGesture(ev, inst) {
|
|
if (ev.srcEvent.type == 'touchstart' || ev.srcEvent.type == 'touchend') {
|
|
this.preventedFirstMove = false;
|
|
|
|
} else if (!this.preventedFirstMove && ev.srcEvent.type == 'touchmove') {
|
|
// Prevent gestures that are not intended for this event handler from firing subsequent times
|
|
if (inst.options.prevent_default_directions.length > 0
|
|
&& inst.options.prevent_default_directions.indexOf(ev.direction) != -1) {
|
|
ev.srcEvent.preventDefault();
|
|
}
|
|
this.preventedFirstMove = true;
|
|
}
|
|
|
|
// current gesture isnt drag, but dragged is true
|
|
// this means an other gesture is busy. now call dragend
|
|
if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
|
|
inst.trigger(this.name + 'end', ev);
|
|
this.triggered = false;
|
|
return;
|
|
}
|
|
|
|
// max touches
|
|
if(inst.options.drag_max_touches > 0 &&
|
|
ev.touches.length > inst.options.drag_max_touches) {
|
|
return;
|
|
}
|
|
|
|
switch(ev.eventType) {
|
|
case ionic.Gestures.EVENT_START:
|
|
this.triggered = false;
|
|
break;
|
|
|
|
case ionic.Gestures.EVENT_MOVE:
|
|
// when the distance we moved is too small we skip this gesture
|
|
// or we can be already in dragging
|
|
if(ev.distance < inst.options.drag_min_distance &&
|
|
ionic.Gestures.detection.current.name != this.name) {
|
|
return;
|
|
}
|
|
|
|
// we are dragging!
|
|
if(ionic.Gestures.detection.current.name != this.name) {
|
|
ionic.Gestures.detection.current.name = this.name;
|
|
if (inst.options.correct_for_drag_min_distance) {
|
|
// When a drag is triggered, set the event center to drag_min_distance pixels from the original event center.
|
|
// Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0.
|
|
// It might be useful to save the original start point somewhere
|
|
var factor = Math.abs(inst.options.drag_min_distance / ev.distance);
|
|
ionic.Gestures.detection.current.startEvent.center.pageX += ev.deltaX * factor;
|
|
ionic.Gestures.detection.current.startEvent.center.pageY += ev.deltaY * factor;
|
|
|
|
// recalculate event data using new start point
|
|
ev = ionic.Gestures.detection.extendEventData(ev);
|
|
}
|
|
}
|
|
|
|
// lock drag to axis?
|
|
if(ionic.Gestures.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance <= ev.distance)) {
|
|
ev.drag_locked_to_axis = true;
|
|
}
|
|
var last_direction = ionic.Gestures.detection.current.lastEvent.direction;
|
|
if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
|
|
// keep direction on the axis that the drag gesture started on
|
|
if(ionic.Gestures.utils.isVertical(last_direction)) {
|
|
ev.direction = (ev.deltaY < 0) ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
|
|
}
|
|
else {
|
|
ev.direction = (ev.deltaX < 0) ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
|
|
}
|
|
}
|
|
|
|
// first time, trigger dragstart event
|
|
if(!this.triggered) {
|
|
inst.trigger(this.name + 'start', ev);
|
|
this.triggered = true;
|
|
}
|
|
|
|
// trigger normal event
|
|
inst.trigger(this.name, ev);
|
|
|
|
// direction event, like dragdown
|
|
inst.trigger(this.name + ev.direction, ev);
|
|
|
|
// block the browser events
|
|
if( (inst.options.drag_block_vertical && ionic.Gestures.utils.isVertical(ev.direction)) ||
|
|
(inst.options.drag_block_horizontal && !ionic.Gestures.utils.isVertical(ev.direction))) {
|
|
ev.preventDefault();
|
|
}
|
|
break;
|
|
|
|
case ionic.Gestures.EVENT_END:
|
|
// trigger dragend
|
|
if(this.triggered) {
|
|
inst.trigger(this.name + 'end', ev);
|
|
}
|
|
|
|
this.triggered = false;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Transform
|
|
* User want to scale or rotate with 2 fingers
|
|
* events transform, pinch, pinchin, pinchout, rotate
|
|
*/
|
|
ionic.Gestures.gestures.Transform = {
|
|
name: 'transform',
|
|
index: 45,
|
|
defaults: {
|
|
// factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
|
|
transform_min_scale: 0.01,
|
|
// rotation in degrees
|
|
transform_min_rotation: 1,
|
|
// prevent default browser behavior when two touches are on the screen
|
|
// but it makes the element a blocking element
|
|
// when you are using the transform gesture, it is a good practice to set this true
|
|
transform_always_block: false
|
|
},
|
|
triggered: false,
|
|
handler: function transformGesture(ev, inst) {
|
|
// current gesture isnt drag, but dragged is true
|
|
// this means an other gesture is busy. now call dragend
|
|
if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
|
|
inst.trigger(this.name + 'end', ev);
|
|
this.triggered = false;
|
|
return;
|
|
}
|
|
|
|
// atleast multitouch
|
|
if(ev.touches.length < 2) {
|
|
return;
|
|
}
|
|
|
|
// prevent default when two fingers are on the screen
|
|
if(inst.options.transform_always_block) {
|
|
ev.preventDefault();
|
|
}
|
|
|
|
switch(ev.eventType) {
|
|
case ionic.Gestures.EVENT_START:
|
|
this.triggered = false;
|
|
break;
|
|
|
|
case ionic.Gestures.EVENT_MOVE:
|
|
var scale_threshold = Math.abs(1 - ev.scale);
|
|
var rotation_threshold = Math.abs(ev.rotation);
|
|
|
|
// when the distance we moved is too small we skip this gesture
|
|
// or we can be already in dragging
|
|
if(scale_threshold < inst.options.transform_min_scale &&
|
|
rotation_threshold < inst.options.transform_min_rotation) {
|
|
return;
|
|
}
|
|
|
|
// we are transforming!
|
|
ionic.Gestures.detection.current.name = this.name;
|
|
|
|
// first time, trigger dragstart event
|
|
if(!this.triggered) {
|
|
inst.trigger(this.name + 'start', ev);
|
|
this.triggered = true;
|
|
}
|
|
|
|
inst.trigger(this.name, ev); // basic transform event
|
|
|
|
// trigger rotate event
|
|
if(rotation_threshold > inst.options.transform_min_rotation) {
|
|
inst.trigger('rotate', ev);
|
|
}
|
|
|
|
// trigger pinch event
|
|
if(scale_threshold > inst.options.transform_min_scale) {
|
|
inst.trigger('pinch', ev);
|
|
inst.trigger('pinch' + ((ev.scale < 1) ? 'in' : 'out'), ev);
|
|
}
|
|
break;
|
|
|
|
case ionic.Gestures.EVENT_END:
|
|
// trigger dragend
|
|
if(this.triggered) {
|
|
inst.trigger(this.name + 'end', ev);
|
|
}
|
|
|
|
this.triggered = false;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Touch
|
|
* Called as first, tells the user has touched the screen
|
|
* events touch
|
|
*/
|
|
ionic.Gestures.gestures.Touch = {
|
|
name: 'touch',
|
|
index: -Infinity,
|
|
defaults: {
|
|
// call preventDefault at touchstart, and makes the element blocking by
|
|
// disabling the scrolling of the page, but it improves gestures like
|
|
// transforming and dragging.
|
|
// be careful with using this, it can be very annoying for users to be stuck
|
|
// on the page
|
|
prevent_default: false,
|
|
|
|
// disable mouse events, so only touch (or pen!) input triggers events
|
|
prevent_mouseevents: false
|
|
},
|
|
handler: function touchGesture(ev, inst) {
|
|
if(inst.options.prevent_mouseevents && ev.pointerType == ionic.Gestures.POINTER_MOUSE) {
|
|
ev.stopDetect();
|
|
return;
|
|
}
|
|
|
|
if(inst.options.prevent_default) {
|
|
ev.preventDefault();
|
|
}
|
|
|
|
if(ev.eventType == ionic.Gestures.EVENT_START) {
|
|
inst.trigger(this.name, ev);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Release
|
|
* Called as last, tells the user has released the screen
|
|
* events release
|
|
*/
|
|
ionic.Gestures.gestures.Release = {
|
|
name: 'release',
|
|
index: Infinity,
|
|
handler: function releaseGesture(ev, inst) {
|
|
if(ev.eventType == ionic.Gestures.EVENT_END) {
|
|
inst.trigger(this.name, ev);
|
|
}
|
|
}
|
|
};
|
|
})(window.ionic);
|
|
|
|
(function(window, document, ionic) {
|
|
|
|
function getParameterByName(name) {
|
|
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
|
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
|
results = regex.exec(location.search);
|
|
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
|
}
|
|
|
|
var IOS = 'ios';
|
|
var ANDROID = 'android';
|
|
var WINDOWS_PHONE = 'windowsphone';
|
|
var EDGE = 'edge';
|
|
var CROSSWALK = 'crosswalk';
|
|
var requestAnimationFrame = ionic.requestAnimationFrame;
|
|
|
|
/**
|
|
* @ngdoc utility
|
|
* @name ionic.Platform
|
|
* @module ionic
|
|
* @description
|
|
* A set of utility methods that can be used to retrieve the device ready state and
|
|
* various other information such as what kind of platform the app is currently installed on.
|
|
*
|
|
* @usage
|
|
* ```js
|
|
* angular.module('PlatformApp', ['ionic'])
|
|
* .controller('PlatformCtrl', function($scope) {
|
|
*
|
|
* ionic.Platform.ready(function(){
|
|
* // will execute when device is ready, or immediately if the device is already ready.
|
|
* });
|
|
*
|
|
* var deviceInformation = ionic.Platform.device();
|
|
*
|
|
* var isWebView = ionic.Platform.isWebView();
|
|
* var isIPad = ionic.Platform.isIPad();
|
|
* var isIOS = ionic.Platform.isIOS();
|
|
* var isAndroid = ionic.Platform.isAndroid();
|
|
* var isWindowsPhone = ionic.Platform.isWindowsPhone();
|
|
*
|
|
* var currentPlatform = ionic.Platform.platform();
|
|
* var currentPlatformVersion = ionic.Platform.version();
|
|
*
|
|
* ionic.Platform.exitApp(); // stops the app
|
|
* });
|
|
* ```
|
|
*/
|
|
var self = ionic.Platform = {
|
|
|
|
// Put navigator on platform so it can be mocked and set
|
|
// the browser does not allow window.navigator to be set
|
|
navigator: window.navigator,
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name ionic.Platform#isReady
|
|
* @returns {boolean} Whether the device is ready.
|
|
*/
|
|
isReady: false,
|
|
/**
|
|
* @ngdoc property
|
|
* @name ionic.Platform#isFullScreen
|
|
* @returns {boolean} Whether the device is fullscreen.
|
|
*/
|
|
isFullScreen: false,
|
|
/**
|
|
* @ngdoc property
|
|
* @name ionic.Platform#platforms
|
|
* @returns {Array(string)} An array of all platforms found.
|
|
*/
|
|
platforms: null,
|
|
/**
|
|
* @ngdoc property
|
|
* @name ionic.Platform#grade
|
|
* @returns {string} What grade the current platform is.
|
|
*/
|
|
grade: null,
|
|
/**
|
|
* @ngdoc property
|
|
* @name ionic.Platform#ua
|
|
* @returns {string} What User Agent is.
|
|
*/
|
|
ua: navigator.userAgent,
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#ready
|
|
* @description
|
|
* Trigger a callback once the device is ready, or immediately
|
|
* if the device is already ready. This method can be run from
|
|
* anywhere and does not need to be wrapped by any additonal methods.
|
|
* When the app is within a WebView (Cordova), it'll fire
|
|
* the callback once the device is ready. If the app is within
|
|
* a web browser, it'll fire the callback after `window.load`.
|
|
* Please remember that Cordova features (Camera, FileSystem, etc) still
|
|
* will not work in a web browser.
|
|
* @param {function} callback The function to call.
|
|
*/
|
|
ready: function(cb) {
|
|
// run through tasks to complete now that the device is ready
|
|
if (self.isReady) {
|
|
cb();
|
|
} else {
|
|
// the platform isn't ready yet, add it to this array
|
|
// which will be called once the platform is ready
|
|
readyCallbacks.push(cb);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
detect: function() {
|
|
self._checkPlatforms();
|
|
|
|
requestAnimationFrame(function() {
|
|
// only add to the body class if we got platform info
|
|
for (var i = 0; i < self.platforms.length; i++) {
|
|
document.body.classList.add('platform-' + self.platforms[i]);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#setGrade
|
|
* @description Set the grade of the device: 'a', 'b', or 'c'. 'a' is the best
|
|
* (most css features enabled), 'c' is the worst. By default, sets the grade
|
|
* depending on the current device.
|
|
* @param {string} grade The new grade to set.
|
|
*/
|
|
setGrade: function(grade) {
|
|
var oldGrade = self.grade;
|
|
self.grade = grade;
|
|
requestAnimationFrame(function() {
|
|
if (oldGrade) {
|
|
document.body.classList.remove('grade-' + oldGrade);
|
|
}
|
|
document.body.classList.add('grade-' + grade);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#device
|
|
* @description Return the current device (given by cordova).
|
|
* @returns {object} The device object.
|
|
*/
|
|
device: function() {
|
|
return window.device || {};
|
|
},
|
|
|
|
_checkPlatforms: function() {
|
|
self.platforms = [];
|
|
var grade = 'a';
|
|
|
|
if (self.isWebView()) {
|
|
self.platforms.push('webview');
|
|
if (!(!window.cordova && !window.PhoneGap && !window.phonegap)) {
|
|
self.platforms.push('cordova');
|
|
} else if (typeof window.forge === 'object') {
|
|
self.platforms.push('trigger');
|
|
}
|
|
} else {
|
|
self.platforms.push('browser');
|
|
}
|
|
if (self.isIPad()) self.platforms.push('ipad');
|
|
|
|
var platform = self.platform();
|
|
if (platform) {
|
|
self.platforms.push(platform);
|
|
|
|
var version = self.version();
|
|
if (version) {
|
|
var v = version.toString();
|
|
if (v.indexOf('.') > 0) {
|
|
v = v.replace('.', '_');
|
|
} else {
|
|
v += '_0';
|
|
}
|
|
self.platforms.push(platform + v.split('_')[0]);
|
|
self.platforms.push(platform + v);
|
|
|
|
if (self.isAndroid() && version < 4.4) {
|
|
grade = (version < 4 ? 'c' : 'b');
|
|
} else if (self.isWindowsPhone()) {
|
|
grade = 'b';
|
|
}
|
|
}
|
|
}
|
|
|
|
self.setGrade(grade);
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#isWebView
|
|
* @returns {boolean} Check if we are running within a WebView (such as Cordova).
|
|
*/
|
|
isWebView: function() {
|
|
return !(!window.cordova && !window.PhoneGap && !window.phonegap && window.forge !== 'object');
|
|
},
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#isIPad
|
|
* @returns {boolean} Whether we are running on iPad.
|
|
*/
|
|
isIPad: function() {
|
|
if (/iPad/i.test(self.navigator.platform)) {
|
|
return true;
|
|
}
|
|
return /iPad/i.test(self.ua);
|
|
},
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#isIOS
|
|
* @returns {boolean} Whether we are running on iOS.
|
|
*/
|
|
isIOS: function() {
|
|
return self.is(IOS);
|
|
},
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#isAndroid
|
|
* @returns {boolean} Whether we are running on Android.
|
|
*/
|
|
isAndroid: function() {
|
|
return self.is(ANDROID);
|
|
},
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#isWindowsPhone
|
|
* @returns {boolean} Whether we are running on Windows Phone.
|
|
*/
|
|
isWindowsPhone: function() {
|
|
return self.is(WINDOWS_PHONE);
|
|
},
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#isEdge
|
|
* @returns {boolean} Whether we are running on MS Edge/Windows 10 (inc. Phone)
|
|
*/
|
|
isEdge: function() {
|
|
return self.is(EDGE);
|
|
},
|
|
|
|
isCrosswalk: function() {
|
|
return self.is(CROSSWALK);
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#platform
|
|
* @returns {string} The name of the current platform.
|
|
*/
|
|
platform: function() {
|
|
// singleton to get the platform name
|
|
if (platformName === null) self.setPlatform(self.device().platform);
|
|
return platformName;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
setPlatform: function(n) {
|
|
if (typeof n != 'undefined' && n !== null && n.length) {
|
|
platformName = n.toLowerCase();
|
|
} else if (getParameterByName('ionicplatform')) {
|
|
platformName = getParameterByName('ionicplatform');
|
|
} else if (self.ua.indexOf('Edge') > -1) {
|
|
platformName = EDGE;
|
|
} else if (self.ua.indexOf('Windows Phone') > -1) {
|
|
platformName = WINDOWS_PHONE;
|
|
} else if (self.ua.indexOf('Android') > 0) {
|
|
platformName = ANDROID;
|
|
} else if (/iPhone|iPad|iPod/.test(self.ua)) {
|
|
platformName = IOS;
|
|
} else {
|
|
platformName = self.navigator.platform && navigator.platform.toLowerCase().split(' ')[0] || '';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#version
|
|
* @returns {number} The version of the current device platform.
|
|
*/
|
|
version: function() {
|
|
// singleton to get the platform version
|
|
if (platformVersion === null) self.setVersion(self.device().version);
|
|
return platformVersion;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
setVersion: function(v) {
|
|
if (typeof v != 'undefined' && v !== null) {
|
|
v = v.split('.');
|
|
v = parseFloat(v[0] + '.' + (v.length > 1 ? v[1] : 0));
|
|
if (!isNaN(v)) {
|
|
platformVersion = v;
|
|
return;
|
|
}
|
|
}
|
|
|
|
platformVersion = 0;
|
|
|
|
// fallback to user-agent checking
|
|
var pName = self.platform();
|
|
var versionMatch = {
|
|
'android': /Android (\d+).(\d+)?/,
|
|
'ios': /OS (\d+)_(\d+)?/,
|
|
'windowsphone': /Windows Phone (\d+).(\d+)?/
|
|
};
|
|
if (versionMatch[pName]) {
|
|
v = self.ua.match(versionMatch[pName]);
|
|
if (v && v.length > 2) {
|
|
platformVersion = parseFloat(v[1] + '.' + v[2]);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#is
|
|
* @param {string} Platform name.
|
|
* @returns {boolean} Whether the platform name provided is detected.
|
|
*/
|
|
is: function(type) {
|
|
type = type.toLowerCase();
|
|
// check if it has an array of platforms
|
|
if (self.platforms) {
|
|
for (var x = 0; x < self.platforms.length; x++) {
|
|
if (self.platforms[x] === type) return true;
|
|
}
|
|
}
|
|
// exact match
|
|
var pName = self.platform();
|
|
if (pName) {
|
|
return pName === type.toLowerCase();
|
|
}
|
|
|
|
// A quick hack for to check userAgent
|
|
return self.ua.toLowerCase().indexOf(type) >= 0;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#exitApp
|
|
* @description Exit the app.
|
|
*/
|
|
exitApp: function() {
|
|
self.ready(function() {
|
|
navigator.app && navigator.app.exitApp && navigator.app.exitApp();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#showStatusBar
|
|
* @description Shows or hides the device status bar (in Cordova). Requires `cordova plugin add org.apache.cordova.statusbar`
|
|
* @param {boolean} shouldShow Whether or not to show the status bar.
|
|
*/
|
|
showStatusBar: function(val) {
|
|
// Only useful when run within cordova
|
|
self._showStatusBar = val;
|
|
self.ready(function() {
|
|
// run this only when or if the platform (cordova) is ready
|
|
requestAnimationFrame(function() {
|
|
if (self._showStatusBar) {
|
|
// they do not want it to be full screen
|
|
window.StatusBar && window.StatusBar.show();
|
|
document.body.classList.remove('status-bar-hide');
|
|
} else {
|
|
// it should be full screen
|
|
window.StatusBar && window.StatusBar.hide();
|
|
document.body.classList.add('status-bar-hide');
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ionic.Platform#fullScreen
|
|
* @description
|
|
* Sets whether the app is fullscreen or not (in Cordova).
|
|
* @param {boolean=} showFullScreen Whether or not to set the app to fullscreen. Defaults to true. Requires `cordova plugin add org.apache.cordova.statusbar`
|
|
* @param {boolean=} showStatusBar Whether or not to show the device's status bar. Defaults to false.
|
|
*/
|
|
fullScreen: function(showFullScreen, showStatusBar) {
|
|
// showFullScreen: default is true if no param provided
|
|
self.isFullScreen = (showFullScreen !== false);
|
|
|
|
// add/remove the fullscreen classname to the body
|
|
ionic.DomUtil.ready(function() {
|
|
// run this only when or if the DOM is ready
|
|
requestAnimationFrame(function() {
|
|
if (self.isFullScreen) {
|
|
document.body.classList.add('fullscreen');
|
|
} else {
|
|
document.body.classList.remove('fullscreen');
|
|
}
|
|
});
|
|
// showStatusBar: default is false if no param provided
|
|
self.showStatusBar((showStatusBar === true));
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
var platformName = null, // just the name, like iOS or Android
|
|
platformVersion = null, // a float of the major and minor, like 7.1
|
|
readyCallbacks = [],
|
|
windowLoadListenderAttached,
|
|
platformReadyTimer = 2000; // How long to wait for platform ready before emitting a warning
|
|
|
|
verifyPlatformReady();
|
|
|
|
// Warn the user if deviceready did not fire in a reasonable amount of time, and how to fix it.
|
|
function verifyPlatformReady() {
|
|
setTimeout(function() {
|
|
if(!self.isReady && self.isWebView()) {
|
|
void 0;
|
|
}
|
|
}, platformReadyTimer);
|
|
}
|
|
|
|
// setup listeners to know when the device is ready to go
|
|
function onWindowLoad() {
|
|
if (self.isWebView()) {
|
|
// the window and scripts are fully loaded, and a cordova/phonegap
|
|
// object exists then let's listen for the deviceready
|
|
document.addEventListener("deviceready", onPlatformReady, false);
|
|
} else {
|
|
// the window and scripts are fully loaded, but the window object doesn't have the
|
|
// cordova/phonegap object, so its just a browser, not a webview wrapped w/ cordova
|
|
onPlatformReady();
|
|
}
|
|
if (windowLoadListenderAttached) {
|
|
window.removeEventListener("load", onWindowLoad, false);
|
|
}
|
|
}
|
|
if (document.readyState === 'complete') {
|
|
onWindowLoad();
|
|
} else {
|
|
windowLoadListenderAttached = true;
|
|
window.addEventListener("load", onWindowLoad, false);
|
|
}
|
|
|
|
function onPlatformReady() {
|
|
// the device is all set to go, init our own stuff then fire off our event
|
|
self.isReady = true;
|
|
self.detect();
|
|
for (var x = 0; x < readyCallbacks.length; x++) {
|
|
// fire off all the callbacks that were added before the platform was ready
|
|
readyCallbacks[x]();
|
|
}
|
|
readyCallbacks = [];
|
|
ionic.trigger('platformready', { target: document });
|
|
|
|
requestAnimationFrame(function() {
|
|
document.body.classList.add('platform-ready');
|
|
});
|
|
}
|
|
|
|
})(window, document, ionic);
|
|
|
|
(function(document, ionic) {
|
|
'use strict';
|
|
|
|
// Ionic CSS polyfills
|
|
ionic.CSS = {};
|
|
ionic.CSS.TRANSITION = [];
|
|
ionic.CSS.TRANSFORM = [];
|
|
|
|
ionic.EVENTS = {};
|
|
|
|
(function() {
|
|
|
|
// transform
|
|
var i, keys = ['webkitTransform', 'transform', '-webkit-transform', 'webkit-transform',
|
|
'-moz-transform', 'moz-transform', 'MozTransform', 'mozTransform', 'msTransform'];
|
|
|
|
for (i = 0; i < keys.length; i++) {
|
|
if (document.documentElement.style[keys[i]] !== undefined) {
|
|
ionic.CSS.TRANSFORM = keys[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// transition
|
|
keys = ['webkitTransition', 'mozTransition', 'msTransition', 'transition'];
|
|
for (i = 0; i < keys.length; i++) {
|
|
if (document.documentElement.style[keys[i]] !== undefined) {
|
|
ionic.CSS.TRANSITION = keys[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Fallback in case the keys don't exist at all
|
|
ionic.CSS.TRANSITION = ionic.CSS.TRANSITION || 'transition';
|
|
|
|
// The only prefix we care about is webkit for transitions.
|
|
var isWebkit = ionic.CSS.TRANSITION.indexOf('webkit') > -1;
|
|
|
|
// transition duration
|
|
ionic.CSS.TRANSITION_DURATION = (isWebkit ? '-webkit-' : '') + 'transition-duration';
|
|
|
|
// To be sure transitionend works everywhere, include *both* the webkit and non-webkit events
|
|
ionic.CSS.TRANSITIONEND = (isWebkit ? 'webkitTransitionEnd ' : '') + 'transitionend';
|
|
})();
|
|
|
|
(function() {
|
|
var touchStartEvent = 'touchstart';
|
|
var touchMoveEvent = 'touchmove';
|
|
var touchEndEvent = 'touchend';
|
|
var touchCancelEvent = 'touchcancel';
|
|
|
|
if (window.navigator.pointerEnabled) {
|
|
touchStartEvent = 'pointerdown';
|
|
touchMoveEvent = 'pointermove';
|
|
touchEndEvent = 'pointerup';
|
|
touchCancelEvent = 'pointercancel';
|
|
} else if (window.navigator.msPointerEnabled) {
|
|
touchStartEvent = 'MSPointerDown';
|
|
touchMoveEvent = 'MSPointerMove';
|
|
touchEndEvent = 'MSPointerUp';
|
|
touchCancelEvent = 'MSPointerCancel';
|
|
}
|
|
|
|
ionic.EVENTS.touchstart = touchStartEvent;
|
|
ionic.EVENTS.touchmove = touchMoveEvent;
|
|
ionic.EVENTS.touchend = touchEndEvent;
|
|
ionic.EVENTS.touchcancel = touchCancelEvent;
|
|
})();
|
|
|
|
// classList polyfill for them older Androids
|
|
// https://gist.github.com/devongovett/1381839
|
|
if (!("classList" in document.documentElement) && Object.defineProperty && typeof HTMLElement !== 'undefined') {
|
|
Object.defineProperty(HTMLElement.prototype, 'classList', {
|
|
get: function() {
|
|
var self = this;
|
|
function update(fn) {
|
|
return function() {
|
|
var x, classes = self.className.split(/\s+/);
|
|
|
|
for (x = 0; x < arguments.length; x++) {
|
|
fn(classes, classes.indexOf(arguments[x]), arguments[x]);
|
|
}
|
|
|
|
self.className = classes.join(" ");
|
|
};
|
|
}
|
|
|
|
return {
|
|
add: update(function(classes, index, value) {
|
|
~index || classes.push(value);
|
|
}),
|
|
|
|
remove: update(function(classes, index) {
|
|
~index && classes.splice(index, 1);
|
|
}),
|
|
|
|
toggle: update(function(classes, index, value) {
|
|
~index ? classes.splice(index, 1) : classes.push(value);
|
|
}),
|
|
|
|
contains: function(value) {
|
|
return !!~self.className.split(/\s+/).indexOf(value);
|
|
},
|
|
|
|
item: function(i) {
|
|
return self.className.split(/\s+/)[i] || null;
|
|
}
|
|
};
|
|
|
|
}
|
|
});
|
|
}
|
|
|
|
})(document, ionic);
|
|
|
|
|
|
/**
|
|
* @ngdoc page
|
|
* @name tap
|
|
* @module ionic
|
|
* @description
|
|
* On touch devices such as a phone or tablet, some browsers implement a 300ms delay between
|
|
* the time the user stops touching the display and the moment the browser executes the
|
|
* click. This delay was initially introduced so the browser can know whether the user wants to
|
|
* double-tap to zoom in on the webpage. Basically, the browser waits roughly 300ms to see if
|
|
* the user is double-tapping, or just tapping on the display once.
|
|
*
|
|
* Out of the box, Ionic automatically removes the 300ms delay in order to make Ionic apps
|
|
* feel more "native" like. Resultingly, other solutions such as
|
|
* [fastclick](https://github.com/ftlabs/fastclick) and Angular's
|
|
* [ngTouch](https://docs.angularjs.org/api/ngTouch) should not be included, to avoid conflicts.
|
|
*
|
|
* Some browsers already remove the delay with certain settings, such as the CSS property
|
|
* `touch-events: none` or with specific meta tag viewport values. However, each of these
|
|
* browsers still handle clicks differently, such as when to fire off or cancel the event
|
|
* (like scrolling when the target is a button, or holding a button down).
|
|
* For browsers that already remove the 300ms delay, consider Ionic's tap system as a way to
|
|
* normalize how clicks are handled across the various devices so there's an expected response
|
|
* no matter what the device, platform or version. Additionally, Ionic will prevent
|
|
* ghostclicks which even browsers that remove the delay still experience.
|
|
*
|
|
* In some cases, third-party libraries may also be working with touch events which can interfere
|
|
* with the tap system. For example, mapping libraries like Google or Leaflet Maps often implement
|
|
* a touch detection system which conflicts with Ionic's tap system.
|
|
*
|
|
* ### Disabling the tap system
|
|
*
|
|
* To disable the tap for an element and all of its children elements,
|
|
* add the attribute `data-tap-disabled="true"`.
|
|
*
|
|
* ```html
|
|
* <div data-tap-disabled="true">
|
|
* <div id="google-map"></div>
|
|
* </div>
|
|
* ```
|
|
*
|
|
* ### Additional Notes:
|
|
*
|
|
* - Ionic tap works with Ionic's JavaScript scrolling
|
|
* - Elements can come and go from the DOM and Ionic tap doesn't keep adding and removing
|
|
* listeners
|
|
* - No "tap delay" after the first "tap" (you can tap as fast as you want, they all click)
|
|
* - Minimal events listeners, only being added to document
|
|
* - Correct focus in/out on each input type (select, textearea, range) on each platform/device
|
|
* - Shows and hides virtual keyboard correctly for each platform/device
|
|
* - Works with labels surrounding inputs
|
|
* - Does not fire off a click if the user moves the pointer too far
|
|
* - Adds and removes an 'activated' css class
|
|
* - Multiple [unit tests](https://github.com/driftyco/ionic/blob/master/test/unit/utils/tap.unit.js) for each scenario
|
|
*
|
|
*/
|
|
/*
|
|
|
|
IONIC TAP
|
|
---------------
|
|
- Both touch and mouse events are added to the document.body on DOM ready
|
|
- If a touch event happens, it does not use mouse event listeners
|
|
- On touchend, if the distance between start and end was small, trigger a click
|
|
- In the triggered click event, add a 'isIonicTap' property
|
|
- The triggered click receives the same x,y coordinates as as the end event
|
|
- On document.body click listener (with useCapture=true), only allow clicks with 'isIonicTap'
|
|
- Triggering clicks with mouse events work the same as touch, except with mousedown/mouseup
|
|
- Tapping inputs is disabled during scrolling
|
|
*/
|
|
|
|
var tapDoc; // the element which the listeners are on (document.body)
|
|
var tapActiveEle; // the element which is active (probably has focus)
|
|
var tapEnabledTouchEvents;
|
|
var tapMouseResetTimer;
|
|
var tapPointerMoved;
|
|
var tapPointerStart;
|
|
var tapTouchFocusedInput;
|
|
var tapLastTouchTarget;
|
|
var tapTouchMoveListener = 'touchmove';
|
|
|
|
// how much the coordinates can be off between start/end, but still a click
|
|
var TAP_RELEASE_TOLERANCE = 12; // default tolerance
|
|
var TAP_RELEASE_BUTTON_TOLERANCE = 50; // button elements should have a larger tolerance
|
|
|
|
var tapEventListeners = {
|
|
'click': tapClickGateKeeper,
|
|
|
|
'mousedown': tapMouseDown,
|
|
'mouseup': tapMouseUp,
|
|
'mousemove': tapMouseMove,
|
|
|
|
'touchstart': tapTouchStart,
|
|
'touchend': tapTouchEnd,
|
|
'touchcancel': tapTouchCancel,
|
|
'touchmove': tapTouchMove,
|
|
|
|
'pointerdown': tapTouchStart,
|
|
'pointerup': tapTouchEnd,
|
|
'pointercancel': tapTouchCancel,
|
|
'pointermove': tapTouchMove,
|
|
|
|
'MSPointerDown': tapTouchStart,
|
|
'MSPointerUp': tapTouchEnd,
|
|
'MSPointerCancel': tapTouchCancel,
|
|
'MSPointerMove': tapTouchMove,
|
|
|
|
'focusin': tapFocusIn,
|
|
'focusout': tapFocusOut
|
|
};
|
|
|
|
ionic.tap = {
|
|
|
|
register: function(ele) {
|
|
tapDoc = ele;
|
|
|
|
tapEventListener('click', true, true);
|
|
tapEventListener('mouseup');
|
|
tapEventListener('mousedown');
|
|
|
|
if (window.navigator.pointerEnabled) {
|
|
tapEventListener('pointerdown');
|
|
tapEventListener('pointerup');
|
|
tapEventListener('pointercancel');
|
|
tapTouchMoveListener = 'pointermove';
|
|
|
|
} else if (window.navigator.msPointerEnabled) {
|
|
tapEventListener('MSPointerDown');
|
|
tapEventListener('MSPointerUp');
|
|
tapEventListener('MSPointerCancel');
|
|
tapTouchMoveListener = 'MSPointerMove';
|
|
|
|
} else {
|
|
tapEventListener('touchstart');
|
|
tapEventListener('touchend');
|
|
tapEventListener('touchcancel');
|
|
}
|
|
|
|
tapEventListener('focusin');
|
|
tapEventListener('focusout');
|
|
|
|
return function() {
|
|
for (var type in tapEventListeners) {
|
|
tapEventListener(type, false);
|
|
}
|
|
tapDoc = null;
|
|
tapActiveEle = null;
|
|
tapEnabledTouchEvents = false;
|
|
tapPointerMoved = false;
|
|
tapPointerStart = null;
|
|
};
|
|
},
|
|
|
|
ignoreScrollStart: function(e) {
|
|
return (e.defaultPrevented) || // defaultPrevented has been assigned by another component handling the event
|
|
(/^(file|range)$/i).test(e.target.type) ||
|
|
(e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-scroll')) == 'true' || // manually set within an elements attributes
|
|
(!!(/^(object|embed)$/i).test(e.target.tagName)) || // flash/movie/object touches should not try to scroll
|
|
ionic.tap.isElementTapDisabled(e.target); // check if this element, or an ancestor, has `data-tap-disabled` attribute
|
|
},
|
|
|
|
isTextInput: function(ele) {
|
|
return !!ele &&
|
|
(ele.tagName == 'TEXTAREA' ||
|
|
ele.contentEditable === 'true' ||
|
|
(ele.tagName == 'INPUT' && !(/^(radio|checkbox|range|file|submit|reset|color|image|button)$/i).test(ele.type)));
|
|
},
|
|
|
|
isDateInput: function(ele) {
|
|
return !!ele &&
|
|
(ele.tagName == 'INPUT' && (/^(date|time|datetime-local|month|week)$/i).test(ele.type));
|
|
},
|
|
|
|
isVideo: function(ele) {
|
|
return !!ele &&
|
|
(ele.tagName == 'VIDEO');
|
|
},
|
|
|
|
isKeyboardElement: function(ele) {
|
|
if ( !ionic.Platform.isIOS() || ionic.Platform.isIPad() ) {
|
|
return ionic.tap.isTextInput(ele) && !ionic.tap.isDateInput(ele);
|
|
} else {
|
|
return ionic.tap.isTextInput(ele) || ( !!ele && ele.tagName == "SELECT");
|
|
}
|
|
},
|
|
|
|
isLabelWithTextInput: function(ele) {
|
|
var container = tapContainingElement(ele, false);
|
|
|
|
return !!container &&
|
|
ionic.tap.isTextInput(tapTargetElement(container));
|
|
},
|
|
|
|
containsOrIsTextInput: function(ele) {
|
|
return ionic.tap.isTextInput(ele) || ionic.tap.isLabelWithTextInput(ele);
|
|
},
|
|
|
|
cloneFocusedInput: function(container) {
|
|
if (ionic.tap.hasCheckedClone) return;
|
|
ionic.tap.hasCheckedClone = true;
|
|
|
|
ionic.requestAnimationFrame(function() {
|
|
var focusInput = container.querySelector(':focus');
|
|
if (ionic.tap.isTextInput(focusInput) && !ionic.tap.isDateInput(focusInput)) {
|
|
var clonedInput = focusInput.cloneNode(true);
|
|
|
|
clonedInput.value = focusInput.value;
|
|
clonedInput.classList.add('cloned-text-input');
|
|
clonedInput.readOnly = true;
|
|
if (focusInput.isContentEditable) {
|
|
clonedInput.contentEditable = focusInput.contentEditable;
|
|
clonedInput.innerHTML = focusInput.innerHTML;
|
|
}
|
|
focusInput.parentElement.insertBefore(clonedInput, focusInput);
|
|
focusInput.classList.add('previous-input-focus');
|
|
|
|
clonedInput.scrollTop = focusInput.scrollTop;
|
|
}
|
|
});
|
|
},
|
|
|
|
hasCheckedClone: false,
|
|
|
|
removeClonedInputs: function(container) {
|
|
ionic.tap.hasCheckedClone = false;
|
|
|
|
ionic.requestAnimationFrame(function() {
|
|
var clonedInputs = container.querySelectorAll('.cloned-text-input');
|
|
var previousInputFocus = container.querySelectorAll('.previous-input-focus');
|
|
var x;
|
|
|
|
for (x = 0; x < clonedInputs.length; x++) {
|
|
clonedInputs[x].parentElement.removeChild(clonedInputs[x]);
|
|
}
|
|
|
|
for (x = 0; x < previousInputFocus.length; x++) {
|
|
previousInputFocus[x].classList.remove('previous-input-focus');
|
|
previousInputFocus[x].style.top = '';
|
|
if ( ionic.keyboard.isOpen && !ionic.keyboard.isClosing ) previousInputFocus[x].focus();
|
|
}
|
|
});
|
|
},
|
|
|
|
requiresNativeClick: function(ele) {
|
|
if (ionic.Platform.isWindowsPhone() && (ele.tagName == 'A' || ele.tagName == 'BUTTON' || ele.hasAttribute('ng-click') || (ele.tagName == 'INPUT' && (ele.type == 'button' || ele.type == 'submit')))) {
|
|
return true; //Windows Phone edge case, prevent ng-click (and similar) events from firing twice on this platform
|
|
}
|
|
if (!ele || ele.disabled || (/^(file|range)$/i).test(ele.type) || (/^(object|video)$/i).test(ele.tagName) || ionic.tap.isLabelContainingFileInput(ele)) {
|
|
return true;
|
|
}
|
|
return ionic.tap.isElementTapDisabled(ele);
|
|
},
|
|
|
|
isLabelContainingFileInput: function(ele) {
|
|
var lbl = tapContainingElement(ele);
|
|
if (lbl.tagName !== 'LABEL') return false;
|
|
var fileInput = lbl.querySelector('input[type=file]');
|
|
if (fileInput && fileInput.disabled === false) return true;
|
|
return false;
|
|
},
|
|
|
|
isElementTapDisabled: function(ele) {
|
|
if (ele && ele.nodeType === 1) {
|
|
var element = ele;
|
|
while (element) {
|
|
if (element.getAttribute && element.getAttribute('data-tap-disabled') == 'true') {
|
|
return true;
|
|
}
|
|
element = element.parentElement;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
setTolerance: function(releaseTolerance, releaseButtonTolerance) {
|
|
TAP_RELEASE_TOLERANCE = releaseTolerance;
|
|
TAP_RELEASE_BUTTON_TOLERANCE = releaseButtonTolerance;
|
|
},
|
|
|
|
cancelClick: function() {
|
|
// used to cancel any simulated clicks which may happen on a touchend/mouseup
|
|
// gestures uses this method within its tap and hold events
|
|
tapPointerMoved = true;
|
|
},
|
|
|
|
pointerCoord: function(event) {
|
|
// This method can get coordinates for both a mouse click
|
|
// or a touch depending on the given event
|
|
var c = { x: 0, y: 0 };
|
|
if (event) {
|
|
var touches = event.touches && event.touches.length ? event.touches : [event];
|
|
var e = (event.changedTouches && event.changedTouches[0]) || touches[0];
|
|
if (e) {
|
|
c.x = e.clientX || e.pageX || 0;
|
|
c.y = e.clientY || e.pageY || 0;
|
|
}
|
|
}
|
|
return c;
|
|
}
|
|
|
|
};
|
|
|
|
function tapEventListener(type, enable, useCapture) {
|
|
if (enable !== false) {
|
|
tapDoc.addEventListener(type, tapEventListeners[type], useCapture);
|
|
} else {
|
|
tapDoc.removeEventListener(type, tapEventListeners[type]);
|
|
}
|
|
}
|
|
|
|
function tapClick(e) {
|
|
// simulate a normal click by running the element's click method then focus on it
|
|
var container = tapContainingElement(e.target);
|
|
var ele = tapTargetElement(container);
|
|
|
|
if (ionic.tap.requiresNativeClick(ele) || tapPointerMoved) return false;
|
|
|
|
var c = ionic.tap.pointerCoord(e);
|
|
|
|
//console.log('tapClick', e.type, ele.tagName, '('+c.x+','+c.y+')');
|
|
triggerMouseEvent('click', ele, c.x, c.y);
|
|
|
|
// if it's an input, focus in on the target, otherwise blur
|
|
tapHandleFocus(ele);
|
|
}
|
|
|
|
function triggerMouseEvent(type, ele, x, y) {
|
|
// using initMouseEvent instead of MouseEvent for our Android friends
|
|
var clickEvent = document.createEvent("MouseEvents");
|
|
clickEvent.initMouseEvent(type, true, true, window, 1, 0, 0, x, y, false, false, false, false, 0, null);
|
|
clickEvent.isIonicTap = true;
|
|
ele.dispatchEvent(clickEvent);
|
|
}
|
|
|
|
function tapClickGateKeeper(e) {
|
|
//console.log('click ' + Date.now() + ' isIonicTap: ' + (e.isIonicTap ? true : false));
|
|
if (e.target.type == 'submit' && e.detail === 0) {
|
|
// do not prevent click if it came from an "Enter" or "Go" keypress submit
|
|
return null;
|
|
}
|
|
|
|
// do not allow through any click events that were not created by ionic.tap
|
|
if ((ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) ||
|
|
(!e.isIonicTap && !ionic.tap.requiresNativeClick(e.target))) {
|
|
//console.log('clickPrevent', e.target.tagName);
|
|
e.stopPropagation();
|
|
|
|
if (!ionic.tap.isLabelWithTextInput(e.target)) {
|
|
// labels clicks from native should not preventDefault othersize keyboard will not show on input focus
|
|
e.preventDefault();
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// MOUSE
|
|
function tapMouseDown(e) {
|
|
//console.log('mousedown ' + Date.now());
|
|
if (e.isIonicTap || tapIgnoreEvent(e)) return null;
|
|
|
|
if (tapEnabledTouchEvents) {
|
|
//console.log('mousedown', 'stop event');
|
|
e.stopPropagation();
|
|
|
|
if (!ionic.Platform.isEdge() && (!ionic.tap.isTextInput(e.target) || tapLastTouchTarget !== e.target) &&
|
|
!isSelectOrOption(e.target.tagName) && !ionic.tap.isVideo(e.target)) {
|
|
// If you preventDefault on a text input then you cannot move its text caret/cursor.
|
|
// Allow through only the text input default. However, without preventDefault on an
|
|
// input the 300ms delay can change focus on inputs after the keyboard shows up.
|
|
// The focusin event handles the chance of focus changing after the keyboard shows.
|
|
// Windows Phone - if you preventDefault on a video element then you cannot operate
|
|
// its native controls.
|
|
e.preventDefault();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
tapPointerMoved = false;
|
|
tapPointerStart = ionic.tap.pointerCoord(e);
|
|
|
|
tapEventListener('mousemove');
|
|
ionic.activator.start(e);
|
|
}
|
|
|
|
function tapMouseUp(e) {
|
|
//console.log("mouseup " + Date.now());
|
|
if (tapEnabledTouchEvents) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
if (tapIgnoreEvent(e) || isSelectOrOption(e.target.tagName)) return false;
|
|
|
|
if (!tapHasPointerMoved(e)) {
|
|
tapClick(e);
|
|
}
|
|
tapEventListener('mousemove', false);
|
|
ionic.activator.end();
|
|
tapPointerMoved = false;
|
|
}
|
|
|
|
function tapMouseMove(e) {
|
|
if (tapHasPointerMoved(e)) {
|
|
tapEventListener('mousemove', false);
|
|
ionic.activator.end();
|
|
tapPointerMoved = true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
// TOUCH
|
|
function tapTouchStart(e) {
|
|
//console.log("touchstart " + Date.now());
|
|
if (tapIgnoreEvent(e)) return;
|
|
|
|
tapPointerMoved = false;
|
|
|
|
tapEnableTouchEvents();
|
|
tapPointerStart = ionic.tap.pointerCoord(e);
|
|
|
|
tapEventListener(tapTouchMoveListener);
|
|
ionic.activator.start(e);
|
|
|
|
if (ionic.Platform.isIOS() && ionic.tap.isLabelWithTextInput(e.target)) {
|
|
// if the tapped element is a label, which has a child input
|
|
// then preventDefault so iOS doesn't ugly auto scroll to the input
|
|
// but do not prevent default on Android or else you cannot move the text caret
|
|
// and do not prevent default on Android or else no virtual keyboard shows up
|
|
|
|
var textInput = tapTargetElement(tapContainingElement(e.target));
|
|
if (textInput !== tapActiveEle) {
|
|
// don't preventDefault on an already focused input or else iOS's text caret isn't usable
|
|
//console.log('Would prevent default here');
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
|
|
function tapTouchEnd(e) {
|
|
//console.log('touchend ' + Date.now());
|
|
if (tapIgnoreEvent(e)) return;
|
|
|
|
tapEnableTouchEvents();
|
|
if (!tapHasPointerMoved(e)) {
|
|
tapClick(e);
|
|
|
|
if (isSelectOrOption(e.target.tagName)) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
tapLastTouchTarget = e.target;
|
|
tapTouchCancel();
|
|
}
|
|
|
|
function tapTouchMove(e) {
|
|
if (tapHasPointerMoved(e)) {
|
|
tapPointerMoved = true;
|
|
tapEventListener(tapTouchMoveListener, false);
|
|
ionic.activator.end();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function tapTouchCancel() {
|
|
tapEventListener(tapTouchMoveListener, false);
|
|
ionic.activator.end();
|
|
tapPointerMoved = false;
|
|
}
|
|
|
|
function tapEnableTouchEvents() {
|
|
tapEnabledTouchEvents = true;
|
|
clearTimeout(tapMouseResetTimer);
|
|
tapMouseResetTimer = setTimeout(function() {
|
|
tapEnabledTouchEvents = false;
|
|
}, 600);
|
|
}
|
|
|
|
function tapIgnoreEvent(e) {
|
|
if (e.isTapHandled) return true;
|
|
e.isTapHandled = true;
|
|
|
|
if(ionic.tap.isElementTapDisabled(e.target)) {
|
|
return true;
|
|
}
|
|
|
|
if (ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) {
|
|
e.preventDefault();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function tapHandleFocus(ele) {
|
|
tapTouchFocusedInput = null;
|
|
|
|
var triggerFocusIn = false;
|
|
|
|
if (ele.tagName == 'SELECT') {
|
|
// trick to force Android options to show up
|
|
triggerMouseEvent('mousedown', ele, 0, 0);
|
|
ele.focus && ele.focus();
|
|
triggerFocusIn = true;
|
|
|
|
} else if (tapActiveElement() === ele) {
|
|
// already is the active element and has focus
|
|
triggerFocusIn = true;
|
|
|
|
} else if ((/^(input|textarea|ion-label)$/i).test(ele.tagName) || ele.isContentEditable) {
|
|
triggerFocusIn = true;
|
|
ele.focus && ele.focus();
|
|
ele.value = ele.value;
|
|
if (tapEnabledTouchEvents) {
|
|
tapTouchFocusedInput = ele;
|
|
}
|
|
|
|
} else {
|
|
tapFocusOutActive();
|
|
}
|
|
|
|
if (triggerFocusIn) {
|
|
tapActiveElement(ele);
|
|
ionic.trigger('ionic.focusin', {
|
|
target: ele
|
|
}, true);
|
|
}
|
|
}
|
|
|
|
function tapFocusOutActive() {
|
|
var ele = tapActiveElement();
|
|
if (ele && ((/^(input|textarea|select)$/i).test(ele.tagName) || ele.isContentEditable)) {
|
|
//console.log('tapFocusOutActive', ele.tagName);
|
|
ele.blur();
|
|
}
|
|
tapActiveElement(null);
|
|
}
|
|
|
|
function tapFocusIn(e) {
|
|
//console.log('focusin ' + Date.now());
|
|
// Because a text input doesn't preventDefault (so the caret still works) there's a chance
|
|
// that its mousedown event 300ms later will change the focus to another element after
|
|
// the keyboard shows up.
|
|
|
|
if (tapEnabledTouchEvents &&
|
|
ionic.tap.isTextInput(tapActiveElement()) &&
|
|
ionic.tap.isTextInput(tapTouchFocusedInput) &&
|
|
tapTouchFocusedInput !== e.target) {
|
|
|
|
// 1) The pointer is from touch events
|
|
// 2) There is an active element which is a text input
|
|
// 3) A text input was just set to be focused on by a touch event
|
|
// 4) A new focus has been set, however the target isn't the one the touch event wanted
|
|
//console.log('focusin', 'tapTouchFocusedInput');
|
|
tapTouchFocusedInput.focus();
|
|
tapTouchFocusedInput = null;
|
|
}
|
|
ionic.scroll.isScrolling = false;
|
|
}
|
|
|
|
function tapFocusOut() {
|
|
//console.log("focusout");
|
|
tapActiveElement(null);
|
|
}
|
|
|
|
function tapActiveElement(ele) {
|
|
if (arguments.length) {
|
|
tapActiveEle = ele;
|
|
}
|
|
return tapActiveEle || document.activeElement;
|
|
}
|
|
|
|
function tapHasPointerMoved(endEvent) {
|
|
if (!endEvent || endEvent.target.nodeType !== 1 || !tapPointerStart || (tapPointerStart.x === 0 && tapPointerStart.y === 0)) {
|
|
return false;
|
|
}
|
|
var endCoordinates = ionic.tap.pointerCoord(endEvent);
|
|
|
|
var hasClassList = !!(endEvent.target.classList && endEvent.target.classList.contains &&
|
|
typeof endEvent.target.classList.contains === 'function');
|
|
var releaseTolerance = hasClassList && endEvent.target.classList.contains('button') ?
|
|
TAP_RELEASE_BUTTON_TOLERANCE :
|
|
TAP_RELEASE_TOLERANCE;
|
|
|
|
return Math.abs(tapPointerStart.x - endCoordinates.x) > releaseTolerance ||
|
|
Math.abs(tapPointerStart.y - endCoordinates.y) > releaseTolerance;
|
|
}
|
|
|
|
function tapContainingElement(ele, allowSelf) {
|
|
var climbEle = ele;
|
|
for (var x = 0; x < 6; x++) {
|
|
if (!climbEle) break;
|
|
if (climbEle.tagName === 'LABEL') return climbEle;
|
|
climbEle = climbEle.parentElement;
|
|
}
|
|
if (allowSelf !== false) return ele;
|
|
}
|
|
|
|
function tapTargetElement(ele) {
|
|
if (ele && ele.tagName === 'LABEL') {
|
|
if (ele.control) return ele.control;
|
|
|
|
// older devices do not support the "control" property
|
|
if (ele.querySelector) {
|
|
var control = ele.querySelector('input,textarea,select');
|
|
if (control) return control;
|
|
}
|
|
}
|
|
return ele;
|
|
}
|
|
|
|
function isSelectOrOption(tagName){
|
|
return (/^(select|option)$/i).test(tagName);
|
|
}
|
|
|
|
ionic.DomUtil.ready(function() {
|
|
var ng = typeof angular !== 'undefined' ? angular : null;
|
|
//do nothing for e2e tests
|
|
if (!ng || (ng && !ng.scenario)) {
|
|
ionic.tap.register(document);
|
|
}
|
|
});
|
|
|
|
(function(document, ionic) {
|
|
'use strict';
|
|
|
|
var queueElements = {}; // elements that should get an active state in XX milliseconds
|
|
var activeElements = {}; // elements that are currently active
|
|
var keyId = 0; // a counter for unique keys for the above ojects
|
|
var ACTIVATED_CLASS = 'activated';
|
|
|
|
ionic.activator = {
|
|
|
|
start: function(e) {
|
|
var hitX = ionic.tap.pointerCoord(e).x;
|
|
if (hitX > 0 && hitX < 30) {
|
|
return;
|
|
}
|
|
|
|
// when an element is touched/clicked, it climbs up a few
|
|
// parents to see if it is an .item or .button element
|
|
ionic.requestAnimationFrame(function() {
|
|
if ((ionic.scroll && ionic.scroll.isScrolling) || ionic.tap.requiresNativeClick(e.target)) return;
|
|
var ele = e.target;
|
|
var eleToActivate;
|
|
|
|
for (var x = 0; x < 6; x++) {
|
|
if (!ele || ele.nodeType !== 1) break;
|
|
if (eleToActivate && ele.classList && ele.classList.contains('item')) {
|
|
eleToActivate = ele;
|
|
break;
|
|
}
|
|
if (ele.tagName == 'A' || ele.tagName == 'BUTTON' || ele.hasAttribute('ng-click')) {
|
|
eleToActivate = ele;
|
|
break;
|
|
}
|
|
if (ele.classList && ele.classList.contains('button')) {
|
|
eleToActivate = ele;
|
|
break;
|
|
}
|
|
// no sense climbing past these
|
|
if (ele.tagName == 'ION-CONTENT' || (ele.classList && ele.classList.contains('pane')) || ele.tagName == 'BODY') {
|
|
break;
|
|
}
|
|
ele = ele.parentElement;
|
|
}
|
|
|
|
if (eleToActivate) {
|
|
// queue that this element should be set to active
|
|
queueElements[keyId] = eleToActivate;
|
|
|
|
// on the next frame, set the queued elements to active
|
|
ionic.requestAnimationFrame(activateElements);
|
|
|
|
keyId = (keyId > 29 ? 0 : keyId + 1);
|
|
}
|
|
|
|
});
|
|
},
|
|
|
|
end: function() {
|
|
// clear out any active/queued elements after XX milliseconds
|
|
setTimeout(clear, 200);
|
|
}
|
|
|
|
};
|
|
|
|
function clear() {
|
|
// clear out any elements that are queued to be set to active
|
|
queueElements = {};
|
|
|
|
// in the next frame, remove the active class from all active elements
|
|
ionic.requestAnimationFrame(deactivateElements);
|
|
}
|
|
|
|
function activateElements() {
|
|
// activate all elements in the queue
|
|
for (var key in queueElements) {
|
|
if (queueElements[key]) {
|
|
queueElements[key].classList.add(ACTIVATED_CLASS);
|
|
activeElements[key] = queueElements[key];
|
|
}
|
|
}
|
|
queueElements = {};
|
|
}
|
|
|
|
function deactivateElements() {
|
|
if (ionic.transition && ionic.transition.isActive) {
|
|
setTimeout(deactivateElements, 400);
|
|
return;
|
|
}
|
|
|
|
for (var key in activeElements) {
|
|
if (activeElements[key]) {
|
|
activeElements[key].classList.remove(ACTIVATED_CLASS);
|
|
delete activeElements[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
})(document, ionic);
|
|
|
|
(function(ionic) {
|
|
/* for nextUid function below */
|
|
var nextId = 0;
|
|
|
|
/**
|
|
* Various utilities used throughout Ionic
|
|
*
|
|
* Some of these are adopted from underscore.js and backbone.js, both also MIT licensed.
|
|
*/
|
|
ionic.Utils = {
|
|
|
|
arrayMove: function(arr, oldIndex, newIndex) {
|
|
if (newIndex >= arr.length) {
|
|
var k = newIndex - arr.length;
|
|
while ((k--) + 1) {
|
|
arr.push(undefined);
|
|
}
|
|
}
|
|
arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0]);
|
|
return arr;
|
|
},
|
|
|
|
/**
|
|
* Return a function that will be called with the given context
|
|
*/
|
|
proxy: function(func, context) {
|
|
var args = Array.prototype.slice.call(arguments, 2);
|
|
return function() {
|
|
return func.apply(context, args.concat(Array.prototype.slice.call(arguments)));
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Only call a function once in the given interval.
|
|
*
|
|
* @param func {Function} the function to call
|
|
* @param wait {int} how long to wait before/after to allow function calls
|
|
* @param immediate {boolean} whether to call immediately or after the wait interval
|
|
*/
|
|
debounce: function(func, wait, immediate) {
|
|
var timeout, args, context, timestamp, result;
|
|
return function() {
|
|
context = this;
|
|
args = arguments;
|
|
timestamp = new Date();
|
|
var later = function() {
|
|
var last = (new Date()) - timestamp;
|
|
if (last < wait) {
|
|
timeout = setTimeout(later, wait - last);
|
|
} else {
|
|
timeout = null;
|
|
if (!immediate) result = func.apply(context, args);
|
|
}
|
|
};
|
|
var callNow = immediate && !timeout;
|
|
if (!timeout) {
|
|
timeout = setTimeout(later, wait);
|
|
}
|
|
if (callNow) result = func.apply(context, args);
|
|
return result;
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Throttle the given fun, only allowing it to be
|
|
* called at most every `wait` ms.
|
|
*/
|
|
throttle: function(func, wait, options) {
|
|
var context, args, result;
|
|
var timeout = null;
|
|
var previous = 0;
|
|
options || (options = {});
|
|
var later = function() {
|
|
previous = options.leading === false ? 0 : Date.now();
|
|
timeout = null;
|
|
result = func.apply(context, args);
|
|
};
|
|
return function() {
|
|
var now = Date.now();
|
|
if (!previous && options.leading === false) previous = now;
|
|
var remaining = wait - (now - previous);
|
|
context = this;
|
|
args = arguments;
|
|
if (remaining <= 0) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
previous = now;
|
|
result = func.apply(context, args);
|
|
} else if (!timeout && options.trailing !== false) {
|
|
timeout = setTimeout(later, remaining);
|
|
}
|
|
return result;
|
|
};
|
|
},
|
|
// Borrowed from Backbone.js's extend
|
|
// Helper function to correctly set up the prototype chain, for subclasses.
|
|
// Similar to `goog.inherits`, but uses a hash of prototype properties and
|
|
// class properties to be extended.
|
|
inherit: function(protoProps, staticProps) {
|
|
var parent = this;
|
|
var child;
|
|
|
|
// The constructor function for the new subclass is either defined by you
|
|
// (the "constructor" property in your `extend` definition), or defaulted
|
|
// by us to simply call the parent's constructor.
|
|
if (protoProps && protoProps.hasOwnProperty('constructor')) {
|
|
child = protoProps.constructor;
|
|
} else {
|
|
child = function() { return parent.apply(this, arguments); };
|
|
}
|
|
|
|
// Add static properties to the constructor function, if supplied.
|
|
ionic.extend(child, parent, staticProps);
|
|
|
|
// Set the prototype chain to inherit from `parent`, without calling
|
|
// `parent`'s constructor function.
|
|
var Surrogate = function() { this.constructor = child; };
|
|
Surrogate.prototype = parent.prototype;
|
|
child.prototype = new Surrogate();
|
|
|
|
// Add prototype properties (instance properties) to the subclass,
|
|
// if supplied.
|
|
if (protoProps) ionic.extend(child.prototype, protoProps);
|
|
|
|
// Set a convenience property in case the parent's prototype is needed
|
|
// later.
|
|
child.__super__ = parent.prototype;
|
|
|
|
return child;
|
|
},
|
|
|
|
// Extend adapted from Underscore.js
|
|
extend: function(obj) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
for (var i = 0; i < args.length; i++) {
|
|
var source = args[i];
|
|
if (source) {
|
|
for (var prop in source) {
|
|
obj[prop] = source[prop];
|
|
}
|
|
}
|
|
}
|
|
return obj;
|
|
},
|
|
|
|
nextUid: function() {
|
|
return 'ion' + (nextId++);
|
|
},
|
|
|
|
disconnectScope: function disconnectScope(scope) {
|
|
if (!scope) return;
|
|
|
|
if (scope.$root === scope) {
|
|
return; // we can't disconnect the root node;
|
|
}
|
|
var parent = scope.$parent;
|
|
scope.$$disconnected = true;
|
|
scope.$broadcast('$ionic.disconnectScope', scope);
|
|
|
|
// See Scope.$destroy
|
|
if (parent.$$childHead === scope) {
|
|
parent.$$childHead = scope.$$nextSibling;
|
|
}
|
|
if (parent.$$childTail === scope) {
|
|
parent.$$childTail = scope.$$prevSibling;
|
|
}
|
|
if (scope.$$prevSibling) {
|
|
scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
|
|
}
|
|
if (scope.$$nextSibling) {
|
|
scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
|
|
}
|
|
scope.$$nextSibling = scope.$$prevSibling = null;
|
|
},
|
|
|
|
reconnectScope: function reconnectScope(scope) {
|
|
if (!scope) return;
|
|
|
|
if (scope.$root === scope) {
|
|
return; // we can't disconnect the root node;
|
|
}
|
|
if (!scope.$$disconnected) {
|
|
return;
|
|
}
|
|
var parent = scope.$parent;
|
|
scope.$$disconnected = false;
|
|
scope.$broadcast('$ionic.reconnectScope', scope);
|
|
// See Scope.$new for this logic...
|
|
scope.$$prevSibling = parent.$$childTail;
|
|
if (parent.$$childHead) {
|
|
parent.$$childTail.$$nextSibling = scope;
|
|
parent.$$childTail = scope;
|
|
} else {
|
|
parent.$$childHead = parent.$$childTail = scope;
|
|
}
|
|
},
|
|
|
|
isScopeDisconnected: function(scope) {
|
|
var climbScope = scope;
|
|
while (climbScope) {
|
|
if (climbScope.$$disconnected) return true;
|
|
climbScope = climbScope.$parent;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Bind a few of the most useful functions to the ionic scope
|
|
ionic.inherit = ionic.Utils.inherit;
|
|
ionic.extend = ionic.Utils.extend;
|
|
ionic.throttle = ionic.Utils.throttle;
|
|
ionic.proxy = ionic.Utils.proxy;
|
|
ionic.debounce = ionic.Utils.debounce;
|
|
|
|
})(window.ionic);
|
|
|
|
/**
|
|
* @ngdoc page
|
|
* @name keyboard
|
|
* @module ionic
|
|
* @description
|
|
* On both Android and iOS, Ionic will attempt to prevent the keyboard from
|
|
* obscuring inputs and focusable elements when it appears by scrolling them
|
|
* into view. In order for this to work, any focusable elements must be within
|
|
* a [Scroll View](http://ionicframework.com/docs/api/directive/ionScroll/)
|
|
* or a directive such as [Content](http://ionicframework.com/docs/api/directive/ionContent/)
|
|
* that has a Scroll View.
|
|
*
|
|
* It will also attempt to prevent the native overflow scrolling on focus,
|
|
* which can cause layout issues such as pushing headers up and out of view.
|
|
*
|
|
* The keyboard fixes work best in conjunction with the
|
|
* [Ionic Keyboard Plugin](https://github.com/driftyco/ionic-plugins-keyboard),
|
|
* although it will perform reasonably well without. However, if you are using
|
|
* Cordova there is no reason not to use the plugin.
|
|
*
|
|
* ### Hide when keyboard shows
|
|
*
|
|
* To hide an element when the keyboard is open, add the class `hide-on-keyboard-open`.
|
|
*
|
|
* ```html
|
|
* <div class="hide-on-keyboard-open">
|
|
* <div id="google-map"></div>
|
|
* </div>
|
|
* ```
|
|
*
|
|
* Note: For performance reasons, elements will not be hidden for 400ms after the start of the `native.keyboardshow` event
|
|
* from the Ionic Keyboard plugin. If you would like them to disappear immediately, you could do something
|
|
* like:
|
|
*
|
|
* ```js
|
|
* window.addEventListener('native.keyboardshow', function(){
|
|
* document.body.classList.add('keyboard-open');
|
|
* });
|
|
* ```
|
|
* This adds the same `keyboard-open` class that is normally added by Ionic 400ms after the keyboard
|
|
* opens. However, bear in mind that adding this class to the body immediately may cause jank in any
|
|
* animations on Android that occur when the keyboard opens (for example, scrolling any obscured inputs into view).
|
|
*
|
|
* ----------
|
|
*
|
|
* ### Plugin Usage
|
|
* Information on using the plugin can be found at
|
|
* [https://github.com/driftyco/ionic-plugins-keyboard](https://github.com/driftyco/ionic-plugins-keyboard).
|
|
*
|
|
* ----------
|
|
*
|
|
* ### Android Notes
|
|
* - If your app is running in fullscreen, i.e. you have
|
|
* `<preference name="Fullscreen" value="true" />` in your `config.xml` file
|
|
* you will need to set `ionic.Platform.isFullScreen = true` manually.
|
|
*
|
|
* - You can configure the behavior of the web view when the keyboard shows by setting
|
|
* [android:windowSoftInputMode](http://developer.android.com/reference/android/R.attr.html#windowSoftInputMode)
|
|
* to either `adjustPan`, `adjustResize` or `adjustNothing` in your app's
|
|
* activity in `AndroidManifest.xml`. `adjustResize` is the recommended setting
|
|
* for Ionic, but if for some reason you do use `adjustPan` you will need to
|
|
* set `ionic.Platform.isFullScreen = true`.
|
|
*
|
|
* ```xml
|
|
* <activity android:windowSoftInputMode="adjustResize">
|
|
*
|
|
* ```
|
|
*
|
|
* ### iOS Notes
|
|
* - If the content of your app (including the header) is being pushed up and
|
|
* out of view on input focus, try setting `cordova.plugins.Keyboard.disableScroll(true)`.
|
|
* This does **not** disable scrolling in the Ionic scroll view, rather it
|
|
* disables the native overflow scrolling that happens automatically as a
|
|
* result of focusing on inputs below the keyboard.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* The current viewport height.
|
|
*/
|
|
var keyboardCurrentViewportHeight = 0;
|
|
|
|
/**
|
|
* The viewport height when in portrait orientation.
|
|
*/
|
|
var keyboardPortraitViewportHeight = 0;
|
|
|
|
/**
|
|
* The viewport height when in landscape orientation.
|
|
*/
|
|
var keyboardLandscapeViewportHeight = 0;
|
|
|
|
/**
|
|
* The currently focused input.
|
|
*/
|
|
var keyboardActiveElement;
|
|
|
|
/**
|
|
* The previously focused input used to reset keyboard after focusing on a
|
|
* new non-keyboard element
|
|
*/
|
|
var lastKeyboardActiveElement;
|
|
|
|
/**
|
|
* The scroll view containing the currently focused input.
|
|
*/
|
|
var scrollView;
|
|
|
|
/**
|
|
* Timer for the setInterval that polls window.innerHeight to determine whether
|
|
* the layout has updated for the keyboard showing/hiding.
|
|
*/
|
|
var waitForResizeTimer;
|
|
|
|
/**
|
|
* Sometimes when switching inputs or orientations, focusout will fire before
|
|
* focusin, so this timer is for the small setTimeout to determine if we should
|
|
* really focusout/hide the keyboard.
|
|
*/
|
|
var keyboardFocusOutTimer;
|
|
|
|
/**
|
|
* on Android, orientationchange will fire before the keyboard plugin notifies
|
|
* the browser that the keyboard will show/is showing, so this flag indicates
|
|
* to nativeShow that there was an orientationChange and we should update
|
|
* the viewport height with an accurate keyboard height value
|
|
*/
|
|
var wasOrientationChange = false;
|
|
|
|
/**
|
|
* CSS class added to the body indicating the keyboard is open.
|
|
*/
|
|
var KEYBOARD_OPEN_CSS = 'keyboard-open';
|
|
|
|
/**
|
|
* CSS class that indicates a scroll container.
|
|
*/
|
|
var SCROLL_CONTAINER_CSS = 'scroll-content';
|
|
|
|
/**
|
|
* Debounced keyboardFocusIn function
|
|
*/
|
|
var debouncedKeyboardFocusIn = ionic.debounce(keyboardFocusIn, 200, true);
|
|
|
|
/**
|
|
* Debounced keyboardNativeShow function
|
|
*/
|
|
var debouncedKeyboardNativeShow = ionic.debounce(keyboardNativeShow, 100, true);
|
|
|
|
/**
|
|
* Ionic keyboard namespace.
|
|
* @namespace keyboard
|
|
*/
|
|
ionic.keyboard = {
|
|
|
|
/**
|
|
* Whether the keyboard is open or not.
|
|
*/
|
|
isOpen: false,
|
|
|
|
/**
|
|
* Whether the keyboard is closing or not.
|
|
*/
|
|
isClosing: false,
|
|
|
|
/**
|
|
* Whether the keyboard is opening or not.
|
|
*/
|
|
isOpening: false,
|
|
|
|
/**
|
|
* The height of the keyboard in pixels, as reported by the keyboard plugin.
|
|
* If the plugin is not available, calculated as the difference in
|
|
* window.innerHeight after the keyboard has shown.
|
|
*/
|
|
height: 0,
|
|
|
|
/**
|
|
* Whether the device is in landscape orientation or not.
|
|
*/
|
|
isLandscape: false,
|
|
|
|
/**
|
|
* Whether the keyboard event listeners have been added or not
|
|
*/
|
|
isInitialized: false,
|
|
|
|
/**
|
|
* Hide the keyboard, if it is open.
|
|
*/
|
|
hide: function() {
|
|
if (keyboardHasPlugin()) {
|
|
cordova.plugins.Keyboard.close();
|
|
}
|
|
keyboardActiveElement && keyboardActiveElement.blur();
|
|
},
|
|
|
|
/**
|
|
* An alias for cordova.plugins.Keyboard.show(). If the keyboard plugin
|
|
* is installed, show the keyboard.
|
|
*/
|
|
show: function() {
|
|
if (keyboardHasPlugin()) {
|
|
cordova.plugins.Keyboard.show();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove all keyboard related event listeners, effectively disabling Ionic's
|
|
* keyboard adjustments.
|
|
*/
|
|
disable: function() {
|
|
if (keyboardHasPlugin()) {
|
|
window.removeEventListener('native.keyboardshow', debouncedKeyboardNativeShow );
|
|
window.removeEventListener('native.keyboardhide', keyboardFocusOut);
|
|
} else {
|
|
document.body.removeEventListener('focusout', keyboardFocusOut);
|
|
}
|
|
|
|
document.body.removeEventListener('ionic.focusin', debouncedKeyboardFocusIn);
|
|
document.body.removeEventListener('focusin', debouncedKeyboardFocusIn);
|
|
|
|
window.removeEventListener('orientationchange', keyboardOrientationChange);
|
|
|
|
if ( window.navigator.msPointerEnabled ) {
|
|
document.removeEventListener("MSPointerDown", keyboardInit);
|
|
} else {
|
|
document.removeEventListener('touchstart', keyboardInit);
|
|
}
|
|
ionic.keyboard.isInitialized = false;
|
|
},
|
|
|
|
/**
|
|
* Alias for keyboardInit, initialize all keyboard related event listeners.
|
|
*/
|
|
enable: function() {
|
|
keyboardInit();
|
|
}
|
|
};
|
|
|
|
// Initialize the viewport height (after ionic.keyboard.height has been
|
|
// defined).
|
|
keyboardCurrentViewportHeight = getViewportHeight();
|
|
|
|
|
|
/* Event handlers */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Event handler for first touch event, initializes all event listeners
|
|
* for keyboard related events. Also aliased by ionic.keyboard.enable.
|
|
*/
|
|
function keyboardInit() {
|
|
|
|
if (ionic.keyboard.isInitialized) return;
|
|
|
|
if (keyboardHasPlugin()) {
|
|
window.addEventListener('native.keyboardshow', debouncedKeyboardNativeShow);
|
|
window.addEventListener('native.keyboardhide', keyboardFocusOut);
|
|
} else {
|
|
document.body.addEventListener('focusout', keyboardFocusOut);
|
|
}
|
|
|
|
document.body.addEventListener('ionic.focusin', debouncedKeyboardFocusIn);
|
|
document.body.addEventListener('focusin', debouncedKeyboardFocusIn);
|
|
|
|
if (window.navigator.msPointerEnabled) {
|
|
document.removeEventListener("MSPointerDown", keyboardInit);
|
|
} else {
|
|
document.removeEventListener('touchstart', keyboardInit);
|
|
}
|
|
|
|
ionic.keyboard.isInitialized = true;
|
|
}
|
|
|
|
/**
|
|
* Event handler for 'native.keyboardshow' event, sets keyboard.height to the
|
|
* reported height and keyboard.isOpening to true. Then calls
|
|
* keyboardWaitForResize with keyboardShow or keyboardUpdateViewportHeight as
|
|
* the callback depending on whether the event was triggered by a focusin or
|
|
* an orientationchange.
|
|
*/
|
|
function keyboardNativeShow(e) {
|
|
clearTimeout(keyboardFocusOutTimer);
|
|
//console.log("keyboardNativeShow fired at: " + Date.now());
|
|
//console.log("keyboardNativeshow window.innerHeight: " + window.innerHeight);
|
|
|
|
if (!ionic.keyboard.isOpen || ionic.keyboard.isClosing) {
|
|
ionic.keyboard.isOpening = true;
|
|
ionic.keyboard.isClosing = false;
|
|
}
|
|
|
|
ionic.keyboard.height = e.keyboardHeight;
|
|
//console.log('nativeshow keyboard height:' + e.keyboardHeight);
|
|
|
|
if (wasOrientationChange) {
|
|
keyboardWaitForResize(keyboardUpdateViewportHeight, true);
|
|
} else {
|
|
keyboardWaitForResize(keyboardShow, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event handler for 'focusin' and 'ionic.focusin' events. Initializes
|
|
* keyboard state (keyboardActiveElement and keyboard.isOpening) for the
|
|
* appropriate adjustments once the window has resized. If not using the
|
|
* keyboard plugin, calls keyboardWaitForResize with keyboardShow as the
|
|
* callback or keyboardShow right away if the keyboard is already open. If
|
|
* using the keyboard plugin does nothing and lets keyboardNativeShow handle
|
|
* adjustments with a more accurate keyboard height.
|
|
*/
|
|
function keyboardFocusIn(e) {
|
|
clearTimeout(keyboardFocusOutTimer);
|
|
//console.log("keyboardFocusIn from: " + e.type + " at: " + Date.now());
|
|
|
|
if (!e.target ||
|
|
e.target.readOnly ||
|
|
!ionic.tap.isKeyboardElement(e.target) ||
|
|
!(scrollView = ionic.DomUtil.getParentWithClass(e.target, SCROLL_CONTAINER_CSS))) {
|
|
if (keyboardActiveElement) {
|
|
lastKeyboardActiveElement = keyboardActiveElement;
|
|
}
|
|
keyboardActiveElement = null;
|
|
return;
|
|
}
|
|
|
|
keyboardActiveElement = e.target;
|
|
|
|
// if using JS scrolling, undo the effects of native overflow scroll so the
|
|
// scroll view is positioned correctly
|
|
if (!scrollView.classList.contains("overflow-scroll")) {
|
|
document.body.scrollTop = 0;
|
|
scrollView.scrollTop = 0;
|
|
ionic.requestAnimationFrame(function(){
|
|
document.body.scrollTop = 0;
|
|
scrollView.scrollTop = 0;
|
|
});
|
|
|
|
// any showing part of the document that isn't within the scroll the user
|
|
// could touchmove and cause some ugly changes to the app, so disable
|
|
// any touchmove events while the keyboard is open using e.preventDefault()
|
|
if (window.navigator.msPointerEnabled) {
|
|
document.addEventListener("MSPointerMove", keyboardPreventDefault, false);
|
|
} else {
|
|
document.addEventListener('touchmove', keyboardPreventDefault, false);
|
|
}
|
|
}
|
|
|
|
if (!ionic.keyboard.isOpen || ionic.keyboard.isClosing) {
|
|
ionic.keyboard.isOpening = true;
|
|
ionic.keyboard.isClosing = false;
|
|
}
|
|
|
|
// attempt to prevent browser from natively scrolling input into view while
|
|
// we are trying to do the same (while we are scrolling) if the user taps the
|
|
// keyboard
|
|
document.addEventListener('keydown', keyboardOnKeyDown, false);
|
|
|
|
|
|
|
|
// if we aren't using the plugin and the keyboard isn't open yet, wait for the
|
|
// window to resize so we can get an accurate estimate of the keyboard size,
|
|
// otherwise we do nothing and let nativeShow call keyboardShow once we have
|
|
// an exact keyboard height
|
|
// if the keyboard is already open, go ahead and scroll the input into view
|
|
// if necessary
|
|
if (!ionic.keyboard.isOpen && !keyboardHasPlugin()) {
|
|
keyboardWaitForResize(keyboardShow, true);
|
|
|
|
} else if (ionic.keyboard.isOpen) {
|
|
keyboardShow();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event handler for 'focusout' events. Sets keyboard.isClosing to true and
|
|
* calls keyboardWaitForResize with keyboardHide as the callback after a small
|
|
* timeout.
|
|
*/
|
|
function keyboardFocusOut() {
|
|
clearTimeout(keyboardFocusOutTimer);
|
|
//console.log("keyboardFocusOut fired at: " + Date.now());
|
|
//console.log("keyboardFocusOut event type: " + e.type);
|
|
|
|
if (ionic.keyboard.isOpen || ionic.keyboard.isOpening) {
|
|
ionic.keyboard.isClosing = true;
|
|
ionic.keyboard.isOpening = false;
|
|
}
|
|
|
|
// Call keyboardHide with a slight delay because sometimes on focus or
|
|
// orientation change focusin is called immediately after, so we give it time
|
|
// to cancel keyboardHide
|
|
keyboardFocusOutTimer = setTimeout(function() {
|
|
ionic.requestAnimationFrame(function() {
|
|
// focusOut during or right after an orientationchange, so we didn't get
|
|
// a chance to update the viewport height yet, do it and keyboardHide
|
|
//console.log("focusOut, wasOrientationChange: " + wasOrientationChange);
|
|
if (wasOrientationChange) {
|
|
keyboardWaitForResize(function(){
|
|
keyboardUpdateViewportHeight();
|
|
keyboardHide();
|
|
}, false);
|
|
} else {
|
|
keyboardWaitForResize(keyboardHide, false);
|
|
}
|
|
});
|
|
}, 50);
|
|
}
|
|
|
|
/**
|
|
* Event handler for 'orientationchange' events. If using the keyboard plugin
|
|
* and the keyboard is open on Android, sets wasOrientationChange to true so
|
|
* nativeShow can update the viewport height with an accurate keyboard height.
|
|
* If the keyboard isn't open or keyboard plugin isn't being used,
|
|
* waits for the window to resize before updating the viewport height.
|
|
*
|
|
* On iOS, where orientationchange fires after the keyboard has already shown,
|
|
* updates the viewport immediately, regardless of if the keyboard is already
|
|
* open.
|
|
*/
|
|
function keyboardOrientationChange() {
|
|
//console.log("orientationchange fired at: " + Date.now());
|
|
//console.log("orientation was: " + (ionic.keyboard.isLandscape ? "landscape" : "portrait"));
|
|
|
|
// toggle orientation
|
|
ionic.keyboard.isLandscape = !ionic.keyboard.isLandscape;
|
|
// //console.log("now orientation is: " + (ionic.keyboard.isLandscape ? "landscape" : "portrait"));
|
|
|
|
// no need to wait for resizing on iOS, and orientationchange always fires
|
|
// after the keyboard has opened, so it doesn't matter if it's open or not
|
|
if (ionic.Platform.isIOS()) {
|
|
keyboardUpdateViewportHeight();
|
|
}
|
|
|
|
// On Android, if the keyboard isn't open or we aren't using the keyboard
|
|
// plugin, update the viewport height once everything has resized. If the
|
|
// keyboard is open and we are using the keyboard plugin do nothing and let
|
|
// nativeShow handle it using an accurate keyboard height.
|
|
if ( ionic.Platform.isAndroid()) {
|
|
if (!ionic.keyboard.isOpen || !keyboardHasPlugin()) {
|
|
keyboardWaitForResize(keyboardUpdateViewportHeight, false);
|
|
} else {
|
|
wasOrientationChange = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event handler for 'keydown' event. Tries to prevent browser from natively
|
|
* scrolling an input into view when a user taps the keyboard while we are
|
|
* scrolling the input into view ourselves with JS.
|
|
*/
|
|
function keyboardOnKeyDown(e) {
|
|
if (ionic.scroll.isScrolling) {
|
|
keyboardPreventDefault(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event for 'touchmove' or 'MSPointerMove'. Prevents native scrolling on
|
|
* elements outside the scroll view while the keyboard is open.
|
|
*/
|
|
function keyboardPreventDefault(e) {
|
|
if (e.target.tagName !== 'TEXTAREA') {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
/* Private API */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Polls window.innerHeight until it has updated to an expected value (or
|
|
* sufficient time has passed) before calling the specified callback function.
|
|
* Only necessary for non-fullscreen Android which sometimes reports multiple
|
|
* window.innerHeight values during interim layouts while it is resizing.
|
|
*
|
|
* On iOS, the window.innerHeight will already be updated, but we use the 50ms
|
|
* delay as essentially a timeout so that scroll view adjustments happen after
|
|
* the keyboard has shown so there isn't a white flash from us resizing too
|
|
* quickly.
|
|
*
|
|
* @param {Function} callback the function to call once the window has resized
|
|
* @param {boolean} isOpening whether the resize is from the keyboard opening
|
|
* or not
|
|
*/
|
|
function keyboardWaitForResize(callback, isOpening) {
|
|
clearInterval(waitForResizeTimer);
|
|
var count = 0;
|
|
var maxCount;
|
|
var initialHeight = getViewportHeight();
|
|
var viewportHeight = initialHeight;
|
|
|
|
//console.log("waitForResize initial viewport height: " + viewportHeight);
|
|
//var start = Date.now();
|
|
//console.log("start: " + start);
|
|
|
|
// want to fail relatively quickly on modern android devices, since it's much
|
|
// more likely we just have a bad keyboard height
|
|
if (ionic.Platform.isAndroid() && ionic.Platform.version() < 4.4) {
|
|
maxCount = 30;
|
|
} else if (ionic.Platform.isAndroid()) {
|
|
maxCount = 10;
|
|
} else {
|
|
maxCount = 1;
|
|
}
|
|
|
|
// poll timer
|
|
waitForResizeTimer = setInterval(function(){
|
|
viewportHeight = getViewportHeight();
|
|
|
|
// height hasn't updated yet, try again in 50ms
|
|
// if not using plugin, wait for maxCount to ensure we have waited long enough
|
|
// to get an accurate keyboard height
|
|
if (++count < maxCount &&
|
|
((!isPortraitViewportHeight(viewportHeight) &&
|
|
!isLandscapeViewportHeight(viewportHeight)) ||
|
|
!ionic.keyboard.height)) {
|
|
return;
|
|
}
|
|
|
|
// infer the keyboard height from the resize if not using the keyboard plugin
|
|
if (!keyboardHasPlugin()) {
|
|
ionic.keyboard.height = Math.abs(initialHeight - window.innerHeight);
|
|
}
|
|
|
|
// set to true if we were waiting for the keyboard to open
|
|
ionic.keyboard.isOpen = isOpening;
|
|
|
|
clearInterval(waitForResizeTimer);
|
|
//var end = Date.now();
|
|
//console.log("waitForResize count: " + count);
|
|
//console.log("end: " + end);
|
|
//console.log("difference: " + ( end - start ) + "ms");
|
|
|
|
//console.log("callback: " + callback.name);
|
|
callback();
|
|
|
|
}, 50);
|
|
|
|
return maxCount; //for tests
|
|
}
|
|
|
|
/**
|
|
* On keyboard close sets keyboard state to closed, resets the scroll view,
|
|
* removes CSS from body indicating keyboard was open, removes any event
|
|
* listeners for when the keyboard is open and on Android blurs the active
|
|
* element (which in some cases will still have focus even if the keyboard
|
|
* is closed and can cause it to reappear on subsequent taps).
|
|
*/
|
|
function keyboardHide() {
|
|
clearTimeout(keyboardFocusOutTimer);
|
|
//console.log("keyboardHide");
|
|
|
|
ionic.keyboard.isOpen = false;
|
|
ionic.keyboard.isClosing = false;
|
|
|
|
if (keyboardActiveElement || lastKeyboardActiveElement) {
|
|
ionic.trigger('resetScrollView', {
|
|
target: keyboardActiveElement || lastKeyboardActiveElement
|
|
}, true);
|
|
}
|
|
|
|
ionic.requestAnimationFrame(function(){
|
|
document.body.classList.remove(KEYBOARD_OPEN_CSS);
|
|
});
|
|
|
|
// the keyboard is gone now, remove the touchmove that disables native scroll
|
|
if (window.navigator.msPointerEnabled) {
|
|
document.removeEventListener("MSPointerMove", keyboardPreventDefault);
|
|
} else {
|
|
document.removeEventListener('touchmove', keyboardPreventDefault);
|
|
}
|
|
document.removeEventListener('keydown', keyboardOnKeyDown);
|
|
|
|
if (ionic.Platform.isAndroid()) {
|
|
// on android closing the keyboard with the back/dismiss button won't remove
|
|
// focus and keyboard can re-appear on subsequent taps (like scrolling)
|
|
if (keyboardHasPlugin()) cordova.plugins.Keyboard.close();
|
|
keyboardActiveElement && keyboardActiveElement.blur();
|
|
}
|
|
|
|
keyboardActiveElement = null;
|
|
lastKeyboardActiveElement = null;
|
|
}
|
|
|
|
/**
|
|
* On keyboard open sets keyboard state to open, adds CSS to the body
|
|
* indicating the keyboard is open and tells the scroll view to resize and
|
|
* the currently focused input into view if necessary.
|
|
*/
|
|
function keyboardShow() {
|
|
|
|
ionic.keyboard.isOpen = true;
|
|
ionic.keyboard.isOpening = false;
|
|
|
|
var details = {
|
|
keyboardHeight: keyboardGetHeight(),
|
|
viewportHeight: keyboardCurrentViewportHeight
|
|
};
|
|
|
|
if (keyboardActiveElement) {
|
|
details.target = keyboardActiveElement;
|
|
|
|
var elementBounds = keyboardActiveElement.getBoundingClientRect();
|
|
|
|
details.elementTop = Math.round(elementBounds.top);
|
|
details.elementBottom = Math.round(elementBounds.bottom);
|
|
|
|
details.windowHeight = details.viewportHeight - details.keyboardHeight;
|
|
//console.log("keyboardShow viewportHeight: " + details.viewportHeight +
|
|
//", windowHeight: " + details.windowHeight +
|
|
//", keyboardHeight: " + details.keyboardHeight);
|
|
|
|
// figure out if the element is under the keyboard
|
|
details.isElementUnderKeyboard = (details.elementBottom > details.windowHeight);
|
|
//console.log("isUnderKeyboard: " + details.isElementUnderKeyboard);
|
|
//console.log("elementBottom: " + details.elementBottom);
|
|
|
|
// send event so the scroll view adjusts
|
|
ionic.trigger('scrollChildIntoView', details, true);
|
|
}
|
|
|
|
setTimeout(function(){
|
|
document.body.classList.add(KEYBOARD_OPEN_CSS);
|
|
}, 400);
|
|
|
|
return details; //for testing
|
|
}
|
|
|
|
/* eslint no-unused-vars:0 */
|
|
function keyboardGetHeight() {
|
|
// check if we already have a keyboard height from the plugin or resize calculations
|
|
if (ionic.keyboard.height) {
|
|
return ionic.keyboard.height;
|
|
}
|
|
|
|
if (ionic.Platform.isAndroid()) {
|
|
// should be using the plugin, no way to know how big the keyboard is, so guess
|
|
if ( ionic.Platform.isFullScreen ) {
|
|
return 275;
|
|
}
|
|
// otherwise just calculate it
|
|
var contentHeight = window.innerHeight;
|
|
if (contentHeight < keyboardCurrentViewportHeight) {
|
|
return keyboardCurrentViewportHeight - contentHeight;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// fallback for when it's the webview without the plugin
|
|
// or for just the standard web browser
|
|
// TODO: have these be based on device
|
|
if (ionic.Platform.isIOS()) {
|
|
if (ionic.keyboard.isLandscape) {
|
|
return 206;
|
|
}
|
|
|
|
if (!ionic.Platform.isWebView()) {
|
|
return 216;
|
|
}
|
|
|
|
return 260;
|
|
}
|
|
|
|
// safe guess
|
|
return 275;
|
|
}
|
|
|
|
function isPortraitViewportHeight(viewportHeight) {
|
|
return !!(!ionic.keyboard.isLandscape &&
|
|
keyboardPortraitViewportHeight &&
|
|
(Math.abs(keyboardPortraitViewportHeight - viewportHeight) < 2));
|
|
}
|
|
|
|
function isLandscapeViewportHeight(viewportHeight) {
|
|
return !!(ionic.keyboard.isLandscape &&
|
|
keyboardLandscapeViewportHeight &&
|
|
(Math.abs(keyboardLandscapeViewportHeight - viewportHeight) < 2));
|
|
}
|
|
|
|
function keyboardUpdateViewportHeight() {
|
|
wasOrientationChange = false;
|
|
keyboardCurrentViewportHeight = getViewportHeight();
|
|
|
|
if (ionic.keyboard.isLandscape && !keyboardLandscapeViewportHeight) {
|
|
//console.log("saved landscape: " + keyboardCurrentViewportHeight);
|
|
keyboardLandscapeViewportHeight = keyboardCurrentViewportHeight;
|
|
|
|
} else if (!ionic.keyboard.isLandscape && !keyboardPortraitViewportHeight) {
|
|
//console.log("saved portrait: " + keyboardCurrentViewportHeight);
|
|
keyboardPortraitViewportHeight = keyboardCurrentViewportHeight;
|
|
}
|
|
|
|
if (keyboardActiveElement) {
|
|
ionic.trigger('resetScrollView', {
|
|
target: keyboardActiveElement
|
|
}, true);
|
|
}
|
|
|
|
if (ionic.keyboard.isOpen && ionic.tap.isTextInput(keyboardActiveElement)) {
|
|
keyboardShow();
|
|
}
|
|
}
|
|
|
|
function keyboardInitViewportHeight() {
|
|
var viewportHeight = getViewportHeight();
|
|
//console.log("Keyboard init VP: " + viewportHeight + " " + window.innerWidth);
|
|
// can't just use window.innerHeight in case the keyboard is opened immediately
|
|
if ((viewportHeight / window.innerWidth) < 1) {
|
|
ionic.keyboard.isLandscape = true;
|
|
}
|
|
//console.log("ionic.keyboard.isLandscape is: " + ionic.keyboard.isLandscape);
|
|
|
|
// initialize or update the current viewport height values
|
|
keyboardCurrentViewportHeight = viewportHeight;
|
|
if (ionic.keyboard.isLandscape && !keyboardLandscapeViewportHeight) {
|
|
keyboardLandscapeViewportHeight = keyboardCurrentViewportHeight;
|
|
} else if (!ionic.keyboard.isLandscape && !keyboardPortraitViewportHeight) {
|
|
keyboardPortraitViewportHeight = keyboardCurrentViewportHeight;
|
|
}
|
|
}
|
|
|
|
function getViewportHeight() {
|
|
var windowHeight = window.innerHeight;
|
|
//console.log('window.innerHeight is: ' + windowHeight);
|
|
//console.log('kb height is: ' + ionic.keyboard.height);
|
|
//console.log('kb isOpen: ' + ionic.keyboard.isOpen);
|
|
|
|
//TODO: add iPad undocked/split kb once kb plugin supports it
|
|
// the keyboard overlays the window on Android fullscreen
|
|
if (!(ionic.Platform.isAndroid() && ionic.Platform.isFullScreen) &&
|
|
(ionic.keyboard.isOpen || ionic.keyboard.isOpening) &&
|
|
!ionic.keyboard.isClosing) {
|
|
|
|
return windowHeight + keyboardGetHeight();
|
|
}
|
|
return windowHeight;
|
|
}
|
|
|
|
function keyboardHasPlugin() {
|
|
return !!(window.cordova && cordova.plugins && cordova.plugins.Keyboard);
|
|
}
|
|
|
|
ionic.Platform.ready(function() {
|
|
keyboardInitViewportHeight();
|
|
|
|
window.addEventListener('orientationchange', keyboardOrientationChange);
|
|
|
|
// if orientation changes while app is in background, update on resuming
|
|
/*
|
|
if ( ionic.Platform.isWebView() ) {
|
|
document.addEventListener('resume', keyboardInitViewportHeight);
|
|
|
|
if (ionic.Platform.isAndroid()) {
|
|
//TODO: onbackpressed to detect keyboard close without focusout or plugin
|
|
}
|
|
}
|
|
*/
|
|
|
|
// if orientation changes while app is in background, update on resuming
|
|
/* if ( ionic.Platform.isWebView() ) {
|
|
document.addEventListener('pause', function() {
|
|
window.removeEventListener('orientationchange', keyboardOrientationChange);
|
|
})
|
|
document.addEventListener('resume', function() {
|
|
keyboardInitViewportHeight();
|
|
window.addEventListener('orientationchange', keyboardOrientationChange)
|
|
});
|
|
}*/
|
|
|
|
// Android sometimes reports bad innerHeight on window.load
|
|
// try it again in a lil bit to play it safe
|
|
setTimeout(keyboardInitViewportHeight, 999);
|
|
|
|
// only initialize the adjustments for the virtual keyboard
|
|
// if a touchstart event happens
|
|
if (window.navigator.msPointerEnabled) {
|
|
document.addEventListener("MSPointerDown", keyboardInit, false);
|
|
} else {
|
|
document.addEventListener('touchstart', keyboardInit, false);
|
|
}
|
|
});
|
|
|
|
|
|
|
|
var viewportTag;
|
|
var viewportProperties = {};
|
|
|
|
ionic.viewport = {
|
|
orientation: function() {
|
|
// 0 = Portrait
|
|
// 90 = Landscape
|
|
// not using window.orientation because each device has a different implementation
|
|
return (window.innerWidth > window.innerHeight ? 90 : 0);
|
|
}
|
|
};
|
|
|
|
function viewportLoadTag() {
|
|
var x;
|
|
|
|
for (x = 0; x < document.head.children.length; x++) {
|
|
if (document.head.children[x].name == 'viewport') {
|
|
viewportTag = document.head.children[x];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (viewportTag) {
|
|
var props = viewportTag.content.toLowerCase().replace(/\s+/g, '').split(',');
|
|
var keyValue;
|
|
for (x = 0; x < props.length; x++) {
|
|
if (props[x]) {
|
|
keyValue = props[x].split('=');
|
|
viewportProperties[ keyValue[0] ] = (keyValue.length > 1 ? keyValue[1] : '_');
|
|
}
|
|
}
|
|
viewportUpdate();
|
|
}
|
|
}
|
|
|
|
function viewportUpdate() {
|
|
// unit tests in viewport.unit.js
|
|
|
|
var initWidth = viewportProperties.width;
|
|
var initHeight = viewportProperties.height;
|
|
var p = ionic.Platform;
|
|
var version = p.version();
|
|
var DEVICE_WIDTH = 'device-width';
|
|
var DEVICE_HEIGHT = 'device-height';
|
|
var orientation = ionic.viewport.orientation();
|
|
|
|
// Most times we're removing the height and adding the width
|
|
// So this is the default to start with, then modify per platform/version/oreintation
|
|
delete viewportProperties.height;
|
|
viewportProperties.width = DEVICE_WIDTH;
|
|
|
|
if (p.isIPad()) {
|
|
// iPad
|
|
|
|
if (version > 7) {
|
|
// iPad >= 7.1
|
|
// https://issues.apache.org/jira/browse/CB-4323
|
|
delete viewportProperties.width;
|
|
|
|
} else {
|
|
// iPad <= 7.0
|
|
|
|
if (p.isWebView()) {
|
|
// iPad <= 7.0 WebView
|
|
|
|
if (orientation == 90) {
|
|
// iPad <= 7.0 WebView Landscape
|
|
viewportProperties.height = '0';
|
|
|
|
} else if (version == 7) {
|
|
// iPad <= 7.0 WebView Portait
|
|
viewportProperties.height = DEVICE_HEIGHT;
|
|
}
|
|
} else {
|
|
// iPad <= 6.1 Browser
|
|
if (version < 7) {
|
|
viewportProperties.height = '0';
|
|
}
|
|
}
|
|
}
|
|
|
|
} else if (p.isIOS()) {
|
|
// iPhone
|
|
|
|
if (p.isWebView()) {
|
|
// iPhone WebView
|
|
|
|
if (version > 7) {
|
|
// iPhone >= 7.1 WebView
|
|
delete viewportProperties.width;
|
|
|
|
} else if (version < 7) {
|
|
// iPhone <= 6.1 WebView
|
|
// if height was set it needs to get removed with this hack for <= 6.1
|
|
if (initHeight) viewportProperties.height = '0';
|
|
|
|
} else if (version == 7) {
|
|
//iPhone == 7.0 WebView
|
|
viewportProperties.height = DEVICE_HEIGHT;
|
|
}
|
|
|
|
} else {
|
|
// iPhone Browser
|
|
|
|
if (version < 7) {
|
|
// iPhone <= 6.1 Browser
|
|
// if height was set it needs to get removed with this hack for <= 6.1
|
|
if (initHeight) viewportProperties.height = '0';
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// only update the viewport tag if there was a change
|
|
if (initWidth !== viewportProperties.width || initHeight !== viewportProperties.height) {
|
|
viewportTagUpdate();
|
|
}
|
|
}
|
|
|
|
function viewportTagUpdate() {
|
|
var key, props = [];
|
|
for (key in viewportProperties) {
|
|
if (viewportProperties[key]) {
|
|
props.push(key + (viewportProperties[key] == '_' ? '' : '=' + viewportProperties[key]));
|
|
}
|
|
}
|
|
|
|
viewportTag.content = props.join(', ');
|
|
}
|
|
|
|
ionic.Platform.ready(function() {
|
|
viewportLoadTag();
|
|
|
|
window.addEventListener("orientationchange", function() {
|
|
setTimeout(viewportUpdate, 1000);
|
|
}, false);
|
|
});
|
|
|
|
(function(ionic) {
|
|
'use strict';
|
|
ionic.views.View = function() {
|
|
this.initialize.apply(this, arguments);
|
|
};
|
|
|
|
ionic.views.View.inherit = ionic.inherit;
|
|
|
|
ionic.extend(ionic.views.View.prototype, {
|
|
initialize: function() {}
|
|
});
|
|
|
|
})(window.ionic);
|
|
|
|
/*
|
|
* Scroller
|
|
* http://github.com/zynga/scroller
|
|
*
|
|
* Copyright 2011, Zynga Inc.
|
|
* Licensed under the MIT License.
|
|
* https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
|
|
*
|
|
* Based on the work of: Unify Project (unify-project.org)
|
|
* http://unify-project.org
|
|
* Copyright 2011, Deutsche Telekom AG
|
|
* License: MIT + Apache (V2)
|
|
*/
|
|
|
|
/* jshint eqnull: true */
|
|
|
|
/**
|
|
* Generic animation class with support for dropped frames both optional easing and duration.
|
|
*
|
|
* Optional duration is useful when the lifetime is defined by another condition than time
|
|
* e.g. speed of an animating object, etc.
|
|
*
|
|
* Dropped frame logic allows to keep using the same updater logic independent from the actual
|
|
* rendering. This eases a lot of cases where it might be pretty complex to break down a state
|
|
* based on the pure time difference.
|
|
*/
|
|
var zyngaCore = { effect: {} };
|
|
(function(global) {
|
|
var time = Date.now || function() {
|
|
return +new Date();
|
|
};
|
|
var desiredFrames = 60;
|
|
var millisecondsPerSecond = 1000;
|
|
var running = {};
|
|
var counter = 1;
|
|
|
|
zyngaCore.effect.Animate = {
|
|
|
|
/**
|
|
* A requestAnimationFrame wrapper / polyfill.
|
|
*
|
|
* @param callback {Function} The callback to be invoked before the next repaint.
|
|
* @param root {HTMLElement} The root element for the repaint
|
|
*/
|
|
requestAnimationFrame: (function() {
|
|
|
|
// Check for request animation Frame support
|
|
var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame;
|
|
var isNative = !!requestFrame;
|
|
|
|
if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
|
|
isNative = false;
|
|
}
|
|
|
|
if (isNative) {
|
|
return function(callback, root) {
|
|
requestFrame(callback, root);
|
|
};
|
|
}
|
|
|
|
var TARGET_FPS = 60;
|
|
var requests = {};
|
|
var requestCount = 0;
|
|
var rafHandle = 1;
|
|
var intervalHandle = null;
|
|
var lastActive = +new Date();
|
|
|
|
return function(callback) {
|
|
var callbackHandle = rafHandle++;
|
|
|
|
// Store callback
|
|
requests[callbackHandle] = callback;
|
|
requestCount++;
|
|
|
|
// Create timeout at first request
|
|
if (intervalHandle === null) {
|
|
|
|
intervalHandle = setInterval(function() {
|
|
|
|
var time = +new Date();
|
|
var currentRequests = requests;
|
|
|
|
// Reset data structure before executing callbacks
|
|
requests = {};
|
|
requestCount = 0;
|
|
|
|
for(var key in currentRequests) {
|
|
if (currentRequests.hasOwnProperty(key)) {
|
|
currentRequests[key](time);
|
|
lastActive = time;
|
|
}
|
|
}
|
|
|
|
// Disable the timeout when nothing happens for a certain
|
|
// period of time
|
|
if (time - lastActive > 2500) {
|
|
clearInterval(intervalHandle);
|
|
intervalHandle = null;
|
|
}
|
|
|
|
}, 1000 / TARGET_FPS);
|
|
}
|
|
|
|
return callbackHandle;
|
|
};
|
|
|
|
})(),
|
|
|
|
|
|
/**
|
|
* Stops the given animation.
|
|
*
|
|
* @param id {Integer} Unique animation ID
|
|
* @return {Boolean} Whether the animation was stopped (aka, was running before)
|
|
*/
|
|
stop: function(id) {
|
|
var cleared = running[id] != null;
|
|
if (cleared) {
|
|
running[id] = null;
|
|
}
|
|
|
|
return cleared;
|
|
},
|
|
|
|
|
|
/**
|
|
* Whether the given animation is still running.
|
|
*
|
|
* @param id {Integer} Unique animation ID
|
|
* @return {Boolean} Whether the animation is still running
|
|
*/
|
|
isRunning: function(id) {
|
|
return running[id] != null;
|
|
},
|
|
|
|
|
|
/**
|
|
* Start the animation.
|
|
*
|
|
* @param stepCallback {Function} Pointer to function which is executed on every step.
|
|
* Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
|
|
* @param verifyCallback {Function} Executed before every animation step.
|
|
* Signature of the method should be `function() { return continueWithAnimation; }`
|
|
* @param completedCallback {Function}
|
|
* Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
|
|
* @param duration {Integer} Milliseconds to run the animation
|
|
* @param easingMethod {Function} Pointer to easing function
|
|
* Signature of the method should be `function(percent) { return modifiedValue; }`
|
|
* @param root {Element} Render root, when available. Used for internal
|
|
* usage of requestAnimationFrame.
|
|
* @return {Integer} Identifier of animation. Can be used to stop it any time.
|
|
*/
|
|
start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
|
|
|
|
var start = time();
|
|
var lastFrame = start;
|
|
var percent = 0;
|
|
var dropCounter = 0;
|
|
var id = counter++;
|
|
|
|
if (!root) {
|
|
root = document.body;
|
|
}
|
|
|
|
// Compacting running db automatically every few new animations
|
|
if (id % 20 === 0) {
|
|
var newRunning = {};
|
|
for (var usedId in running) {
|
|
newRunning[usedId] = true;
|
|
}
|
|
running = newRunning;
|
|
}
|
|
|
|
// This is the internal step method which is called every few milliseconds
|
|
var step = function(virtual) {
|
|
|
|
// Normalize virtual value
|
|
var render = virtual !== true;
|
|
|
|
// Get current time
|
|
var now = time();
|
|
|
|
// Verification is executed before next animation step
|
|
if (!running[id] || (verifyCallback && !verifyCallback(id))) {
|
|
|
|
running[id] = null;
|
|
completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false);
|
|
return;
|
|
|
|
}
|
|
|
|
// For the current rendering to apply let's update omitted steps in memory.
|
|
// This is important to bring internal state variables up-to-date with progress in time.
|
|
if (render) {
|
|
|
|
var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
|
|
for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
|
|
step(true);
|
|
dropCounter++;
|
|
}
|
|
|
|
}
|
|
|
|
// Compute percent value
|
|
if (duration) {
|
|
percent = (now - start) / duration;
|
|
if (percent > 1) {
|
|
percent = 1;
|
|
}
|
|
}
|
|
|
|
// Execute step callback, then...
|
|
var value = easingMethod ? easingMethod(percent) : percent;
|
|
if ((stepCallback(value, now, render) === false || percent === 1) && render) {
|
|
running[id] = null;
|
|
completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null);
|
|
} else if (render) {
|
|
lastFrame = now;
|
|
zyngaCore.effect.Animate.requestAnimationFrame(step, root);
|
|
}
|
|
};
|
|
|
|
// Mark as running
|
|
running[id] = true;
|
|
|
|
// Init first step
|
|
zyngaCore.effect.Animate.requestAnimationFrame(step, root);
|
|
|
|
// Return unique animation ID
|
|
return id;
|
|
}
|
|
};
|
|
})(window);
|
|
|
|
/*
|
|
* Scroller
|
|
* http://github.com/zynga/scroller
|
|
*
|
|
* Copyright 2011, Zynga Inc.
|
|
* Licensed under the MIT License.
|
|
* https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
|
|
*
|
|
* Based on the work of: Unify Project (unify-project.org)
|
|
* http://unify-project.org
|
|
* Copyright 2011, Deutsche Telekom AG
|
|
* License: MIT + Apache (V2)
|
|
*/
|
|
|
|
(function(ionic) {
|
|
var NOOP = function(){};
|
|
|
|
// Easing Equations (c) 2003 Robert Penner, all rights reserved.
|
|
// Open source under the BSD License.
|
|
|
|
/**
|
|
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
|
|
**/
|
|
var easeOutCubic = function(pos) {
|
|
return (Math.pow((pos - 1), 3) + 1);
|
|
};
|
|
|
|
/**
|
|
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
|
|
**/
|
|
var easeInOutCubic = function(pos) {
|
|
if ((pos /= 0.5) < 1) {
|
|
return 0.5 * Math.pow(pos, 3);
|
|
}
|
|
|
|
return 0.5 * (Math.pow((pos - 2), 3) + 2);
|
|
};
|
|
|
|
|
|
/**
|
|
* ionic.views.Scroll
|
|
* A powerful scroll view with support for bouncing, pull to refresh, and paging.
|
|
* @param {Object} options options for the scroll view
|
|
* @class A scroll view system
|
|
* @memberof ionic.views
|
|
*/
|
|
ionic.views.Scroll = ionic.views.View.inherit({
|
|
initialize: function(options) {
|
|
var self = this;
|
|
|
|
self.__container = options.el;
|
|
self.__content = options.el.firstElementChild;
|
|
|
|
//Remove any scrollTop attached to these elements; they are virtual scroll now
|
|
//This also stops on-load-scroll-to-window.location.hash that the browser does
|
|
setTimeout(function() {
|
|
if (self.__container && self.__content) {
|
|
self.__container.scrollTop = 0;
|
|
self.__content.scrollTop = 0;
|
|
}
|
|
});
|
|
|
|
self.options = {
|
|
|
|
/** Disable scrolling on x-axis by default */
|
|
scrollingX: false,
|
|
scrollbarX: true,
|
|
|
|
/** Enable scrolling on y-axis */
|
|
scrollingY: true,
|
|
scrollbarY: true,
|
|
|
|
startX: 0,
|
|
startY: 0,
|
|
|
|
/** The amount to dampen mousewheel events */
|
|
wheelDampen: 6,
|
|
|
|
/** The minimum size the scrollbars scale to while scrolling */
|
|
minScrollbarSizeX: 5,
|
|
minScrollbarSizeY: 5,
|
|
|
|
/** Scrollbar fading after scrolling */
|
|
scrollbarsFade: true,
|
|
scrollbarFadeDelay: 300,
|
|
/** The initial fade delay when the pane is resized or initialized */
|
|
scrollbarResizeFadeDelay: 1000,
|
|
|
|
/** Enable animations for deceleration, snap back, zooming and scrolling */
|
|
animating: true,
|
|
|
|
/** duration for animations triggered by scrollTo/zoomTo */
|
|
animationDuration: 250,
|
|
|
|
/** The velocity required to make the scroll view "slide" after touchend */
|
|
decelVelocityThreshold: 4,
|
|
|
|
/** The velocity required to make the scroll view "slide" after touchend when using paging */
|
|
decelVelocityThresholdPaging: 4,
|
|
|
|
/** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
|
|
bouncing: true,
|
|
|
|
/** Enable locking to the main axis if user moves only slightly on one of them at start */
|
|
locking: true,
|
|
|
|
/** Enable pagination mode (switching between full page content panes) */
|
|
paging: false,
|
|
|
|
/** Enable snapping of content to a configured pixel grid */
|
|
snapping: false,
|
|
|
|
/** Enable zooming of content via API, fingers and mouse wheel */
|
|
zooming: false,
|
|
|
|
/** Minimum zoom level */
|
|
minZoom: 0.5,
|
|
|
|
/** Maximum zoom level */
|
|
maxZoom: 3,
|
|
|
|
/** Multiply or decrease scrolling speed **/
|
|
speedMultiplier: 1,
|
|
|
|
deceleration: 0.97,
|
|
|
|
/** Whether to prevent default on a scroll operation to capture drag events **/
|
|
preventDefault: false,
|
|
|
|
/** Callback that is fired on the later of touch end or deceleration end,
|
|
provided that another scrolling action has not begun. Used to know
|
|
when to fade out a scrollbar. */
|
|
scrollingComplete: NOOP,
|
|
|
|
/** This configures the amount of change applied to deceleration when reaching boundaries **/
|
|
penetrationDeceleration: 0.03,
|
|
|
|
/** This configures the amount of change applied to acceleration when reaching boundaries **/
|
|
penetrationAcceleration: 0.08,
|
|
|
|
// The ms interval for triggering scroll events
|
|
scrollEventInterval: 10,
|
|
|
|
freeze: false,
|
|
|
|
getContentWidth: function() {
|
|
return Math.max(self.__content.scrollWidth, self.__content.offsetWidth);
|
|
},
|
|
getContentHeight: function() {
|
|
return Math.max(self.__content.scrollHeight, self.__content.offsetHeight + (self.__content.offsetTop * 2));
|
|
}
|
|
};
|
|
|
|
for (var key in options) {
|
|
self.options[key] = options[key];
|
|
}
|
|
|
|
self.hintResize = ionic.debounce(function() {
|
|
self.resize();
|
|
}, 1000, true);
|
|
|
|
self.onScroll = function() {
|
|
|
|
if (!ionic.scroll.isScrolling) {
|
|
setTimeout(self.setScrollStart, 50);
|
|
} else {
|
|
clearTimeout(self.scrollTimer);
|
|
self.scrollTimer = setTimeout(self.setScrollStop, 80);
|
|
}
|
|
|
|
};
|
|
|
|
self.freeze = function(shouldFreeze) {
|
|
if (arguments.length) {
|
|
self.options.freeze = shouldFreeze;
|
|
}
|
|
return self.options.freeze;
|
|
};
|
|
|
|
// We can just use the standard freeze pop in our mouth
|
|
self.freezeShut = self.freeze;
|
|
|
|
self.setScrollStart = function() {
|
|
ionic.scroll.isScrolling = Math.abs(ionic.scroll.lastTop - self.__scrollTop) > 1;
|
|
clearTimeout(self.scrollTimer);
|
|
self.scrollTimer = setTimeout(self.setScrollStop, 80);
|
|
};
|
|
|
|
self.setScrollStop = function() {
|
|
ionic.scroll.isScrolling = false;
|
|
ionic.scroll.lastTop = self.__scrollTop;
|
|
};
|
|
|
|
self.triggerScrollEvent = ionic.throttle(function() {
|
|
self.onScroll();
|
|
ionic.trigger('scroll', {
|
|
scrollTop: self.__scrollTop,
|
|
scrollLeft: self.__scrollLeft,
|
|
target: self.__container
|
|
});
|
|
}, self.options.scrollEventInterval);
|
|
|
|
self.triggerScrollEndEvent = function() {
|
|
ionic.trigger('scrollend', {
|
|
scrollTop: self.__scrollTop,
|
|
scrollLeft: self.__scrollLeft,
|
|
target: self.__container
|
|
});
|
|
};
|
|
|
|
self.__scrollLeft = self.options.startX;
|
|
self.__scrollTop = self.options.startY;
|
|
|
|
// Get the render update function, initialize event handlers,
|
|
// and calculate the size of the scroll container
|
|
self.__callback = self.getRenderFn();
|
|
self.__initEventHandlers();
|
|
self.__createScrollbars();
|
|
|
|
},
|
|
|
|
run: function() {
|
|
this.resize();
|
|
|
|
// Fade them out
|
|
this.__fadeScrollbars('out', this.options.scrollbarResizeFadeDelay);
|
|
},
|
|
|
|
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
INTERNAL FIELDS :: STATUS
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/** Whether only a single finger is used in touch handling */
|
|
__isSingleTouch: false,
|
|
|
|
/** Whether a touch event sequence is in progress */
|
|
__isTracking: false,
|
|
|
|
/** Whether a deceleration animation went to completion. */
|
|
__didDecelerationComplete: false,
|
|
|
|
/**
|
|
* Whether a gesture zoom/rotate event is in progress. Activates when
|
|
* a gesturestart event happens. This has higher priority than dragging.
|
|
*/
|
|
__isGesturing: false,
|
|
|
|
/**
|
|
* Whether the user has moved by such a distance that we have enabled
|
|
* dragging mode. Hint: It's only enabled after some pixels of movement to
|
|
* not interrupt with clicks etc.
|
|
*/
|
|
__isDragging: false,
|
|
|
|
/**
|
|
* Not touching and dragging anymore, and smoothly animating the
|
|
* touch sequence using deceleration.
|
|
*/
|
|
__isDecelerating: false,
|
|
|
|
/**
|
|
* Smoothly animating the currently configured change
|
|
*/
|
|
__isAnimating: false,
|
|
|
|
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
INTERNAL FIELDS :: DIMENSIONS
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/** Available outer left position (from document perspective) */
|
|
__clientLeft: 0,
|
|
|
|
/** Available outer top position (from document perspective) */
|
|
__clientTop: 0,
|
|
|
|
/** Available outer width */
|
|
__clientWidth: 0,
|
|
|
|
/** Available outer height */
|
|
__clientHeight: 0,
|
|
|
|
/** Outer width of content */
|
|
__contentWidth: 0,
|
|
|
|
/** Outer height of content */
|
|
__contentHeight: 0,
|
|
|
|
/** Snapping width for content */
|
|
__snapWidth: 100,
|
|
|
|
/** Snapping height for content */
|
|
__snapHeight: 100,
|
|
|
|
/** Height to assign to refresh area */
|
|
__refreshHeight: null,
|
|
|
|
/** Whether the refresh process is enabled when the event is released now */
|
|
__refreshActive: false,
|
|
|
|
/** Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
|
|
__refreshActivate: null,
|
|
|
|
/** Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
|
|
__refreshDeactivate: null,
|
|
|
|
/** Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
|
|
__refreshStart: null,
|
|
|
|
/** Zoom level */
|
|
__zoomLevel: 1,
|
|
|
|
/** Scroll position on x-axis */
|
|
__scrollLeft: 0,
|
|
|
|
/** Scroll position on y-axis */
|
|
__scrollTop: 0,
|
|
|
|
/** Maximum allowed scroll position on x-axis */
|
|
__maxScrollLeft: 0,
|
|
|
|
/** Maximum allowed scroll position on y-axis */
|
|
__maxScrollTop: 0,
|
|
|
|
/* Scheduled left position (final position when animating) */
|
|
__scheduledLeft: 0,
|
|
|
|
/* Scheduled top position (final position when animating) */
|
|
__scheduledTop: 0,
|
|
|
|
/* Scheduled zoom level (final scale when animating) */
|
|
__scheduledZoom: 0,
|
|
|
|
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
INTERNAL FIELDS :: LAST POSITIONS
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/** Left position of finger at start */
|
|
__lastTouchLeft: null,
|
|
|
|
/** Top position of finger at start */
|
|
__lastTouchTop: null,
|
|
|
|
/** Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
|
|
__lastTouchMove: null,
|
|
|
|
/** List of positions, uses three indexes for each state: left, top, timestamp */
|
|
__positions: null,
|
|
|
|
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
INTERNAL FIELDS :: DECELERATION SUPPORT
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/** Minimum left scroll position during deceleration */
|
|
__minDecelerationScrollLeft: null,
|
|
|
|
/** Minimum top scroll position during deceleration */
|
|
__minDecelerationScrollTop: null,
|
|
|
|
/** Maximum left scroll position during deceleration */
|
|
__maxDecelerationScrollLeft: null,
|
|
|
|
/** Maximum top scroll position during deceleration */
|
|
__maxDecelerationScrollTop: null,
|
|
|
|
/** Current factor to modify horizontal scroll position with on every step */
|
|
__decelerationVelocityX: null,
|
|
|
|
/** Current factor to modify vertical scroll position with on every step */
|
|
__decelerationVelocityY: null,
|
|
|
|
|
|
/** the browser-specific property to use for transforms */
|
|
__transformProperty: null,
|
|
__perspectiveProperty: null,
|
|
|
|
/** scrollbar indicators */
|
|
__indicatorX: null,
|
|
__indicatorY: null,
|
|
|
|
/** Timeout for scrollbar fading */
|
|
__scrollbarFadeTimeout: null,
|
|
|
|
/** whether we've tried to wait for size already */
|
|
__didWaitForSize: null,
|
|
__sizerTimeout: null,
|
|
|
|
__initEventHandlers: function() {
|
|
var self = this;
|
|
|
|
// Event Handler
|
|
var container = self.__container;
|
|
|
|
// save height when scroll view is shrunk so we don't need to reflow
|
|
var scrollViewOffsetHeight;
|
|
|
|
/**
|
|
* Shrink the scroll view when the keyboard is up if necessary and if the
|
|
* focused input is below the bottom of the shrunk scroll view, scroll it
|
|
* into view.
|
|
*/
|
|
self.scrollChildIntoView = function(e) {
|
|
//console.log("scrollChildIntoView at: " + Date.now());
|
|
|
|
// D
|
|
var scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
|
|
// D - A
|
|
scrollViewOffsetHeight = container.offsetHeight;
|
|
var alreadyShrunk = self.isShrunkForKeyboard;
|
|
|
|
var isModal = container.parentNode.classList.contains('modal');
|
|
// 680px is when the media query for 60% modal width kicks in
|
|
var isInsetModal = isModal && window.innerWidth >= 680;
|
|
|
|
/*
|
|
* _______
|
|
* |---A---| <- top of scroll view
|
|
* | |
|
|
* |---B---| <- keyboard
|
|
* | C | <- input
|
|
* |---D---| <- initial bottom of scroll view
|
|
* |___E___| <- bottom of viewport
|
|
*
|
|
* All commented calculations relative to the top of the viewport (ie E
|
|
* is the viewport height, not 0)
|
|
*/
|
|
if (!alreadyShrunk) {
|
|
// shrink scrollview so we can actually scroll if the input is hidden
|
|
// if it isn't shrink so we can scroll to inputs under the keyboard
|
|
// inset modals won't shrink on Android on their own when the keyboard appears
|
|
if ( ionic.Platform.isIOS() || ionic.Platform.isFullScreen || isInsetModal ) {
|
|
// if there are things below the scroll view account for them and
|
|
// subtract them from the keyboard height when resizing
|
|
// E - D E D
|
|
var scrollBottomOffsetToBottom = e.detail.viewportHeight - scrollBottomOffsetToTop;
|
|
|
|
// 0 or D - B if D > B E - B E - D
|
|
var keyboardOffset = Math.max(0, e.detail.keyboardHeight - scrollBottomOffsetToBottom);
|
|
|
|
ionic.requestAnimationFrame(function(){
|
|
// D - A or B - A if D > B D - A max(0, D - B)
|
|
scrollViewOffsetHeight = scrollViewOffsetHeight - keyboardOffset;
|
|
container.style.height = scrollViewOffsetHeight + "px";
|
|
container.style.overflow = "visible";
|
|
|
|
//update scroll view
|
|
self.resize();
|
|
});
|
|
}
|
|
|
|
self.isShrunkForKeyboard = true;
|
|
}
|
|
|
|
/*
|
|
* _______
|
|
* |---A---| <- top of scroll view
|
|
* | * | <- where we want to scroll to
|
|
* |--B-D--| <- keyboard, bottom of scroll view
|
|
* | C | <- input
|
|
* | |
|
|
* |___E___| <- bottom of viewport
|
|
*
|
|
* All commented calculations relative to the top of the viewport (ie E
|
|
* is the viewport height, not 0)
|
|
*/
|
|
// if the element is positioned under the keyboard scroll it into view
|
|
if (e.detail.isElementUnderKeyboard) {
|
|
|
|
ionic.requestAnimationFrame(function(){
|
|
container.scrollTop = 0;
|
|
// update D if we shrunk
|
|
if (self.isShrunkForKeyboard && !alreadyShrunk) {
|
|
scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
|
|
}
|
|
|
|
// middle of the scrollview, this is where we want to scroll to
|
|
// (D - A) / 2
|
|
var scrollMidpointOffset = scrollViewOffsetHeight * 0.5;
|
|
//console.log("container.offsetHeight: " + scrollViewOffsetHeight);
|
|
|
|
// middle of the input we want to scroll into view
|
|
// C
|
|
var inputMidpoint = ((e.detail.elementBottom + e.detail.elementTop) / 2);
|
|
|
|
// distance from middle of input to the bottom of the scroll view
|
|
// C - D C D
|
|
var inputMidpointOffsetToScrollBottom = inputMidpoint - scrollBottomOffsetToTop;
|
|
|
|
//C - D + (D - A)/2 C - D (D - A)/ 2
|
|
var scrollTop = inputMidpointOffsetToScrollBottom + scrollMidpointOffset;
|
|
|
|
if ( scrollTop > 0) {
|
|
if (ionic.Platform.isIOS()) ionic.tap.cloneFocusedInput(container, self);
|
|
self.scrollBy(0, scrollTop, true);
|
|
self.onScroll();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Only the first scrollView parent of the element that broadcasted this event
|
|
// (the active element that needs to be shown) should receive this event
|
|
e.stopPropagation();
|
|
};
|
|
|
|
self.resetScrollView = function() {
|
|
//return scrollview to original height once keyboard has hidden
|
|
if ( self.isShrunkForKeyboard ) {
|
|
self.isShrunkForKeyboard = false;
|
|
container.style.height = "";
|
|
container.style.overflow = "";
|
|
}
|
|
self.resize();
|
|
};
|
|
|
|
//Broadcasted when keyboard is shown on some platforms.
|
|
//See js/utils/keyboard.js
|
|
container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);
|
|
|
|
// Listen on document because container may not have had the last
|
|
// keyboardActiveElement, for example after closing a modal with a focused
|
|
// input and returning to a previously resized scroll view in an ion-content.
|
|
// Since we can only resize scroll views that are currently visible, just resize
|
|
// the current scroll view when the keyboard is closed.
|
|
document.addEventListener('resetScrollView', self.resetScrollView);
|
|
|
|
function getEventTouches(e) {
|
|
return e.touches && e.touches.length ? e.touches : [{
|
|
pageX: e.pageX,
|
|
pageY: e.pageY
|
|
}];
|
|
}
|
|
|
|
self.touchStart = function(e) {
|
|
self.startCoordinates = ionic.tap.pointerCoord(e);
|
|
|
|
if ( ionic.tap.ignoreScrollStart(e) ) {
|
|
return;
|
|
}
|
|
|
|
self.__isDown = true;
|
|
|
|
if ( ionic.tap.containsOrIsTextInput(e.target) || e.target.tagName === 'SELECT' ) {
|
|
// do not start if the target is a text input
|
|
// if there is a touchmove on this input, then we can start the scroll
|
|
self.__hasStarted = false;
|
|
return;
|
|
}
|
|
|
|
self.__isSelectable = true;
|
|
self.__enableScrollY = true;
|
|
self.__hasStarted = true;
|
|
self.doTouchStart(getEventTouches(e), e.timeStamp);
|
|
e.preventDefault();
|
|
};
|
|
|
|
self.touchMove = function(e) {
|
|
if (self.options.freeze || !self.__isDown ||
|
|
(!self.__isDown && e.defaultPrevented) ||
|
|
(e.target.tagName === 'TEXTAREA' && e.target.parentElement.querySelector(':focus')) ) {
|
|
return;
|
|
}
|
|
|
|
if ( !self.__hasStarted && ( ionic.tap.containsOrIsTextInput(e.target) || e.target.tagName === 'SELECT' ) ) {
|
|
// the target is a text input and scroll has started
|
|
// since the text input doesn't start on touchStart, do it here
|
|
self.__hasStarted = true;
|
|
self.doTouchStart(getEventTouches(e), e.timeStamp);
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (self.startCoordinates) {
|
|
// we have start coordinates, so get this touch move's current coordinates
|
|
var currentCoordinates = ionic.tap.pointerCoord(e);
|
|
|
|
if ( self.__isSelectable &&
|
|
ionic.tap.isTextInput(e.target) &&
|
|
Math.abs(self.startCoordinates.x - currentCoordinates.x) > 20 ) {
|
|
// user slid the text input's caret on its x axis, disable any future y scrolling
|
|
self.__enableScrollY = false;
|
|
self.__isSelectable = true;
|
|
}
|
|
|
|
if ( self.__enableScrollY && Math.abs(self.startCoordinates.y - currentCoordinates.y) > 10 ) {
|
|
// user scrolled the entire view on the y axis
|
|
// disabled being able to select text on an input
|
|
// hide the input which has focus, and show a cloned one that doesn't have focus
|
|
self.__isSelectable = false;
|
|
ionic.tap.cloneFocusedInput(container, self);
|
|
}
|
|
}
|
|
|
|
self.doTouchMove(getEventTouches(e), e.timeStamp, e.scale);
|
|
self.__isDown = true;
|
|
};
|
|
|
|
self.touchMoveBubble = function(e) {
|
|
if(self.__isDown && self.options.preventDefault) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
self.touchEnd = function(e) {
|
|
if (!self.__isDown) return;
|
|
|
|
self.doTouchEnd(e, e.timeStamp);
|
|
self.__isDown = false;
|
|
self.__hasStarted = false;
|
|
self.__isSelectable = true;
|
|
self.__enableScrollY = true;
|
|
|
|
if ( !self.__isDragging && !self.__isDecelerating && !self.__isAnimating ) {
|
|
ionic.tap.removeClonedInputs(container, self);
|
|
}
|
|
};
|
|
|
|
self.mouseWheel = ionic.animationFrameThrottle(function(e) {
|
|
var scrollParent = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'ionic-scroll');
|
|
if (!self.options.freeze && scrollParent === self.__container) {
|
|
|
|
self.hintResize();
|
|
self.scrollBy(
|
|
(e.wheelDeltaX || e.deltaX || 0) / self.options.wheelDampen,
|
|
(-e.wheelDeltaY || e.deltaY || 0) / self.options.wheelDampen
|
|
);
|
|
|
|
self.__fadeScrollbars('in');
|
|
clearTimeout(self.__wheelHideBarTimeout);
|
|
self.__wheelHideBarTimeout = setTimeout(function() {
|
|
self.__fadeScrollbars('out');
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
if ('ontouchstart' in window) {
|
|
// Touch Events
|
|
container.addEventListener("touchstart", self.touchStart, false);
|
|
if(self.options.preventDefault) container.addEventListener("touchmove", self.touchMoveBubble, false);
|
|
document.addEventListener("touchmove", self.touchMove, false);
|
|
document.addEventListener("touchend", self.touchEnd, false);
|
|
document.addEventListener("touchcancel", self.touchEnd, false);
|
|
document.addEventListener("wheel", self.mouseWheel, false);
|
|
|
|
} else if (window.navigator.pointerEnabled) {
|
|
// Pointer Events
|
|
container.addEventListener("pointerdown", self.touchStart, false);
|
|
if(self.options.preventDefault) container.addEventListener("pointermove", self.touchMoveBubble, false);
|
|
document.addEventListener("pointermove", self.touchMove, false);
|
|
document.addEventListener("pointerup", self.touchEnd, false);
|
|
document.addEventListener("pointercancel", self.touchEnd, false);
|
|
document.addEventListener("wheel", self.mouseWheel, false);
|
|
|
|
} else if (window.navigator.msPointerEnabled) {
|
|
// IE10, WP8 (Pointer Events)
|
|
container.addEventListener("MSPointerDown", self.touchStart, false);
|
|
if(self.options.preventDefault) container.addEventListener("MSPointerMove", self.touchMoveBubble, false);
|
|
document.addEventListener("MSPointerMove", self.touchMove, false);
|
|
document.addEventListener("MSPointerUp", self.touchEnd, false);
|
|
document.addEventListener("MSPointerCancel", self.touchEnd, false);
|
|
document.addEventListener("wheel", self.mouseWheel, false);
|
|
|
|
} else {
|
|
// Mouse Events
|
|
var mousedown = false;
|
|
|
|
self.mouseDown = function(e) {
|
|
if ( ionic.tap.ignoreScrollStart(e) || e.target.tagName === 'SELECT' ) {
|
|
return;
|
|
}
|
|
self.doTouchStart(getEventTouches(e), e.timeStamp);
|
|
|
|
if ( !ionic.tap.isTextInput(e.target) ) {
|
|
e.preventDefault();
|
|
}
|
|
mousedown = true;
|
|
};
|
|
|
|
self.mouseMove = function(e) {
|
|
if (self.options.freeze || !mousedown || (!mousedown && e.defaultPrevented)) {
|
|
return;
|
|
}
|
|
|
|
self.doTouchMove(getEventTouches(e), e.timeStamp);
|
|
|
|
mousedown = true;
|
|
};
|
|
|
|
self.mouseMoveBubble = function(e) {
|
|
if (mousedown && self.options.preventDefault) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
self.mouseUp = function(e) {
|
|
if (!mousedown) {
|
|
return;
|
|
}
|
|
|
|
self.doTouchEnd(e, e.timeStamp);
|
|
|
|
mousedown = false;
|
|
};
|
|
|
|
container.addEventListener("mousedown", self.mouseDown, false);
|
|
if(self.options.preventDefault) container.addEventListener("mousemove", self.mouseMoveBubble, false);
|
|
document.addEventListener("mousemove", self.mouseMove, false);
|
|
document.addEventListener("mouseup", self.mouseUp, false);
|
|
document.addEventListener('mousewheel', self.mouseWheel, false);
|
|
document.addEventListener('wheel', self.mouseWheel, false);
|
|
}
|
|
},
|
|
|
|
__cleanup: function() {
|
|
var self = this;
|
|
var container = self.__container;
|
|
|
|
container.removeEventListener('touchstart', self.touchStart);
|
|
container.removeEventListener('touchmove', self.touchMoveBubble);
|
|
document.removeEventListener('touchmove', self.touchMove);
|
|
document.removeEventListener('touchend', self.touchEnd);
|
|
document.removeEventListener('touchcancel', self.touchEnd);
|
|
|
|
container.removeEventListener("pointerdown", self.touchStart);
|
|
container.removeEventListener("pointermove", self.touchMoveBubble);
|
|
document.removeEventListener("pointermove", self.touchMove);
|
|
document.removeEventListener("pointerup", self.touchEnd);
|
|
document.removeEventListener("pointercancel", self.touchEnd);
|
|
|
|
container.removeEventListener("MSPointerDown", self.touchStart);
|
|
container.removeEventListener("MSPointerMove", self.touchMoveBubble);
|
|
document.removeEventListener("MSPointerMove", self.touchMove);
|
|
document.removeEventListener("MSPointerUp", self.touchEnd);
|
|
document.removeEventListener("MSPointerCancel", self.touchEnd);
|
|
|
|
container.removeEventListener("mousedown", self.mouseDown);
|
|
container.removeEventListener("mousemove", self.mouseMoveBubble);
|
|
document.removeEventListener("mousemove", self.mouseMove);
|
|
document.removeEventListener("mouseup", self.mouseUp);
|
|
document.removeEventListener('mousewheel', self.mouseWheel);
|
|
document.removeEventListener('wheel', self.mouseWheel);
|
|
|
|
container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView);
|
|
document.removeEventListener('resetScrollView', self.resetScrollView);
|
|
|
|
ionic.tap.removeClonedInputs(container, self);
|
|
|
|
delete self.__container;
|
|
delete self.__content;
|
|
delete self.__indicatorX;
|
|
delete self.__indicatorY;
|
|
delete self.options.el;
|
|
|
|
self.__callback = self.scrollChildIntoView = self.resetScrollView = NOOP;
|
|
|
|
self.mouseMove = self.mouseDown = self.mouseUp = self.mouseWheel =
|
|
self.touchStart = self.touchMove = self.touchEnd = self.touchCancel = NOOP;
|
|
|
|
self.resize = self.scrollTo = self.zoomTo =
|
|
self.__scrollingComplete = NOOP;
|
|
container = null;
|
|
},
|
|
|
|
/** Create a scroll bar div with the given direction **/
|
|
__createScrollbar: function(direction) {
|
|
var bar = document.createElement('div'),
|
|
indicator = document.createElement('div');
|
|
|
|
indicator.className = 'scroll-bar-indicator scroll-bar-fade-out';
|
|
|
|
if (direction == 'h') {
|
|
bar.className = 'scroll-bar scroll-bar-h';
|
|
} else {
|
|
bar.className = 'scroll-bar scroll-bar-v';
|
|
}
|
|
|
|
bar.appendChild(indicator);
|
|
return bar;
|
|
},
|
|
|
|
__createScrollbars: function() {
|
|
var self = this;
|
|
var indicatorX, indicatorY;
|
|
|
|
if (self.options.scrollingX) {
|
|
indicatorX = {
|
|
el: self.__createScrollbar('h'),
|
|
sizeRatio: 1
|
|
};
|
|
indicatorX.indicator = indicatorX.el.children[0];
|
|
|
|
if (self.options.scrollbarX) {
|
|
self.__container.appendChild(indicatorX.el);
|
|
}
|
|
self.__indicatorX = indicatorX;
|
|
}
|
|
|
|
if (self.options.scrollingY) {
|
|
indicatorY = {
|
|
el: self.__createScrollbar('v'),
|
|
sizeRatio: 1
|
|
};
|
|
indicatorY.indicator = indicatorY.el.children[0];
|
|
|
|
if (self.options.scrollbarY) {
|
|
self.__container.appendChild(indicatorY.el);
|
|
}
|
|
self.__indicatorY = indicatorY;
|
|
}
|
|
},
|
|
|
|
__resizeScrollbars: function() {
|
|
var self = this;
|
|
|
|
// Update horiz bar
|
|
if (self.__indicatorX) {
|
|
var width = Math.max(Math.round(self.__clientWidth * self.__clientWidth / (self.__contentWidth)), 20);
|
|
if (width > self.__contentWidth) {
|
|
width = 0;
|
|
}
|
|
if (width !== self.__indicatorX.size) {
|
|
ionic.requestAnimationFrame(function(){
|
|
self.__indicatorX.indicator.style.width = width + 'px';
|
|
});
|
|
}
|
|
self.__indicatorX.size = width;
|
|
self.__indicatorX.minScale = self.options.minScrollbarSizeX / width;
|
|
self.__indicatorX.maxPos = self.__clientWidth - width;
|
|
self.__indicatorX.sizeRatio = self.__maxScrollLeft ? self.__indicatorX.maxPos / self.__maxScrollLeft : 1;
|
|
}
|
|
|
|
// Update vert bar
|
|
if (self.__indicatorY) {
|
|
var height = Math.max(Math.round(self.__clientHeight * self.__clientHeight / (self.__contentHeight)), 20);
|
|
if (height > self.__contentHeight) {
|
|
height = 0;
|
|
}
|
|
if (height !== self.__indicatorY.size) {
|
|
ionic.requestAnimationFrame(function(){
|
|
self.__indicatorY && (self.__indicatorY.indicator.style.height = height + 'px');
|
|
});
|
|
}
|
|
self.__indicatorY.size = height;
|
|
self.__indicatorY.minScale = self.options.minScrollbarSizeY / height;
|
|
self.__indicatorY.maxPos = self.__clientHeight - height;
|
|
self.__indicatorY.sizeRatio = self.__maxScrollTop ? self.__indicatorY.maxPos / self.__maxScrollTop : 1;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Move and scale the scrollbars as the page scrolls.
|
|
*/
|
|
__repositionScrollbars: function() {
|
|
var self = this,
|
|
heightScale, widthScale,
|
|
widthDiff, heightDiff,
|
|
x, y,
|
|
xstop = 0, ystop = 0;
|
|
|
|
if (self.__indicatorX) {
|
|
// Handle the X scrollbar
|
|
|
|
// Don't go all the way to the right if we have a vertical scrollbar as well
|
|
if (self.__indicatorY) xstop = 10;
|
|
|
|
x = Math.round(self.__indicatorX.sizeRatio * self.__scrollLeft) || 0;
|
|
|
|
// The the difference between the last content X position, and our overscrolled one
|
|
widthDiff = self.__scrollLeft - (self.__maxScrollLeft - xstop);
|
|
|
|
if (self.__scrollLeft < 0) {
|
|
|
|
widthScale = Math.max(self.__indicatorX.minScale,
|
|
(self.__indicatorX.size - Math.abs(self.__scrollLeft)) / self.__indicatorX.size);
|
|
|
|
// Stay at left
|
|
x = 0;
|
|
|
|
// Make sure scale is transformed from the left/center origin point
|
|
self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'left center';
|
|
} else if (widthDiff > 0) {
|
|
|
|
widthScale = Math.max(self.__indicatorX.minScale,
|
|
(self.__indicatorX.size - widthDiff) / self.__indicatorX.size);
|
|
|
|
// Stay at the furthest x for the scrollable viewport
|
|
x = self.__indicatorX.maxPos - xstop;
|
|
|
|
// Make sure scale is transformed from the right/center origin point
|
|
self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'right center';
|
|
|
|
} else {
|
|
|
|
// Normal motion
|
|
x = Math.min(self.__maxScrollLeft, Math.max(0, x));
|
|
widthScale = 1;
|
|
|
|
}
|
|
|
|
var translate3dX = 'translate3d(' + x + 'px, 0, 0) scaleX(' + widthScale + ')';
|
|
if (self.__indicatorX.transformProp !== translate3dX) {
|
|
self.__indicatorX.indicator.style[self.__transformProperty] = translate3dX;
|
|
self.__indicatorX.transformProp = translate3dX;
|
|
}
|
|
}
|
|
|
|
if (self.__indicatorY) {
|
|
|
|
y = Math.round(self.__indicatorY.sizeRatio * self.__scrollTop) || 0;
|
|
|
|
// Don't go all the way to the right if we have a vertical scrollbar as well
|
|
if (self.__indicatorX) ystop = 10;
|
|
|
|
heightDiff = self.__scrollTop - (self.__maxScrollTop - ystop);
|
|
|
|
if (self.__scrollTop < 0) {
|
|
|
|
heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - Math.abs(self.__scrollTop)) / self.__indicatorY.size);
|
|
|
|
// Stay at top
|
|
y = 0;
|
|
|
|
// Make sure scale is transformed from the center/top origin point
|
|
if (self.__indicatorY.originProp !== 'center top') {
|
|
self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center top';
|
|
self.__indicatorY.originProp = 'center top';
|
|
}
|
|
|
|
} else if (heightDiff > 0) {
|
|
|
|
heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - heightDiff) / self.__indicatorY.size);
|
|
|
|
// Stay at bottom of scrollable viewport
|
|
y = self.__indicatorY.maxPos - ystop;
|
|
|
|
// Make sure scale is transformed from the center/bottom origin point
|
|
if (self.__indicatorY.originProp !== 'center bottom') {
|
|
self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center bottom';
|
|
self.__indicatorY.originProp = 'center bottom';
|
|
}
|
|
|
|
} else {
|
|
|
|
// Normal motion
|
|
y = Math.min(self.__maxScrollTop, Math.max(0, y));
|
|
heightScale = 1;
|
|
|
|
}
|
|
|
|
var translate3dY = 'translate3d(0,' + y + 'px, 0) scaleY(' + heightScale + ')';
|
|
if (self.__indicatorY.transformProp !== translate3dY) {
|
|
self.__indicatorY.indicator.style[self.__transformProperty] = translate3dY;
|
|
self.__indicatorY.transformProp = translate3dY;
|
|
}
|
|
}
|
|
},
|
|
|
|
__fadeScrollbars: function(direction, delay) {
|
|
var self = this;
|
|
|
|
if (!self.options.scrollbarsFade) {
|
|
return;
|
|
}
|
|
|
|
var className = 'scroll-bar-fade-out';
|
|
|
|
if (self.options.scrollbarsFade === true) {
|
|
clearTimeout(self.__scrollbarFadeTimeout);
|
|
|
|
if (direction == 'in') {
|
|
if (self.__indicatorX) { self.__indicatorX.indicator.classList.remove(className); }
|
|
if (self.__indicatorY) { self.__indicatorY.indicator.classList.remove(className); }
|
|
} else {
|
|
self.__scrollbarFadeTimeout = setTimeout(function() {
|
|
if (self.__indicatorX) { self.__indicatorX.indicator.classList.add(className); }
|
|
if (self.__indicatorY) { self.__indicatorY.indicator.classList.add(className); }
|
|
}, delay || self.options.scrollbarFadeDelay);
|
|
}
|
|
}
|
|
},
|
|
|
|
__scrollingComplete: function() {
|
|
this.options.scrollingComplete();
|
|
ionic.tap.removeClonedInputs(this.__container, this);
|
|
this.__fadeScrollbars('out');
|
|
},
|
|
|
|
resize: function(continueScrolling) {
|
|
var self = this;
|
|
if (!self.__container || !self.options) return;
|
|
|
|
// Update Scroller dimensions for changed content
|
|
// Add padding to bottom of content
|
|
self.setDimensions(
|
|
self.__container.clientWidth,
|
|
self.__container.clientHeight,
|
|
self.options.getContentWidth(),
|
|
self.options.getContentHeight(),
|
|
continueScrolling
|
|
);
|
|
},
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
PUBLIC API
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
getRenderFn: function() {
|
|
var self = this;
|
|
|
|
var content = self.__content;
|
|
|
|
var docStyle = document.documentElement.style;
|
|
|
|
var engine;
|
|
if ('MozAppearance' in docStyle) {
|
|
engine = 'gecko';
|
|
} else if ('WebkitAppearance' in docStyle) {
|
|
engine = 'webkit';
|
|
} else if (typeof navigator.cpuClass === 'string') {
|
|
engine = 'trident';
|
|
}
|
|
|
|
var vendorPrefix = {
|
|
trident: 'ms',
|
|
gecko: 'Moz',
|
|
webkit: 'Webkit',
|
|
presto: 'O'
|
|
}[engine];
|
|
|
|
var helperElem = document.createElement("div");
|
|
var undef;
|
|
|
|
var perspectiveProperty = vendorPrefix + "Perspective";
|
|
var transformProperty = vendorPrefix + "Transform";
|
|
var transformOriginProperty = vendorPrefix + 'TransformOrigin';
|
|
|
|
self.__perspectiveProperty = transformProperty;
|
|
self.__transformProperty = transformProperty;
|
|
self.__transformOriginProperty = transformOriginProperty;
|
|
|
|
if (helperElem.style[perspectiveProperty] !== undef) {
|
|
|
|
return function(left, top, zoom, wasResize) {
|
|
var translate3d = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0) scale(' + zoom + ')';
|
|
if (translate3d !== self.contentTransform) {
|
|
content.style[transformProperty] = translate3d;
|
|
self.contentTransform = translate3d;
|
|
}
|
|
self.__repositionScrollbars();
|
|
if (!wasResize) {
|
|
self.triggerScrollEvent();
|
|
}
|
|
};
|
|
|
|
} else if (helperElem.style[transformProperty] !== undef) {
|
|
|
|
return function(left, top, zoom, wasResize) {
|
|
content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px) scale(' + zoom + ')';
|
|
self.__repositionScrollbars();
|
|
if (!wasResize) {
|
|
self.triggerScrollEvent();
|
|
}
|
|
};
|
|
|
|
} else {
|
|
|
|
return function(left, top, zoom, wasResize) {
|
|
content.style.marginLeft = left ? (-left / zoom) + 'px' : '';
|
|
content.style.marginTop = top ? (-top / zoom) + 'px' : '';
|
|
content.style.zoom = zoom || '';
|
|
self.__repositionScrollbars();
|
|
if (!wasResize) {
|
|
self.triggerScrollEvent();
|
|
}
|
|
};
|
|
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* Configures the dimensions of the client (outer) and content (inner) elements.
|
|
* Requires the available space for the outer element and the outer size of the inner element.
|
|
* All values which are falsy (null or zero etc.) are ignored and the old value is kept.
|
|
*
|
|
* @param clientWidth {Integer} Inner width of outer element
|
|
* @param clientHeight {Integer} Inner height of outer element
|
|
* @param contentWidth {Integer} Outer width of inner element
|
|
* @param contentHeight {Integer} Outer height of inner element
|
|
*/
|
|
setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight, continueScrolling) {
|
|
var self = this;
|
|
|
|
if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) {
|
|
// this scrollview isn't rendered, don't bother
|
|
return;
|
|
}
|
|
|
|
// Only update values which are defined
|
|
if (clientWidth === +clientWidth) {
|
|
self.__clientWidth = clientWidth;
|
|
}
|
|
|
|
if (clientHeight === +clientHeight) {
|
|
self.__clientHeight = clientHeight;
|
|
}
|
|
|
|
if (contentWidth === +contentWidth) {
|
|
self.__contentWidth = contentWidth;
|
|
}
|
|
|
|
if (contentHeight === +contentHeight) {
|
|
self.__contentHeight = contentHeight;
|
|
}
|
|
|
|
// Refresh maximums
|
|
self.__computeScrollMax();
|
|
self.__resizeScrollbars();
|
|
|
|
// Refresh scroll position
|
|
if (!continueScrolling) {
|
|
self.scrollTo(self.__scrollLeft, self.__scrollTop, true, null, true);
|
|
}
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Sets the client coordinates in relation to the document.
|
|
*
|
|
* @param left {Integer} Left position of outer element
|
|
* @param top {Integer} Top position of outer element
|
|
*/
|
|
setPosition: function(left, top) {
|
|
this.__clientLeft = left || 0;
|
|
this.__clientTop = top || 0;
|
|
},
|
|
|
|
|
|
/**
|
|
* Configures the snapping (when snapping is active)
|
|
*
|
|
* @param width {Integer} Snapping width
|
|
* @param height {Integer} Snapping height
|
|
*/
|
|
setSnapSize: function(width, height) {
|
|
this.__snapWidth = width;
|
|
this.__snapHeight = height;
|
|
},
|
|
|
|
|
|
/**
|
|
* Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
|
|
* the user event is released during visibility of this zone. This was introduced by some apps on iOS like
|
|
* the official Twitter client.
|
|
*
|
|
* @param height {Integer} Height of pull-to-refresh zone on top of rendered list
|
|
* @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
|
|
* @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
|
|
* @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
|
|
* @param showCallback {Function} Callback to execute when the refresher should be shown. This is for showing the refresher during a negative scrollTop.
|
|
* @param hideCallback {Function} Callback to execute when the refresher should be hidden. This is for hiding the refresher when it's behind the nav bar.
|
|
* @param tailCallback {Function} Callback to execute just before the refresher returns to it's original state. This is for zooming out the refresher.
|
|
* @param pullProgressCallback Callback to state the progress while pulling to refresh
|
|
*/
|
|
activatePullToRefresh: function(height, refresherMethods) {
|
|
var self = this;
|
|
|
|
self.__refreshHeight = height;
|
|
self.__refreshActivate = function() { ionic.requestAnimationFrame(refresherMethods.activate); };
|
|
self.__refreshDeactivate = function() { ionic.requestAnimationFrame(refresherMethods.deactivate); };
|
|
self.__refreshStart = function() { ionic.requestAnimationFrame(refresherMethods.start); };
|
|
self.__refreshShow = function() { ionic.requestAnimationFrame(refresherMethods.show); };
|
|
self.__refreshHide = function() { ionic.requestAnimationFrame(refresherMethods.hide); };
|
|
self.__refreshTail = function() { ionic.requestAnimationFrame(refresherMethods.tail); };
|
|
self.__refreshTailTime = 100;
|
|
self.__minSpinTime = 600;
|
|
},
|
|
|
|
|
|
/**
|
|
* Starts pull-to-refresh manually.
|
|
*/
|
|
triggerPullToRefresh: function() {
|
|
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
|
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
|
this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);
|
|
|
|
var d = new Date();
|
|
this.refreshStartTime = d.getTime();
|
|
|
|
if (this.__refreshStart) {
|
|
this.__refreshStart();
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* Signalizes that pull-to-refresh is finished.
|
|
*/
|
|
finishPullToRefresh: function() {
|
|
var self = this;
|
|
// delay to make sure the spinner has a chance to spin for a split second before it's dismissed
|
|
var d = new Date();
|
|
var delay = 0;
|
|
if (self.refreshStartTime + self.__minSpinTime > d.getTime()) {
|
|
delay = self.refreshStartTime + self.__minSpinTime - d.getTime();
|
|
}
|
|
setTimeout(function() {
|
|
if (self.__refreshTail) {
|
|
self.__refreshTail();
|
|
}
|
|
setTimeout(function() {
|
|
self.__refreshActive = false;
|
|
if (self.__refreshDeactivate) {
|
|
self.__refreshDeactivate();
|
|
}
|
|
if (self.__refreshHide) {
|
|
self.__refreshHide();
|
|
}
|
|
|
|
self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
|
|
}, self.__refreshTailTime);
|
|
}, delay);
|
|
},
|
|
|
|
|
|
/**
|
|
* Returns the scroll position and zooming values
|
|
*
|
|
* @return {Map} `left` and `top` scroll position and `zoom` level
|
|
*/
|
|
getValues: function() {
|
|
return {
|
|
left: this.__scrollLeft,
|
|
top: this.__scrollTop,
|
|
zoom: this.__zoomLevel
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* Returns the maximum scroll values
|
|
*
|
|
* @return {Map} `left` and `top` maximum scroll values
|
|
*/
|
|
getScrollMax: function() {
|
|
return {
|
|
left: this.__maxScrollLeft,
|
|
top: this.__maxScrollTop
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* Zooms to the given level. Supports optional animation. Zooms
|
|
* the center when no coordinates are given.
|
|
*
|
|
* @param level {Number} Level to zoom to
|
|
* @param animate {Boolean} Whether to use animation
|
|
* @param originLeft {Number} Zoom in at given left coordinate
|
|
* @param originTop {Number} Zoom in at given top coordinate
|
|
*/
|
|
zoomTo: function(level, animate, originLeft, originTop) {
|
|
var self = this;
|
|
|
|
if (!self.options.zooming) {
|
|
throw new Error("Zooming is not enabled!");
|
|
}
|
|
|
|
// Stop deceleration
|
|
if (self.__isDecelerating) {
|
|
zyngaCore.effect.Animate.stop(self.__isDecelerating);
|
|
self.__isDecelerating = false;
|
|
}
|
|
|
|
var oldLevel = self.__zoomLevel;
|
|
|
|
// Normalize input origin to center of viewport if not defined
|
|
if (originLeft == null) {
|
|
originLeft = self.__clientWidth / 2;
|
|
}
|
|
|
|
if (originTop == null) {
|
|
originTop = self.__clientHeight / 2;
|
|
}
|
|
|
|
// Limit level according to configuration
|
|
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
|
|
|
|
// Recompute maximum values while temporary tweaking maximum scroll ranges
|
|
self.__computeScrollMax(level);
|
|
|
|
// Recompute left and top coordinates based on new zoom level
|
|
var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft;
|
|
var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop;
|
|
|
|
// Limit x-axis
|
|
if (left > self.__maxScrollLeft) {
|
|
left = self.__maxScrollLeft;
|
|
} else if (left < 0) {
|
|
left = 0;
|
|
}
|
|
|
|
// Limit y-axis
|
|
if (top > self.__maxScrollTop) {
|
|
top = self.__maxScrollTop;
|
|
} else if (top < 0) {
|
|
top = 0;
|
|
}
|
|
|
|
// Push values out
|
|
self.__publish(left, top, level, animate);
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Zooms the content by the given factor.
|
|
*
|
|
* @param factor {Number} Zoom by given factor
|
|
* @param animate {Boolean} Whether to use animation
|
|
* @param originLeft {Number} Zoom in at given left coordinate
|
|
* @param originTop {Number} Zoom in at given top coordinate
|
|
*/
|
|
zoomBy: function(factor, animate, originLeft, originTop) {
|
|
this.zoomTo(this.__zoomLevel * factor, animate, originLeft, originTop);
|
|
},
|
|
|
|
|
|
/**
|
|
* Scrolls to the given position. Respect limitations and snapping automatically.
|
|
*
|
|
* @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
|
|
* @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
|
|
* @param animate {Boolean} Whether the scrolling should happen using an animation
|
|
* @param zoom {Number} Zoom level to go to
|
|
*/
|
|
scrollTo: function(left, top, animate, zoom, wasResize) {
|
|
var self = this;
|
|
|
|
// Stop deceleration
|
|
if (self.__isDecelerating) {
|
|
zyngaCore.effect.Animate.stop(self.__isDecelerating);
|
|
self.__isDecelerating = false;
|
|
}
|
|
|
|
// Correct coordinates based on new zoom level
|
|
if (zoom != null && zoom !== self.__zoomLevel) {
|
|
|
|
if (!self.options.zooming) {
|
|
throw new Error("Zooming is not enabled!");
|
|
}
|
|
|
|
left *= zoom;
|
|
top *= zoom;
|
|
|
|
// Recompute maximum values while temporary tweaking maximum scroll ranges
|
|
self.__computeScrollMax(zoom);
|
|
|
|
} else {
|
|
|
|
// Keep zoom when not defined
|
|
zoom = self.__zoomLevel;
|
|
|
|
}
|
|
|
|
if (!self.options.scrollingX) {
|
|
|
|
left = self.__scrollLeft;
|
|
|
|
} else {
|
|
|
|
if (self.options.paging) {
|
|
left = Math.round(left / self.__clientWidth) * self.__clientWidth;
|
|
} else if (self.options.snapping) {
|
|
left = Math.round(left / self.__snapWidth) * self.__snapWidth;
|
|
}
|
|
|
|
}
|
|
|
|
if (!self.options.scrollingY) {
|
|
|
|
top = self.__scrollTop;
|
|
|
|
} else {
|
|
|
|
if (self.options.paging) {
|
|
top = Math.round(top / self.__clientHeight) * self.__clientHeight;
|
|
} else if (self.options.snapping) {
|
|
top = Math.round(top / self.__snapHeight) * self.__snapHeight;
|
|
}
|
|
|
|
}
|
|
|
|
// Limit for allowed ranges
|
|
left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
|
|
top = Math.max(Math.min(self.__maxScrollTop, top), 0);
|
|
|
|
// Don't animate when no change detected, still call publish to make sure
|
|
// that rendered position is really in-sync with internal data
|
|
if (left === self.__scrollLeft && top === self.__scrollTop) {
|
|
animate = false;
|
|
}
|
|
|
|
// Publish new values
|
|
self.__publish(left, top, zoom, animate, wasResize);
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Scroll by the given offset
|
|
*
|
|
* @param left {Number} Scroll x-axis by given offset
|
|
* @param top {Number} Scroll y-axis by given offset
|
|
* @param animate {Boolean} Whether to animate the given change
|
|
*/
|
|
scrollBy: function(left, top, animate) {
|
|
var self = this;
|
|
|
|
var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
|
|
var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
|
|
|
|
self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
|
|
},
|
|
|
|
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
EVENT CALLBACKS
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Mouse wheel handler for zooming support
|
|
*/
|
|
doMouseZoom: function(wheelDelta, timeStamp, pageX, pageY) {
|
|
var change = wheelDelta > 0 ? 0.97 : 1.03;
|
|
return this.zoomTo(this.__zoomLevel * change, false, pageX - this.__clientLeft, pageY - this.__clientTop);
|
|
},
|
|
|
|
/**
|
|
* Touch start handler for scrolling support
|
|
*/
|
|
doTouchStart: function(touches, timeStamp) {
|
|
var self = this;
|
|
|
|
// remember if the deceleration was just stopped
|
|
self.__decStopped = !!(self.__isDecelerating || self.__isAnimating);
|
|
|
|
self.hintResize();
|
|
|
|
if (timeStamp instanceof Date) {
|
|
timeStamp = timeStamp.valueOf();
|
|
}
|
|
if (typeof timeStamp !== "number") {
|
|
timeStamp = Date.now();
|
|
}
|
|
|
|
// Reset interruptedAnimation flag
|
|
self.__interruptedAnimation = true;
|
|
|
|
// Stop deceleration
|
|
if (self.__isDecelerating) {
|
|
zyngaCore.effect.Animate.stop(self.__isDecelerating);
|
|
self.__isDecelerating = false;
|
|
self.__interruptedAnimation = true;
|
|
}
|
|
|
|
// Stop animation
|
|
if (self.__isAnimating) {
|
|
zyngaCore.effect.Animate.stop(self.__isAnimating);
|
|
self.__isAnimating = false;
|
|
self.__interruptedAnimation = true;
|
|
}
|
|
|
|
// Use center point when dealing with two fingers
|
|
var currentTouchLeft, currentTouchTop;
|
|
var isSingleTouch = touches.length === 1;
|
|
if (isSingleTouch) {
|
|
currentTouchLeft = touches[0].pageX;
|
|
currentTouchTop = touches[0].pageY;
|
|
} else {
|
|
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
|
|
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
|
|
}
|
|
|
|
// Store initial positions
|
|
self.__initialTouchLeft = currentTouchLeft;
|
|
self.__initialTouchTop = currentTouchTop;
|
|
|
|
// Store initial touchList for scale calculation
|
|
self.__initialTouches = touches;
|
|
|
|
// Store current zoom level
|
|
self.__zoomLevelStart = self.__zoomLevel;
|
|
|
|
// Store initial touch positions
|
|
self.__lastTouchLeft = currentTouchLeft;
|
|
self.__lastTouchTop = currentTouchTop;
|
|
|
|
// Store initial move time stamp
|
|
self.__lastTouchMove = timeStamp;
|
|
|
|
// Reset initial scale
|
|
self.__lastScale = 1;
|
|
|
|
// Reset locking flags
|
|
self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
|
|
self.__enableScrollY = !isSingleTouch && self.options.scrollingY;
|
|
|
|
// Reset tracking flag
|
|
self.__isTracking = true;
|
|
|
|
// Reset deceleration complete flag
|
|
self.__didDecelerationComplete = false;
|
|
|
|
// Dragging starts directly with two fingers, otherwise lazy with an offset
|
|
self.__isDragging = !isSingleTouch;
|
|
|
|
// Some features are disabled in multi touch scenarios
|
|
self.__isSingleTouch = isSingleTouch;
|
|
|
|
// Clearing data structure
|
|
self.__positions = [];
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Touch move handler for scrolling support
|
|
*/
|
|
doTouchMove: function(touches, timeStamp, scale) {
|
|
if (timeStamp instanceof Date) {
|
|
timeStamp = timeStamp.valueOf();
|
|
}
|
|
if (typeof timeStamp !== "number") {
|
|
timeStamp = Date.now();
|
|
}
|
|
|
|
var self = this;
|
|
|
|
// Ignore event when tracking is not enabled (event might be outside of element)
|
|
if (!self.__isTracking) {
|
|
return;
|
|
}
|
|
|
|
var currentTouchLeft, currentTouchTop;
|
|
|
|
// Compute move based around of center of fingers
|
|
if (touches.length === 2) {
|
|
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
|
|
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
|
|
|
|
// Calculate scale when not present and only when touches are used
|
|
if (!scale && self.options.zooming) {
|
|
scale = self.__getScale(self.__initialTouches, touches);
|
|
}
|
|
} else {
|
|
currentTouchLeft = touches[0].pageX;
|
|
currentTouchTop = touches[0].pageY;
|
|
}
|
|
|
|
var positions = self.__positions;
|
|
|
|
// Are we already is dragging mode?
|
|
if (self.__isDragging) {
|
|
self.__decStopped = false;
|
|
|
|
// Compute move distance
|
|
var moveX = currentTouchLeft - self.__lastTouchLeft;
|
|
var moveY = currentTouchTop - self.__lastTouchTop;
|
|
|
|
// Read previous scroll position and zooming
|
|
var scrollLeft = self.__scrollLeft;
|
|
var scrollTop = self.__scrollTop;
|
|
var level = self.__zoomLevel;
|
|
|
|
// Work with scaling
|
|
if (scale != null && self.options.zooming) {
|
|
|
|
var oldLevel = level;
|
|
|
|
// Recompute level based on previous scale and new scale
|
|
level = level / self.__lastScale * scale;
|
|
|
|
// Limit level according to configuration
|
|
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
|
|
|
|
// Only do further compution when change happened
|
|
if (oldLevel !== level) {
|
|
|
|
// Compute relative event position to container
|
|
var currentTouchLeftRel = currentTouchLeft - self.__clientLeft;
|
|
var currentTouchTopRel = currentTouchTop - self.__clientTop;
|
|
|
|
// Recompute left and top coordinates based on new zoom level
|
|
scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel;
|
|
scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel;
|
|
|
|
// Recompute max scroll values
|
|
self.__computeScrollMax(level);
|
|
|
|
}
|
|
}
|
|
|
|
if (self.__enableScrollX) {
|
|
|
|
scrollLeft -= moveX * self.options.speedMultiplier;
|
|
var maxScrollLeft = self.__maxScrollLeft;
|
|
|
|
if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
|
|
|
|
// Slow down on the edges
|
|
if (self.options.bouncing) {
|
|
|
|
scrollLeft += (moveX / 2 * self.options.speedMultiplier);
|
|
|
|
} else if (scrollLeft > maxScrollLeft) {
|
|
|
|
scrollLeft = maxScrollLeft;
|
|
|
|
} else {
|
|
|
|
scrollLeft = 0;
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compute new vertical scroll position
|
|
if (self.__enableScrollY) {
|
|
|
|
scrollTop -= moveY * self.options.speedMultiplier;
|
|
var maxScrollTop = self.__maxScrollTop;
|
|
|
|
if (scrollTop > maxScrollTop || scrollTop < 0) {
|
|
|
|
// Slow down on the edges
|
|
if (self.options.bouncing || (self.__refreshHeight && scrollTop < 0)) {
|
|
|
|
scrollTop += (moveY / 2 * self.options.speedMultiplier);
|
|
|
|
// Support pull-to-refresh (only when only y is scrollable)
|
|
if (!self.__enableScrollX && self.__refreshHeight != null) {
|
|
|
|
// hide the refresher when it's behind the header bar in case of header transparency
|
|
if (scrollTop < 0) {
|
|
self.__refreshHidden = false;
|
|
self.__refreshShow();
|
|
} else {
|
|
self.__refreshHide();
|
|
self.__refreshHidden = true;
|
|
}
|
|
|
|
if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {
|
|
|
|
self.__refreshActive = true;
|
|
if (self.__refreshActivate) {
|
|
self.__refreshActivate();
|
|
}
|
|
|
|
} else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {
|
|
|
|
self.__refreshActive = false;
|
|
if (self.__refreshDeactivate) {
|
|
self.__refreshDeactivate();
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
} else if (scrollTop > maxScrollTop) {
|
|
|
|
scrollTop = maxScrollTop;
|
|
|
|
} else {
|
|
|
|
scrollTop = 0;
|
|
|
|
}
|
|
} else if (self.__refreshHeight && !self.__refreshHidden) {
|
|
// if a positive scroll value and the refresher is still not hidden, hide it
|
|
self.__refreshHide();
|
|
self.__refreshHidden = true;
|
|
}
|
|
}
|
|
|
|
// Keep list from growing infinitely (holding min 10, max 20 measure points)
|
|
if (positions.length > 60) {
|
|
positions.splice(0, 30);
|
|
}
|
|
|
|
// Track scroll movement for decleration
|
|
positions.push(scrollLeft, scrollTop, timeStamp);
|
|
|
|
// Sync scroll position
|
|
self.__publish(scrollLeft, scrollTop, level);
|
|
|
|
// Otherwise figure out whether we are switching into dragging mode now.
|
|
} else {
|
|
|
|
var minimumTrackingForScroll = self.options.locking ? 3 : 0;
|
|
var minimumTrackingForDrag = 5;
|
|
|
|
var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
|
|
var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);
|
|
|
|
self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
|
|
self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;
|
|
|
|
positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);
|
|
|
|
self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
|
|
if (self.__isDragging) {
|
|
self.__interruptedAnimation = false;
|
|
self.__fadeScrollbars('in');
|
|
}
|
|
|
|
}
|
|
|
|
// Update last touch positions and time stamp for next event
|
|
self.__lastTouchLeft = currentTouchLeft;
|
|
self.__lastTouchTop = currentTouchTop;
|
|
self.__lastTouchMove = timeStamp;
|
|
self.__lastScale = scale;
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Touch end handler for scrolling support
|
|
*/
|
|
doTouchEnd: function(e, timeStamp) {
|
|
if (timeStamp instanceof Date) {
|
|
timeStamp = timeStamp.valueOf();
|
|
}
|
|
if (typeof timeStamp !== "number") {
|
|
timeStamp = Date.now();
|
|
}
|
|
|
|
var self = this;
|
|
|
|
// Ignore event when tracking is not enabled (no touchstart event on element)
|
|
// This is required as this listener ('touchmove') sits on the document and not on the element itself.
|
|
if (!self.__isTracking) {
|
|
return;
|
|
}
|
|
|
|
// Not touching anymore (when two finger hit the screen there are two touch end events)
|
|
self.__isTracking = false;
|
|
|
|
// Be sure to reset the dragging flag now. Here we also detect whether
|
|
// the finger has moved fast enough to switch into a deceleration animation.
|
|
if (self.__isDragging) {
|
|
|
|
// Reset dragging flag
|
|
self.__isDragging = false;
|
|
|
|
// Start deceleration
|
|
// Verify that the last move detected was in some relevant time frame
|
|
if (self.__isSingleTouch && self.options.animating && (timeStamp - self.__lastTouchMove) <= 100) {
|
|
|
|
// Then figure out what the scroll position was about 100ms ago
|
|
var positions = self.__positions;
|
|
var endPos = positions.length - 1;
|
|
var startPos = endPos;
|
|
|
|
// Move pointer to position measured 100ms ago
|
|
for (var i = endPos; i > 0 && positions[i] > (self.__lastTouchMove - 100); i -= 3) {
|
|
startPos = i;
|
|
}
|
|
|
|
// If start and stop position is identical in a 100ms timeframe,
|
|
// we cannot compute any useful deceleration.
|
|
if (startPos !== endPos) {
|
|
|
|
// Compute relative movement between these two points
|
|
var timeOffset = positions[endPos] - positions[startPos];
|
|
var movedLeft = self.__scrollLeft - positions[startPos - 2];
|
|
var movedTop = self.__scrollTop - positions[startPos - 1];
|
|
|
|
// Based on 50ms compute the movement to apply for each render step
|
|
self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60);
|
|
self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);
|
|
|
|
// How much velocity is required to start the deceleration
|
|
var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? self.options.decelVelocityThresholdPaging : self.options.decelVelocityThreshold;
|
|
|
|
// Verify that we have enough velocity to start deceleration
|
|
if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {
|
|
|
|
// Deactivate pull-to-refresh when decelerating
|
|
if (!self.__refreshActive) {
|
|
self.__startDeceleration(timeStamp);
|
|
}
|
|
}
|
|
} else {
|
|
self.__scrollingComplete();
|
|
}
|
|
} else if ((timeStamp - self.__lastTouchMove) > 100) {
|
|
self.__scrollingComplete();
|
|
}
|
|
|
|
} else if (self.__decStopped) {
|
|
// the deceleration was stopped
|
|
// user flicked the scroll fast, and stop dragging, then did a touchstart to stop the srolling
|
|
// tell the touchend event code to do nothing, we don't want to actually send a click
|
|
e.isTapHandled = true;
|
|
self.__decStopped = false;
|
|
}
|
|
|
|
// If this was a slower move it is per default non decelerated, but this
|
|
// still means that we want snap back to the bounds which is done here.
|
|
// This is placed outside the condition above to improve edge case stability
|
|
// e.g. touchend fired without enabled dragging. This should normally do not
|
|
// have modified the scroll positions or even showed the scrollbars though.
|
|
if (!self.__isDecelerating) {
|
|
|
|
if (self.__refreshActive && self.__refreshStart) {
|
|
|
|
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
|
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
|
self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);
|
|
|
|
var d = new Date();
|
|
self.refreshStartTime = d.getTime();
|
|
|
|
if (self.__refreshStart) {
|
|
self.__refreshStart();
|
|
}
|
|
// for iOS-ey style scrolling
|
|
if (!ionic.Platform.isAndroid())self.__startDeceleration();
|
|
} else {
|
|
|
|
if (self.__interruptedAnimation || self.__isDragging) {
|
|
self.__scrollingComplete();
|
|
}
|
|
self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);
|
|
|
|
// Directly signalize deactivation (nothing todo on refresh?)
|
|
if (self.__refreshActive) {
|
|
|
|
self.__refreshActive = false;
|
|
if (self.__refreshDeactivate) {
|
|
self.__refreshDeactivate();
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fully cleanup list
|
|
self.__positions.length = 0;
|
|
|
|
},
|
|
|
|
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
PRIVATE API
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Applies the scroll position to the content element
|
|
*
|
|
* @param left {Number} Left scroll position
|
|
* @param top {Number} Top scroll position
|
|
* @param animate {Boolean} Whether animation should be used to move to the new coordinates
|
|
*/
|
|
__publish: function(left, top, zoom, animate, wasResize) {
|
|
|
|
var self = this;
|
|
|
|
// Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
|
|
var wasAnimating = self.__isAnimating;
|
|
if (wasAnimating) {
|
|
zyngaCore.effect.Animate.stop(wasAnimating);
|
|
self.__isAnimating = false;
|
|
}
|
|
|
|
if (animate && self.options.animating) {
|
|
|
|
// Keep scheduled positions for scrollBy/zoomBy functionality
|
|
self.__scheduledLeft = left;
|
|
self.__scheduledTop = top;
|
|
self.__scheduledZoom = zoom;
|
|
|
|
var oldLeft = self.__scrollLeft;
|
|
var oldTop = self.__scrollTop;
|
|
var oldZoom = self.__zoomLevel;
|
|
|
|
var diffLeft = left - oldLeft;
|
|
var diffTop = top - oldTop;
|
|
var diffZoom = zoom - oldZoom;
|
|
|
|
var step = function(percent, now, render) {
|
|
|
|
if (render) {
|
|
|
|
self.__scrollLeft = oldLeft + (diffLeft * percent);
|
|
self.__scrollTop = oldTop + (diffTop * percent);
|
|
self.__zoomLevel = oldZoom + (diffZoom * percent);
|
|
|
|
// Push values out
|
|
if (self.__callback) {
|
|
self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel, wasResize);
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
var verify = function(id) {
|
|
return self.__isAnimating === id;
|
|
};
|
|
|
|
var completed = function(renderedFramesPerSecond, animationId, wasFinished) {
|
|
if (animationId === self.__isAnimating) {
|
|
self.__isAnimating = false;
|
|
}
|
|
if (self.__didDecelerationComplete || wasFinished) {
|
|
self.__scrollingComplete();
|
|
}
|
|
|
|
if (self.options.zooming) {
|
|
self.__computeScrollMax();
|
|
}
|
|
};
|
|
|
|
// When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
|
|
self.__isAnimating = zyngaCore.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);
|
|
|
|
} else {
|
|
|
|
self.__scheduledLeft = self.__scrollLeft = left;
|
|
self.__scheduledTop = self.__scrollTop = top;
|
|
self.__scheduledZoom = self.__zoomLevel = zoom;
|
|
|
|
// Push values out
|
|
if (self.__callback) {
|
|
self.__callback(left, top, zoom, wasResize);
|
|
}
|
|
|
|
// Fix max scroll ranges
|
|
if (self.options.zooming) {
|
|
self.__computeScrollMax();
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* Recomputes scroll minimum values based on client dimensions and content dimensions.
|
|
*/
|
|
__computeScrollMax: function(zoomLevel) {
|
|
var self = this;
|
|
|
|
if (zoomLevel == null) {
|
|
zoomLevel = self.__zoomLevel;
|
|
}
|
|
|
|
self.__maxScrollLeft = Math.max((self.__contentWidth * zoomLevel) - self.__clientWidth, 0);
|
|
self.__maxScrollTop = Math.max((self.__contentHeight * zoomLevel) - self.__clientHeight, 0);
|
|
|
|
if (!self.__didWaitForSize && !self.__maxScrollLeft && !self.__maxScrollTop) {
|
|
self.__didWaitForSize = true;
|
|
self.__waitForSize();
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* If the scroll view isn't sized correctly on start, wait until we have at least some size
|
|
*/
|
|
__waitForSize: function() {
|
|
var self = this;
|
|
|
|
clearTimeout(self.__sizerTimeout);
|
|
|
|
var sizer = function() {
|
|
self.resize(true);
|
|
};
|
|
|
|
sizer();
|
|
self.__sizerTimeout = setTimeout(sizer, 500);
|
|
},
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
ANIMATION (DECELERATION) SUPPORT
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Called when a touch sequence end and the speed of the finger was high enough
|
|
* to switch into deceleration mode.
|
|
*/
|
|
__startDeceleration: function() {
|
|
var self = this;
|
|
|
|
if (self.options.paging) {
|
|
|
|
var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0);
|
|
var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0);
|
|
var clientWidth = self.__clientWidth;
|
|
var clientHeight = self.__clientHeight;
|
|
|
|
// We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
|
|
// Each page should have exactly the size of the client area.
|
|
self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
|
|
self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
|
|
self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
|
|
self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;
|
|
|
|
} else {
|
|
|
|
self.__minDecelerationScrollLeft = 0;
|
|
self.__minDecelerationScrollTop = 0;
|
|
self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
|
|
self.__maxDecelerationScrollTop = self.__maxScrollTop;
|
|
if (self.__refreshActive) self.__minDecelerationScrollTop = self.__refreshHeight * -1;
|
|
}
|
|
|
|
// Wrap class method
|
|
var step = function(percent, now, render) {
|
|
self.__stepThroughDeceleration(render);
|
|
};
|
|
|
|
// How much velocity is required to keep the deceleration running
|
|
self.__minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1;
|
|
|
|
// Detect whether it's still worth to continue animating steps
|
|
// If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
|
|
var verify = function() {
|
|
var shouldContinue = Math.abs(self.__decelerationVelocityX) >= self.__minVelocityToKeepDecelerating ||
|
|
Math.abs(self.__decelerationVelocityY) >= self.__minVelocityToKeepDecelerating;
|
|
if (!shouldContinue) {
|
|
self.__didDecelerationComplete = true;
|
|
|
|
//Make sure the scroll values are within the boundaries after a bounce,
|
|
//not below 0 or above maximum
|
|
if (self.options.bouncing && !self.__refreshActive) {
|
|
self.scrollTo(
|
|
Math.min( Math.max(self.__scrollLeft, 0), self.__maxScrollLeft ),
|
|
Math.min( Math.max(self.__scrollTop, 0), self.__maxScrollTop ),
|
|
self.__refreshActive
|
|
);
|
|
}
|
|
}
|
|
return shouldContinue;
|
|
};
|
|
|
|
var completed = function() {
|
|
self.__isDecelerating = false;
|
|
if (self.__didDecelerationComplete) {
|
|
self.__scrollingComplete();
|
|
}
|
|
|
|
// Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
|
|
if (self.options.paging) {
|
|
self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
|
|
}
|
|
};
|
|
|
|
// Start animation and switch on flag
|
|
self.__isDecelerating = zyngaCore.effect.Animate.start(step, verify, completed);
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Called on every step of the animation
|
|
*
|
|
* @param inMemory {Boolean} Whether to not render the current step, but keep it in memory only. Used internally only!
|
|
*/
|
|
__stepThroughDeceleration: function(render) {
|
|
var self = this;
|
|
|
|
|
|
//
|
|
// COMPUTE NEXT SCROLL POSITION
|
|
//
|
|
|
|
// Add deceleration to scroll position
|
|
var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;// * self.options.deceleration);
|
|
var scrollTop = self.__scrollTop + self.__decelerationVelocityY;// * self.options.deceleration);
|
|
|
|
|
|
//
|
|
// HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
|
|
//
|
|
|
|
if (!self.options.bouncing) {
|
|
|
|
var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
|
|
if (scrollLeftFixed !== scrollLeft) {
|
|
scrollLeft = scrollLeftFixed;
|
|
self.__decelerationVelocityX = 0;
|
|
}
|
|
|
|
var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
|
|
if (scrollTopFixed !== scrollTop) {
|
|
scrollTop = scrollTopFixed;
|
|
self.__decelerationVelocityY = 0;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
//
|
|
// UPDATE SCROLL POSITION
|
|
//
|
|
|
|
if (render) {
|
|
|
|
self.__publish(scrollLeft, scrollTop, self.__zoomLevel);
|
|
|
|
} else {
|
|
|
|
self.__scrollLeft = scrollLeft;
|
|
self.__scrollTop = scrollTop;
|
|
|
|
}
|
|
|
|
|
|
//
|
|
// SLOW DOWN
|
|
//
|
|
|
|
// Slow down velocity on every iteration
|
|
if (!self.options.paging) {
|
|
|
|
// This is the factor applied to every iteration of the animation
|
|
// to slow down the process. This should emulate natural behavior where
|
|
// objects slow down when the initiator of the movement is removed
|
|
var frictionFactor = self.options.deceleration;
|
|
|
|
self.__decelerationVelocityX *= frictionFactor;
|
|
self.__decelerationVelocityY *= frictionFactor;
|
|
|
|
}
|
|
|
|
|
|
//
|
|
// BOUNCING SUPPORT
|
|
//
|
|
|
|
if (self.options.bouncing) {
|
|
|
|
var scrollOutsideX = 0;
|
|
var scrollOutsideY = 0;
|
|
|
|
// This configures the amount of change applied to deceleration/acceleration when reaching boundaries
|
|
var penetrationDeceleration = self.options.penetrationDeceleration;
|
|
var penetrationAcceleration = self.options.penetrationAcceleration;
|
|
|
|
// Check limits
|
|
if (scrollLeft < self.__minDecelerationScrollLeft) {
|
|
scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
|
|
} else if (scrollLeft > self.__maxDecelerationScrollLeft) {
|
|
scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
|
|
}
|
|
|
|
if (scrollTop < self.__minDecelerationScrollTop) {
|
|
scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
|
|
} else if (scrollTop > self.__maxDecelerationScrollTop) {
|
|
scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
|
|
}
|
|
|
|
// Slow down until slow enough, then flip back to snap position
|
|
if (scrollOutsideX !== 0) {
|
|
var isHeadingOutwardsX = scrollOutsideX * self.__decelerationVelocityX <= self.__minDecelerationScrollLeft;
|
|
if (isHeadingOutwardsX) {
|
|
self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
|
|
}
|
|
var isStoppedX = Math.abs(self.__decelerationVelocityX) <= self.__minVelocityToKeepDecelerating;
|
|
//If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds
|
|
if (!isHeadingOutwardsX || isStoppedX) {
|
|
self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
|
|
}
|
|
}
|
|
|
|
if (scrollOutsideY !== 0) {
|
|
var isHeadingOutwardsY = scrollOutsideY * self.__decelerationVelocityY <= self.__minDecelerationScrollTop;
|
|
if (isHeadingOutwardsY) {
|
|
self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
|
|
}
|
|
var isStoppedY = Math.abs(self.__decelerationVelocityY) <= self.__minVelocityToKeepDecelerating;
|
|
//If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds
|
|
if (!isHeadingOutwardsY || isStoppedY) {
|
|
self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* calculate the distance between two touches
|
|
* @param {Touch} touch1
|
|
* @param {Touch} touch2
|
|
* @returns {Number} distance
|
|
*/
|
|
__getDistance: function getDistance(touch1, touch2) {
|
|
var x = touch2.pageX - touch1.pageX,
|
|
y = touch2.pageY - touch1.pageY;
|
|
return Math.sqrt((x * x) + (y * y));
|
|
},
|
|
|
|
|
|
/**
|
|
* calculate the scale factor between two touchLists (fingers)
|
|
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
|
|
* @param {Array} start
|
|
* @param {Array} end
|
|
* @returns {Number} scale
|
|
*/
|
|
__getScale: function getScale(start, end) {
|
|
// need two fingers...
|
|
if (start.length >= 2 && end.length >= 2) {
|
|
return this.__getDistance(end[0], end[1]) /
|
|
this.__getDistance(start[0], start[1]);
|
|
}
|
|
return 1;
|
|
}
|
|
});
|
|
|
|
ionic.scroll = {
|
|
isScrolling: false,
|
|
lastTop: 0
|
|
};
|
|
|
|
})(ionic);
|
|
|
|
(function(ionic) {
|
|
var NOOP = function() {};
|
|
var deprecated = function(name) {
|
|
void 0;
|
|
};
|
|
ionic.views.ScrollNative = ionic.views.View.inherit({
|
|
|
|
initialize: function(options) {
|
|
var self = this;
|
|
self.__container = self.el = options.el;
|
|
self.__content = options.el.firstElementChild;
|
|
// Whether scrolling is frozen or not
|
|
self.__frozen = false;
|
|
self.isNative = true;
|
|
|
|
self.__scrollTop = self.el.scrollTop;
|
|
self.__scrollLeft = self.el.scrollLeft;
|
|
self.__clientHeight = self.__content.clientHeight;
|
|
self.__clientWidth = self.__content.clientWidth;
|
|
self.__maxScrollTop = Math.max((self.__contentHeight) - self.__clientHeight, 0);
|
|
self.__maxScrollLeft = Math.max((self.__contentWidth) - self.__clientWidth, 0);
|
|
|
|
if(options.startY >= 0 || options.startX >= 0) {
|
|
ionic.requestAnimationFrame(function() {
|
|
self.el.scrollTop = options.startY || 0;
|
|
self.el.scrollLeft = options.startX || 0;
|
|
|
|
self.__scrollTop = self.el.scrollTop;
|
|
self.__scrollLeft = self.el.scrollLeft;
|
|
});
|
|
}
|
|
|
|
self.options = {
|
|
|
|
freeze: false,
|
|
|
|
getContentWidth: function() {
|
|
return Math.max(self.__content.scrollWidth, self.__content.offsetWidth);
|
|
},
|
|
|
|
getContentHeight: function() {
|
|
return Math.max(self.__content.scrollHeight, self.__content.offsetHeight + (self.__content.offsetTop * 2));
|
|
}
|
|
|
|
};
|
|
|
|
for (var key in options) {
|
|
self.options[key] = options[key];
|
|
}
|
|
|
|
/**
|
|
* Sets isScrolling to true, and automatically deactivates if not called again in 80ms.
|
|
*/
|
|
self.onScroll = function() {
|
|
if (!ionic.scroll.isScrolling) {
|
|
ionic.scroll.isScrolling = true;
|
|
}
|
|
|
|
clearTimeout(self.scrollTimer);
|
|
self.scrollTimer = setTimeout(function() {
|
|
ionic.scroll.isScrolling = false;
|
|
}, 80);
|
|
};
|
|
|
|
self.freeze = function(shouldFreeze) {
|
|
self.__frozen = shouldFreeze;
|
|
};
|
|
// A more powerful freeze pop that dominates all other freeze pops
|
|
self.freezeShut = function(shouldFreezeShut) {
|
|
self.__frozenShut = shouldFreezeShut;
|
|
};
|
|
|
|
self.__initEventHandlers();
|
|
},
|
|
|
|
/** Methods not used in native scrolling */
|
|
__callback: function() { deprecated('__callback'); },
|
|
zoomTo: function() { deprecated('zoomTo'); },
|
|
zoomBy: function() { deprecated('zoomBy'); },
|
|
activatePullToRefresh: function() { deprecated('activatePullToRefresh'); },
|
|
|
|
/**
|
|
* Returns the scroll position and zooming values
|
|
*
|
|
* @return {Map} `left` and `top` scroll position and `zoom` level
|
|
*/
|
|
resize: function(continueScrolling) {
|
|
var self = this;
|
|
if (!self.__container || !self.options) return;
|
|
|
|
// Update Scroller dimensions for changed content
|
|
// Add padding to bottom of content
|
|
self.setDimensions(
|
|
self.__container.clientWidth,
|
|
self.__container.clientHeight,
|
|
self.options.getContentWidth(),
|
|
self.options.getContentHeight(),
|
|
continueScrolling
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Initialize the scrollview
|
|
* In native scrolling, this only means we need to gather size information
|
|
*/
|
|
run: function() {
|
|
this.resize();
|
|
},
|
|
|
|
/**
|
|
* Returns the scroll position and zooming values
|
|
*
|
|
* @return {Map} `left` and `top` scroll position and `zoom` level
|
|
*/
|
|
getValues: function() {
|
|
var self = this;
|
|
self.update();
|
|
return {
|
|
left: self.__scrollLeft,
|
|
top: self.__scrollTop,
|
|
zoom: 1
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Updates the __scrollLeft and __scrollTop values to el's current value
|
|
*/
|
|
update: function() {
|
|
var self = this;
|
|
self.__scrollLeft = self.el.scrollLeft;
|
|
self.__scrollTop = self.el.scrollTop;
|
|
},
|
|
|
|
/**
|
|
* Configures the dimensions of the client (outer) and content (inner) elements.
|
|
* Requires the available space for the outer element and the outer size of the inner element.
|
|
* All values which are falsy (null or zero etc.) are ignored and the old value is kept.
|
|
*
|
|
* @param clientWidth {Integer} Inner width of outer element
|
|
* @param clientHeight {Integer} Inner height of outer element
|
|
* @param contentWidth {Integer} Outer width of inner element
|
|
* @param contentHeight {Integer} Outer height of inner element
|
|
*/
|
|
setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight) {
|
|
var self = this;
|
|
|
|
if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) {
|
|
// this scrollview isn't rendered, don't bother
|
|
return;
|
|
}
|
|
|
|
// Only update values which are defined
|
|
if (clientWidth === +clientWidth) {
|
|
self.__clientWidth = clientWidth;
|
|
}
|
|
|
|
if (clientHeight === +clientHeight) {
|
|
self.__clientHeight = clientHeight;
|
|
}
|
|
|
|
if (contentWidth === +contentWidth) {
|
|
self.__contentWidth = contentWidth;
|
|
}
|
|
|
|
if (contentHeight === +contentHeight) {
|
|
self.__contentHeight = contentHeight;
|
|
}
|
|
|
|
// Refresh maximums
|
|
self.__computeScrollMax();
|
|
},
|
|
|
|
/**
|
|
* Returns the maximum scroll values
|
|
*
|
|
* @return {Map} `left` and `top` maximum scroll values
|
|
*/
|
|
getScrollMax: function() {
|
|
return {
|
|
left: this.__maxScrollLeft,
|
|
top: this.__maxScrollTop
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Scrolls by the given amount in px.
|
|
*
|
|
* @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
|
|
* @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
|
|
* @param animate {Boolean} Whether the scrolling should happen using an animation
|
|
*/
|
|
|
|
scrollBy: function(left, top, animate) {
|
|
var self = this;
|
|
|
|
// update scroll vars before refferencing them
|
|
self.update();
|
|
|
|
var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
|
|
var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
|
|
|
|
self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
|
|
},
|
|
|
|
/**
|
|
* Scrolls to the given position in px.
|
|
*
|
|
* @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
|
|
* @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
|
|
* @param animate {Boolean} Whether the scrolling should happen using an animation
|
|
*/
|
|
scrollTo: function(left, top, animate) {
|
|
var self = this;
|
|
if (!animate) {
|
|
self.el.scrollTop = top;
|
|
self.el.scrollLeft = left;
|
|
self.resize();
|
|
return;
|
|
}
|
|
|
|
var oldOverflowX = self.el.style.overflowX;
|
|
var oldOverflowY = self.el.style.overflowY;
|
|
|
|
clearTimeout(self.__scrollToCleanupTimeout);
|
|
self.__scrollToCleanupTimeout = setTimeout(function() {
|
|
self.el.style.overflowX = oldOverflowX;
|
|
self.el.style.overflowY = oldOverflowY;
|
|
}, 500);
|
|
|
|
self.el.style.overflowY = 'hidden';
|
|
self.el.style.overflowX = 'hidden';
|
|
|
|
animateScroll(top, left);
|
|
|
|
function animateScroll(Y, X) {
|
|
// scroll animation loop w/ easing
|
|
// credit https://gist.github.com/dezinezync/5487119
|
|
var start = Date.now(),
|
|
duration = 250, //milliseconds
|
|
fromY = self.el.scrollTop,
|
|
fromX = self.el.scrollLeft;
|
|
|
|
if (fromY === Y && fromX === X) {
|
|
self.el.style.overflowX = oldOverflowX;
|
|
self.el.style.overflowY = oldOverflowY;
|
|
self.resize();
|
|
return; /* Prevent scrolling to the Y point if already there */
|
|
}
|
|
|
|
// decelerating to zero velocity
|
|
function easeOutCubic(t) {
|
|
return (--t) * t * t + 1;
|
|
}
|
|
|
|
// scroll loop
|
|
function animateScrollStep() {
|
|
var currentTime = Date.now(),
|
|
time = Math.min(1, ((currentTime - start) / duration)),
|
|
// where .5 would be 50% of time on a linear scale easedT gives a
|
|
// fraction based on the easing method
|
|
easedT = easeOutCubic(time);
|
|
|
|
if (fromY != Y) {
|
|
self.el.scrollTop = parseInt((easedT * (Y - fromY)) + fromY, 10);
|
|
}
|
|
if (fromX != X) {
|
|
self.el.scrollLeft = parseInt((easedT * (X - fromX)) + fromX, 10);
|
|
}
|
|
|
|
if (time < 1) {
|
|
ionic.requestAnimationFrame(animateScrollStep);
|
|
|
|
} else {
|
|
// done
|
|
ionic.tap.removeClonedInputs(self.__container, self);
|
|
self.el.style.overflowX = oldOverflowX;
|
|
self.el.style.overflowY = oldOverflowY;
|
|
self.resize();
|
|
}
|
|
}
|
|
|
|
// start scroll loop
|
|
ionic.requestAnimationFrame(animateScrollStep);
|
|
}
|
|
},
|
|
|
|
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
PRIVATE API
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* If the scroll view isn't sized correctly on start, wait until we have at least some size
|
|
*/
|
|
__waitForSize: function() {
|
|
var self = this;
|
|
|
|
clearTimeout(self.__sizerTimeout);
|
|
|
|
var sizer = function() {
|
|
self.resize(true);
|
|
};
|
|
|
|
sizer();
|
|
self.__sizerTimeout = setTimeout(sizer, 500);
|
|
},
|
|
|
|
|
|
/**
|
|
* Recomputes scroll minimum values based on client dimensions and content dimensions.
|
|
*/
|
|
__computeScrollMax: function() {
|
|
var self = this;
|
|
|
|
self.__maxScrollLeft = Math.max((self.__contentWidth) - self.__clientWidth, 0);
|
|
self.__maxScrollTop = Math.max((self.__contentHeight) - self.__clientHeight, 0);
|
|
|
|
if (!self.__didWaitForSize && !self.__maxScrollLeft && !self.__maxScrollTop) {
|
|
self.__didWaitForSize = true;
|
|
self.__waitForSize();
|
|
}
|
|
},
|
|
|
|
__initEventHandlers: function() {
|
|
var self = this;
|
|
|
|
// Event Handler
|
|
var container = self.__container;
|
|
// save height when scroll view is shrunk so we don't need to reflow
|
|
var scrollViewOffsetHeight;
|
|
|
|
var lastKeyboardHeight;
|
|
|
|
/**
|
|
* Shrink the scroll view when the keyboard is up if necessary and if the
|
|
* focused input is below the bottom of the shrunk scroll view, scroll it
|
|
* into view.
|
|
*/
|
|
self.scrollChildIntoView = function(e) {
|
|
var rect = container.getBoundingClientRect();
|
|
if(!self.__originalContainerHeight) {
|
|
self.__originalContainerHeight = rect.height;
|
|
}
|
|
|
|
// D
|
|
//var scrollBottomOffsetToTop = rect.bottom;
|
|
// D - A
|
|
scrollViewOffsetHeight = self.__originalContainerHeight;
|
|
//console.log('Scroll view offset height', scrollViewOffsetHeight);
|
|
//console.dir(container);
|
|
var alreadyShrunk = self.isShrunkForKeyboard;
|
|
|
|
var isModal = container.parentNode.classList.contains('modal');
|
|
var isPopover = container.parentNode.classList.contains('popover');
|
|
// 680px is when the media query for 60% modal width kicks in
|
|
var isInsetModal = isModal && window.innerWidth >= 680;
|
|
|
|
/*
|
|
* _______
|
|
* |---A---| <- top of scroll view
|
|
* | |
|
|
* |---B---| <- keyboard
|
|
* | C | <- input
|
|
* |---D---| <- initial bottom of scroll view
|
|
* |___E___| <- bottom of viewport
|
|
*
|
|
* All commented calculations relative to the top of the viewport (ie E
|
|
* is the viewport height, not 0)
|
|
*/
|
|
|
|
|
|
var changedKeyboardHeight = lastKeyboardHeight && (lastKeyboardHeight !== e.detail.keyboardHeight);
|
|
|
|
if (!alreadyShrunk || changedKeyboardHeight) {
|
|
// shrink scrollview so we can actually scroll if the input is hidden
|
|
// if it isn't shrink so we can scroll to inputs under the keyboard
|
|
// inset modals won't shrink on Android on their own when the keyboard appears
|
|
if ( !isPopover && (ionic.Platform.isIOS() || ionic.Platform.isFullScreen || isInsetModal) ) {
|
|
// if there are things below the scroll view account for them and
|
|
// subtract them from the keyboard height when resizing
|
|
// E - D E D
|
|
//var scrollBottomOffsetToBottom = e.detail.viewportHeight - scrollBottomOffsetToTop;
|
|
|
|
// 0 or D - B if D > B E - B E - D
|
|
//var keyboardOffset = e.detail.keyboardHeight - scrollBottomOffsetToBottom;
|
|
|
|
ionic.requestAnimationFrame(function(){
|
|
// D - A or B - A if D > B D - A max(0, D - B)
|
|
scrollViewOffsetHeight = Math.max(0, Math.min(self.__originalContainerHeight, self.__originalContainerHeight - (e.detail.keyboardHeight - 43)));//keyboardOffset >= 0 ? scrollViewOffsetHeight - keyboardOffset : scrollViewOffsetHeight + keyboardOffset;
|
|
|
|
//console.log('Old container height', self.__originalContainerHeight, 'New container height', scrollViewOffsetHeight, 'Keyboard height', e.detail.keyboardHeight);
|
|
|
|
container.style.height = scrollViewOffsetHeight + "px";
|
|
|
|
/*
|
|
if (ionic.Platform.isIOS()) {
|
|
// Force redraw to avoid disappearing content
|
|
var disp = container.style.display;
|
|
container.style.display = 'none';
|
|
var trick = container.offsetHeight;
|
|
container.style.display = disp;
|
|
}
|
|
*/
|
|
container.classList.add('keyboard-up');
|
|
//update scroll view
|
|
self.resize();
|
|
});
|
|
}
|
|
|
|
self.isShrunkForKeyboard = true;
|
|
}
|
|
|
|
lastKeyboardHeight = e.detail.keyboardHeight;
|
|
|
|
/*
|
|
* _______
|
|
* |---A---| <- top of scroll view
|
|
* | * | <- where we want to scroll to
|
|
* |--B-D--| <- keyboard, bottom of scroll view
|
|
* | C | <- input
|
|
* | |
|
|
* |___E___| <- bottom of viewport
|
|
*
|
|
* All commented calculations relative to the top of the viewport (ie E
|
|
* is the viewport height, not 0)
|
|
*/
|
|
// if the element is positioned under the keyboard scroll it into view
|
|
if (e.detail.isElementUnderKeyboard) {
|
|
|
|
ionic.requestAnimationFrame(function(){
|
|
var pos = ionic.DomUtil.getOffsetTop(e.detail.target);
|
|
setTimeout(function() {
|
|
if (ionic.Platform.isIOS()) {
|
|
ionic.tap.cloneFocusedInput(container, self);
|
|
}
|
|
// Scroll the input into view, with a 100px buffer
|
|
self.scrollTo(0, pos - (rect.top + 100), true);
|
|
self.onScroll();
|
|
}, 32);
|
|
|
|
/*
|
|
// update D if we shrunk
|
|
if (self.isShrunkForKeyboard && !alreadyShrunk) {
|
|
scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
|
|
console.log('Scroll bottom', scrollBottomOffsetToTop);
|
|
}
|
|
|
|
// middle of the scrollview, this is where we want to scroll to
|
|
// (D - A) / 2
|
|
var scrollMidpointOffset = scrollViewOffsetHeight * 0.5;
|
|
console.log('Midpoint', scrollMidpointOffset);
|
|
//console.log("container.offsetHeight: " + scrollViewOffsetHeight);
|
|
|
|
// middle of the input we want to scroll into view
|
|
// C
|
|
var inputMidpoint = ((e.detail.elementBottom + e.detail.elementTop) / 2);
|
|
console.log('Input midpoint');
|
|
|
|
// distance from middle of input to the bottom of the scroll view
|
|
// C - D C D
|
|
var inputMidpointOffsetToScrollBottom = inputMidpoint - scrollBottomOffsetToTop;
|
|
console.log('Input midpoint offset', inputMidpointOffsetToScrollBottom);
|
|
|
|
//C - D + (D - A)/2 C - D (D - A)/ 2
|
|
var scrollTop = inputMidpointOffsetToScrollBottom + scrollMidpointOffset;
|
|
console.log('Scroll top', scrollTop);
|
|
|
|
if ( scrollTop > 0) {
|
|
if (ionic.Platform.isIOS()) {
|
|
//just shrank scroll view, give it some breathing room before scrolling
|
|
setTimeout(function(){
|
|
ionic.tap.cloneFocusedInput(container, self);
|
|
self.scrollBy(0, scrollTop, true);
|
|
self.onScroll();
|
|
}, 32);
|
|
} else {
|
|
self.scrollBy(0, scrollTop, true);
|
|
self.onScroll();
|
|
}
|
|
}
|
|
*/
|
|
});
|
|
}
|
|
|
|
// Only the first scrollView parent of the element that broadcasted this event
|
|
// (the active element that needs to be shown) should receive this event
|
|
e.stopPropagation();
|
|
};
|
|
|
|
self.resetScrollView = function() {
|
|
//return scrollview to original height once keyboard has hidden
|
|
if (self.isShrunkForKeyboard) {
|
|
self.isShrunkForKeyboard = false;
|
|
container.style.height = "";
|
|
|
|
/*
|
|
if (ionic.Platform.isIOS()) {
|
|
// Force redraw to avoid disappearing content
|
|
var disp = container.style.display;
|
|
container.style.display = 'none';
|
|
var trick = container.offsetHeight;
|
|
container.style.display = disp;
|
|
}
|
|
*/
|
|
|
|
self.__originalContainerHeight = container.getBoundingClientRect().height;
|
|
|
|
if (ionic.Platform.isIOS()) {
|
|
ionic.requestAnimationFrame(function() {
|
|
container.classList.remove('keyboard-up');
|
|
});
|
|
}
|
|
|
|
}
|
|
self.resize();
|
|
};
|
|
|
|
self.handleTouchMove = function(e) {
|
|
if (self.__frozenShut) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return false;
|
|
|
|
} else if ( self.__frozen ){
|
|
e.preventDefault();
|
|
// let it propagate so other events such as drag events can happen,
|
|
// but don't let it actually scroll
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
container.addEventListener('scroll', self.onScroll);
|
|
|
|
//Broadcasted when keyboard is shown on some platforms.
|
|
//See js/utils/keyboard.js
|
|
container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);
|
|
|
|
container.addEventListener(ionic.EVENTS.touchstart, self.handleTouchMove);
|
|
container.addEventListener(ionic.EVENTS.touchmove, self.handleTouchMove);
|
|
|
|
// Listen on document because container may not have had the last
|
|
// keyboardActiveElement, for example after closing a modal with a focused
|
|
// input and returning to a previously resized scroll view in an ion-content.
|
|
// Since we can only resize scroll views that are currently visible, just resize
|
|
// the current scroll view when the keyboard is closed.
|
|
document.addEventListener('resetScrollView', self.resetScrollView);
|
|
},
|
|
|
|
__cleanup: function() {
|
|
var self = this;
|
|
var container = self.__container;
|
|
|
|
container.removeEventListener('resetScrollView', self.resetScrollView);
|
|
container.removeEventListener('scroll', self.onScroll);
|
|
|
|
container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView);
|
|
container.removeEventListener('resetScrollView', self.resetScrollView);
|
|
|
|
container.removeEventListener(ionic.EVENTS.touchstart, self.handleTouchMove);
|
|
container.removeEventListener(ionic.EVENTS.touchmove, self.handleTouchMove);
|
|
|
|
ionic.tap.removeClonedInputs(container, self);
|
|
|
|
delete self.__container;
|
|
delete self.__content;
|
|
delete self.__indicatorX;
|
|
delete self.__indicatorY;
|
|
delete self.options.el;
|
|
|
|
self.resize = self.scrollTo = self.onScroll = self.resetScrollView = NOOP;
|
|
self.scrollChildIntoView = NOOP;
|
|
container = null;
|
|
}
|
|
});
|
|
|
|
})(ionic);
|
|
|
|
(function(ionic) {
|
|
'use strict';
|
|
|
|
var ITEM_CLASS = 'item';
|
|
var ITEM_CONTENT_CLASS = 'item-content';
|
|
var ITEM_SLIDING_CLASS = 'item-sliding';
|
|
var ITEM_OPTIONS_CLASS = 'item-options';
|
|
var ITEM_PLACEHOLDER_CLASS = 'item-placeholder';
|
|
var ITEM_REORDERING_CLASS = 'item-reordering';
|
|
var ITEM_REORDER_BTN_CLASS = 'item-reorder';
|
|
|
|
var DragOp = function() {};
|
|
DragOp.prototype = {
|
|
start: function(){},
|
|
drag: function(){},
|
|
end: function(){},
|
|
isSameItem: function() {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
var SlideDrag = function(opts) {
|
|
this.dragThresholdX = opts.dragThresholdX || 10;
|
|
this.el = opts.el;
|
|
this.item = opts.item;
|
|
this.canSwipe = opts.canSwipe;
|
|
};
|
|
|
|
SlideDrag.prototype = new DragOp();
|
|
|
|
SlideDrag.prototype.start = function(e) {
|
|
var content, buttons, offsetX, buttonsWidth;
|
|
|
|
if (!this.canSwipe()) {
|
|
return;
|
|
}
|
|
|
|
if (e.target.classList.contains(ITEM_CONTENT_CLASS)) {
|
|
content = e.target;
|
|
} else if (e.target.classList.contains(ITEM_CLASS)) {
|
|
content = e.target.querySelector('.' + ITEM_CONTENT_CLASS);
|
|
} else {
|
|
content = ionic.DomUtil.getParentWithClass(e.target, ITEM_CONTENT_CLASS);
|
|
}
|
|
|
|
// If we don't have a content area as one of our children (or ourselves), skip
|
|
if (!content) {
|
|
return;
|
|
}
|
|
|
|
// Make sure we aren't animating as we slide
|
|
content.classList.remove(ITEM_SLIDING_CLASS);
|
|
|
|
// Grab the starting X point for the item (for example, so we can tell whether it is open or closed to start)
|
|
offsetX = parseFloat(content.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]) || 0;
|
|
|
|
// Grab the buttons
|
|
buttons = content.parentNode.querySelector('.' + ITEM_OPTIONS_CLASS);
|
|
if (!buttons) {
|
|
return;
|
|
}
|
|
buttons.classList.remove('invisible');
|
|
|
|
buttonsWidth = buttons.offsetWidth;
|
|
|
|
this._currentDrag = {
|
|
buttons: buttons,
|
|
buttonsWidth: buttonsWidth,
|
|
content: content,
|
|
startOffsetX: offsetX
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Check if this is the same item that was previously dragged.
|
|
*/
|
|
SlideDrag.prototype.isSameItem = function(op) {
|
|
if (op._lastDrag && this._currentDrag) {
|
|
return this._currentDrag.content == op._lastDrag.content;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
SlideDrag.prototype.clean = function(isInstant) {
|
|
var lastDrag = this._lastDrag;
|
|
|
|
if (!lastDrag || !lastDrag.content) return;
|
|
|
|
lastDrag.content.style[ionic.CSS.TRANSITION] = '';
|
|
lastDrag.content.style[ionic.CSS.TRANSFORM] = '';
|
|
if (isInstant) {
|
|
lastDrag.content.style[ionic.CSS.TRANSITION] = 'none';
|
|
makeInvisible();
|
|
ionic.requestAnimationFrame(function() {
|
|
lastDrag.content.style[ionic.CSS.TRANSITION] = '';
|
|
});
|
|
} else {
|
|
ionic.requestAnimationFrame(function() {
|
|
setTimeout(makeInvisible, 250);
|
|
});
|
|
}
|
|
function makeInvisible() {
|
|
lastDrag.buttons && lastDrag.buttons.classList.add('invisible');
|
|
}
|
|
};
|
|
|
|
SlideDrag.prototype.drag = ionic.animationFrameThrottle(function(e) {
|
|
var buttonsWidth;
|
|
|
|
// We really aren't dragging
|
|
if (!this._currentDrag) {
|
|
return;
|
|
}
|
|
|
|
// Check if we should start dragging. Check if we've dragged past the threshold,
|
|
// or we are starting from the open state.
|
|
if (!this._isDragging &&
|
|
((Math.abs(e.gesture.deltaX) > this.dragThresholdX) ||
|
|
(Math.abs(this._currentDrag.startOffsetX) > 0))) {
|
|
this._isDragging = true;
|
|
}
|
|
|
|
if (this._isDragging) {
|
|
buttonsWidth = this._currentDrag.buttonsWidth;
|
|
|
|
// Grab the new X point, capping it at zero
|
|
var newX = Math.min(0, this._currentDrag.startOffsetX + e.gesture.deltaX);
|
|
|
|
// If the new X position is past the buttons, we need to slow down the drag (rubber band style)
|
|
if (newX < -buttonsWidth) {
|
|
// Calculate the new X position, capped at the top of the buttons
|
|
newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4)));
|
|
}
|
|
|
|
this._currentDrag.content.$$ionicOptionsOpen = newX !== 0;
|
|
|
|
this._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + newX + 'px, 0, 0)';
|
|
this._currentDrag.content.style[ionic.CSS.TRANSITION] = 'none';
|
|
}
|
|
});
|
|
|
|
SlideDrag.prototype.end = function(e, doneCallback) {
|
|
var self = this;
|
|
|
|
// There is no drag, just end immediately
|
|
if (!self._currentDrag) {
|
|
doneCallback && doneCallback();
|
|
return;
|
|
}
|
|
|
|
// If we are currently dragging, we want to snap back into place
|
|
// The final resting point X will be the width of the exposed buttons
|
|
var restingPoint = -self._currentDrag.buttonsWidth;
|
|
|
|
// Check if the drag didn't clear the buttons mid-point
|
|
// and we aren't moving fast enough to swipe open
|
|
if (e.gesture.deltaX > -(self._currentDrag.buttonsWidth / 2)) {
|
|
|
|
// If we are going left but too slow, or going right, go back to resting
|
|
if (e.gesture.direction == "left" && Math.abs(e.gesture.velocityX) < 0.3) {
|
|
restingPoint = 0;
|
|
|
|
} else if (e.gesture.direction == "right") {
|
|
restingPoint = 0;
|
|
}
|
|
|
|
}
|
|
|
|
ionic.requestAnimationFrame(function() {
|
|
if (restingPoint === 0) {
|
|
self._currentDrag.content.style[ionic.CSS.TRANSFORM] = '';
|
|
var buttons = self._currentDrag.buttons;
|
|
setTimeout(function() {
|
|
buttons && buttons.classList.add('invisible');
|
|
}, 250);
|
|
} else {
|
|
self._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + restingPoint + 'px,0,0)';
|
|
}
|
|
self._currentDrag.content.style[ionic.CSS.TRANSITION] = '';
|
|
|
|
|
|
// Kill the current drag
|
|
if (!self._lastDrag) {
|
|
self._lastDrag = {};
|
|
}
|
|
ionic.extend(self._lastDrag, self._currentDrag);
|
|
if (self._currentDrag) {
|
|
self._currentDrag.buttons = null;
|
|
self._currentDrag.content = null;
|
|
}
|
|
self._currentDrag = null;
|
|
|
|
// We are done, notify caller
|
|
doneCallback && doneCallback();
|
|
});
|
|
};
|
|
|
|
var ReorderDrag = function(opts) {
|
|
var self = this;
|
|
|
|
self.dragThresholdY = opts.dragThresholdY || 0;
|
|
self.onReorder = opts.onReorder;
|
|
self.listEl = opts.listEl;
|
|
self.el = self.item = opts.el;
|
|
self.scrollEl = opts.scrollEl;
|
|
self.scrollView = opts.scrollView;
|
|
// Get the True Top of the list el http://www.quirksmode.org/js/findpos.html
|
|
self.listElTrueTop = 0;
|
|
if (self.listEl.offsetParent) {
|
|
var obj = self.listEl;
|
|
do {
|
|
self.listElTrueTop += obj.offsetTop;
|
|
obj = obj.offsetParent;
|
|
} while (obj);
|
|
}
|
|
};
|
|
|
|
ReorderDrag.prototype = new DragOp();
|
|
|
|
ReorderDrag.prototype._moveElement = function(e) {
|
|
var y = e.gesture.center.pageY +
|
|
this.scrollView.getValues().top -
|
|
(this._currentDrag.elementHeight / 2) -
|
|
this.listElTrueTop;
|
|
this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(0, ' + y + 'px, 0)';
|
|
};
|
|
|
|
ReorderDrag.prototype.deregister = function() {
|
|
this.listEl = this.el = this.scrollEl = this.scrollView = null;
|
|
};
|
|
|
|
ReorderDrag.prototype.start = function(e) {
|
|
|
|
var startIndex = ionic.DomUtil.getChildIndex(this.el, this.el.nodeName.toLowerCase());
|
|
var elementHeight = this.el.scrollHeight;
|
|
var placeholder = this.el.cloneNode(true);
|
|
|
|
placeholder.classList.add(ITEM_PLACEHOLDER_CLASS);
|
|
|
|
this.el.parentNode.insertBefore(placeholder, this.el);
|
|
this.el.classList.add(ITEM_REORDERING_CLASS);
|
|
|
|
this._currentDrag = {
|
|
elementHeight: elementHeight,
|
|
startIndex: startIndex,
|
|
placeholder: placeholder,
|
|
scrollHeight: scroll,
|
|
list: placeholder.parentNode
|
|
};
|
|
|
|
this._moveElement(e);
|
|
};
|
|
|
|
ReorderDrag.prototype.drag = ionic.animationFrameThrottle(function(e) {
|
|
// We really aren't dragging
|
|
var self = this;
|
|
if (!this._currentDrag) {
|
|
return;
|
|
}
|
|
|
|
var scrollY = 0;
|
|
var pageY = e.gesture.center.pageY;
|
|
var offset = this.listElTrueTop;
|
|
|
|
//If we have a scrollView, check scroll boundaries for dragged element and scroll if necessary
|
|
if (this.scrollView) {
|
|
|
|
var container = this.scrollView.__container;
|
|
scrollY = this.scrollView.getValues().top;
|
|
|
|
var containerTop = container.offsetTop;
|
|
var pixelsPastTop = containerTop - pageY + this._currentDrag.elementHeight / 2;
|
|
var pixelsPastBottom = pageY + this._currentDrag.elementHeight / 2 - containerTop - container.offsetHeight;
|
|
|
|
if (e.gesture.deltaY < 0 && pixelsPastTop > 0 && scrollY > 0) {
|
|
this.scrollView.scrollBy(null, -pixelsPastTop);
|
|
//Trigger another drag so the scrolling keeps going
|
|
ionic.requestAnimationFrame(function() {
|
|
self.drag(e);
|
|
});
|
|
}
|
|
if (e.gesture.deltaY > 0 && pixelsPastBottom > 0) {
|
|
if (scrollY < this.scrollView.getScrollMax().top) {
|
|
this.scrollView.scrollBy(null, pixelsPastBottom);
|
|
//Trigger another drag so the scrolling keeps going
|
|
ionic.requestAnimationFrame(function() {
|
|
self.drag(e);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we should start dragging. Check if we've dragged past the threshold,
|
|
// or we are starting from the open state.
|
|
if (!this._isDragging && Math.abs(e.gesture.deltaY) > this.dragThresholdY) {
|
|
this._isDragging = true;
|
|
}
|
|
|
|
if (this._isDragging) {
|
|
this._moveElement(e);
|
|
|
|
this._currentDrag.currentY = scrollY + pageY - offset;
|
|
|
|
// this._reorderItems();
|
|
}
|
|
});
|
|
|
|
// When an item is dragged, we need to reorder any items for sorting purposes
|
|
ReorderDrag.prototype._getReorderIndex = function() {
|
|
var self = this;
|
|
|
|
var siblings = Array.prototype.slice.call(self._currentDrag.placeholder.parentNode.children)
|
|
.filter(function(el) {
|
|
return el.nodeName === self.el.nodeName && el !== self.el;
|
|
});
|
|
|
|
var dragOffsetTop = self._currentDrag.currentY;
|
|
var el;
|
|
for (var i = 0, len = siblings.length; i < len; i++) {
|
|
el = siblings[i];
|
|
if (i === len - 1) {
|
|
if (dragOffsetTop > el.offsetTop) {
|
|
return i;
|
|
}
|
|
} else if (i === 0) {
|
|
if (dragOffsetTop < el.offsetTop + el.offsetHeight) {
|
|
return i;
|
|
}
|
|
} else if (dragOffsetTop > el.offsetTop - el.offsetHeight / 2 &&
|
|
dragOffsetTop < el.offsetTop + el.offsetHeight) {
|
|
return i;
|
|
}
|
|
}
|
|
return self._currentDrag.startIndex;
|
|
};
|
|
|
|
ReorderDrag.prototype.end = function(e, doneCallback) {
|
|
if (!this._currentDrag) {
|
|
doneCallback && doneCallback();
|
|
return;
|
|
}
|
|
|
|
var placeholder = this._currentDrag.placeholder;
|
|
var finalIndex = this._getReorderIndex();
|
|
|
|
// Reposition the element
|
|
this.el.classList.remove(ITEM_REORDERING_CLASS);
|
|
this.el.style[ionic.CSS.TRANSFORM] = '';
|
|
|
|
placeholder.parentNode.insertBefore(this.el, placeholder);
|
|
placeholder.parentNode.removeChild(placeholder);
|
|
|
|
this.onReorder && this.onReorder(this.el, this._currentDrag.startIndex, finalIndex);
|
|
|
|
this._currentDrag = {
|
|
placeholder: null,
|
|
content: null
|
|
};
|
|
this._currentDrag = null;
|
|
doneCallback && doneCallback();
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* The ListView handles a list of items. It will process drag animations, edit mode,
|
|
* and other operations that are common on mobile lists or table views.
|
|
*/
|
|
ionic.views.ListView = ionic.views.View.inherit({
|
|
initialize: function(opts) {
|
|
var self = this;
|
|
|
|
opts = ionic.extend({
|
|
onReorder: function() {},
|
|
virtualRemoveThreshold: -200,
|
|
virtualAddThreshold: 200,
|
|
canSwipe: function() {
|
|
return true;
|
|
}
|
|
}, opts);
|
|
|
|
ionic.extend(self, opts);
|
|
|
|
if (!self.itemHeight && self.listEl) {
|
|
self.itemHeight = self.listEl.children[0] && parseInt(self.listEl.children[0].style.height, 10);
|
|
}
|
|
|
|
self.onRefresh = opts.onRefresh || function() {};
|
|
self.onRefreshOpening = opts.onRefreshOpening || function() {};
|
|
self.onRefreshHolding = opts.onRefreshHolding || function() {};
|
|
|
|
var gestureOpts = {};
|
|
// don't prevent native scrolling
|
|
if (ionic.DomUtil.getParentOrSelfWithClass(self.el, 'overflow-scroll')) {
|
|
gestureOpts.prevent_default_directions = ['left', 'right'];
|
|
}
|
|
|
|
window.ionic.onGesture('release', function(e) {
|
|
self._handleEndDrag(e);
|
|
}, self.el, gestureOpts);
|
|
|
|
window.ionic.onGesture('drag', function(e) {
|
|
self._handleDrag(e);
|
|
}, self.el, gestureOpts);
|
|
// Start the drag states
|
|
self._initDrag();
|
|
},
|
|
|
|
/**
|
|
* Be sure to cleanup references.
|
|
*/
|
|
deregister: function() {
|
|
this.el = this.listEl = this.scrollEl = this.scrollView = null;
|
|
|
|
// ensure no scrolls have been left frozen
|
|
if (this.isScrollFreeze) {
|
|
self.scrollView.freeze(false);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called to tell the list to stop refreshing. This is useful
|
|
* if you are refreshing the list and are done with refreshing.
|
|
*/
|
|
stopRefreshing: function() {
|
|
var refresher = this.el.querySelector('.list-refresher');
|
|
refresher.style.height = '0';
|
|
},
|
|
|
|
/**
|
|
* If we scrolled and have virtual mode enabled, compute the window
|
|
* of active elements in order to figure out the viewport to render.
|
|
*/
|
|
didScroll: function(e) {
|
|
var self = this;
|
|
|
|
if (self.isVirtual) {
|
|
var itemHeight = self.itemHeight;
|
|
|
|
// Grab the total height of the list
|
|
var scrollHeight = e.target.scrollHeight;
|
|
|
|
// Get the viewport height
|
|
var viewportHeight = self.el.parentNode.offsetHeight;
|
|
|
|
// High water is the pixel position of the first element to include (everything before
|
|
// that will be removed)
|
|
var highWater = Math.max(0, e.scrollTop + self.virtualRemoveThreshold);
|
|
|
|
// Low water is the pixel position of the last element to include (everything after
|
|
// that will be removed)
|
|
var lowWater = Math.min(scrollHeight, Math.abs(e.scrollTop) + viewportHeight + self.virtualAddThreshold);
|
|
|
|
// Get the first and last elements in the list based on how many can fit
|
|
// between the pixel range of lowWater and highWater
|
|
var first = parseInt(Math.abs(highWater / itemHeight), 10);
|
|
var last = parseInt(Math.abs(lowWater / itemHeight), 10);
|
|
|
|
// Get the items we need to remove
|
|
self._virtualItemsToRemove = Array.prototype.slice.call(self.listEl.children, 0, first);
|
|
|
|
self.renderViewport && self.renderViewport(highWater, lowWater, first, last);
|
|
}
|
|
},
|
|
|
|
didStopScrolling: function() {
|
|
if (this.isVirtual) {
|
|
for (var i = 0; i < this._virtualItemsToRemove.length; i++) {
|
|
//el.parentNode.removeChild(el);
|
|
this.didHideItem && this.didHideItem(i);
|
|
}
|
|
// Once scrolling stops, check if we need to remove old items
|
|
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear any active drag effects on the list.
|
|
*/
|
|
clearDragEffects: function(isInstant) {
|
|
if (this._lastDragOp) {
|
|
this._lastDragOp.clean && this._lastDragOp.clean(isInstant);
|
|
this._lastDragOp.deregister && this._lastDragOp.deregister();
|
|
this._lastDragOp = null;
|
|
}
|
|
},
|
|
|
|
_initDrag: function() {
|
|
// Store the last one
|
|
if (this._lastDragOp) {
|
|
this._lastDragOp.deregister && this._lastDragOp.deregister();
|
|
}
|
|
this._lastDragOp = this._dragOp;
|
|
|
|
this._dragOp = null;
|
|
},
|
|
|
|
// Return the list item from the given target
|
|
_getItem: function(target) {
|
|
while (target) {
|
|
if (target.classList && target.classList.contains(ITEM_CLASS)) {
|
|
return target;
|
|
}
|
|
target = target.parentNode;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
|
|
_startDrag: function(e) {
|
|
var self = this;
|
|
|
|
self._isDragging = false;
|
|
|
|
var lastDragOp = self._lastDragOp;
|
|
var item;
|
|
|
|
// If we have an open SlideDrag and we're scrolling the list. Clear it.
|
|
if (self._didDragUpOrDown && lastDragOp instanceof SlideDrag) {
|
|
lastDragOp.clean && lastDragOp.clean();
|
|
}
|
|
|
|
// Check if this is a reorder drag
|
|
if (ionic.DomUtil.getParentOrSelfWithClass(e.target, ITEM_REORDER_BTN_CLASS) && (e.gesture.direction == 'up' || e.gesture.direction == 'down')) {
|
|
item = self._getItem(e.target);
|
|
|
|
if (item) {
|
|
self._dragOp = new ReorderDrag({
|
|
listEl: self.el,
|
|
el: item,
|
|
scrollEl: self.scrollEl,
|
|
scrollView: self.scrollView,
|
|
onReorder: function(el, start, end) {
|
|
self.onReorder && self.onReorder(el, start, end);
|
|
}
|
|
});
|
|
self._dragOp.start(e);
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
// Or check if this is a swipe to the side drag
|
|
else if (!self._didDragUpOrDown && (e.gesture.direction == 'left' || e.gesture.direction == 'right') && Math.abs(e.gesture.deltaX) > 5) {
|
|
|
|
// Make sure this is an item with buttons
|
|
item = self._getItem(e.target);
|
|
if (item && item.querySelector('.item-options')) {
|
|
self._dragOp = new SlideDrag({
|
|
el: self.el,
|
|
item: item,
|
|
canSwipe: self.canSwipe
|
|
});
|
|
self._dragOp.start(e);
|
|
e.preventDefault();
|
|
self.isScrollFreeze = self.scrollView.freeze(true);
|
|
}
|
|
}
|
|
|
|
// If we had a last drag operation and this is a new one on a different item, clean that last one
|
|
if (lastDragOp && self._dragOp && !self._dragOp.isSameItem(lastDragOp) && e.defaultPrevented) {
|
|
lastDragOp.clean && lastDragOp.clean();
|
|
}
|
|
},
|
|
|
|
|
|
_handleEndDrag: function(e) {
|
|
var self = this;
|
|
|
|
if (self.scrollView) {
|
|
self.isScrollFreeze = self.scrollView.freeze(false);
|
|
}
|
|
|
|
self._didDragUpOrDown = false;
|
|
|
|
if (!self._dragOp) {
|
|
return;
|
|
}
|
|
|
|
self._dragOp.end(e, function() {
|
|
self._initDrag();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Process the drag event to move the item to the left or right.
|
|
*/
|
|
_handleDrag: function(e) {
|
|
var self = this;
|
|
|
|
if (Math.abs(e.gesture.deltaY) > 5) {
|
|
self._didDragUpOrDown = true;
|
|
}
|
|
|
|
// If we get a drag event, make sure we aren't in another drag, then check if we should
|
|
// start one
|
|
if (!self.isDragging && !self._dragOp) {
|
|
self._startDrag(e);
|
|
}
|
|
|
|
// No drag still, pass it up
|
|
if (!self._dragOp) {
|
|
return;
|
|
}
|
|
|
|
e.gesture.srcEvent.preventDefault();
|
|
self._dragOp.drag(e);
|
|
}
|
|
|
|
});
|
|
|
|
})(ionic);
|
|
|
|
(function(ionic) {
|
|
'use strict';
|
|
|
|
ionic.views.Modal = ionic.views.View.inherit({
|
|
initialize: function(opts) {
|
|
opts = ionic.extend({
|
|
focusFirstInput: false,
|
|
unfocusOnHide: true,
|
|
focusFirstDelay: 600,
|
|
backdropClickToClose: true,
|
|
hardwareBackButtonClose: true,
|
|
}, opts);
|
|
|
|
ionic.extend(this, opts);
|
|
|
|
this.el = opts.el;
|
|
},
|
|
show: function() {
|
|
var self = this;
|
|
|
|
if(self.focusFirstInput) {
|
|
// Let any animations run first
|
|
window.setTimeout(function() {
|
|
var input = self.el.querySelector('input, textarea');
|
|
input && input.focus && input.focus();
|
|
}, self.focusFirstDelay);
|
|
}
|
|
},
|
|
hide: function() {
|
|
// Unfocus all elements
|
|
if(this.unfocusOnHide) {
|
|
var inputs = this.el.querySelectorAll('input, textarea');
|
|
// Let any animations run first
|
|
window.setTimeout(function() {
|
|
for(var i = 0; i < inputs.length; i++) {
|
|
inputs[i].blur && inputs[i].blur();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
})(ionic);
|
|
|
|
(function(ionic) {
|
|
'use strict';
|
|
|
|
/**
|
|
* The side menu view handles one of the side menu's in a Side Menu Controller
|
|
* configuration.
|
|
* It takes a DOM reference to that side menu element.
|
|
*/
|
|
ionic.views.SideMenu = ionic.views.View.inherit({
|
|
initialize: function(opts) {
|
|
this.el = opts.el;
|
|
this.isEnabled = (typeof opts.isEnabled === 'undefined') ? true : opts.isEnabled;
|
|
this.setWidth(opts.width);
|
|
},
|
|
getFullWidth: function() {
|
|
return this.width;
|
|
},
|
|
setWidth: function(width) {
|
|
this.width = width;
|
|
this.el.style.width = width + 'px';
|
|
},
|
|
setIsEnabled: function(isEnabled) {
|
|
this.isEnabled = isEnabled;
|
|
},
|
|
bringUp: function() {
|
|
if(this.el.style.zIndex !== '0') {
|
|
this.el.style.zIndex = '0';
|
|
}
|
|
},
|
|
pushDown: function() {
|
|
if(this.el.style.zIndex !== '-1') {
|
|
this.el.style.zIndex = '-1';
|
|
}
|
|
}
|
|
});
|
|
|
|
ionic.views.SideMenuContent = ionic.views.View.inherit({
|
|
initialize: function(opts) {
|
|
ionic.extend(this, {
|
|
animationClass: 'menu-animated',
|
|
onDrag: function() {},
|
|
onEndDrag: function() {}
|
|
}, opts);
|
|
|
|
ionic.onGesture('drag', ionic.proxy(this._onDrag, this), this.el);
|
|
ionic.onGesture('release', ionic.proxy(this._onEndDrag, this), this.el);
|
|
},
|
|
_onDrag: function(e) {
|
|
this.onDrag && this.onDrag(e);
|
|
},
|
|
_onEndDrag: function(e) {
|
|
this.onEndDrag && this.onEndDrag(e);
|
|
},
|
|
disableAnimation: function() {
|
|
this.el.classList.remove(this.animationClass);
|
|
},
|
|
enableAnimation: function() {
|
|
this.el.classList.add(this.animationClass);
|
|
},
|
|
getTranslateX: function() {
|
|
return parseFloat(this.el.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]);
|
|
},
|
|
setTranslateX: ionic.animationFrameThrottle(function(x) {
|
|
this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(' + x + 'px, 0, 0)';
|
|
})
|
|
});
|
|
|
|
})(ionic);
|
|
|
|
/*
|
|
* Adapted from Swipe.js 2.0
|
|
*
|
|
* Brad Birdsall
|
|
* Copyright 2013, MIT License
|
|
*
|
|
*/
|
|
|
|
(function(ionic) {
|
|
'use strict';
|
|
|
|
ionic.views.Slider = ionic.views.View.inherit({
|
|
initialize: function (options) {
|
|
var slider = this;
|
|
|
|
var touchStartEvent, touchMoveEvent, touchEndEvent;
|
|
if (window.navigator.pointerEnabled) {
|
|
touchStartEvent = 'pointerdown';
|
|
touchMoveEvent = 'pointermove';
|
|
touchEndEvent = 'pointerup';
|
|
} else if (window.navigator.msPointerEnabled) {
|
|
touchStartEvent = 'MSPointerDown';
|
|
touchMoveEvent = 'MSPointerMove';
|
|
touchEndEvent = 'MSPointerUp';
|
|
} else {
|
|
touchStartEvent = 'touchstart';
|
|
touchMoveEvent = 'touchmove';
|
|
touchEndEvent = 'touchend';
|
|
}
|
|
|
|
var mouseStartEvent = 'mousedown';
|
|
var mouseMoveEvent = 'mousemove';
|
|
var mouseEndEvent = 'mouseup';
|
|
|
|
// utilities
|
|
var noop = function() {}; // simple no operation function
|
|
var offloadFn = function(fn) { setTimeout(fn || noop, 0); }; // offload a functions execution
|
|
|
|
// check browser capabilities
|
|
var browser = {
|
|
addEventListener: !!window.addEventListener,
|
|
transitions: (function(temp) {
|
|
var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition'];
|
|
for ( var i in props ) if (temp.style[ props[i] ] !== undefined) return true;
|
|
return false;
|
|
})(document.createElement('swipe'))
|
|
};
|
|
|
|
|
|
var container = options.el;
|
|
|
|
// quit if no root element
|
|
if (!container) return;
|
|
var element = container.children[0];
|
|
var slides, slidePos, width, length;
|
|
options = options || {};
|
|
var index = parseInt(options.startSlide, 10) || 0;
|
|
var speed = options.speed || 300;
|
|
options.continuous = options.continuous !== undefined ? options.continuous : true;
|
|
|
|
function setup() {
|
|
|
|
// do not setup if the container has no width
|
|
if (!container.offsetWidth) {
|
|
return;
|
|
}
|
|
|
|
// cache slides
|
|
slides = element.children;
|
|
length = slides.length;
|
|
|
|
// set continuous to false if only one slide
|
|
if (slides.length < 2) options.continuous = false;
|
|
|
|
//special case if two slides
|
|
if (browser.transitions && options.continuous && slides.length < 3) {
|
|
element.appendChild(slides[0].cloneNode(true));
|
|
element.appendChild(element.children[1].cloneNode(true));
|
|
slides = element.children;
|
|
}
|
|
|
|
// create an array to store current positions of each slide
|
|
slidePos = new Array(slides.length);
|
|
|
|
// determine width of each slide
|
|
width = container.offsetWidth || container.getBoundingClientRect().width;
|
|
|
|
element.style.width = (slides.length * width) + 'px';
|
|
|
|
// stack elements
|
|
var pos = slides.length;
|
|
while(pos--) {
|
|
|
|
var slide = slides[pos];
|
|
|
|
slide.style.width = width + 'px';
|
|
slide.setAttribute('data-index', pos);
|
|
|
|
if (browser.transitions) {
|
|
slide.style.left = (pos * -width) + 'px';
|
|
move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
|
|
}
|
|
|
|
}
|
|
|
|
// reposition elements before and after index
|
|
if (options.continuous && browser.transitions) {
|
|
move(circle(index - 1), -width, 0);
|
|
move(circle(index + 1), width, 0);
|
|
}
|
|
|
|
if (!browser.transitions) element.style.left = (index * -width) + 'px';
|
|
|
|
container.style.visibility = 'visible';
|
|
|
|
options.slidesChanged && options.slidesChanged();
|
|
}
|
|
|
|
function prev(slideSpeed) {
|
|
|
|
if (options.continuous) slide(index - 1, slideSpeed);
|
|
else if (index) slide(index - 1, slideSpeed);
|
|
|
|
}
|
|
|
|
function next(slideSpeed) {
|
|
|
|
if (options.continuous) slide(index + 1, slideSpeed);
|
|
else if (index < slides.length - 1) slide(index + 1, slideSpeed);
|
|
|
|
}
|
|
|
|
function circle(index) {
|
|
|
|
// a simple positive modulo using slides.length
|
|
return (slides.length + (index % slides.length)) % slides.length;
|
|
|
|
}
|
|
|
|
function slide(to, slideSpeed) {
|
|
|
|
// do nothing if already on requested slide
|
|
if (index == to) return;
|
|
|
|
if (!slides) {
|
|
index = to;
|
|
return;
|
|
}
|
|
|
|
if (browser.transitions) {
|
|
|
|
var direction = Math.abs(index - to) / (index - to); // 1: backward, -1: forward
|
|
|
|
// get the actual position of the slide
|
|
if (options.continuous) {
|
|
var naturalDirection = direction;
|
|
direction = -slidePos[circle(to)] / width;
|
|
|
|
// if going forward but to < index, use to = slides.length + to
|
|
// if going backward but to > index, use to = -slides.length + to
|
|
if (direction !== naturalDirection) to = -direction * slides.length + to;
|
|
|
|
}
|
|
|
|
var diff = Math.abs(index - to) - 1;
|
|
|
|
// move all the slides between index and to in the right direction
|
|
while (diff--) move( circle((to > index ? to : index) - diff - 1), width * direction, 0);
|
|
|
|
to = circle(to);
|
|
|
|
move(index, width * direction, slideSpeed || speed);
|
|
move(to, 0, slideSpeed || speed);
|
|
|
|
if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place
|
|
|
|
} else {
|
|
|
|
to = circle(to);
|
|
animate(index * -width, to * -width, slideSpeed || speed);
|
|
//no fallback for a circular continuous if the browser does not accept transitions
|
|
}
|
|
|
|
index = to;
|
|
offloadFn(options.callback && options.callback(index, slides[index]));
|
|
}
|
|
|
|
function move(index, dist, speed) {
|
|
|
|
translate(index, dist, speed);
|
|
slidePos[index] = dist;
|
|
|
|
}
|
|
|
|
function translate(index, dist, speed) {
|
|
|
|
var slide = slides[index];
|
|
var style = slide && slide.style;
|
|
|
|
if (!style) return;
|
|
|
|
style.webkitTransitionDuration =
|
|
style.MozTransitionDuration =
|
|
style.msTransitionDuration =
|
|
style.OTransitionDuration =
|
|
style.transitionDuration = speed + 'ms';
|
|
|
|
style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)';
|
|
style.msTransform =
|
|
style.MozTransform =
|
|
style.OTransform = 'translateX(' + dist + 'px)';
|
|
|
|
}
|
|
|
|
function animate(from, to, speed) {
|
|
|
|
// if not an animation, just reposition
|
|
if (!speed) {
|
|
|
|
element.style.left = to + 'px';
|
|
return;
|
|
|
|
}
|
|
|
|
var start = +new Date();
|
|
|
|
var timer = setInterval(function() {
|
|
|
|
var timeElap = +new Date() - start;
|
|
|
|
if (timeElap > speed) {
|
|
|
|
element.style.left = to + 'px';
|
|
|
|
if (delay) begin();
|
|
|
|
options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
|
|
|
|
clearInterval(timer);
|
|
return;
|
|
|
|
}
|
|
|
|
element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px';
|
|
|
|
}, 4);
|
|
|
|
}
|
|
|
|
// setup auto slideshow
|
|
var delay = options.auto || 0;
|
|
var interval;
|
|
|
|
function begin() {
|
|
|
|
interval = setTimeout(next, delay);
|
|
|
|
}
|
|
|
|
function stop() {
|
|
|
|
delay = options.auto || 0;
|
|
clearTimeout(interval);
|
|
|
|
}
|
|
|
|
|
|
// setup initial vars
|
|
var start = {};
|
|
var delta = {};
|
|
var isScrolling;
|
|
|
|
// setup event capturing
|
|
var events = {
|
|
|
|
handleEvent: function(event) {
|
|
if(!event.touches && event.pageX && event.pageY) {
|
|
event.touches = [{
|
|
pageX: event.pageX,
|
|
pageY: event.pageY
|
|
}];
|
|
}
|
|
|
|
switch (event.type) {
|
|
case touchStartEvent: this.start(event); break;
|
|
case mouseStartEvent: this.start(event); break;
|
|
case touchMoveEvent: this.touchmove(event); break;
|
|
case mouseMoveEvent: this.touchmove(event); break;
|
|
case touchEndEvent: offloadFn(this.end(event)); break;
|
|
case mouseEndEvent: offloadFn(this.end(event)); break;
|
|
case 'webkitTransitionEnd':
|
|
case 'msTransitionEnd':
|
|
case 'oTransitionEnd':
|
|
case 'otransitionend':
|
|
case 'transitionend': offloadFn(this.transitionEnd(event)); break;
|
|
case 'resize': offloadFn(setup); break;
|
|
}
|
|
|
|
if (options.stopPropagation) event.stopPropagation();
|
|
|
|
},
|
|
start: function(event) {
|
|
|
|
// prevent to start if there is no valid event
|
|
if (!event.touches) {
|
|
return;
|
|
}
|
|
|
|
var touches = event.touches[0];
|
|
|
|
// measure start values
|
|
start = {
|
|
|
|
// get initial touch coords
|
|
x: touches.pageX,
|
|
y: touches.pageY,
|
|
|
|
// store time to determine touch duration
|
|
time: +new Date()
|
|
|
|
};
|
|
|
|
// used for testing first move event
|
|
isScrolling = undefined;
|
|
|
|
// reset delta and end measurements
|
|
delta = {};
|
|
|
|
// attach touchmove and touchend listeners
|
|
element.addEventListener(touchMoveEvent, this, false);
|
|
element.addEventListener(mouseMoveEvent, this, false);
|
|
|
|
element.addEventListener(touchEndEvent, this, false);
|
|
element.addEventListener(mouseEndEvent, this, false);
|
|
|
|
document.addEventListener(touchEndEvent, this, false);
|
|
document.addEventListener(mouseEndEvent, this, false);
|
|
},
|
|
touchmove: function(event) {
|
|
|
|
// ensure there is a valid event
|
|
// ensure swiping with one touch and not pinching
|
|
// ensure sliding is enabled
|
|
if (!event.touches ||
|
|
event.touches.length > 1 ||
|
|
event.scale && event.scale !== 1 ||
|
|
slider.slideIsDisabled) {
|
|
return;
|
|
}
|
|
|
|
if (options.disableScroll) event.preventDefault();
|
|
|
|
var touches = event.touches[0];
|
|
|
|
// measure change in x and y
|
|
delta = {
|
|
x: touches.pageX - start.x,
|
|
y: touches.pageY - start.y
|
|
};
|
|
|
|
// determine if scrolling test has run - one time test
|
|
if ( typeof isScrolling == 'undefined') {
|
|
isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) );
|
|
}
|
|
|
|
// if user is not trying to scroll vertically
|
|
if (!isScrolling) {
|
|
|
|
// prevent native scrolling
|
|
event.preventDefault();
|
|
|
|
// stop slideshow
|
|
stop();
|
|
|
|
// increase resistance if first or last slide
|
|
if (options.continuous) { // we don't add resistance at the end
|
|
|
|
translate(circle(index - 1), delta.x + slidePos[circle(index - 1)], 0);
|
|
translate(index, delta.x + slidePos[index], 0);
|
|
translate(circle(index + 1), delta.x + slidePos[circle(index + 1)], 0);
|
|
|
|
} else {
|
|
// If the slider bounces, do the bounce!
|
|
if(options.bouncing) {
|
|
delta.x =
|
|
delta.x /
|
|
( (!index && delta.x > 0 || // if first slide and sliding left
|
|
index == slides.length - 1 && // or if last slide and sliding right
|
|
delta.x < 0 // and if sliding at all
|
|
) ?
|
|
( Math.abs(delta.x) / width + 1 ) // determine resistance level
|
|
: 1 ); // no resistance if false
|
|
} else {
|
|
if(width * index - delta.x < 0) { //We are trying scroll past left boundary
|
|
delta.x = Math.min(delta.x, width * index); //Set delta.x so we don't go past left screen
|
|
}
|
|
if(Math.abs(delta.x) > width * (slides.length - index - 1)){ //We are trying to scroll past right bondary
|
|
delta.x = Math.max( -width * (slides.length - index - 1), delta.x); //Set delta.x so we don't go past right screen
|
|
}
|
|
}
|
|
|
|
// translate 1:1
|
|
translate(index - 1, delta.x + slidePos[index - 1], 0);
|
|
translate(index, delta.x + slidePos[index], 0);
|
|
translate(index + 1, delta.x + slidePos[index + 1], 0);
|
|
}
|
|
|
|
options.onDrag && options.onDrag();
|
|
}
|
|
|
|
},
|
|
end: function() {
|
|
|
|
// measure duration
|
|
var duration = +new Date() - start.time;
|
|
|
|
// determine if slide attempt triggers next/prev slide
|
|
var isValidSlide =
|
|
Number(duration) < 250 && // if slide duration is less than 250ms
|
|
Math.abs(delta.x) > 20 || // and if slide amt is greater than 20px
|
|
Math.abs(delta.x) > width / 2; // or if slide amt is greater than half the width
|
|
|
|
// determine if slide attempt is past start and end
|
|
var isPastBounds = (!index && delta.x > 0) || // if first slide and slide amt is greater than 0
|
|
(index == slides.length - 1 && delta.x < 0); // or if last slide and slide amt is less than 0
|
|
|
|
if (options.continuous) isPastBounds = false;
|
|
|
|
// determine direction of swipe (true:right, false:left)
|
|
var direction = delta.x < 0;
|
|
|
|
// if not scrolling vertically
|
|
if (!isScrolling) {
|
|
|
|
if (isValidSlide && !isPastBounds) {
|
|
|
|
if (direction) {
|
|
|
|
if (options.continuous) { // we need to get the next in this direction in place
|
|
|
|
move(circle(index - 1), -width, 0);
|
|
move(circle(index + 2), width, 0);
|
|
|
|
} else {
|
|
move(index - 1, -width, 0);
|
|
}
|
|
|
|
move(index, slidePos[index] - width, speed);
|
|
move(circle(index + 1), slidePos[circle(index + 1)] - width, speed);
|
|
index = circle(index + 1);
|
|
|
|
} else {
|
|
if (options.continuous) { // we need to get the next in this direction in place
|
|
|
|
move(circle(index + 1), width, 0);
|
|
move(circle(index - 2), -width, 0);
|
|
|
|
} else {
|
|
move(index + 1, width, 0);
|
|
}
|
|
|
|
move(index, slidePos[index] + width, speed);
|
|
move(circle(index - 1), slidePos[circle(index - 1)] + width, speed);
|
|
index = circle(index - 1);
|
|
|
|
}
|
|
|
|
options.callback && options.callback(index, slides[index]);
|
|
|
|
} else {
|
|
|
|
if (options.continuous) {
|
|
|
|
move(circle(index - 1), -width, speed);
|
|
move(index, 0, speed);
|
|
move(circle(index + 1), width, speed);
|
|
|
|
} else {
|
|
|
|
move(index - 1, -width, speed);
|
|
move(index, 0, speed);
|
|
move(index + 1, width, speed);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// kill touchmove and touchend event listeners until touchstart called again
|
|
element.removeEventListener(touchMoveEvent, events, false);
|
|
element.removeEventListener(mouseMoveEvent, events, false);
|
|
|
|
element.removeEventListener(touchEndEvent, events, false);
|
|
element.removeEventListener(mouseEndEvent, events, false);
|
|
|
|
document.removeEventListener(touchEndEvent, events, false);
|
|
document.removeEventListener(mouseEndEvent, events, false);
|
|
|
|
options.onDragEnd && options.onDragEnd();
|
|
},
|
|
transitionEnd: function(event) {
|
|
|
|
if (parseInt(event.target.getAttribute('data-index'), 10) == index) {
|
|
|
|
if (delay) begin();
|
|
|
|
options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// Public API
|
|
this.update = function() {
|
|
setTimeout(setup);
|
|
};
|
|
this.setup = function() {
|
|
setup();
|
|
};
|
|
|
|
this.loop = function(value) {
|
|
if (arguments.length) options.continuous = !!value;
|
|
return options.continuous;
|
|
};
|
|
|
|
this.enableSlide = function(shouldEnable) {
|
|
if (arguments.length) {
|
|
this.slideIsDisabled = !shouldEnable;
|
|
}
|
|
return !this.slideIsDisabled;
|
|
};
|
|
|
|
this.slide = this.select = function(to, speed) {
|
|
// cancel slideshow
|
|
stop();
|
|
|
|
slide(to, speed);
|
|
};
|
|
|
|
this.prev = this.previous = function() {
|
|
// cancel slideshow
|
|
stop();
|
|
|
|
prev();
|
|
};
|
|
|
|
this.next = function() {
|
|
// cancel slideshow
|
|
stop();
|
|
|
|
next();
|
|
};
|
|
|
|
this.stop = function() {
|
|
// cancel slideshow
|
|
stop();
|
|
};
|
|
|
|
this.start = function() {
|
|
begin();
|
|
};
|
|
|
|
this.autoPlay = function(newDelay) {
|
|
if (!delay || delay < 0) {
|
|
stop();
|
|
} else {
|
|
delay = newDelay;
|
|
begin();
|
|
}
|
|
};
|
|
|
|
this.currentIndex = this.selected = function() {
|
|
// return current index position
|
|
return index;
|
|
};
|
|
|
|
this.slidesCount = this.count = function() {
|
|
// return total number of slides
|
|
return length;
|
|
};
|
|
|
|
this.kill = function() {
|
|
// cancel slideshow
|
|
stop();
|
|
|
|
// reset element
|
|
element.style.width = '';
|
|
element.style.left = '';
|
|
|
|
// reset slides so no refs are held on to
|
|
slides && (slides = []);
|
|
|
|
// removed event listeners
|
|
if (browser.addEventListener) {
|
|
|
|
// remove current event listeners
|
|
element.removeEventListener(touchStartEvent, events, false);
|
|
element.removeEventListener(mouseStartEvent, events, false);
|
|
element.removeEventListener('webkitTransitionEnd', events, false);
|
|
element.removeEventListener('msTransitionEnd', events, false);
|
|
element.removeEventListener('oTransitionEnd', events, false);
|
|
element.removeEventListener('otransitionend', events, false);
|
|
element.removeEventListener('transitionend', events, false);
|
|
window.removeEventListener('resize', events, false);
|
|
|
|
}
|
|
else {
|
|
|
|
window.onresize = null;
|
|
|
|
}
|
|
};
|
|
|
|
this.load = function() {
|
|
// trigger setup
|
|
setup();
|
|
|
|
// start auto slideshow if applicable
|
|
if (delay) begin();
|
|
|
|
|
|
// add event listeners
|
|
if (browser.addEventListener) {
|
|
|
|
// set touchstart event on element
|
|
element.addEventListener(touchStartEvent, events, false);
|
|
element.addEventListener(mouseStartEvent, events, false);
|
|
|
|
if (browser.transitions) {
|
|
element.addEventListener('webkitTransitionEnd', events, false);
|
|
element.addEventListener('msTransitionEnd', events, false);
|
|
element.addEventListener('oTransitionEnd', events, false);
|
|
element.addEventListener('otransitionend', events, false);
|
|
element.addEventListener('transitionend', events, false);
|
|
}
|
|
|
|
// set resize event on window
|
|
window.addEventListener('resize', events, false);
|
|
|
|
} else {
|
|
|
|
window.onresize = function () { setup(); }; // to play nice with old IE
|
|
|
|
}
|
|
};
|
|
|
|
}
|
|
});
|
|
|
|
})(ionic);
|
|
|
|
/*eslint space-after-keywords: 0*/
|
|
|
|
/**
|
|
* Swiper 3.2.7
|
|
* Most modern mobile touch slider and framework with hardware accelerated transitions
|
|
*
|
|
* http://www.idangero.us/swiper/
|
|
*
|
|
* Copyright 2015, Vladimir Kharlampidi
|
|
* The iDangero.us
|
|
* http://www.idangero.us/
|
|
*
|
|
* Licensed under MIT
|
|
*
|
|
* Released on: December 7, 2015
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
var $;
|
|
/*===========================
|
|
Swiper
|
|
===========================*/
|
|
var Swiper = function (container, params, _scope, $compile) {
|
|
|
|
if (!(this instanceof Swiper)) return new Swiper(container, params);
|
|
|
|
var defaults = {
|
|
direction: 'horizontal',
|
|
touchEventsTarget: 'container',
|
|
initialSlide: 0,
|
|
speed: 300,
|
|
// autoplay
|
|
autoplay: false,
|
|
autoplayDisableOnInteraction: true,
|
|
// To support iOS's swipe-to-go-back gesture (when being used in-app, with UIWebView).
|
|
iOSEdgeSwipeDetection: false,
|
|
iOSEdgeSwipeThreshold: 20,
|
|
// Free mode
|
|
freeMode: false,
|
|
freeModeMomentum: true,
|
|
freeModeMomentumRatio: 1,
|
|
freeModeMomentumBounce: true,
|
|
freeModeMomentumBounceRatio: 1,
|
|
freeModeSticky: false,
|
|
freeModeMinimumVelocity: 0.02,
|
|
// Autoheight
|
|
autoHeight: false,
|
|
// Set wrapper width
|
|
setWrapperSize: false,
|
|
// Virtual Translate
|
|
virtualTranslate: false,
|
|
// Effects
|
|
effect: 'slide', // 'slide' or 'fade' or 'cube' or 'coverflow'
|
|
coverflow: {
|
|
rotate: 50,
|
|
stretch: 0,
|
|
depth: 100,
|
|
modifier: 1,
|
|
slideShadows : true
|
|
},
|
|
cube: {
|
|
slideShadows: true,
|
|
shadow: true,
|
|
shadowOffset: 20,
|
|
shadowScale: 0.94
|
|
},
|
|
fade: {
|
|
crossFade: false
|
|
},
|
|
// Parallax
|
|
parallax: false,
|
|
// Scrollbar
|
|
scrollbar: null,
|
|
scrollbarHide: true,
|
|
scrollbarDraggable: false,
|
|
scrollbarSnapOnRelease: false,
|
|
// Keyboard Mousewheel
|
|
keyboardControl: false,
|
|
mousewheelControl: false,
|
|
mousewheelReleaseOnEdges: false,
|
|
mousewheelInvert: false,
|
|
mousewheelForceToAxis: false,
|
|
mousewheelSensitivity: 1,
|
|
// Hash Navigation
|
|
hashnav: false,
|
|
// Breakpoints
|
|
breakpoints: undefined,
|
|
// Slides grid
|
|
spaceBetween: 0,
|
|
slidesPerView: 1,
|
|
slidesPerColumn: 1,
|
|
slidesPerColumnFill: 'column',
|
|
slidesPerGroup: 1,
|
|
centeredSlides: false,
|
|
slidesOffsetBefore: 0, // in px
|
|
slidesOffsetAfter: 0, // in px
|
|
// Round length
|
|
roundLengths: false,
|
|
// Touches
|
|
touchRatio: 1,
|
|
touchAngle: 45,
|
|
simulateTouch: true,
|
|
shortSwipes: true,
|
|
longSwipes: true,
|
|
longSwipesRatio: 0.5,
|
|
longSwipesMs: 300,
|
|
followFinger: true,
|
|
onlyExternal: false,
|
|
threshold: 0,
|
|
touchMoveStopPropagation: true,
|
|
// Pagination
|
|
pagination: null,
|
|
paginationElement: 'span',
|
|
paginationClickable: false,
|
|
paginationHide: false,
|
|
paginationBulletRender: null,
|
|
// Resistance
|
|
resistance: true,
|
|
resistanceRatio: 0.85,
|
|
// Next/prev buttons
|
|
nextButton: null,
|
|
prevButton: null,
|
|
// Progress
|
|
watchSlidesProgress: false,
|
|
watchSlidesVisibility: false,
|
|
// Cursor
|
|
grabCursor: false,
|
|
// Clicks
|
|
preventClicks: true,
|
|
preventClicksPropagation: true,
|
|
slideToClickedSlide: false,
|
|
// Lazy Loading
|
|
lazyLoading: false,
|
|
lazyLoadingInPrevNext: false,
|
|
lazyLoadingOnTransitionStart: false,
|
|
// Images
|
|
preloadImages: true,
|
|
updateOnImagesReady: true,
|
|
// loop
|
|
loop: false,
|
|
loopAdditionalSlides: 0,
|
|
loopedSlides: null,
|
|
// Control
|
|
control: undefined,
|
|
controlInverse: false,
|
|
controlBy: 'slide', //or 'container'
|
|
// Swiping/no swiping
|
|
allowSwipeToPrev: true,
|
|
allowSwipeToNext: true,
|
|
swipeHandler: null, //'.swipe-handler',
|
|
noSwiping: true,
|
|
noSwipingClass: 'swiper-no-swiping',
|
|
// NS
|
|
slideClass: 'swiper-slide',
|
|
slideActiveClass: 'swiper-slide-active',
|
|
slideVisibleClass: 'swiper-slide-visible',
|
|
slideDuplicateClass: 'swiper-slide-duplicate',
|
|
slideNextClass: 'swiper-slide-next',
|
|
slidePrevClass: 'swiper-slide-prev',
|
|
wrapperClass: 'swiper-wrapper',
|
|
bulletClass: 'swiper-pagination-bullet',
|
|
bulletActiveClass: 'swiper-pagination-bullet-active',
|
|
buttonDisabledClass: 'swiper-button-disabled',
|
|
paginationHiddenClass: 'swiper-pagination-hidden',
|
|
// Observer
|
|
observer: false,
|
|
observeParents: false,
|
|
// Accessibility
|
|
a11y: false,
|
|
prevSlideMessage: 'Previous slide',
|
|
nextSlideMessage: 'Next slide',
|
|
firstSlideMessage: 'This is the first slide',
|
|
lastSlideMessage: 'This is the last slide',
|
|
paginationBulletMessage: 'Go to slide {{index}}',
|
|
// Callbacks
|
|
runCallbacksOnInit: true
|
|
/*
|
|
Callbacks:
|
|
onInit: function (swiper)
|
|
onDestroy: function (swiper)
|
|
onClick: function (swiper, e)
|
|
onTap: function (swiper, e)
|
|
onDoubleTap: function (swiper, e)
|
|
onSliderMove: function (swiper, e)
|
|
onSlideChangeStart: function (swiper)
|
|
onSlideChangeEnd: function (swiper)
|
|
onTransitionStart: function (swiper)
|
|
onTransitionEnd: function (swiper)
|
|
onImagesReady: function (swiper)
|
|
onProgress: function (swiper, progress)
|
|
onTouchStart: function (swiper, e)
|
|
onTouchMove: function (swiper, e)
|
|
onTouchMoveOpposite: function (swiper, e)
|
|
onTouchEnd: function (swiper, e)
|
|
onReachBeginning: function (swiper)
|
|
onReachEnd: function (swiper)
|
|
onSetTransition: function (swiper, duration)
|
|
onSetTranslate: function (swiper, translate)
|
|
onAutoplayStart: function (swiper)
|
|
onAutoplayStop: function (swiper),
|
|
onLazyImageLoad: function (swiper, slide, image)
|
|
onLazyImageReady: function (swiper, slide, image)
|
|
*/
|
|
|
|
};
|
|
var initialVirtualTranslate = params && params.virtualTranslate;
|
|
|
|
params = params || {};
|
|
var originalParams = {};
|
|
for (var param in params) {
|
|
if (typeof params[param] === 'object' && !(params[param].nodeType || params[param] === window || params[param] === document || (typeof Dom7 !== 'undefined' && params[param] instanceof Dom7) || (typeof jQuery !== 'undefined' && params[param] instanceof jQuery))) {
|
|
originalParams[param] = {};
|
|
for (var deepParam in params[param]) {
|
|
originalParams[param][deepParam] = params[param][deepParam];
|
|
}
|
|
}
|
|
else {
|
|
originalParams[param] = params[param];
|
|
}
|
|
}
|
|
for (var def in defaults) {
|
|
if (typeof params[def] === 'undefined') {
|
|
params[def] = defaults[def];
|
|
}
|
|
else if (typeof params[def] === 'object') {
|
|
for (var deepDef in defaults[def]) {
|
|
if (typeof params[def][deepDef] === 'undefined') {
|
|
params[def][deepDef] = defaults[def][deepDef];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Swiper
|
|
var s = this;
|
|
|
|
// Params
|
|
s.params = params;
|
|
s.originalParams = originalParams;
|
|
|
|
// Classname
|
|
s.classNames = [];
|
|
/*=========================
|
|
Dom Library and plugins
|
|
===========================*/
|
|
if (typeof $ !== 'undefined' && typeof Dom7 !== 'undefined'){
|
|
$ = Dom7;
|
|
}
|
|
if (typeof $ === 'undefined') {
|
|
if (typeof Dom7 === 'undefined') {
|
|
$ = window.Dom7 || window.Zepto || window.jQuery;
|
|
}
|
|
else {
|
|
$ = Dom7;
|
|
}
|
|
if (!$) return;
|
|
}
|
|
// Export it to Swiper instance
|
|
s.$ = $;
|
|
|
|
/*=========================
|
|
Breakpoints
|
|
===========================*/
|
|
s.currentBreakpoint = undefined;
|
|
s.getActiveBreakpoint = function () {
|
|
//Get breakpoint for window width
|
|
if (!s.params.breakpoints) return false;
|
|
var breakpoint = false;
|
|
var points = [], point;
|
|
for ( point in s.params.breakpoints ) {
|
|
if (s.params.breakpoints.hasOwnProperty(point)) {
|
|
points.push(point);
|
|
}
|
|
}
|
|
points.sort(function (a, b) {
|
|
return parseInt(a, 10) > parseInt(b, 10);
|
|
});
|
|
for (var i = 0; i < points.length; i++) {
|
|
point = points[i];
|
|
if (point >= window.innerWidth && !breakpoint) {
|
|
breakpoint = point;
|
|
}
|
|
}
|
|
return breakpoint || 'max';
|
|
};
|
|
s.setBreakpoint = function () {
|
|
//Set breakpoint for window width and update parameters
|
|
var breakpoint = s.getActiveBreakpoint();
|
|
if (breakpoint && s.currentBreakpoint !== breakpoint) {
|
|
var breakPointsParams = breakpoint in s.params.breakpoints ? s.params.breakpoints[breakpoint] : s.originalParams;
|
|
for ( var param in breakPointsParams ) {
|
|
s.params[param] = breakPointsParams[param];
|
|
}
|
|
s.currentBreakpoint = breakpoint;
|
|
}
|
|
};
|
|
// Set breakpoint on load
|
|
if (s.params.breakpoints) {
|
|
s.setBreakpoint();
|
|
}
|
|
|
|
/*=========================
|
|
Preparation - Define Container, Wrapper and Pagination
|
|
===========================*/
|
|
s.container = $(container);
|
|
if (s.container.length === 0) return;
|
|
if (s.container.length > 1) {
|
|
s.container.each(function () {
|
|
new Swiper(this, params);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Save instance in container HTML Element and in data
|
|
s.container[0].swiper = s;
|
|
s.container.data('swiper', s);
|
|
|
|
s.classNames.push('swiper-container-' + s.params.direction);
|
|
|
|
if (s.params.freeMode) {
|
|
s.classNames.push('swiper-container-free-mode');
|
|
}
|
|
if (!s.support.flexbox) {
|
|
s.classNames.push('swiper-container-no-flexbox');
|
|
s.params.slidesPerColumn = 1;
|
|
}
|
|
if (s.params.autoHeight) {
|
|
s.classNames.push('swiper-container-autoheight');
|
|
}
|
|
// Enable slides progress when required
|
|
if (s.params.parallax || s.params.watchSlidesVisibility) {
|
|
s.params.watchSlidesProgress = true;
|
|
}
|
|
// Coverflow / 3D
|
|
if (['cube', 'coverflow'].indexOf(s.params.effect) >= 0) {
|
|
if (s.support.transforms3d) {
|
|
s.params.watchSlidesProgress = true;
|
|
s.classNames.push('swiper-container-3d');
|
|
}
|
|
else {
|
|
s.params.effect = 'slide';
|
|
}
|
|
}
|
|
if (s.params.effect !== 'slide') {
|
|
s.classNames.push('swiper-container-' + s.params.effect);
|
|
}
|
|
if (s.params.effect === 'cube') {
|
|
s.params.resistanceRatio = 0;
|
|
s.params.slidesPerView = 1;
|
|
s.params.slidesPerColumn = 1;
|
|
s.params.slidesPerGroup = 1;
|
|
s.params.centeredSlides = false;
|
|
s.params.spaceBetween = 0;
|
|
s.params.virtualTranslate = true;
|
|
s.params.setWrapperSize = false;
|
|
}
|
|
if (s.params.effect === 'fade') {
|
|
s.params.slidesPerView = 1;
|
|
s.params.slidesPerColumn = 1;
|
|
s.params.slidesPerGroup = 1;
|
|
s.params.watchSlidesProgress = true;
|
|
s.params.spaceBetween = 0;
|
|
if (typeof initialVirtualTranslate === 'undefined') {
|
|
s.params.virtualTranslate = true;
|
|
}
|
|
}
|
|
|
|
// Grab Cursor
|
|
if (s.params.grabCursor && s.support.touch) {
|
|
s.params.grabCursor = false;
|
|
}
|
|
|
|
// Wrapper
|
|
s.wrapper = s.container.children('.' + s.params.wrapperClass);
|
|
|
|
// Pagination
|
|
if (s.params.pagination) {
|
|
s.paginationContainer = $(s.params.pagination);
|
|
if (s.params.paginationClickable) {
|
|
s.paginationContainer.addClass('swiper-pagination-clickable');
|
|
}
|
|
}
|
|
|
|
// Is Horizontal
|
|
function isH() {
|
|
return s.params.direction === 'horizontal';
|
|
}
|
|
|
|
// RTL
|
|
s.rtl = isH() && (s.container[0].dir.toLowerCase() === 'rtl' || s.container.css('direction') === 'rtl');
|
|
if (s.rtl) {
|
|
s.classNames.push('swiper-container-rtl');
|
|
}
|
|
|
|
// Wrong RTL support
|
|
if (s.rtl) {
|
|
s.wrongRTL = s.wrapper.css('display') === '-webkit-box';
|
|
}
|
|
|
|
// Columns
|
|
if (s.params.slidesPerColumn > 1) {
|
|
s.classNames.push('swiper-container-multirow');
|
|
}
|
|
|
|
// Check for Android
|
|
if (s.device.android) {
|
|
s.classNames.push('swiper-container-android');
|
|
}
|
|
|
|
// Add classes
|
|
s.container.addClass(s.classNames.join(' '));
|
|
|
|
// Translate
|
|
s.translate = 0;
|
|
|
|
// Progress
|
|
s.progress = 0;
|
|
|
|
// Velocity
|
|
s.velocity = 0;
|
|
|
|
/*=========================
|
|
Locks, unlocks
|
|
===========================*/
|
|
s.lockSwipeToNext = function () {
|
|
s.params.allowSwipeToNext = false;
|
|
};
|
|
s.lockSwipeToPrev = function () {
|
|
s.params.allowSwipeToPrev = false;
|
|
};
|
|
s.lockSwipes = function () {
|
|
s.params.allowSwipeToNext = s.params.allowSwipeToPrev = false;
|
|
};
|
|
s.unlockSwipeToNext = function () {
|
|
s.params.allowSwipeToNext = true;
|
|
};
|
|
s.unlockSwipeToPrev = function () {
|
|
s.params.allowSwipeToPrev = true;
|
|
};
|
|
s.unlockSwipes = function () {
|
|
s.params.allowSwipeToNext = s.params.allowSwipeToPrev = true;
|
|
};
|
|
|
|
/*=========================
|
|
Round helper
|
|
===========================*/
|
|
function round(a) {
|
|
return Math.floor(a);
|
|
}
|
|
/*=========================
|
|
Set grab cursor
|
|
===========================*/
|
|
if (s.params.grabCursor) {
|
|
s.container[0].style.cursor = 'move';
|
|
s.container[0].style.cursor = '-webkit-grab';
|
|
s.container[0].style.cursor = '-moz-grab';
|
|
s.container[0].style.cursor = 'grab';
|
|
}
|
|
/*=========================
|
|
Update on Images Ready
|
|
===========================*/
|
|
s.imagesToLoad = [];
|
|
s.imagesLoaded = 0;
|
|
|
|
s.loadImage = function (imgElement, src, srcset, checkForComplete, callback) {
|
|
var image;
|
|
function onReady () {
|
|
if (callback) callback();
|
|
}
|
|
if (!imgElement.complete || !checkForComplete) {
|
|
if (src) {
|
|
image = new window.Image();
|
|
image.onload = onReady;
|
|
image.onerror = onReady;
|
|
if (srcset) {
|
|
image.srcset = srcset;
|
|
}
|
|
if (src) {
|
|
image.src = src;
|
|
}
|
|
} else {
|
|
onReady();
|
|
}
|
|
|
|
} else {//image already loaded...
|
|
onReady();
|
|
}
|
|
};
|
|
s.preloadImages = function () {
|
|
s.imagesToLoad = s.container.find('img');
|
|
function _onReady() {
|
|
if (typeof s === 'undefined' || s === null) return;
|
|
if (s.imagesLoaded !== undefined) s.imagesLoaded++;
|
|
if (s.imagesLoaded === s.imagesToLoad.length) {
|
|
if (s.params.updateOnImagesReady) s.update();
|
|
s.emit('onImagesReady', s);
|
|
}
|
|
}
|
|
for (var i = 0; i < s.imagesToLoad.length; i++) {
|
|
s.loadImage(s.imagesToLoad[i], (s.imagesToLoad[i].currentSrc || s.imagesToLoad[i].getAttribute('src')), (s.imagesToLoad[i].srcset || s.imagesToLoad[i].getAttribute('srcset')), true, _onReady);
|
|
}
|
|
};
|
|
|
|
/*=========================
|
|
Autoplay
|
|
===========================*/
|
|
s.autoplayTimeoutId = undefined;
|
|
s.autoplaying = false;
|
|
s.autoplayPaused = false;
|
|
function autoplay() {
|
|
s.autoplayTimeoutId = setTimeout(function () {
|
|
if (s.params.loop) {
|
|
s.fixLoop();
|
|
s._slideNext();
|
|
}
|
|
else {
|
|
if (!s.isEnd) {
|
|
s._slideNext();
|
|
}
|
|
else {
|
|
if (!params.autoplayStopOnLast) {
|
|
s._slideTo(0);
|
|
}
|
|
else {
|
|
s.stopAutoplay();
|
|
}
|
|
}
|
|
}
|
|
}, s.params.autoplay);
|
|
}
|
|
s.startAutoplay = function () {
|
|
if (typeof s.autoplayTimeoutId !== 'undefined') return false;
|
|
if (!s.params.autoplay) return false;
|
|
if (s.autoplaying) return false;
|
|
s.autoplaying = true;
|
|
s.emit('onAutoplayStart', s);
|
|
autoplay();
|
|
};
|
|
s.stopAutoplay = function (internal) {
|
|
if (!s.autoplayTimeoutId) return;
|
|
if (s.autoplayTimeoutId) clearTimeout(s.autoplayTimeoutId);
|
|
s.autoplaying = false;
|
|
s.autoplayTimeoutId = undefined;
|
|
s.emit('onAutoplayStop', s);
|
|
};
|
|
s.pauseAutoplay = function (speed) {
|
|
if (s.autoplayPaused) return;
|
|
if (s.autoplayTimeoutId) clearTimeout(s.autoplayTimeoutId);
|
|
s.autoplayPaused = true;
|
|
if (speed === 0) {
|
|
s.autoplayPaused = false;
|
|
autoplay();
|
|
}
|
|
else {
|
|
s.wrapper.transitionEnd(function () {
|
|
if (!s) return;
|
|
s.autoplayPaused = false;
|
|
if (!s.autoplaying) {
|
|
s.stopAutoplay();
|
|
}
|
|
else {
|
|
autoplay();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
/*=========================
|
|
Min/Max Translate
|
|
===========================*/
|
|
s.minTranslate = function () {
|
|
return (-s.snapGrid[0]);
|
|
};
|
|
s.maxTranslate = function () {
|
|
return (-s.snapGrid[s.snapGrid.length - 1]);
|
|
};
|
|
/*=========================
|
|
Slider/slides sizes
|
|
===========================*/
|
|
s.updateAutoHeight = function () {
|
|
// Update Height
|
|
var newHeight = s.slides.eq(s.activeIndex)[0].offsetHeight;
|
|
if (newHeight) s.wrapper.css('height', s.slides.eq(s.activeIndex)[0].offsetHeight + 'px');
|
|
};
|
|
s.updateContainerSize = function () {
|
|
var width, height;
|
|
if (typeof s.params.width !== 'undefined') {
|
|
width = s.params.width;
|
|
}
|
|
else {
|
|
width = s.container[0].clientWidth;
|
|
}
|
|
if (typeof s.params.height !== 'undefined') {
|
|
height = s.params.height;
|
|
}
|
|
else {
|
|
height = s.container[0].clientHeight;
|
|
}
|
|
if (width === 0 && isH() || height === 0 && !isH()) {
|
|
return;
|
|
}
|
|
|
|
//Subtract paddings
|
|
width = width - parseInt(s.container.css('padding-left'), 10) - parseInt(s.container.css('padding-right'), 10);
|
|
height = height - parseInt(s.container.css('padding-top'), 10) - parseInt(s.container.css('padding-bottom'), 10);
|
|
|
|
// Store values
|
|
s.width = width;
|
|
s.height = height;
|
|
s.size = isH() ? s.width : s.height;
|
|
};
|
|
|
|
s.updateSlidesSize = function () {
|
|
s.slides = s.wrapper.children('.' + s.params.slideClass);
|
|
s.snapGrid = [];
|
|
s.slidesGrid = [];
|
|
s.slidesSizesGrid = [];
|
|
|
|
var spaceBetween = s.params.spaceBetween,
|
|
slidePosition = -s.params.slidesOffsetBefore,
|
|
i,
|
|
prevSlideSize = 0,
|
|
index = 0;
|
|
if (typeof spaceBetween === 'string' && spaceBetween.indexOf('%') >= 0) {
|
|
spaceBetween = parseFloat(spaceBetween.replace('%', '')) / 100 * s.size;
|
|
}
|
|
|
|
s.virtualSize = -spaceBetween;
|
|
// reset margins
|
|
if (s.rtl) s.slides.css({marginLeft: '', marginTop: ''});
|
|
else s.slides.css({marginRight: '', marginBottom: ''});
|
|
|
|
var slidesNumberEvenToRows;
|
|
if (s.params.slidesPerColumn > 1) {
|
|
if (Math.floor(s.slides.length / s.params.slidesPerColumn) === s.slides.length / s.params.slidesPerColumn) {
|
|
slidesNumberEvenToRows = s.slides.length;
|
|
}
|
|
else {
|
|
slidesNumberEvenToRows = Math.ceil(s.slides.length / s.params.slidesPerColumn) * s.params.slidesPerColumn;
|
|
}
|
|
if (s.params.slidesPerView !== 'auto' && s.params.slidesPerColumnFill === 'row') {
|
|
slidesNumberEvenToRows = Math.max(slidesNumberEvenToRows, s.params.slidesPerView * s.params.slidesPerColumn);
|
|
}
|
|
}
|
|
|
|
// Calc slides
|
|
var slideSize;
|
|
var slidesPerColumn = s.params.slidesPerColumn;
|
|
var slidesPerRow = slidesNumberEvenToRows / slidesPerColumn;
|
|
var numFullColumns = slidesPerRow - (s.params.slidesPerColumn * slidesPerRow - s.slides.length);
|
|
for (i = 0; i < s.slides.length; i++) {
|
|
slideSize = 0;
|
|
var slide = s.slides.eq(i);
|
|
if (s.params.slidesPerColumn > 1) {
|
|
// Set slides order
|
|
var newSlideOrderIndex;
|
|
var column, row;
|
|
if (s.params.slidesPerColumnFill === 'column') {
|
|
column = Math.floor(i / slidesPerColumn);
|
|
row = i - column * slidesPerColumn;
|
|
if (column > numFullColumns || (column === numFullColumns && row === slidesPerColumn-1)) {
|
|
if (++row >= slidesPerColumn) {
|
|
row = 0;
|
|
column++;
|
|
}
|
|
}
|
|
newSlideOrderIndex = column + row * slidesNumberEvenToRows / slidesPerColumn;
|
|
slide
|
|
.css({
|
|
'-webkit-box-ordinal-group': newSlideOrderIndex,
|
|
'-moz-box-ordinal-group': newSlideOrderIndex,
|
|
'-ms-flex-order': newSlideOrderIndex,
|
|
'-webkit-order': newSlideOrderIndex,
|
|
'order': newSlideOrderIndex
|
|
});
|
|
}
|
|
else {
|
|
row = Math.floor(i / slidesPerRow);
|
|
column = i - row * slidesPerRow;
|
|
}
|
|
slide
|
|
.css({
|
|
'margin-top': (row !== 0 && s.params.spaceBetween) && (s.params.spaceBetween + 'px')
|
|
})
|
|
.attr('data-swiper-column', column)
|
|
.attr('data-swiper-row', row);
|
|
|
|
}
|
|
if (slide.css('display') === 'none') continue;
|
|
if (s.params.slidesPerView === 'auto') {
|
|
slideSize = isH() ? slide.outerWidth(true) : slide.outerHeight(true);
|
|
if (s.params.roundLengths) slideSize = round(slideSize);
|
|
}
|
|
else {
|
|
slideSize = (s.size - (s.params.slidesPerView - 1) * spaceBetween) / s.params.slidesPerView;
|
|
if (s.params.roundLengths) slideSize = round(slideSize);
|
|
|
|
if (isH()) {
|
|
s.slides[i].style.width = slideSize + 'px';
|
|
}
|
|
else {
|
|
s.slides[i].style.height = slideSize + 'px';
|
|
}
|
|
}
|
|
s.slides[i].swiperSlideSize = slideSize;
|
|
s.slidesSizesGrid.push(slideSize);
|
|
|
|
|
|
if (s.params.centeredSlides) {
|
|
slidePosition = slidePosition + slideSize / 2 + prevSlideSize / 2 + spaceBetween;
|
|
if (i === 0) slidePosition = slidePosition - s.size / 2 - spaceBetween;
|
|
if (Math.abs(slidePosition) < 1 / 1000) slidePosition = 0;
|
|
if ((index) % s.params.slidesPerGroup === 0) s.snapGrid.push(slidePosition);
|
|
s.slidesGrid.push(slidePosition);
|
|
}
|
|
else {
|
|
if ((index) % s.params.slidesPerGroup === 0) s.snapGrid.push(slidePosition);
|
|
s.slidesGrid.push(slidePosition);
|
|
slidePosition = slidePosition + slideSize + spaceBetween;
|
|
}
|
|
|
|
s.virtualSize += slideSize + spaceBetween;
|
|
|
|
prevSlideSize = slideSize;
|
|
|
|
index ++;
|
|
}
|
|
s.virtualSize = Math.max(s.virtualSize, s.size) + s.params.slidesOffsetAfter;
|
|
var newSlidesGrid;
|
|
|
|
if (
|
|
s.rtl && s.wrongRTL && (s.params.effect === 'slide' || s.params.effect === 'coverflow')) {
|
|
s.wrapper.css({width: s.virtualSize + s.params.spaceBetween + 'px'});
|
|
}
|
|
if (!s.support.flexbox || s.params.setWrapperSize) {
|
|
if (isH()) s.wrapper.css({width: s.virtualSize + s.params.spaceBetween + 'px'});
|
|
else s.wrapper.css({height: s.virtualSize + s.params.spaceBetween + 'px'});
|
|
}
|
|
|
|
if (s.params.slidesPerColumn > 1) {
|
|
s.virtualSize = (slideSize + s.params.spaceBetween) * slidesNumberEvenToRows;
|
|
s.virtualSize = Math.ceil(s.virtualSize / s.params.slidesPerColumn) - s.params.spaceBetween;
|
|
s.wrapper.css({width: s.virtualSize + s.params.spaceBetween + 'px'});
|
|
if (s.params.centeredSlides) {
|
|
newSlidesGrid = [];
|
|
for (i = 0; i < s.snapGrid.length; i++) {
|
|
if (s.snapGrid[i] < s.virtualSize + s.snapGrid[0]) newSlidesGrid.push(s.snapGrid[i]);
|
|
}
|
|
s.snapGrid = newSlidesGrid;
|
|
}
|
|
}
|
|
|
|
// Remove last grid elements depending on width
|
|
if (!s.params.centeredSlides) {
|
|
newSlidesGrid = [];
|
|
for (i = 0; i < s.snapGrid.length; i++) {
|
|
if (s.snapGrid[i] <= s.virtualSize - s.size) {
|
|
newSlidesGrid.push(s.snapGrid[i]);
|
|
}
|
|
}
|
|
s.snapGrid = newSlidesGrid;
|
|
if (Math.floor(s.virtualSize - s.size) > Math.floor(s.snapGrid[s.snapGrid.length - 1])) {
|
|
s.snapGrid.push(s.virtualSize - s.size);
|
|
}
|
|
}
|
|
if (s.snapGrid.length === 0) s.snapGrid = [0];
|
|
|
|
if (s.params.spaceBetween !== 0) {
|
|
if (isH()) {
|
|
if (s.rtl) s.slides.css({marginLeft: spaceBetween + 'px'});
|
|
else s.slides.css({marginRight: spaceBetween + 'px'});
|
|
}
|
|
else s.slides.css({marginBottom: spaceBetween + 'px'});
|
|
}
|
|
if (s.params.watchSlidesProgress) {
|
|
s.updateSlidesOffset();
|
|
}
|
|
};
|
|
s.updateSlidesOffset = function () {
|
|
for (var i = 0; i < s.slides.length; i++) {
|
|
s.slides[i].swiperSlideOffset = isH() ? s.slides[i].offsetLeft : s.slides[i].offsetTop;
|
|
}
|
|
};
|
|
|
|
/*=========================
|
|
Slider/slides progress
|
|
===========================*/
|
|
s.updateSlidesProgress = function (translate) {
|
|
if (typeof translate === 'undefined') {
|
|
translate = s.translate || 0;
|
|
}
|
|
if (s.slides.length === 0) return;
|
|
if (typeof s.slides[0].swiperSlideOffset === 'undefined') s.updateSlidesOffset();
|
|
|
|
var offsetCenter = -translate;
|
|
if (s.rtl) offsetCenter = translate;
|
|
|
|
// Visible Slides
|
|
s.slides.removeClass(s.params.slideVisibleClass);
|
|
for (var i = 0; i < s.slides.length; i++) {
|
|
var slide = s.slides[i];
|
|
var slideProgress = (offsetCenter - slide.swiperSlideOffset) / (slide.swiperSlideSize + s.params.spaceBetween);
|
|
if (s.params.watchSlidesVisibility) {
|
|
var slideBefore = -(offsetCenter - slide.swiperSlideOffset);
|
|
var slideAfter = slideBefore + s.slidesSizesGrid[i];
|
|
var isVisible =
|
|
(slideBefore >= 0 && slideBefore < s.size) ||
|
|
(slideAfter > 0 && slideAfter <= s.size) ||
|
|
(slideBefore <= 0 && slideAfter >= s.size);
|
|
if (isVisible) {
|
|
s.slides.eq(i).addClass(s.params.slideVisibleClass);
|
|
}
|
|
}
|
|
slide.progress = s.rtl ? -slideProgress : slideProgress;
|
|
}
|
|
};
|
|
s.updateProgress = function (translate) {
|
|
if (typeof translate === 'undefined') {
|
|
translate = s.translate || 0;
|
|
}
|
|
var translatesDiff = s.maxTranslate() - s.minTranslate();
|
|
var wasBeginning = s.isBeginning;
|
|
var wasEnd = s.isEnd;
|
|
if (translatesDiff === 0) {
|
|
s.progress = 0;
|
|
s.isBeginning = s.isEnd = true;
|
|
}
|
|
else {
|
|
s.progress = (translate - s.minTranslate()) / (translatesDiff);
|
|
s.isBeginning = s.progress <= 0;
|
|
s.isEnd = s.progress >= 1;
|
|
}
|
|
if (s.isBeginning && !wasBeginning) s.emit('onReachBeginning', s);
|
|
if (s.isEnd && !wasEnd) s.emit('onReachEnd', s);
|
|
|
|
if (s.params.watchSlidesProgress) s.updateSlidesProgress(translate);
|
|
s.emit('onProgress', s, s.progress);
|
|
};
|
|
s.updateActiveIndex = function () {
|
|
var translate = s.rtl ? s.translate : -s.translate;
|
|
var newActiveIndex, i, snapIndex;
|
|
for (i = 0; i < s.slidesGrid.length; i ++) {
|
|
if (typeof s.slidesGrid[i + 1] !== 'undefined') {
|
|
if (translate >= s.slidesGrid[i] && translate < s.slidesGrid[i + 1] - (s.slidesGrid[i + 1] - s.slidesGrid[i]) / 2) {
|
|
newActiveIndex = i;
|
|
}
|
|
else if (translate >= s.slidesGrid[i] && translate < s.slidesGrid[i + 1]) {
|
|
newActiveIndex = i + 1;
|
|
}
|
|
}
|
|
else {
|
|
if (translate >= s.slidesGrid[i]) {
|
|
newActiveIndex = i;
|
|
}
|
|
}
|
|
}
|
|
// Normalize slideIndex
|
|
if (newActiveIndex < 0 || typeof newActiveIndex === 'undefined') newActiveIndex = 0;
|
|
// for (i = 0; i < s.slidesGrid.length; i++) {
|
|
// if (- translate >= s.slidesGrid[i]) {
|
|
// newActiveIndex = i;
|
|
// }
|
|
// }
|
|
snapIndex = Math.floor(newActiveIndex / s.params.slidesPerGroup);
|
|
if (snapIndex >= s.snapGrid.length) snapIndex = s.snapGrid.length - 1;
|
|
|
|
if (newActiveIndex === s.activeIndex) {
|
|
return;
|
|
}
|
|
s.snapIndex = snapIndex;
|
|
s.previousIndex = s.activeIndex;
|
|
s.activeIndex = newActiveIndex;
|
|
s.updateClasses();
|
|
};
|
|
|
|
/*=========================
|
|
Classes
|
|
===========================*/
|
|
s.updateClasses = function () {
|
|
s.slides.removeClass(s.params.slideActiveClass + ' ' + s.params.slideNextClass + ' ' + s.params.slidePrevClass);
|
|
var activeSlide = s.slides.eq(s.activeIndex);
|
|
// Active classes
|
|
activeSlide.addClass(s.params.slideActiveClass);
|
|
activeSlide.next('.' + s.params.slideClass).addClass(s.params.slideNextClass);
|
|
activeSlide.prev('.' + s.params.slideClass).addClass(s.params.slidePrevClass);
|
|
|
|
// Pagination
|
|
if (s.bullets && s.bullets.length > 0) {
|
|
s.bullets.removeClass(s.params.bulletActiveClass);
|
|
var bulletIndex;
|
|
if (s.params.loop) {
|
|
bulletIndex = Math.ceil(s.activeIndex - s.loopedSlides)/s.params.slidesPerGroup;
|
|
if (bulletIndex > s.slides.length - 1 - s.loopedSlides * 2) {
|
|
bulletIndex = bulletIndex - (s.slides.length - s.loopedSlides * 2);
|
|
}
|
|
if (bulletIndex > s.bullets.length - 1) bulletIndex = bulletIndex - s.bullets.length;
|
|
}
|
|
else {
|
|
if (typeof s.snapIndex !== 'undefined') {
|
|
bulletIndex = s.snapIndex;
|
|
}
|
|
else {
|
|
bulletIndex = s.activeIndex || 0;
|
|
}
|
|
}
|
|
if (s.paginationContainer.length > 1) {
|
|
s.bullets.each(function () {
|
|
if ($(this).index() === bulletIndex) $(this).addClass(s.params.bulletActiveClass);
|
|
});
|
|
}
|
|
else {
|
|
s.bullets.eq(bulletIndex).addClass(s.params.bulletActiveClass);
|
|
}
|
|
}
|
|
|
|
// Next/active buttons
|
|
if (!s.params.loop) {
|
|
if (s.params.prevButton) {
|
|
if (s.isBeginning) {
|
|
$(s.params.prevButton).addClass(s.params.buttonDisabledClass);
|
|
if (s.params.a11y && s.a11y) s.a11y.disable($(s.params.prevButton));
|
|
}
|
|
else {
|
|
$(s.params.prevButton).removeClass(s.params.buttonDisabledClass);
|
|
if (s.params.a11y && s.a11y) s.a11y.enable($(s.params.prevButton));
|
|
}
|
|
}
|
|
if (s.params.nextButton) {
|
|
if (s.isEnd) {
|
|
$(s.params.nextButton).addClass(s.params.buttonDisabledClass);
|
|
if (s.params.a11y && s.a11y) s.a11y.disable($(s.params.nextButton));
|
|
}
|
|
else {
|
|
$(s.params.nextButton).removeClass(s.params.buttonDisabledClass);
|
|
if (s.params.a11y && s.a11y) s.a11y.enable($(s.params.nextButton));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/*=========================
|
|
Pagination
|
|
===========================*/
|
|
s.updatePagination = function () {
|
|
if (!s.params.pagination) return;
|
|
if (s.paginationContainer && s.paginationContainer.length > 0) {
|
|
var bulletsHTML = '';
|
|
var numberOfBullets = s.params.loop ? Math.ceil((s.slides.length - s.loopedSlides * 2) / s.params.slidesPerGroup) : s.snapGrid.length;
|
|
for (var i = 0; i < numberOfBullets; i++) {
|
|
if (s.params.paginationBulletRender) {
|
|
bulletsHTML += s.params.paginationBulletRender(i, s.params.bulletClass);
|
|
}
|
|
else {
|
|
bulletsHTML += '<' + s.params.paginationElement+' class="' + s.params.bulletClass + '"></' + s.params.paginationElement + '>';
|
|
}
|
|
}
|
|
s.paginationContainer.html(bulletsHTML);
|
|
s.bullets = s.paginationContainer.find('.' + s.params.bulletClass);
|
|
if (s.params.paginationClickable && s.params.a11y && s.a11y) {
|
|
s.a11y.initPagination();
|
|
}
|
|
}
|
|
};
|
|
/*=========================
|
|
Common update method
|
|
===========================*/
|
|
s.update = function (updateTranslate) {
|
|
s.updateContainerSize();
|
|
s.updateSlidesSize();
|
|
s.updateProgress();
|
|
s.updatePagination();
|
|
s.updateClasses();
|
|
if (s.params.scrollbar && s.scrollbar) {
|
|
s.scrollbar.set();
|
|
}
|
|
function forceSetTranslate() {
|
|
newTranslate = Math.min(Math.max(s.translate, s.maxTranslate()), s.minTranslate());
|
|
s.setWrapperTranslate(newTranslate);
|
|
s.updateActiveIndex();
|
|
s.updateClasses();
|
|
}
|
|
if (updateTranslate) {
|
|
var translated, newTranslate;
|
|
if (s.controller && s.controller.spline) {
|
|
s.controller.spline = undefined;
|
|
}
|
|
if (s.params.freeMode) {
|
|
forceSetTranslate();
|
|
if (s.params.autoHeight) {
|
|
s.updateAutoHeight();
|
|
}
|
|
}
|
|
else {
|
|
if ((s.params.slidesPerView === 'auto' || s.params.slidesPerView > 1) && s.isEnd && !s.params.centeredSlides) {
|
|
translated = s.slideTo(s.slides.length - 1, 0, false, true);
|
|
}
|
|
else {
|
|
translated = s.slideTo(s.activeIndex, 0, false, true);
|
|
}
|
|
if (!translated) {
|
|
forceSetTranslate();
|
|
}
|
|
}
|
|
}
|
|
else if (s.params.autoHeight) {
|
|
s.updateAutoHeight();
|
|
}
|
|
};
|
|
|
|
/*=========================
|
|
Resize Handler
|
|
===========================*/
|
|
s.onResize = function (forceUpdatePagination) {
|
|
//Breakpoints
|
|
if (s.params.breakpoints) {
|
|
s.setBreakpoint();
|
|
}
|
|
|
|
// Disable locks on resize
|
|
var allowSwipeToPrev = s.params.allowSwipeToPrev;
|
|
var allowSwipeToNext = s.params.allowSwipeToNext;
|
|
s.params.allowSwipeToPrev = s.params.allowSwipeToNext = true;
|
|
|
|
s.updateContainerSize();
|
|
s.updateSlidesSize();
|
|
if (s.params.slidesPerView === 'auto' || s.params.freeMode || forceUpdatePagination) s.updatePagination();
|
|
if (s.params.scrollbar && s.scrollbar) {
|
|
s.scrollbar.set();
|
|
}
|
|
if (s.controller && s.controller.spline) {
|
|
s.controller.spline = undefined;
|
|
}
|
|
if (s.params.freeMode) {
|
|
var newTranslate = Math.min(Math.max(s.translate, s.maxTranslate()), s.minTranslate());
|
|
s.setWrapperTranslate(newTranslate);
|
|
s.updateActiveIndex();
|
|
s.updateClasses();
|
|
|
|
if (s.params.autoHeight) {
|
|
s.updateAutoHeight();
|
|
}
|
|
}
|
|
else {
|
|
s.updateClasses();
|
|
if ((s.params.slidesPerView === 'auto' || s.params.slidesPerView > 1) && s.isEnd && !s.params.centeredSlides) {
|
|
s.slideTo(s.slides.length - 1, 0, false, true);
|
|
}
|
|
else {
|
|
s.slideTo(s.activeIndex, 0, false, true);
|
|
}
|
|
}
|
|
// Return locks after resize
|
|
s.params.allowSwipeToPrev = allowSwipeToPrev;
|
|
s.params.allowSwipeToNext = allowSwipeToNext;
|
|
};
|
|
|
|
/*=========================
|
|
Events
|
|
===========================*/
|
|
|
|
//Define Touch Events
|
|
var desktopEvents = ['mousedown', 'mousemove', 'mouseup'];
|
|
if (window.navigator.pointerEnabled) desktopEvents = ['pointerdown', 'pointermove', 'pointerup'];
|
|
else if (window.navigator.msPointerEnabled) desktopEvents = ['MSPointerDown', 'MSPointerMove', 'MSPointerUp'];
|
|
s.touchEvents = {
|
|
start : s.support.touch || !s.params.simulateTouch ? 'touchstart' : desktopEvents[0],
|
|
move : s.support.touch || !s.params.simulateTouch ? 'touchmove' : desktopEvents[1],
|
|
end : s.support.touch || !s.params.simulateTouch ? 'touchend' : desktopEvents[2]
|
|
};
|
|
|
|
|
|
// WP8 Touch Events Fix
|
|
if (window.navigator.pointerEnabled || window.navigator.msPointerEnabled) {
|
|
(s.params.touchEventsTarget === 'container' ? s.container : s.wrapper).addClass('swiper-wp8-' + s.params.direction);
|
|
}
|
|
|
|
// Attach/detach events
|
|
s.initEvents = function (detach) {
|
|
var actionDom = detach ? 'off' : 'on';
|
|
var action = detach ? 'removeEventListener' : 'addEventListener';
|
|
var touchEventsTarget = s.params.touchEventsTarget === 'container' ? s.container[0] : s.wrapper[0];
|
|
var target = s.support.touch ? touchEventsTarget : document;
|
|
|
|
var moveCapture = s.params.nested ? true : false;
|
|
|
|
//Touch Events
|
|
if (s.browser.ie) {
|
|
touchEventsTarget[action](s.touchEvents.start, s.onTouchStart, false);
|
|
target[action](s.touchEvents.move, s.onTouchMove, moveCapture);
|
|
target[action](s.touchEvents.end, s.onTouchEnd, false);
|
|
}
|
|
else {
|
|
if (s.support.touch) {
|
|
touchEventsTarget[action](s.touchEvents.start, s.onTouchStart, false);
|
|
touchEventsTarget[action](s.touchEvents.move, s.onTouchMove, moveCapture);
|
|
touchEventsTarget[action](s.touchEvents.end, s.onTouchEnd, false);
|
|
}
|
|
if (params.simulateTouch && !s.device.ios && !s.device.android) {
|
|
touchEventsTarget[action]('mousedown', s.onTouchStart, false);
|
|
document[action]('mousemove', s.onTouchMove, moveCapture);
|
|
document[action]('mouseup', s.onTouchEnd, false);
|
|
}
|
|
}
|
|
window[action]('resize', s.onResize);
|
|
|
|
// Next, Prev, Index
|
|
if (s.params.nextButton) {
|
|
$(s.params.nextButton)[actionDom]('click', s.onClickNext);
|
|
if (s.params.a11y && s.a11y) $(s.params.nextButton)[actionDom]('keydown', s.a11y.onEnterKey);
|
|
}
|
|
if (s.params.prevButton) {
|
|
$(s.params.prevButton)[actionDom]('click', s.onClickPrev);
|
|
if (s.params.a11y && s.a11y) $(s.params.prevButton)[actionDom]('keydown', s.a11y.onEnterKey);
|
|
}
|
|
if (s.params.pagination && s.params.paginationClickable) {
|
|
$(s.paginationContainer)[actionDom]('click', '.' + s.params.bulletClass, s.onClickIndex);
|
|
if (s.params.a11y && s.a11y) $(s.paginationContainer)[actionDom]('keydown', '.' + s.params.bulletClass, s.a11y.onEnterKey);
|
|
}
|
|
|
|
// Prevent Links Clicks
|
|
if (s.params.preventClicks || s.params.preventClicksPropagation) touchEventsTarget[action]('click', s.preventClicks, true);
|
|
};
|
|
s.attachEvents = function (detach) {
|
|
s.initEvents();
|
|
};
|
|
s.detachEvents = function () {
|
|
s.initEvents(true);
|
|
};
|
|
|
|
/*=========================
|
|
Handle Clicks
|
|
===========================*/
|
|
// Prevent Clicks
|
|
s.allowClick = true;
|
|
s.preventClicks = function (e) {
|
|
if (!s.allowClick) {
|
|
if (s.params.preventClicks) e.preventDefault();
|
|
if (s.params.preventClicksPropagation && s.animating) {
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
}
|
|
}
|
|
};
|
|
// Clicks
|
|
s.onClickNext = function (e) {
|
|
e.preventDefault();
|
|
if (s.isEnd && !s.params.loop) return;
|
|
s.slideNext();
|
|
};
|
|
s.onClickPrev = function (e) {
|
|
e.preventDefault();
|
|
if (s.isBeginning && !s.params.loop) return;
|
|
s.slidePrev();
|
|
};
|
|
s.onClickIndex = function (e) {
|
|
e.preventDefault();
|
|
var index = $(this).index() * s.params.slidesPerGroup;
|
|
if (s.params.loop) index = index + s.loopedSlides;
|
|
s.slideTo(index);
|
|
};
|
|
|
|
/*=========================
|
|
Handle Touches
|
|
===========================*/
|
|
function findElementInEvent(e, selector) {
|
|
var el = $(e.target);
|
|
if (!el.is(selector)) {
|
|
if (typeof selector === 'string') {
|
|
el = el.parents(selector);
|
|
}
|
|
else if (selector.nodeType) {
|
|
var found;
|
|
el.parents().each(function (index, _el) {
|
|
if (_el === selector) found = selector;
|
|
});
|
|
if (!found) return undefined;
|
|
else return selector;
|
|
}
|
|
}
|
|
if (el.length === 0) {
|
|
return undefined;
|
|
}
|
|
return el[0];
|
|
}
|
|
s.updateClickedSlide = function (e) {
|
|
var slide = findElementInEvent(e, '.' + s.params.slideClass);
|
|
var slideFound = false;
|
|
if (slide) {
|
|
for (var i = 0; i < s.slides.length; i++) {
|
|
if (s.slides[i] === slide) slideFound = true;
|
|
}
|
|
}
|
|
|
|
if (slide && slideFound) {
|
|
s.clickedSlide = slide;
|
|
s.clickedIndex = $(slide).index();
|
|
}
|
|
else {
|
|
s.clickedSlide = undefined;
|
|
s.clickedIndex = undefined;
|
|
return;
|
|
}
|
|
if (s.params.slideToClickedSlide && s.clickedIndex !== undefined && s.clickedIndex !== s.activeIndex) {
|
|
var slideToIndex = s.clickedIndex,
|
|
realIndex,
|
|
duplicatedSlides;
|
|
if (s.params.loop) {
|
|
if (s.animating) return;
|
|
realIndex = $(s.clickedSlide).attr('data-swiper-slide-index');
|
|
if (s.params.centeredSlides) {
|
|
if ((slideToIndex < s.loopedSlides - s.params.slidesPerView/2) || (slideToIndex > s.slides.length - s.loopedSlides + s.params.slidesPerView/2)) {
|
|
s.fixLoop();
|
|
slideToIndex = s.wrapper.children('.' + s.params.slideClass + '[data-swiper-slide-index="' + realIndex + '"]:not(.swiper-slide-duplicate)').eq(0).index();
|
|
setTimeout(function () {
|
|
s.slideTo(slideToIndex);
|
|
}, 0);
|
|
}
|
|
else {
|
|
s.slideTo(slideToIndex);
|
|
}
|
|
}
|
|
else {
|
|
if (slideToIndex > s.slides.length - s.params.slidesPerView) {
|
|
s.fixLoop();
|
|
slideToIndex = s.wrapper.children('.' + s.params.slideClass + '[data-swiper-slide-index="' + realIndex + '"]:not(.swiper-slide-duplicate)').eq(0).index();
|
|
setTimeout(function () {
|
|
s.slideTo(slideToIndex);
|
|
}, 0);
|
|
}
|
|
else {
|
|
s.slideTo(slideToIndex);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
s.slideTo(slideToIndex);
|
|
}
|
|
}
|
|
};
|
|
|
|
var isTouched,
|
|
isMoved,
|
|
allowTouchCallbacks,
|
|
touchStartTime,
|
|
isScrolling,
|
|
currentTranslate,
|
|
startTranslate,
|
|
allowThresholdMove,
|
|
// Form elements to match
|
|
formElements = 'input, select, textarea, button',
|
|
// Last click time
|
|
lastClickTime = Date.now(), clickTimeout,
|
|
//Velocities
|
|
velocities = [],
|
|
allowMomentumBounce;
|
|
|
|
// Animating Flag
|
|
s.animating = false;
|
|
|
|
// Touches information
|
|
s.touches = {
|
|
startX: 0,
|
|
startY: 0,
|
|
currentX: 0,
|
|
currentY: 0,
|
|
diff: 0
|
|
};
|
|
|
|
// Touch handlers
|
|
var isTouchEvent, startMoving;
|
|
s.onTouchStart = function (e) {
|
|
if (e.originalEvent) e = e.originalEvent;
|
|
isTouchEvent = e.type === 'touchstart';
|
|
if (!isTouchEvent && 'which' in e && e.which === 3) return;
|
|
if (s.params.noSwiping && findElementInEvent(e, '.' + s.params.noSwipingClass)) {
|
|
s.allowClick = true;
|
|
return;
|
|
}
|
|
if (s.params.swipeHandler) {
|
|
if (!findElementInEvent(e, s.params.swipeHandler)) return;
|
|
}
|
|
|
|
var startX = s.touches.currentX = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX;
|
|
var startY = s.touches.currentY = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY;
|
|
|
|
// Do NOT start if iOS edge swipe is detected. Otherwise iOS app (UIWebView) cannot swipe-to-go-back anymore
|
|
if(s.device.ios && s.params.iOSEdgeSwipeDetection && startX <= s.params.iOSEdgeSwipeThreshold) {
|
|
return;
|
|
}
|
|
|
|
isTouched = true;
|
|
isMoved = false;
|
|
allowTouchCallbacks = true;
|
|
isScrolling = undefined;
|
|
startMoving = undefined;
|
|
s.touches.startX = startX;
|
|
s.touches.startY = startY;
|
|
touchStartTime = Date.now();
|
|
s.allowClick = true;
|
|
s.updateContainerSize();
|
|
s.swipeDirection = undefined;
|
|
if (s.params.threshold > 0) allowThresholdMove = false;
|
|
if (e.type !== 'touchstart') {
|
|
var preventDefault = true;
|
|
if ($(e.target).is(formElements)) preventDefault = false;
|
|
if (document.activeElement && $(document.activeElement).is(formElements)) {
|
|
document.activeElement.blur();
|
|
}
|
|
if (preventDefault) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
s.emit('onTouchStart', s, e);
|
|
};
|
|
|
|
s.onTouchMove = function (e) {
|
|
if (e.originalEvent) e = e.originalEvent;
|
|
if (isTouchEvent && e.type === 'mousemove') return;
|
|
if (e.preventedByNestedSwiper) return;
|
|
if (s.params.onlyExternal) {
|
|
// isMoved = true;
|
|
s.allowClick = false;
|
|
if (isTouched) {
|
|
s.touches.startX = s.touches.currentX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;
|
|
s.touches.startY = s.touches.currentY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY;
|
|
touchStartTime = Date.now();
|
|
}
|
|
return;
|
|
}
|
|
if (isTouchEvent && document.activeElement) {
|
|
if (e.target === document.activeElement && $(e.target).is(formElements)) {
|
|
isMoved = true;
|
|
s.allowClick = false;
|
|
return;
|
|
}
|
|
}
|
|
if (allowTouchCallbacks) {
|
|
s.emit('onTouchMove', s, e);
|
|
}
|
|
if (e.targetTouches && e.targetTouches.length > 1) return;
|
|
|
|
s.touches.currentX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;
|
|
s.touches.currentY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY;
|
|
|
|
if (typeof isScrolling === 'undefined') {
|
|
var touchAngle = Math.atan2(Math.abs(s.touches.currentY - s.touches.startY), Math.abs(s.touches.currentX - s.touches.startX)) * 180 / Math.PI;
|
|
isScrolling = isH() ? touchAngle > s.params.touchAngle : (90 - touchAngle > s.params.touchAngle);
|
|
}
|
|
if (isScrolling) {
|
|
s.emit('onTouchMoveOpposite', s, e);
|
|
}
|
|
if (typeof startMoving === 'undefined' && s.browser.ieTouch) {
|
|
if (s.touches.currentX !== s.touches.startX || s.touches.currentY !== s.touches.startY) {
|
|
startMoving = true;
|
|
}
|
|
}
|
|
if (!isTouched) return;
|
|
if (isScrolling) {
|
|
isTouched = false;
|
|
return;
|
|
}
|
|
if (!startMoving && s.browser.ieTouch) {
|
|
return;
|
|
}
|
|
s.allowClick = false;
|
|
s.emit('onSliderMove', s, e);
|
|
e.preventDefault();
|
|
if (s.params.touchMoveStopPropagation && !s.params.nested) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
if (!isMoved) {
|
|
if (params.loop) {
|
|
s.fixLoop();
|
|
}
|
|
startTranslate = s.getWrapperTranslate();
|
|
s.setWrapperTransition(0);
|
|
if (s.animating) {
|
|
s.wrapper.trigger('webkitTransitionEnd transitionend oTransitionEnd MSTransitionEnd msTransitionEnd');
|
|
}
|
|
if (s.params.autoplay && s.autoplaying) {
|
|
if (s.params.autoplayDisableOnInteraction) {
|
|
s.stopAutoplay();
|
|
}
|
|
else {
|
|
s.pauseAutoplay();
|
|
}
|
|
}
|
|
allowMomentumBounce = false;
|
|
//Grab Cursor
|
|
if (s.params.grabCursor) {
|
|
s.container[0].style.cursor = 'move';
|
|
s.container[0].style.cursor = '-webkit-grabbing';
|
|
s.container[0].style.cursor = '-moz-grabbin';
|
|
s.container[0].style.cursor = 'grabbing';
|
|
}
|
|
}
|
|
isMoved = true;
|
|
|
|
var diff = s.touches.diff = isH() ? s.touches.currentX - s.touches.startX : s.touches.currentY - s.touches.startY;
|
|
|
|
diff = diff * s.params.touchRatio;
|
|
if (s.rtl) diff = -diff;
|
|
|
|
s.swipeDirection = diff > 0 ? 'prev' : 'next';
|
|
currentTranslate = diff + startTranslate;
|
|
|
|
var disableParentSwiper = true;
|
|
if ((diff > 0 && currentTranslate > s.minTranslate())) {
|
|
disableParentSwiper = false;
|
|
if (s.params.resistance) currentTranslate = s.minTranslate() - 1 + Math.pow(-s.minTranslate() + startTranslate + diff, s.params.resistanceRatio);
|
|
}
|
|
else if (diff < 0 && currentTranslate < s.maxTranslate()) {
|
|
disableParentSwiper = false;
|
|
if (s.params.resistance) currentTranslate = s.maxTranslate() + 1 - Math.pow(s.maxTranslate() - startTranslate - diff, s.params.resistanceRatio);
|
|
}
|
|
|
|
if (disableParentSwiper) {
|
|
e.preventedByNestedSwiper = true;
|
|
}
|
|
|
|
// Directions locks
|
|
if (!s.params.allowSwipeToNext && s.swipeDirection === 'next' && currentTranslate < startTranslate) {
|
|
currentTranslate = startTranslate;
|
|
}
|
|
if (!s.params.allowSwipeToPrev && s.swipeDirection === 'prev' && currentTranslate > startTranslate) {
|
|
currentTranslate = startTranslate;
|
|
}
|
|
|
|
if (!s.params.followFinger) return;
|
|
|
|
// Threshold
|
|
if (s.params.threshold > 0) {
|
|
if (Math.abs(diff) > s.params.threshold || allowThresholdMove) {
|
|
if (!allowThresholdMove) {
|
|
allowThresholdMove = true;
|
|
s.touches.startX = s.touches.currentX;
|
|
s.touches.startY = s.touches.currentY;
|
|
currentTranslate = startTranslate;
|
|
s.touches.diff = isH() ? s.touches.currentX - s.touches.startX : s.touches.currentY - s.touches.startY;
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
currentTranslate = startTranslate;
|
|
return;
|
|
}
|
|
}
|
|
// Update active index in free mode
|
|
if (s.params.freeMode || s.params.watchSlidesProgress) {
|
|
s.updateActiveIndex();
|
|
}
|
|
if (s.params.freeMode) {
|
|
//Velocity
|
|
if (velocities.length === 0) {
|
|
velocities.push({
|
|
position: s.touches[isH() ? 'startX' : 'startY'],
|
|
time: touchStartTime
|
|
});
|
|
}
|
|
velocities.push({
|
|
position: s.touches[isH() ? 'currentX' : 'currentY'],
|
|
time: (new window.Date()).getTime()
|
|
});
|
|
}
|
|
// Update progress
|
|
s.updateProgress(currentTranslate);
|
|
// Update translate
|
|
s.setWrapperTranslate(currentTranslate);
|
|
};
|
|
s.onTouchEnd = function (e) {
|
|
if (e.originalEvent) e = e.originalEvent;
|
|
if (allowTouchCallbacks) {
|
|
s.emit('onTouchEnd', s, e);
|
|
}
|
|
allowTouchCallbacks = false;
|
|
if (!isTouched) return;
|
|
//Return Grab Cursor
|
|
if (s.params.grabCursor && isMoved && isTouched) {
|
|
s.container[0].style.cursor = 'move';
|
|
s.container[0].style.cursor = '-webkit-grab';
|
|
s.container[0].style.cursor = '-moz-grab';
|
|
s.container[0].style.cursor = 'grab';
|
|
}
|
|
|
|
// Time diff
|
|
var touchEndTime = Date.now();
|
|
var timeDiff = touchEndTime - touchStartTime;
|
|
|
|
// Tap, doubleTap, Click
|
|
if (s.allowClick) {
|
|
s.updateClickedSlide(e);
|
|
s.emit('onTap', s, e);
|
|
if (timeDiff < 300 && (touchEndTime - lastClickTime) > 300) {
|
|
if (clickTimeout) clearTimeout(clickTimeout);
|
|
clickTimeout = setTimeout(function () {
|
|
if (!s) return;
|
|
if (s.params.paginationHide && s.paginationContainer.length > 0 && !$(e.target).hasClass(s.params.bulletClass)) {
|
|
s.paginationContainer.toggleClass(s.params.paginationHiddenClass);
|
|
}
|
|
s.emit('onClick', s, e);
|
|
}, 300);
|
|
|
|
}
|
|
if (timeDiff < 300 && (touchEndTime - lastClickTime) < 300) {
|
|
if (clickTimeout) clearTimeout(clickTimeout);
|
|
s.emit('onDoubleTap', s, e);
|
|
}
|
|
}
|
|
|
|
lastClickTime = Date.now();
|
|
setTimeout(function () {
|
|
if (s) s.allowClick = true;
|
|
}, 0);
|
|
|
|
if (!isTouched || !isMoved || !s.swipeDirection || s.touches.diff === 0 || currentTranslate === startTranslate) {
|
|
isTouched = isMoved = false;
|
|
return;
|
|
}
|
|
isTouched = isMoved = false;
|
|
|
|
var currentPos;
|
|
if (s.params.followFinger) {
|
|
currentPos = s.rtl ? s.translate : -s.translate;
|
|
}
|
|
else {
|
|
currentPos = -currentTranslate;
|
|
}
|
|
if (s.params.freeMode) {
|
|
if (currentPos < -s.minTranslate()) {
|
|
s.slideTo(s.activeIndex);
|
|
return;
|
|
}
|
|
else if (currentPos > -s.maxTranslate()) {
|
|
if (s.slides.length < s.snapGrid.length) {
|
|
s.slideTo(s.snapGrid.length - 1);
|
|
}
|
|
else {
|
|
s.slideTo(s.slides.length - 1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (s.params.freeModeMomentum) {
|
|
if (velocities.length > 1) {
|
|
var lastMoveEvent = velocities.pop(), velocityEvent = velocities.pop();
|
|
|
|
var distance = lastMoveEvent.position - velocityEvent.position;
|
|
var time = lastMoveEvent.time - velocityEvent.time;
|
|
s.velocity = distance / time;
|
|
s.velocity = s.velocity / 2;
|
|
if (Math.abs(s.velocity) < s.params.freeModeMinimumVelocity) {
|
|
s.velocity = 0;
|
|
}
|
|
// this implies that the user stopped moving a finger then released.
|
|
// There would be no events with distance zero, so the last event is stale.
|
|
if (time > 150 || (new window.Date().getTime() - lastMoveEvent.time) > 300) {
|
|
s.velocity = 0;
|
|
}
|
|
} else {
|
|
s.velocity = 0;
|
|
}
|
|
|
|
velocities.length = 0;
|
|
var momentumDuration = 1000 * s.params.freeModeMomentumRatio;
|
|
var momentumDistance = s.velocity * momentumDuration;
|
|
|
|
var newPosition = s.translate + momentumDistance;
|
|
if (s.rtl) newPosition = - newPosition;
|
|
var doBounce = false;
|
|
var afterBouncePosition;
|
|
var bounceAmount = Math.abs(s.velocity) * 20 * s.params.freeModeMomentumBounceRatio;
|
|
if (newPosition < s.maxTranslate()) {
|
|
if (s.params.freeModeMomentumBounce) {
|
|
if (newPosition + s.maxTranslate() < -bounceAmount) {
|
|
newPosition = s.maxTranslate() - bounceAmount;
|
|
}
|
|
afterBouncePosition = s.maxTranslate();
|
|
doBounce = true;
|
|
allowMomentumBounce = true;
|
|
}
|
|
else {
|
|
newPosition = s.maxTranslate();
|
|
}
|
|
}
|
|
else if (newPosition > s.minTranslate()) {
|
|
if (s.params.freeModeMomentumBounce) {
|
|
if (newPosition - s.minTranslate() > bounceAmount) {
|
|
newPosition = s.minTranslate() + bounceAmount;
|
|
}
|
|
afterBouncePosition = s.minTranslate();
|
|
doBounce = true;
|
|
allowMomentumBounce = true;
|
|
}
|
|
else {
|
|
newPosition = s.minTranslate();
|
|
}
|
|
}
|
|
else if (s.params.freeModeSticky) {
|
|
var j = 0,
|
|
nextSlide;
|
|
for (j = 0; j < s.snapGrid.length; j += 1) {
|
|
if (s.snapGrid[j] > -newPosition) {
|
|
nextSlide = j;
|
|
break;
|
|
}
|
|
|
|
}
|
|
if (Math.abs(s.snapGrid[nextSlide] - newPosition) < Math.abs(s.snapGrid[nextSlide - 1] - newPosition) || s.swipeDirection === 'next') {
|
|
newPosition = s.snapGrid[nextSlide];
|
|
} else {
|
|
newPosition = s.snapGrid[nextSlide - 1];
|
|
}
|
|
if (!s.rtl) newPosition = - newPosition;
|
|
}
|
|
//Fix duration
|
|
if (s.velocity !== 0) {
|
|
if (s.rtl) {
|
|
momentumDuration = Math.abs((-newPosition - s.translate) / s.velocity);
|
|
}
|
|
else {
|
|
momentumDuration = Math.abs((newPosition - s.translate) / s.velocity);
|
|
}
|
|
}
|
|
else if (s.params.freeModeSticky) {
|
|
s.slideReset();
|
|
return;
|
|
}
|
|
|
|
if (s.params.freeModeMomentumBounce && doBounce) {
|
|
s.updateProgress(afterBouncePosition);
|
|
s.setWrapperTransition(momentumDuration);
|
|
s.setWrapperTranslate(newPosition);
|
|
s.onTransitionStart();
|
|
s.animating = true;
|
|
s.wrapper.transitionEnd(function () {
|
|
if (!s || !allowMomentumBounce) return;
|
|
s.emit('onMomentumBounce', s);
|
|
|
|
s.setWrapperTransition(s.params.speed);
|
|
s.setWrapperTranslate(afterBouncePosition);
|
|
s.wrapper.transitionEnd(function () {
|
|
if (!s) return;
|
|
s.onTransitionEnd();
|
|
});
|
|
});
|
|
} else if (s.velocity) {
|
|
s.updateProgress(newPosition);
|
|
s.setWrapperTransition(momentumDuration);
|
|
s.setWrapperTranslate(newPosition);
|
|
s.onTransitionStart();
|
|
if (!s.animating) {
|
|
s.animating = true;
|
|
s.wrapper.transitionEnd(function () {
|
|
if (!s) return;
|
|
s.onTransitionEnd();
|
|
});
|
|
}
|
|
|
|
} else {
|
|
s.updateProgress(newPosition);
|
|
}
|
|
|
|
s.updateActiveIndex();
|
|
}
|
|
if (!s.params.freeModeMomentum || timeDiff >= s.params.longSwipesMs) {
|
|
s.updateProgress();
|
|
s.updateActiveIndex();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Find current slide
|
|
var i, stopIndex = 0, groupSize = s.slidesSizesGrid[0];
|
|
for (i = 0; i < s.slidesGrid.length; i += s.params.slidesPerGroup) {
|
|
if (typeof s.slidesGrid[i + s.params.slidesPerGroup] !== 'undefined') {
|
|
if (currentPos >= s.slidesGrid[i] && currentPos < s.slidesGrid[i + s.params.slidesPerGroup]) {
|
|
stopIndex = i;
|
|
groupSize = s.slidesGrid[i + s.params.slidesPerGroup] - s.slidesGrid[i];
|
|
}
|
|
}
|
|
else {
|
|
if (currentPos >= s.slidesGrid[i]) {
|
|
stopIndex = i;
|
|
groupSize = s.slidesGrid[s.slidesGrid.length - 1] - s.slidesGrid[s.slidesGrid.length - 2];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find current slide size
|
|
var ratio = (currentPos - s.slidesGrid[stopIndex]) / groupSize;
|
|
|
|
if (timeDiff > s.params.longSwipesMs) {
|
|
// Long touches
|
|
if (!s.params.longSwipes) {
|
|
s.slideTo(s.activeIndex);
|
|
return;
|
|
}
|
|
if (s.swipeDirection === 'next') {
|
|
if (ratio >= s.params.longSwipesRatio) s.slideTo(stopIndex + s.params.slidesPerGroup);
|
|
else s.slideTo(stopIndex);
|
|
|
|
}
|
|
if (s.swipeDirection === 'prev') {
|
|
if (ratio > (1 - s.params.longSwipesRatio)) s.slideTo(stopIndex + s.params.slidesPerGroup);
|
|
else s.slideTo(stopIndex);
|
|
}
|
|
}
|
|
else {
|
|
// Short swipes
|
|
if (!s.params.shortSwipes) {
|
|
s.slideTo(s.activeIndex);
|
|
return;
|
|
}
|
|
if (s.swipeDirection === 'next') {
|
|
s.slideTo(stopIndex + s.params.slidesPerGroup);
|
|
|
|
}
|
|
if (s.swipeDirection === 'prev') {
|
|
s.slideTo(stopIndex);
|
|
}
|
|
}
|
|
};
|
|
/*=========================
|
|
Transitions
|
|
===========================*/
|
|
s._slideTo = function (slideIndex, speed) {
|
|
return s.slideTo(slideIndex, speed, true, true);
|
|
};
|
|
s.slideTo = function (slideIndex, speed, runCallbacks, internal) {
|
|
if (typeof runCallbacks === 'undefined') runCallbacks = true;
|
|
if (typeof slideIndex === 'undefined') slideIndex = 0;
|
|
if (slideIndex < 0) slideIndex = 0;
|
|
s.snapIndex = Math.floor(slideIndex / s.params.slidesPerGroup);
|
|
if (s.snapIndex >= s.snapGrid.length) s.snapIndex = s.snapGrid.length - 1;
|
|
|
|
var translate = - s.snapGrid[s.snapIndex];
|
|
// Stop autoplay
|
|
if (s.params.autoplay && s.autoplaying) {
|
|
if (internal || !s.params.autoplayDisableOnInteraction) {
|
|
s.pauseAutoplay(speed);
|
|
}
|
|
else {
|
|
s.stopAutoplay();
|
|
}
|
|
}
|
|
// Update progress
|
|
s.updateProgress(translate);
|
|
|
|
// Normalize slideIndex
|
|
for (var i = 0; i < s.slidesGrid.length; i++) {
|
|
if (- Math.floor(translate * 100) >= Math.floor(s.slidesGrid[i] * 100)) {
|
|
slideIndex = i;
|
|
}
|
|
}
|
|
|
|
// Directions locks
|
|
if (!s.params.allowSwipeToNext && translate < s.translate && translate < s.minTranslate()) {
|
|
return false;
|
|
}
|
|
if (!s.params.allowSwipeToPrev && translate > s.translate && translate > s.maxTranslate()) {
|
|
if ((s.activeIndex || 0) !== slideIndex ) return false;
|
|
}
|
|
|
|
// Update Index
|
|
if (typeof speed === 'undefined') speed = s.params.speed;
|
|
s.previousIndex = s.activeIndex || 0;
|
|
s.activeIndex = slideIndex;
|
|
|
|
if ((s.rtl && -translate === s.translate) || (!s.rtl && translate === s.translate)) {
|
|
// Update Height
|
|
if (s.params.autoHeight) {
|
|
s.updateAutoHeight();
|
|
}
|
|
s.updateClasses();
|
|
if (s.params.effect !== 'slide') {
|
|
s.setWrapperTranslate(translate);
|
|
}
|
|
return false;
|
|
}
|
|
s.updateClasses();
|
|
s.onTransitionStart(runCallbacks);
|
|
|
|
if (speed === 0) {
|
|
s.setWrapperTranslate(translate);
|
|
s.setWrapperTransition(0);
|
|
s.onTransitionEnd(runCallbacks);
|
|
}
|
|
else {
|
|
s.setWrapperTranslate(translate);
|
|
s.setWrapperTransition(speed);
|
|
if (!s.animating) {
|
|
s.animating = true;
|
|
s.wrapper.transitionEnd(function () {
|
|
if (!s) return;
|
|
s.onTransitionEnd(runCallbacks);
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
s.onTransitionStart = function (runCallbacks) {
|
|
if (typeof runCallbacks === 'undefined') runCallbacks = true;
|
|
if (s.params.autoHeight) {
|
|
s.updateAutoHeight();
|
|
}
|
|
if (s.lazy) s.lazy.onTransitionStart();
|
|
if (runCallbacks) {
|
|
s.emit('onTransitionStart', s);
|
|
if (s.activeIndex !== s.previousIndex) {
|
|
s.emit('onSlideChangeStart', s);
|
|
_scope.$emit("$ionicSlides.slideChangeStart", {
|
|
slider: s,
|
|
activeIndex: s.getSlideDataIndex(s.activeIndex),
|
|
previousIndex: s.getSlideDataIndex(s.previousIndex)
|
|
});
|
|
if (s.activeIndex > s.previousIndex) {
|
|
s.emit('onSlideNextStart', s);
|
|
}
|
|
else {
|
|
s.emit('onSlidePrevStart', s);
|
|
}
|
|
}
|
|
|
|
}
|
|
};
|
|
s.onTransitionEnd = function (runCallbacks) {
|
|
s.animating = false;
|
|
s.setWrapperTransition(0);
|
|
if (typeof runCallbacks === 'undefined') runCallbacks = true;
|
|
if (s.lazy) s.lazy.onTransitionEnd();
|
|
if (runCallbacks) {
|
|
s.emit('onTransitionEnd', s);
|
|
if (s.activeIndex !== s.previousIndex) {
|
|
s.emit('onSlideChangeEnd', s);
|
|
_scope.$emit("$ionicSlides.slideChangeEnd", {
|
|
slider: s,
|
|
activeIndex: s.getSlideDataIndex(s.activeIndex),
|
|
previousIndex: s.getSlideDataIndex(s.previousIndex)
|
|
});
|
|
if (s.activeIndex > s.previousIndex) {
|
|
s.emit('onSlideNextEnd', s);
|
|
}
|
|
else {
|
|
s.emit('onSlidePrevEnd', s);
|
|
}
|
|
}
|
|
}
|
|
if (s.params.hashnav && s.hashnav) {
|
|
s.hashnav.setHash();
|
|
}
|
|
|
|
};
|
|
s.slideNext = function (runCallbacks, speed, internal) {
|
|
if (s.params.loop) {
|
|
if (s.animating) return false;
|
|
s.fixLoop();
|
|
var clientLeft = s.container[0].clientLeft;
|
|
return s.slideTo(s.activeIndex + s.params.slidesPerGroup, speed, runCallbacks, internal);
|
|
}
|
|
else return s.slideTo(s.activeIndex + s.params.slidesPerGroup, speed, runCallbacks, internal);
|
|
};
|
|
s._slideNext = function (speed) {
|
|
return s.slideNext(true, speed, true);
|
|
};
|
|
s.slidePrev = function (runCallbacks, speed, internal) {
|
|
if (s.params.loop) {
|
|
if (s.animating) return false;
|
|
s.fixLoop();
|
|
var clientLeft = s.container[0].clientLeft;
|
|
return s.slideTo(s.activeIndex - 1, speed, runCallbacks, internal);
|
|
}
|
|
else return s.slideTo(s.activeIndex - 1, speed, runCallbacks, internal);
|
|
};
|
|
s._slidePrev = function (speed) {
|
|
return s.slidePrev(true, speed, true);
|
|
};
|
|
s.slideReset = function (runCallbacks, speed, internal) {
|
|
return s.slideTo(s.activeIndex, speed, runCallbacks);
|
|
};
|
|
|
|
/*=========================
|
|
Translate/transition helpers
|
|
===========================*/
|
|
s.setWrapperTransition = function (duration, byController) {
|
|
s.wrapper.transition(duration);
|
|
if (s.params.effect !== 'slide' && s.effects[s.params.effect]) {
|
|
s.effects[s.params.effect].setTransition(duration);
|
|
}
|
|
if (s.params.parallax && s.parallax) {
|
|
s.parallax.setTransition(duration);
|
|
}
|
|
if (s.params.scrollbar && s.scrollbar) {
|
|
s.scrollbar.setTransition(duration);
|
|
}
|
|
if (s.params.control && s.controller) {
|
|
s.controller.setTransition(duration, byController);
|
|
}
|
|
s.emit('onSetTransition', s, duration);
|
|
};
|
|
s.setWrapperTranslate = function (translate, updateActiveIndex, byController) {
|
|
var x = 0, y = 0, z = 0;
|
|
if (isH()) {
|
|
x = s.rtl ? -translate : translate;
|
|
}
|
|
else {
|
|
y = translate;
|
|
}
|
|
|
|
if (s.params.roundLengths) {
|
|
x = round(x);
|
|
y = round(y);
|
|
}
|
|
|
|
if (!s.params.virtualTranslate) {
|
|
if (s.support.transforms3d) s.wrapper.transform('translate3d(' + x + 'px, ' + y + 'px, ' + z + 'px)');
|
|
else s.wrapper.transform('translate(' + x + 'px, ' + y + 'px)');
|
|
}
|
|
|
|
s.translate = isH() ? x : y;
|
|
|
|
// Check if we need to update progress
|
|
var progress;
|
|
var translatesDiff = s.maxTranslate() - s.minTranslate();
|
|
if (translatesDiff === 0) {
|
|
progress = 0;
|
|
}
|
|
else {
|
|
progress = (translate - s.minTranslate()) / (translatesDiff);
|
|
}
|
|
if (progress !== s.progress) {
|
|
s.updateProgress(translate);
|
|
}
|
|
|
|
if (updateActiveIndex) s.updateActiveIndex();
|
|
if (s.params.effect !== 'slide' && s.effects[s.params.effect]) {
|
|
s.effects[s.params.effect].setTranslate(s.translate);
|
|
}
|
|
if (s.params.parallax && s.parallax) {
|
|
s.parallax.setTranslate(s.translate);
|
|
}
|
|
if (s.params.scrollbar && s.scrollbar) {
|
|
s.scrollbar.setTranslate(s.translate);
|
|
}
|
|
if (s.params.control && s.controller) {
|
|
s.controller.setTranslate(s.translate, byController);
|
|
}
|
|
s.emit('onSetTranslate', s, s.translate);
|
|
};
|
|
|
|
s.getTranslate = function (el, axis) {
|
|
var matrix, curTransform, curStyle, transformMatrix;
|
|
|
|
// automatic axis detection
|
|
if (typeof axis === 'undefined') {
|
|
axis = 'x';
|
|
}
|
|
|
|
if (s.params.virtualTranslate) {
|
|
return s.rtl ? -s.translate : s.translate;
|
|
}
|
|
|
|
curStyle = window.getComputedStyle(el, null);
|
|
if (window.WebKitCSSMatrix) {
|
|
curTransform = curStyle.transform || curStyle.webkitTransform;
|
|
if (curTransform.split(',').length > 6) {
|
|
curTransform = curTransform.split(', ').map(function(a){
|
|
return a.replace(',','.');
|
|
}).join(', ');
|
|
}
|
|
// Some old versions of Webkit choke when 'none' is passed; pass
|
|
// empty string instead in this case
|
|
transformMatrix = new window.WebKitCSSMatrix(curTransform === 'none' ? '' : curTransform);
|
|
}
|
|
else {
|
|
transformMatrix = curStyle.MozTransform || curStyle.OTransform || curStyle.MsTransform || curStyle.msTransform || curStyle.transform || curStyle.getPropertyValue('transform').replace('translate(', 'matrix(1, 0, 0, 1,');
|
|
matrix = transformMatrix.toString().split(',');
|
|
}
|
|
|
|
if (axis === 'x') {
|
|
//Latest Chrome and webkits Fix
|
|
if (window.WebKitCSSMatrix)
|
|
curTransform = transformMatrix.m41;
|
|
//Crazy IE10 Matrix
|
|
else if (matrix.length === 16)
|
|
curTransform = parseFloat(matrix[12]);
|
|
//Normal Browsers
|
|
else
|
|
curTransform = parseFloat(matrix[4]);
|
|
}
|
|
if (axis === 'y') {
|
|
//Latest Chrome and webkits Fix
|
|
if (window.WebKitCSSMatrix)
|
|
curTransform = transformMatrix.m42;
|
|
//Crazy IE10 Matrix
|
|
else if (matrix.length === 16)
|
|
curTransform = parseFloat(matrix[13]);
|
|
//Normal Browsers
|
|
else
|
|
curTransform = parseFloat(matrix[5]);
|
|
}
|
|
if (s.rtl && curTransform) curTransform = -curTransform;
|
|
return curTransform || 0;
|
|
};
|
|
s.getWrapperTranslate = function (axis) {
|
|
if (typeof axis === 'undefined') {
|
|
axis = isH() ? 'x' : 'y';
|
|
}
|
|
return s.getTranslate(s.wrapper[0], axis);
|
|
};
|
|
|
|
/*=========================
|
|
Observer
|
|
===========================*/
|
|
s.observers = [];
|
|
function initObserver(target, options) {
|
|
options = options || {};
|
|
// create an observer instance
|
|
var ObserverFunc = window.MutationObserver || window.WebkitMutationObserver;
|
|
var observer = new ObserverFunc(function (mutations) {
|
|
mutations.forEach(function (mutation) {
|
|
s.onResize(true);
|
|
s.emit('onObserverUpdate', s, mutation);
|
|
});
|
|
});
|
|
|
|
observer.observe(target, {
|
|
attributes: typeof options.attributes === 'undefined' ? true : options.attributes,
|
|
childList: typeof options.childList === 'undefined' ? true : options.childList,
|
|
characterData: typeof options.characterData === 'undefined' ? true : options.characterData
|
|
});
|
|
|
|
s.observers.push(observer);
|
|
}
|
|
s.initObservers = function () {
|
|
if (s.params.observeParents) {
|
|
var containerParents = s.container.parents();
|
|
for (var i = 0; i < containerParents.length; i++) {
|
|
initObserver(containerParents[i]);
|
|
}
|
|
}
|
|
|
|
// Observe container
|
|
initObserver(s.container[0], {childList: false});
|
|
|
|
// Observe wrapper
|
|
initObserver(s.wrapper[0], {attributes: false});
|
|
};
|
|
s.disconnectObservers = function () {
|
|
for (var i = 0; i < s.observers.length; i++) {
|
|
s.observers[i].disconnect();
|
|
}
|
|
s.observers = [];
|
|
};
|
|
|
|
s.updateLoop = function(){
|
|
var currentSlide = s.slides.eq(s.activeIndex);
|
|
if ( angular.element(currentSlide).hasClass(s.params.slideDuplicateClass) ){
|
|
// we're on a duplicate, so slide to the non-duplicate
|
|
var swiperSlideIndex = angular.element(currentSlide).attr("data-swiper-slide-index");
|
|
var slides = s.wrapper.children('.' + s.params.slideClass);
|
|
for ( var i = 0; i < slides.length; i++ ){
|
|
if ( !angular.element(slides[i]).hasClass(s.params.slideDuplicateClass) && angular.element(slides[i]).attr("data-swiper-slide-index") === swiperSlideIndex ){
|
|
s.slideTo(i, 0, false, true);
|
|
break;
|
|
}
|
|
}
|
|
// if we needed to switch slides, we did that. So, now call the createLoop function internally
|
|
setTimeout(function(){
|
|
s.createLoop();
|
|
}, 50);
|
|
}
|
|
}
|
|
|
|
s.getSlideDataIndex = function(slideIndex){
|
|
// this is an Ionic custom function
|
|
// Swiper loops utilize duplicate DOM elements for slides when in a loop
|
|
// which means that we cannot rely on the actual slide index for our events
|
|
// because index 0 does not necessarily point to index 0
|
|
// and index n+1 does not necessarily point to the expected piece of data
|
|
// therefore, rather than using the actual slide index we should
|
|
// use the data index that swiper includes as an attribute on the dom elements
|
|
// because this is what will be meaningful to the consumer of our events
|
|
var slide = s.slides.eq(slideIndex);
|
|
var attributeIndex = angular.element(slide).attr("data-swiper-slide-index");
|
|
return parseInt(attributeIndex);
|
|
}
|
|
|
|
/*=========================
|
|
Loop
|
|
===========================*/
|
|
// Create looped slides
|
|
s.createLoop = function () {
|
|
//console.log("Slider create loop method");
|
|
//var toRemove = s.wrapper.children('.' + s.params.slideClass + '.' + s.params.slideDuplicateClass);
|
|
//angular.element(toRemove).remove();
|
|
s.wrapper.children('.' + s.params.slideClass + '.' + s.params.slideDuplicateClass).remove();
|
|
|
|
var slides = s.wrapper.children('.' + s.params.slideClass);
|
|
|
|
if(s.params.slidesPerView === 'auto' && !s.params.loopedSlides) s.params.loopedSlides = slides.length;
|
|
|
|
s.loopedSlides = parseInt(s.params.loopedSlides || s.params.slidesPerView, 10);
|
|
s.loopedSlides = s.loopedSlides + s.params.loopAdditionalSlides;
|
|
if (s.loopedSlides > slides.length) {
|
|
s.loopedSlides = slides.length;
|
|
}
|
|
|
|
var prependSlides = [], appendSlides = [], i, scope, newNode;
|
|
slides.each(function (index, el) {
|
|
var slide = $(this);
|
|
if (index < s.loopedSlides) appendSlides.push(el);
|
|
if (index < slides.length && index >= slides.length - s.loopedSlides) prependSlides.push(el);
|
|
slide.attr('data-swiper-slide-index', index);
|
|
});
|
|
for (i = 0; i < appendSlides.length; i++) {
|
|
|
|
newNode = angular.element(appendSlides[i]).clone().addClass(s.params.slideDuplicateClass);
|
|
newNode.removeAttr('ng-transclude');
|
|
newNode.removeAttr('ng-repeat');
|
|
scope = angular.element(appendSlides[i]).scope();
|
|
newNode = $compile(newNode)(scope);
|
|
angular.element(s.wrapper).append(newNode);
|
|
//s.wrapper.append($(appendSlides[i].cloneNode(true)).addClass(s.params.slideDuplicateClass));
|
|
}
|
|
for (i = prependSlides.length - 1; i >= 0; i--) {
|
|
//s.wrapper.prepend($(prependSlides[i].cloneNode(true)).addClass(s.params.slideDuplicateClass));
|
|
|
|
newNode = angular.element(prependSlides[i]).clone().addClass(s.params.slideDuplicateClass);
|
|
newNode.removeAttr('ng-transclude');
|
|
newNode.removeAttr('ng-repeat');
|
|
|
|
scope = angular.element(prependSlides[i]).scope();
|
|
newNode = $compile(newNode)(scope);
|
|
angular.element(s.wrapper).prepend(newNode);
|
|
}
|
|
};
|
|
s.destroyLoop = function () {
|
|
s.wrapper.children('.' + s.params.slideClass + '.' + s.params.slideDuplicateClass).remove();
|
|
s.slides.removeAttr('data-swiper-slide-index');
|
|
};
|
|
s.fixLoop = function () {
|
|
var newIndex;
|
|
//Fix For Negative Oversliding
|
|
if (s.activeIndex < s.loopedSlides) {
|
|
newIndex = s.slides.length - s.loopedSlides * 3 + s.activeIndex;
|
|
newIndex = newIndex + s.loopedSlides;
|
|
s.slideTo(newIndex, 0, false, true);
|
|
}
|
|
//Fix For Positive Oversliding
|
|
else if ((s.params.slidesPerView === 'auto' && s.activeIndex >= s.loopedSlides * 2) || (s.activeIndex > s.slides.length - s.params.slidesPerView * 2)) {
|
|
newIndex = -s.slides.length + s.activeIndex + s.loopedSlides;
|
|
newIndex = newIndex + s.loopedSlides;
|
|
s.slideTo(newIndex, 0, false, true);
|
|
}
|
|
};
|
|
/*=========================
|
|
Append/Prepend/Remove Slides
|
|
===========================*/
|
|
s.appendSlide = function (slides) {
|
|
if (s.params.loop) {
|
|
s.destroyLoop();
|
|
}
|
|
if (typeof slides === 'object' && slides.length) {
|
|
for (var i = 0; i < slides.length; i++) {
|
|
if (slides[i]) s.wrapper.append(slides[i]);
|
|
}
|
|
}
|
|
else {
|
|
s.wrapper.append(slides);
|
|
}
|
|
if (s.params.loop) {
|
|
s.createLoop();
|
|
}
|
|
if (!(s.params.observer && s.support.observer)) {
|
|
s.update(true);
|
|
}
|
|
};
|
|
s.prependSlide = function (slides) {
|
|
if (s.params.loop) {
|
|
s.destroyLoop();
|
|
}
|
|
var newActiveIndex = s.activeIndex + 1;
|
|
if (typeof slides === 'object' && slides.length) {
|
|
for (var i = 0; i < slides.length; i++) {
|
|
if (slides[i]) s.wrapper.prepend(slides[i]);
|
|
}
|
|
newActiveIndex = s.activeIndex + slides.length;
|
|
}
|
|
else {
|
|
s.wrapper.prepend(slides);
|
|
}
|
|
if (s.params.loop) {
|
|
s.createLoop();
|
|
}
|
|
if (!(s.params.observer && s.support.observer)) {
|
|
s.update(true);
|
|
}
|
|
s.slideTo(newActiveIndex, 0, false);
|
|
};
|
|
s.removeSlide = function (slidesIndexes) {
|
|
if (s.params.loop) {
|
|
s.destroyLoop();
|
|
s.slides = s.wrapper.children('.' + s.params.slideClass);
|
|
}
|
|
var newActiveIndex = s.activeIndex,
|
|
indexToRemove;
|
|
if (typeof slidesIndexes === 'object' && slidesIndexes.length) {
|
|
for (var i = 0; i < slidesIndexes.length; i++) {
|
|
indexToRemove = slidesIndexes[i];
|
|
if (s.slides[indexToRemove]) s.slides.eq(indexToRemove).remove();
|
|
if (indexToRemove < newActiveIndex) newActiveIndex--;
|
|
}
|
|
newActiveIndex = Math.max(newActiveIndex, 0);
|
|
}
|
|
else {
|
|
indexToRemove = slidesIndexes;
|
|
if (s.slides[indexToRemove]) s.slides.eq(indexToRemove).remove();
|
|
if (indexToRemove < newActiveIndex) newActiveIndex--;
|
|
newActiveIndex = Math.max(newActiveIndex, 0);
|
|
}
|
|
|
|
if (s.params.loop) {
|
|
s.createLoop();
|
|
}
|
|
|
|
if (!(s.params.observer && s.support.observer)) {
|
|
s.update(true);
|
|
}
|
|
if (s.params.loop) {
|
|
s.slideTo(newActiveIndex + s.loopedSlides, 0, false);
|
|
}
|
|
else {
|
|
s.slideTo(newActiveIndex, 0, false);
|
|
}
|
|
|
|
};
|
|
s.removeAllSlides = function () {
|
|
var slidesIndexes = [];
|
|
for (var i = 0; i < s.slides.length; i++) {
|
|
slidesIndexes.push(i);
|
|
}
|
|
s.removeSlide(slidesIndexes);
|
|
};
|
|
|
|
|
|
/*=========================
|
|
Effects
|
|
===========================*/
|
|
s.effects = {
|
|
fade: {
|
|
setTranslate: function () {
|
|
for (var i = 0; i < s.slides.length; i++) {
|
|
var slide = s.slides.eq(i);
|
|
var offset = slide[0].swiperSlideOffset;
|
|
var tx = -offset;
|
|
if (!s.params.virtualTranslate) tx = tx - s.translate;
|
|
var ty = 0;
|
|
if (!isH()) {
|
|
ty = tx;
|
|
tx = 0;
|
|
}
|
|
var slideOpacity = s.params.fade.crossFade ?
|
|
Math.max(1 - Math.abs(slide[0].progress), 0) :
|
|
1 + Math.min(Math.max(slide[0].progress, -1), 0);
|
|
slide
|
|
.css({
|
|
opacity: slideOpacity
|
|
})
|
|
.transform('translate3d(' + tx + 'px, ' + ty + 'px, 0px)');
|
|
|
|
}
|
|
|
|
},
|
|
setTransition: function (duration) {
|
|
s.slides.transition(duration);
|
|
if (s.params.virtualTranslate && duration !== 0) {
|
|
var eventTriggered = false;
|
|
s.slides.transitionEnd(function () {
|
|
if (eventTriggered) return;
|
|
if (!s) return;
|
|
eventTriggered = true;
|
|
s.animating = false;
|
|
var triggerEvents = ['webkitTransitionEnd', 'transitionend', 'oTransitionEnd', 'MSTransitionEnd', 'msTransitionEnd'];
|
|
for (var i = 0; i < triggerEvents.length; i++) {
|
|
s.wrapper.trigger(triggerEvents[i]);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
cube: {
|
|
setTranslate: function () {
|
|
var wrapperRotate = 0, cubeShadow;
|
|
if (s.params.cube.shadow) {
|
|
if (isH()) {
|
|
cubeShadow = s.wrapper.find('.swiper-cube-shadow');
|
|
if (cubeShadow.length === 0) {
|
|
cubeShadow = $('<div class="swiper-cube-shadow"></div>');
|
|
s.wrapper.append(cubeShadow);
|
|
}
|
|
cubeShadow.css({height: s.width + 'px'});
|
|
}
|
|
else {
|
|
cubeShadow = s.container.find('.swiper-cube-shadow');
|
|
if (cubeShadow.length === 0) {
|
|
cubeShadow = $('<div class="swiper-cube-shadow"></div>');
|
|
s.container.append(cubeShadow);
|
|
}
|
|
}
|
|
}
|
|
for (var i = 0; i < s.slides.length; i++) {
|
|
var slide = s.slides.eq(i);
|
|
var slideAngle = i * 90;
|
|
var round = Math.floor(slideAngle / 360);
|
|
if (s.rtl) {
|
|
slideAngle = -slideAngle;
|
|
round = Math.floor(-slideAngle / 360);
|
|
}
|
|
var progress = Math.max(Math.min(slide[0].progress, 1), -1);
|
|
var tx = 0, ty = 0, tz = 0;
|
|
if (i % 4 === 0) {
|
|
tx = - round * 4 * s.size;
|
|
tz = 0;
|
|
}
|
|
else if ((i - 1) % 4 === 0) {
|
|
tx = 0;
|
|
tz = - round * 4 * s.size;
|
|
}
|
|
else if ((i - 2) % 4 === 0) {
|
|
tx = s.size + round * 4 * s.size;
|
|
tz = s.size;
|
|
}
|
|
else if ((i - 3) % 4 === 0) {
|
|
tx = - s.size;
|
|
tz = 3 * s.size + s.size * 4 * round;
|
|
}
|
|
if (s.rtl) {
|
|
tx = -tx;
|
|
}
|
|
|
|
if (!isH()) {
|
|
ty = tx;
|
|
tx = 0;
|
|
}
|
|
|
|
var transform = 'rotateX(' + (isH() ? 0 : -slideAngle) + 'deg) rotateY(' + (isH() ? slideAngle : 0) + 'deg) translate3d(' + tx + 'px, ' + ty + 'px, ' + tz + 'px)';
|
|
if (progress <= 1 && progress > -1) {
|
|
wrapperRotate = i * 90 + progress * 90;
|
|
if (s.rtl) wrapperRotate = -i * 90 - progress * 90;
|
|
}
|
|
slide.transform(transform);
|
|
if (s.params.cube.slideShadows) {
|
|
//Set shadows
|
|
var shadowBefore = isH() ? slide.find('.swiper-slide-shadow-left') : slide.find('.swiper-slide-shadow-top');
|
|
var shadowAfter = isH() ? slide.find('.swiper-slide-shadow-right') : slide.find('.swiper-slide-shadow-bottom');
|
|
if (shadowBefore.length === 0) {
|
|
shadowBefore = $('<div class="swiper-slide-shadow-' + (isH() ? 'left' : 'top') + '"></div>');
|
|
slide.append(shadowBefore);
|
|
}
|
|
if (shadowAfter.length === 0) {
|
|
shadowAfter = $('<div class="swiper-slide-shadow-' + (isH() ? 'right' : 'bottom') + '"></div>');
|
|
slide.append(shadowAfter);
|
|
}
|
|
var shadowOpacity = slide[0].progress;
|
|
if (shadowBefore.length) shadowBefore[0].style.opacity = -slide[0].progress;
|
|
if (shadowAfter.length) shadowAfter[0].style.opacity = slide[0].progress;
|
|
}
|
|
}
|
|
s.wrapper.css({
|
|
'-webkit-transform-origin': '50% 50% -' + (s.size / 2) + 'px',
|
|
'-moz-transform-origin': '50% 50% -' + (s.size / 2) + 'px',
|
|
'-ms-transform-origin': '50% 50% -' + (s.size / 2) + 'px',
|
|
'transform-origin': '50% 50% -' + (s.size / 2) + 'px'
|
|
});
|
|
|
|
if (s.params.cube.shadow) {
|
|
if (isH()) {
|
|
cubeShadow.transform('translate3d(0px, ' + (s.width / 2 + s.params.cube.shadowOffset) + 'px, ' + (-s.width / 2) + 'px) rotateX(90deg) rotateZ(0deg) scale(' + (s.params.cube.shadowScale) + ')');
|
|
}
|
|
else {
|
|
var shadowAngle = Math.abs(wrapperRotate) - Math.floor(Math.abs(wrapperRotate) / 90) * 90;
|
|
var multiplier = 1.5 - (Math.sin(shadowAngle * 2 * Math.PI / 360) / 2 + Math.cos(shadowAngle * 2 * Math.PI / 360) / 2);
|
|
var scale1 = s.params.cube.shadowScale,
|
|
scale2 = s.params.cube.shadowScale / multiplier,
|
|
offset = s.params.cube.shadowOffset;
|
|
cubeShadow.transform('scale3d(' + scale1 + ', 1, ' + scale2 + ') translate3d(0px, ' + (s.height / 2 + offset) + 'px, ' + (-s.height / 2 / scale2) + 'px) rotateX(-90deg)');
|
|
}
|
|
}
|
|
var zFactor = (s.isSafari || s.isUiWebView) ? (-s.size / 2) : 0;
|
|
s.wrapper.transform('translate3d(0px,0,' + zFactor + 'px) rotateX(' + (isH() ? 0 : wrapperRotate) + 'deg) rotateY(' + (isH() ? -wrapperRotate : 0) + 'deg)');
|
|
},
|
|
setTransition: function (duration) {
|
|
s.slides.transition(duration).find('.swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left').transition(duration);
|
|
if (s.params.cube.shadow && !isH()) {
|
|
s.container.find('.swiper-cube-shadow').transition(duration);
|
|
}
|
|
}
|
|
},
|
|
coverflow: {
|
|
setTranslate: function () {
|
|
var transform = s.translate;
|
|
var center = isH() ? -transform + s.width / 2 : -transform + s.height / 2;
|
|
var rotate = isH() ? s.params.coverflow.rotate: -s.params.coverflow.rotate;
|
|
var translate = s.params.coverflow.depth;
|
|
//Each slide offset from center
|
|
for (var i = 0, length = s.slides.length; i < length; i++) {
|
|
var slide = s.slides.eq(i);
|
|
var slideSize = s.slidesSizesGrid[i];
|
|
var slideOffset = slide[0].swiperSlideOffset;
|
|
var offsetMultiplier = (center - slideOffset - slideSize / 2) / slideSize * s.params.coverflow.modifier;
|
|
|
|
var rotateY = isH() ? rotate * offsetMultiplier : 0;
|
|
var rotateX = isH() ? 0 : rotate * offsetMultiplier;
|
|
// var rotateZ = 0
|
|
var translateZ = -translate * Math.abs(offsetMultiplier);
|
|
|
|
var translateY = isH() ? 0 : s.params.coverflow.stretch * (offsetMultiplier);
|
|
var translateX = isH() ? s.params.coverflow.stretch * (offsetMultiplier) : 0;
|
|
|
|
//Fix for ultra small values
|
|
if (Math.abs(translateX) < 0.001) translateX = 0;
|
|
if (Math.abs(translateY) < 0.001) translateY = 0;
|
|
if (Math.abs(translateZ) < 0.001) translateZ = 0;
|
|
if (Math.abs(rotateY) < 0.001) rotateY = 0;
|
|
if (Math.abs(rotateX) < 0.001) rotateX = 0;
|
|
|
|
var slideTransform = 'translate3d(' + translateX + 'px,' + translateY + 'px,' + translateZ + 'px) rotateX(' + rotateX + 'deg) rotateY(' + rotateY + 'deg)';
|
|
|
|
slide.transform(slideTransform);
|
|
slide[0].style.zIndex = -Math.abs(Math.round(offsetMultiplier)) + 1;
|
|
if (s.params.coverflow.slideShadows) {
|
|
//Set shadows
|
|
var shadowBefore = isH() ? slide.find('.swiper-slide-shadow-left') : slide.find('.swiper-slide-shadow-top');
|
|
var shadowAfter = isH() ? slide.find('.swiper-slide-shadow-right') : slide.find('.swiper-slide-shadow-bottom');
|
|
if (shadowBefore.length === 0) {
|
|
shadowBefore = $('<div class="swiper-slide-shadow-' + (isH() ? 'left' : 'top') + '"></div>');
|
|
slide.append(shadowBefore);
|
|
}
|
|
if (shadowAfter.length === 0) {
|
|
shadowAfter = $('<div class="swiper-slide-shadow-' + (isH() ? 'right' : 'bottom') + '"></div>');
|
|
slide.append(shadowAfter);
|
|
}
|
|
if (shadowBefore.length) shadowBefore[0].style.opacity = offsetMultiplier > 0 ? offsetMultiplier : 0;
|
|
if (shadowAfter.length) shadowAfter[0].style.opacity = (-offsetMultiplier) > 0 ? -offsetMultiplier : 0;
|
|
}
|
|
}
|
|
|
|
//Set correct perspective for IE10
|
|
if (s.browser.ie) {
|
|
var ws = s.wrapper[0].style;
|
|
ws.perspectiveOrigin = center + 'px 50%';
|
|
}
|
|
},
|
|
setTransition: function (duration) {
|
|
s.slides.transition(duration).find('.swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left').transition(duration);
|
|
}
|
|
}
|
|
};
|
|
|
|
/*=========================
|
|
Images Lazy Loading
|
|
===========================*/
|
|
s.lazy = {
|
|
initialImageLoaded: false,
|
|
loadImageInSlide: function (index, loadInDuplicate) {
|
|
if (typeof index === 'undefined') return;
|
|
if (typeof loadInDuplicate === 'undefined') loadInDuplicate = true;
|
|
if (s.slides.length === 0) return;
|
|
|
|
var slide = s.slides.eq(index);
|
|
var img = slide.find('.swiper-lazy:not(.swiper-lazy-loaded):not(.swiper-lazy-loading)');
|
|
if (slide.hasClass('swiper-lazy') && !slide.hasClass('swiper-lazy-loaded') && !slide.hasClass('swiper-lazy-loading')) {
|
|
img = img.add(slide[0]);
|
|
}
|
|
if (img.length === 0) return;
|
|
|
|
img.each(function () {
|
|
var _img = $(this);
|
|
_img.addClass('swiper-lazy-loading');
|
|
var background = _img.attr('data-background');
|
|
var src = _img.attr('data-src'),
|
|
srcset = _img.attr('data-srcset');
|
|
s.loadImage(_img[0], (src || background), srcset, false, function () {
|
|
if (background) {
|
|
_img.css('background-image', 'url(' + background + ')');
|
|
_img.removeAttr('data-background');
|
|
}
|
|
else {
|
|
if (srcset) {
|
|
_img.attr('srcset', srcset);
|
|
_img.removeAttr('data-srcset');
|
|
}
|
|
if (src) {
|
|
_img.attr('src', src);
|
|
_img.removeAttr('data-src');
|
|
}
|
|
|
|
}
|
|
|
|
_img.addClass('swiper-lazy-loaded').removeClass('swiper-lazy-loading');
|
|
slide.find('.swiper-lazy-preloader, .preloader').remove();
|
|
if (s.params.loop && loadInDuplicate) {
|
|
var slideOriginalIndex = slide.attr('data-swiper-slide-index');
|
|
if (slide.hasClass(s.params.slideDuplicateClass)) {
|
|
var originalSlide = s.wrapper.children('[data-swiper-slide-index="' + slideOriginalIndex + '"]:not(.' + s.params.slideDuplicateClass + ')');
|
|
s.lazy.loadImageInSlide(originalSlide.index(), false);
|
|
}
|
|
else {
|
|
var duplicatedSlide = s.wrapper.children('.' + s.params.slideDuplicateClass + '[data-swiper-slide-index="' + slideOriginalIndex + '"]');
|
|
s.lazy.loadImageInSlide(duplicatedSlide.index(), false);
|
|
}
|
|
}
|
|
s.emit('onLazyImageReady', s, slide[0], _img[0]);
|
|
});
|
|
|
|
s.emit('onLazyImageLoad', s, slide[0], _img[0]);
|
|
});
|
|
|
|
},
|
|
load: function () {
|
|
var i;
|
|
if (s.params.watchSlidesVisibility) {
|
|
s.wrapper.children('.' + s.params.slideVisibleClass).each(function () {
|
|
s.lazy.loadImageInSlide($(this).index());
|
|
});
|
|
}
|
|
else {
|
|
if (s.params.slidesPerView > 1) {
|
|
for (i = s.activeIndex; i < s.activeIndex + s.params.slidesPerView ; i++) {
|
|
if (s.slides[i]) s.lazy.loadImageInSlide(i);
|
|
}
|
|
}
|
|
else {
|
|
s.lazy.loadImageInSlide(s.activeIndex);
|
|
}
|
|
}
|
|
if (s.params.lazyLoadingInPrevNext) {
|
|
if (s.params.slidesPerView > 1) {
|
|
// Next Slides
|
|
for (i = s.activeIndex + s.params.slidesPerView; i < s.activeIndex + s.params.slidesPerView + s.params.slidesPerView; i++) {
|
|
if (s.slides[i]) s.lazy.loadImageInSlide(i);
|
|
}
|
|
// Prev Slides
|
|
for (i = s.activeIndex - s.params.slidesPerView; i < s.activeIndex ; i++) {
|
|
if (s.slides[i]) s.lazy.loadImageInSlide(i);
|
|
}
|
|
}
|
|
else {
|
|
var nextSlide = s.wrapper.children('.' + s.params.slideNextClass);
|
|
if (nextSlide.length > 0) s.lazy.loadImageInSlide(nextSlide.index());
|
|
|
|
var prevSlide = s.wrapper.children('.' + s.params.slidePrevClass);
|
|
if (prevSlide.length > 0) s.lazy.loadImageInSlide(prevSlide.index());
|
|
}
|
|
}
|
|
},
|
|
onTransitionStart: function () {
|
|
if (s.params.lazyLoading) {
|
|
if (s.params.lazyLoadingOnTransitionStart || (!s.params.lazyLoadingOnTransitionStart && !s.lazy.initialImageLoaded)) {
|
|
s.lazy.load();
|
|
}
|
|
}
|
|
},
|
|
onTransitionEnd: function () {
|
|
if (s.params.lazyLoading && !s.params.lazyLoadingOnTransitionStart) {
|
|
s.lazy.load();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/*=========================
|
|
Scrollbar
|
|
===========================*/
|
|
s.scrollbar = {
|
|
isTouched: false,
|
|
setDragPosition: function (e) {
|
|
var sb = s.scrollbar;
|
|
var x = 0, y = 0;
|
|
var translate;
|
|
var pointerPosition = isH() ?
|
|
((e.type === 'touchstart' || e.type === 'touchmove') ? e.targetTouches[0].pageX : e.pageX || e.clientX) :
|
|
((e.type === 'touchstart' || e.type === 'touchmove') ? e.targetTouches[0].pageY : e.pageY || e.clientY) ;
|
|
var position = (pointerPosition) - sb.track.offset()[isH() ? 'left' : 'top'] - sb.dragSize / 2;
|
|
var positionMin = -s.minTranslate() * sb.moveDivider;
|
|
var positionMax = -s.maxTranslate() * sb.moveDivider;
|
|
if (position < positionMin) {
|
|
position = positionMin;
|
|
}
|
|
else if (position > positionMax) {
|
|
position = positionMax;
|
|
}
|
|
position = -position / sb.moveDivider;
|
|
s.updateProgress(position);
|
|
s.setWrapperTranslate(position, true);
|
|
},
|
|
dragStart: function (e) {
|
|
var sb = s.scrollbar;
|
|
sb.isTouched = true;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
sb.setDragPosition(e);
|
|
clearTimeout(sb.dragTimeout);
|
|
|
|
sb.track.transition(0);
|
|
if (s.params.scrollbarHide) {
|
|
sb.track.css('opacity', 1);
|
|
}
|
|
s.wrapper.transition(100);
|
|
sb.drag.transition(100);
|
|
s.emit('onScrollbarDragStart', s);
|
|
},
|
|
dragMove: function (e) {
|
|
var sb = s.scrollbar;
|
|
if (!sb.isTouched) return;
|
|
if (e.preventDefault) e.preventDefault();
|
|
else e.returnValue = false;
|
|
sb.setDragPosition(e);
|
|
s.wrapper.transition(0);
|
|
sb.track.transition(0);
|
|
sb.drag.transition(0);
|
|
s.emit('onScrollbarDragMove', s);
|
|
},
|
|
dragEnd: function (e) {
|
|
var sb = s.scrollbar;
|
|
if (!sb.isTouched) return;
|
|
sb.isTouched = false;
|
|
if (s.params.scrollbarHide) {
|
|
clearTimeout(sb.dragTimeout);
|
|
sb.dragTimeout = setTimeout(function () {
|
|
sb.track.css('opacity', 0);
|
|
sb.track.transition(400);
|
|
}, 1000);
|
|
|
|
}
|
|
s.emit('onScrollbarDragEnd', s);
|
|
if (s.params.scrollbarSnapOnRelease) {
|
|
s.slideReset();
|
|
}
|
|
},
|
|
enableDraggable: function () {
|
|
var sb = s.scrollbar;
|
|
var target = s.support.touch ? sb.track : document;
|
|
$(sb.track).on(s.touchEvents.start, sb.dragStart);
|
|
$(target).on(s.touchEvents.move, sb.dragMove);
|
|
$(target).on(s.touchEvents.end, sb.dragEnd);
|
|
},
|
|
disableDraggable: function () {
|
|
var sb = s.scrollbar;
|
|
var target = s.support.touch ? sb.track : document;
|
|
$(sb.track).off(s.touchEvents.start, sb.dragStart);
|
|
$(target).off(s.touchEvents.move, sb.dragMove);
|
|
$(target).off(s.touchEvents.end, sb.dragEnd);
|
|
},
|
|
set: function () {
|
|
if (!s.params.scrollbar) return;
|
|
var sb = s.scrollbar;
|
|
sb.track = $(s.params.scrollbar);
|
|
sb.drag = sb.track.find('.swiper-scrollbar-drag');
|
|
if (sb.drag.length === 0) {
|
|
sb.drag = $('<div class="swiper-scrollbar-drag"></div>');
|
|
sb.track.append(sb.drag);
|
|
}
|
|
sb.drag[0].style.width = '';
|
|
sb.drag[0].style.height = '';
|
|
sb.trackSize = isH() ? sb.track[0].offsetWidth : sb.track[0].offsetHeight;
|
|
|
|
sb.divider = s.size / s.virtualSize;
|
|
sb.moveDivider = sb.divider * (sb.trackSize / s.size);
|
|
sb.dragSize = sb.trackSize * sb.divider;
|
|
|
|
if (isH()) {
|
|
sb.drag[0].style.width = sb.dragSize + 'px';
|
|
}
|
|
else {
|
|
sb.drag[0].style.height = sb.dragSize + 'px';
|
|
}
|
|
|
|
if (sb.divider >= 1) {
|
|
sb.track[0].style.display = 'none';
|
|
}
|
|
else {
|
|
sb.track[0].style.display = '';
|
|
}
|
|
if (s.params.scrollbarHide) {
|
|
sb.track[0].style.opacity = 0;
|
|
}
|
|
},
|
|
setTranslate: function () {
|
|
if (!s.params.scrollbar) return;
|
|
var diff;
|
|
var sb = s.scrollbar;
|
|
var translate = s.translate || 0;
|
|
var newPos;
|
|
|
|
var newSize = sb.dragSize;
|
|
newPos = (sb.trackSize - sb.dragSize) * s.progress;
|
|
if (s.rtl && isH()) {
|
|
newPos = -newPos;
|
|
if (newPos > 0) {
|
|
newSize = sb.dragSize - newPos;
|
|
newPos = 0;
|
|
}
|
|
else if (-newPos + sb.dragSize > sb.trackSize) {
|
|
newSize = sb.trackSize + newPos;
|
|
}
|
|
}
|
|
else {
|
|
if (newPos < 0) {
|
|
newSize = sb.dragSize + newPos;
|
|
newPos = 0;
|
|
}
|
|
else if (newPos + sb.dragSize > sb.trackSize) {
|
|
newSize = sb.trackSize - newPos;
|
|
}
|
|
}
|
|
if (isH()) {
|
|
if (s.support.transforms3d) {
|
|
sb.drag.transform('translate3d(' + (newPos) + 'px, 0, 0)');
|
|
}
|
|
else {
|
|
sb.drag.transform('translateX(' + (newPos) + 'px)');
|
|
}
|
|
sb.drag[0].style.width = newSize + 'px';
|
|
}
|
|
else {
|
|
if (s.support.transforms3d) {
|
|
sb.drag.transform('translate3d(0px, ' + (newPos) + 'px, 0)');
|
|
}
|
|
else {
|
|
sb.drag.transform('translateY(' + (newPos) + 'px)');
|
|
}
|
|
sb.drag[0].style.height = newSize + 'px';
|
|
}
|
|
if (s.params.scrollbarHide) {
|
|
clearTimeout(sb.timeout);
|
|
sb.track[0].style.opacity = 1;
|
|
sb.timeout = setTimeout(function () {
|
|
sb.track[0].style.opacity = 0;
|
|
sb.track.transition(400);
|
|
}, 1000);
|
|
}
|
|
},
|
|
setTransition: function (duration) {
|
|
if (!s.params.scrollbar) return;
|
|
s.scrollbar.drag.transition(duration);
|
|
}
|
|
};
|
|
|
|
/*=========================
|
|
Controller
|
|
===========================*/
|
|
s.controller = {
|
|
LinearSpline: function (x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.lastIndex = x.length - 1;
|
|
// Given an x value (x2), return the expected y2 value:
|
|
// (x1,y1) is the known point before given value,
|
|
// (x3,y3) is the known point after given value.
|
|
var i1, i3;
|
|
var l = this.x.length;
|
|
|
|
this.interpolate = function (x2) {
|
|
if (!x2) return 0;
|
|
|
|
// Get the indexes of x1 and x3 (the array indexes before and after given x2):
|
|
i3 = binarySearch(this.x, x2);
|
|
i1 = i3 - 1;
|
|
|
|
// We have our indexes i1 & i3, so we can calculate already:
|
|
// y2 := ((x2−x1) × (y3−y1)) ÷ (x3−x1) + y1
|
|
return ((x2 - this.x[i1]) * (this.y[i3] - this.y[i1])) / (this.x[i3] - this.x[i1]) + this.y[i1];
|
|
};
|
|
|
|
var binarySearch = (function() {
|
|
var maxIndex, minIndex, guess;
|
|
return function(array, val) {
|
|
minIndex = -1;
|
|
maxIndex = array.length;
|
|
while (maxIndex - minIndex > 1)
|
|
if (array[guess = maxIndex + minIndex >> 1] <= val) {
|
|
minIndex = guess;
|
|
} else {
|
|
maxIndex = guess;
|
|
}
|
|
return maxIndex;
|
|
};
|
|
})();
|
|
},
|
|
//xxx: for now i will just save one spline function to to
|
|
getInterpolateFunction: function(c){
|
|
if(!s.controller.spline) s.controller.spline = s.params.loop ?
|
|
new s.controller.LinearSpline(s.slidesGrid, c.slidesGrid) :
|
|
new s.controller.LinearSpline(s.snapGrid, c.snapGrid);
|
|
},
|
|
setTranslate: function (translate, byController) {
|
|
var controlled = s.params.control;
|
|
var multiplier, controlledTranslate;
|
|
function setControlledTranslate(c) {
|
|
// this will create an Interpolate function based on the snapGrids
|
|
// x is the Grid of the scrolled scroller and y will be the controlled scroller
|
|
// it makes sense to create this only once and recall it for the interpolation
|
|
// the function does a lot of value caching for performance
|
|
translate = c.rtl && c.params.direction === 'horizontal' ? -s.translate : s.translate;
|
|
if (s.params.controlBy === 'slide') {
|
|
s.controller.getInterpolateFunction(c);
|
|
// i am not sure why the values have to be multiplicated this way, tried to invert the snapGrid
|
|
// but it did not work out
|
|
controlledTranslate = -s.controller.spline.interpolate(-translate);
|
|
}
|
|
|
|
if(!controlledTranslate || s.params.controlBy === 'container'){
|
|
multiplier = (c.maxTranslate() - c.minTranslate()) / (s.maxTranslate() - s.minTranslate());
|
|
controlledTranslate = (translate - s.minTranslate()) * multiplier + c.minTranslate();
|
|
}
|
|
|
|
if (s.params.controlInverse) {
|
|
controlledTranslate = c.maxTranslate() - controlledTranslate;
|
|
}
|
|
c.updateProgress(controlledTranslate);
|
|
c.setWrapperTranslate(controlledTranslate, false, s);
|
|
c.updateActiveIndex();
|
|
}
|
|
if (s.isArray(controlled)) {
|
|
for (var i = 0; i < controlled.length; i++) {
|
|
if (controlled[i] !== byController && controlled[i] instanceof Swiper) {
|
|
setControlledTranslate(controlled[i]);
|
|
}
|
|
}
|
|
}
|
|
else if (controlled instanceof Swiper && byController !== controlled) {
|
|
|
|
setControlledTranslate(controlled);
|
|
}
|
|
},
|
|
setTransition: function (duration, byController) {
|
|
var controlled = s.params.control;
|
|
var i;
|
|
function setControlledTransition(c) {
|
|
c.setWrapperTransition(duration, s);
|
|
if (duration !== 0) {
|
|
c.onTransitionStart();
|
|
c.wrapper.transitionEnd(function(){
|
|
if (!controlled) return;
|
|
if (c.params.loop && s.params.controlBy === 'slide') {
|
|
c.fixLoop();
|
|
}
|
|
c.onTransitionEnd();
|
|
|
|
});
|
|
}
|
|
}
|
|
if (s.isArray(controlled)) {
|
|
for (i = 0; i < controlled.length; i++) {
|
|
if (controlled[i] !== byController && controlled[i] instanceof Swiper) {
|
|
setControlledTransition(controlled[i]);
|
|
}
|
|
}
|
|
}
|
|
else if (controlled instanceof Swiper && byController !== controlled) {
|
|
setControlledTransition(controlled);
|
|
}
|
|
}
|
|
};
|
|
|
|
/*=========================
|
|
Hash Navigation
|
|
===========================*/
|
|
s.hashnav = {
|
|
init: function () {
|
|
if (!s.params.hashnav) return;
|
|
s.hashnav.initialized = true;
|
|
var hash = document.location.hash.replace('#', '');
|
|
if (!hash) return;
|
|
var speed = 0;
|
|
for (var i = 0, length = s.slides.length; i < length; i++) {
|
|
var slide = s.slides.eq(i);
|
|
var slideHash = slide.attr('data-hash');
|
|
if (slideHash === hash && !slide.hasClass(s.params.slideDuplicateClass)) {
|
|
var index = slide.index();
|
|
s.slideTo(index, speed, s.params.runCallbacksOnInit, true);
|
|
}
|
|
}
|
|
},
|
|
setHash: function () {
|
|
if (!s.hashnav.initialized || !s.params.hashnav) return;
|
|
document.location.hash = s.slides.eq(s.activeIndex).attr('data-hash') || '';
|
|
}
|
|
};
|
|
|
|
/*=========================
|
|
Keyboard Control
|
|
===========================*/
|
|
function handleKeyboard(e) {
|
|
if (e.originalEvent) e = e.originalEvent; //jquery fix
|
|
var kc = e.keyCode || e.charCode;
|
|
// Directions locks
|
|
if (!s.params.allowSwipeToNext && (isH() && kc === 39 || !isH() && kc === 40)) {
|
|
return false;
|
|
}
|
|
if (!s.params.allowSwipeToPrev && (isH() && kc === 37 || !isH() && kc === 38)) {
|
|
return false;
|
|
}
|
|
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) {
|
|
return;
|
|
}
|
|
if (document.activeElement && document.activeElement.nodeName && (document.activeElement.nodeName.toLowerCase() === 'input' || document.activeElement.nodeName.toLowerCase() === 'textarea')) {
|
|
return;
|
|
}
|
|
if (kc === 37 || kc === 39 || kc === 38 || kc === 40) {
|
|
var inView = false;
|
|
//Check that swiper should be inside of visible area of window
|
|
if (s.container.parents('.swiper-slide').length > 0 && s.container.parents('.swiper-slide-active').length === 0) {
|
|
return;
|
|
}
|
|
var windowScroll = {
|
|
left: window.pageXOffset,
|
|
top: window.pageYOffset
|
|
};
|
|
var windowWidth = window.innerWidth;
|
|
var windowHeight = window.innerHeight;
|
|
var swiperOffset = s.container.offset();
|
|
if (s.rtl) swiperOffset.left = swiperOffset.left - s.container[0].scrollLeft;
|
|
var swiperCoord = [
|
|
[swiperOffset.left, swiperOffset.top],
|
|
[swiperOffset.left + s.width, swiperOffset.top],
|
|
[swiperOffset.left, swiperOffset.top + s.height],
|
|
[swiperOffset.left + s.width, swiperOffset.top + s.height]
|
|
];
|
|
for (var i = 0; i < swiperCoord.length; i++) {
|
|
var point = swiperCoord[i];
|
|
if (
|
|
point[0] >= windowScroll.left && point[0] <= windowScroll.left + windowWidth &&
|
|
point[1] >= windowScroll.top && point[1] <= windowScroll.top + windowHeight
|
|
) {
|
|
inView = true;
|
|
}
|
|
|
|
}
|
|
if (!inView) return;
|
|
}
|
|
if (isH()) {
|
|
if (kc === 37 || kc === 39) {
|
|
if (e.preventDefault) e.preventDefault();
|
|
else e.returnValue = false;
|
|
}
|
|
if ((kc === 39 && !s.rtl) || (kc === 37 && s.rtl)) s.slideNext();
|
|
if ((kc === 37 && !s.rtl) || (kc === 39 && s.rtl)) s.slidePrev();
|
|
}
|
|
else {
|
|
if (kc === 38 || kc === 40) {
|
|
if (e.preventDefault) e.preventDefault();
|
|
else e.returnValue = false;
|
|
}
|
|
if (kc === 40) s.slideNext();
|
|
if (kc === 38) s.slidePrev();
|
|
}
|
|
}
|
|
s.disableKeyboardControl = function () {
|
|
s.params.keyboardControl = false;
|
|
$(document).off('keydown', handleKeyboard);
|
|
};
|
|
s.enableKeyboardControl = function () {
|
|
s.params.keyboardControl = true;
|
|
$(document).on('keydown', handleKeyboard);
|
|
};
|
|
|
|
|
|
/*=========================
|
|
Mousewheel Control
|
|
===========================*/
|
|
s.mousewheel = {
|
|
event: false,
|
|
lastScrollTime: (new window.Date()).getTime()
|
|
};
|
|
if (s.params.mousewheelControl) {
|
|
try {
|
|
new window.WheelEvent('wheel');
|
|
s.mousewheel.event = 'wheel';
|
|
} catch (e) {}
|
|
|
|
if (!s.mousewheel.event && document.onmousewheel !== undefined) {
|
|
s.mousewheel.event = 'mousewheel';
|
|
}
|
|
if (!s.mousewheel.event) {
|
|
s.mousewheel.event = 'DOMMouseScroll';
|
|
}
|
|
}
|
|
function handleMousewheel(e) {
|
|
if (e.originalEvent) e = e.originalEvent; //jquery fix
|
|
var we = s.mousewheel.event;
|
|
var delta = 0;
|
|
var rtlFactor = s.rtl ? -1 : 1;
|
|
//Opera & IE
|
|
if (e.detail) delta = -e.detail;
|
|
//WebKits
|
|
else if (we === 'mousewheel') {
|
|
if (s.params.mousewheelForceToAxis) {
|
|
if (isH()) {
|
|
if (Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY)) delta = e.wheelDeltaX * rtlFactor;
|
|
else return;
|
|
}
|
|
else {
|
|
if (Math.abs(e.wheelDeltaY) > Math.abs(e.wheelDeltaX)) delta = e.wheelDeltaY;
|
|
else return;
|
|
}
|
|
}
|
|
else {
|
|
delta = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) ? - e.wheelDeltaX * rtlFactor : - e.wheelDeltaY;
|
|
}
|
|
}
|
|
//Old FireFox
|
|
else if (we === 'DOMMouseScroll') delta = -e.detail;
|
|
//New FireFox
|
|
else if (we === 'wheel') {
|
|
if (s.params.mousewheelForceToAxis) {
|
|
if (isH()) {
|
|
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) delta = -e.deltaX * rtlFactor;
|
|
else return;
|
|
}
|
|
else {
|
|
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) delta = -e.deltaY;
|
|
else return;
|
|
}
|
|
}
|
|
else {
|
|
delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? - e.deltaX * rtlFactor : - e.deltaY;
|
|
}
|
|
}
|
|
if (delta === 0) return;
|
|
|
|
if (s.params.mousewheelInvert) delta = -delta;
|
|
|
|
if (!s.params.freeMode) {
|
|
if ((new window.Date()).getTime() - s.mousewheel.lastScrollTime > 60) {
|
|
if (delta < 0) {
|
|
if ((!s.isEnd || s.params.loop) && !s.animating) s.slideNext();
|
|
else if (s.params.mousewheelReleaseOnEdges) return true;
|
|
}
|
|
else {
|
|
if ((!s.isBeginning || s.params.loop) && !s.animating) s.slidePrev();
|
|
else if (s.params.mousewheelReleaseOnEdges) return true;
|
|
}
|
|
}
|
|
s.mousewheel.lastScrollTime = (new window.Date()).getTime();
|
|
|
|
}
|
|
else {
|
|
//Freemode or scrollContainer:
|
|
var position = s.getWrapperTranslate() + delta * s.params.mousewheelSensitivity;
|
|
var wasBeginning = s.isBeginning,
|
|
wasEnd = s.isEnd;
|
|
|
|
if (position >= s.minTranslate()) position = s.minTranslate();
|
|
if (position <= s.maxTranslate()) position = s.maxTranslate();
|
|
|
|
s.setWrapperTransition(0);
|
|
s.setWrapperTranslate(position);
|
|
s.updateProgress();
|
|
s.updateActiveIndex();
|
|
|
|
if (!wasBeginning && s.isBeginning || !wasEnd && s.isEnd) {
|
|
s.updateClasses();
|
|
}
|
|
|
|
if (s.params.freeModeSticky) {
|
|
clearTimeout(s.mousewheel.timeout);
|
|
s.mousewheel.timeout = setTimeout(function () {
|
|
s.slideReset();
|
|
}, 300);
|
|
}
|
|
|
|
// Return page scroll on edge positions
|
|
if (position === 0 || position === s.maxTranslate()) return;
|
|
}
|
|
if (s.params.autoplay) s.stopAutoplay();
|
|
|
|
if (e.preventDefault) e.preventDefault();
|
|
else e.returnValue = false;
|
|
return false;
|
|
}
|
|
s.disableMousewheelControl = function () {
|
|
if (!s.mousewheel.event) return false;
|
|
s.container.off(s.mousewheel.event, handleMousewheel);
|
|
return true;
|
|
};
|
|
|
|
s.enableMousewheelControl = function () {
|
|
if (!s.mousewheel.event) return false;
|
|
s.container.on(s.mousewheel.event, handleMousewheel);
|
|
return true;
|
|
};
|
|
|
|
|
|
/*=========================
|
|
Parallax
|
|
===========================*/
|
|
function setParallaxTransform(el, progress) {
|
|
el = $(el);
|
|
var p, pX, pY;
|
|
var rtlFactor = s.rtl ? -1 : 1;
|
|
|
|
p = el.attr('data-swiper-parallax') || '0';
|
|
pX = el.attr('data-swiper-parallax-x');
|
|
pY = el.attr('data-swiper-parallax-y');
|
|
if (pX || pY) {
|
|
pX = pX || '0';
|
|
pY = pY || '0';
|
|
}
|
|
else {
|
|
if (isH()) {
|
|
pX = p;
|
|
pY = '0';
|
|
}
|
|
else {
|
|
pY = p;
|
|
pX = '0';
|
|
}
|
|
}
|
|
|
|
if ((pX).indexOf('%') >= 0) {
|
|
pX = parseInt(pX, 10) * progress * rtlFactor + '%';
|
|
}
|
|
else {
|
|
pX = pX * progress * rtlFactor + 'px' ;
|
|
}
|
|
if ((pY).indexOf('%') >= 0) {
|
|
pY = parseInt(pY, 10) * progress + '%';
|
|
}
|
|
else {
|
|
pY = pY * progress + 'px' ;
|
|
}
|
|
|
|
el.transform('translate3d(' + pX + ', ' + pY + ',0px)');
|
|
}
|
|
s.parallax = {
|
|
setTranslate: function () {
|
|
s.container.children('[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y]').each(function(){
|
|
setParallaxTransform(this, s.progress);
|
|
|
|
});
|
|
s.slides.each(function () {
|
|
var slide = $(this);
|
|
slide.find('[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y]').each(function () {
|
|
var progress = Math.min(Math.max(slide[0].progress, -1), 1);
|
|
setParallaxTransform(this, progress);
|
|
});
|
|
});
|
|
},
|
|
setTransition: function (duration) {
|
|
if (typeof duration === 'undefined') duration = s.params.speed;
|
|
s.container.find('[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y]').each(function(){
|
|
var el = $(this);
|
|
var parallaxDuration = parseInt(el.attr('data-swiper-parallax-duration'), 10) || duration;
|
|
if (duration === 0) parallaxDuration = 0;
|
|
el.transition(parallaxDuration);
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
/*=========================
|
|
Plugins API. Collect all and init all plugins
|
|
===========================*/
|
|
s._plugins = [];
|
|
for (var plugin in s.plugins) {
|
|
var p = s.plugins[plugin](s, s.params[plugin]);
|
|
if (p) s._plugins.push(p);
|
|
}
|
|
// Method to call all plugins event/method
|
|
s.callPlugins = function (eventName) {
|
|
for (var i = 0; i < s._plugins.length; i++) {
|
|
if (eventName in s._plugins[i]) {
|
|
s._plugins[i][eventName](arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
|
|
}
|
|
}
|
|
};
|
|
|
|
/*=========================
|
|
Events/Callbacks/Plugins Emitter
|
|
===========================*/
|
|
function normalizeEventName (eventName) {
|
|
if (eventName.indexOf('on') !== 0) {
|
|
if (eventName[0] !== eventName[0].toUpperCase()) {
|
|
eventName = 'on' + eventName[0].toUpperCase() + eventName.substring(1);
|
|
}
|
|
else {
|
|
eventName = 'on' + eventName;
|
|
}
|
|
}
|
|
return eventName;
|
|
}
|
|
s.emitterEventListeners = {
|
|
|
|
};
|
|
s.emit = function (eventName) {
|
|
// Trigger callbacks
|
|
if (s.params[eventName]) {
|
|
s.params[eventName](arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
|
|
}
|
|
var i;
|
|
// Trigger events
|
|
if (s.emitterEventListeners[eventName]) {
|
|
for (i = 0; i < s.emitterEventListeners[eventName].length; i++) {
|
|
s.emitterEventListeners[eventName][i](arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
|
|
}
|
|
}
|
|
// Trigger plugins
|
|
if (s.callPlugins) s.callPlugins(eventName, arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
|
|
};
|
|
s.on = function (eventName, handler) {
|
|
eventName = normalizeEventName(eventName);
|
|
if (!s.emitterEventListeners[eventName]) s.emitterEventListeners[eventName] = [];
|
|
s.emitterEventListeners[eventName].push(handler);
|
|
return s;
|
|
};
|
|
s.off = function (eventName, handler) {
|
|
var i;
|
|
eventName = normalizeEventName(eventName);
|
|
if (typeof handler === 'undefined') {
|
|
// Remove all handlers for such event
|
|
s.emitterEventListeners[eventName] = [];
|
|
return s;
|
|
}
|
|
if (!s.emitterEventListeners[eventName] || s.emitterEventListeners[eventName].length === 0) return;
|
|
for (i = 0; i < s.emitterEventListeners[eventName].length; i++) {
|
|
if(s.emitterEventListeners[eventName][i] === handler) s.emitterEventListeners[eventName].splice(i, 1);
|
|
}
|
|
return s;
|
|
};
|
|
s.once = function (eventName, handler) {
|
|
eventName = normalizeEventName(eventName);
|
|
var _handler = function () {
|
|
handler(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
|
|
s.off(eventName, _handler);
|
|
};
|
|
s.on(eventName, _handler);
|
|
return s;
|
|
};
|
|
|
|
// Accessibility tools
|
|
s.a11y = {
|
|
makeFocusable: function ($el) {
|
|
$el.attr('tabIndex', '0');
|
|
return $el;
|
|
},
|
|
addRole: function ($el, role) {
|
|
$el.attr('role', role);
|
|
return $el;
|
|
},
|
|
|
|
addLabel: function ($el, label) {
|
|
$el.attr('aria-label', label);
|
|
return $el;
|
|
},
|
|
|
|
disable: function ($el) {
|
|
$el.attr('aria-disabled', true);
|
|
return $el;
|
|
},
|
|
|
|
enable: function ($el) {
|
|
$el.attr('aria-disabled', false);
|
|
return $el;
|
|
},
|
|
|
|
onEnterKey: function (event) {
|
|
if (event.keyCode !== 13) return;
|
|
if ($(event.target).is(s.params.nextButton)) {
|
|
s.onClickNext(event);
|
|
if (s.isEnd) {
|
|
s.a11y.notify(s.params.lastSlideMessage);
|
|
}
|
|
else {
|
|
s.a11y.notify(s.params.nextSlideMessage);
|
|
}
|
|
}
|
|
else if ($(event.target).is(s.params.prevButton)) {
|
|
s.onClickPrev(event);
|
|
if (s.isBeginning) {
|
|
s.a11y.notify(s.params.firstSlideMessage);
|
|
}
|
|
else {
|
|
s.a11y.notify(s.params.prevSlideMessage);
|
|
}
|
|
}
|
|
if ($(event.target).is('.' + s.params.bulletClass)) {
|
|
$(event.target)[0].click();
|
|
}
|
|
},
|
|
|
|
liveRegion: $('<span class="swiper-notification" aria-live="assertive" aria-atomic="true"></span>'),
|
|
|
|
notify: function (message) {
|
|
var notification = s.a11y.liveRegion;
|
|
if (notification.length === 0) return;
|
|
notification.html('');
|
|
notification.html(message);
|
|
},
|
|
init: function () {
|
|
// Setup accessibility
|
|
if (s.params.nextButton) {
|
|
var nextButton = $(s.params.nextButton);
|
|
s.a11y.makeFocusable(nextButton);
|
|
s.a11y.addRole(nextButton, 'button');
|
|
s.a11y.addLabel(nextButton, s.params.nextSlideMessage);
|
|
}
|
|
if (s.params.prevButton) {
|
|
var prevButton = $(s.params.prevButton);
|
|
s.a11y.makeFocusable(prevButton);
|
|
s.a11y.addRole(prevButton, 'button');
|
|
s.a11y.addLabel(prevButton, s.params.prevSlideMessage);
|
|
}
|
|
|
|
$(s.container).append(s.a11y.liveRegion);
|
|
},
|
|
initPagination: function () {
|
|
if (s.params.pagination && s.params.paginationClickable && s.bullets && s.bullets.length) {
|
|
s.bullets.each(function () {
|
|
var bullet = $(this);
|
|
s.a11y.makeFocusable(bullet);
|
|
s.a11y.addRole(bullet, 'button');
|
|
s.a11y.addLabel(bullet, s.params.paginationBulletMessage.replace(/{{index}}/, bullet.index() + 1));
|
|
});
|
|
}
|
|
},
|
|
destroy: function () {
|
|
if (s.a11y.liveRegion && s.a11y.liveRegion.length > 0) s.a11y.liveRegion.remove();
|
|
}
|
|
};
|
|
|
|
|
|
/*=========================
|
|
Init/Destroy
|
|
===========================*/
|
|
s.init = function () {
|
|
if (s.params.loop) s.createLoop();
|
|
s.updateContainerSize();
|
|
s.updateSlidesSize();
|
|
s.updatePagination();
|
|
if (s.params.scrollbar && s.scrollbar) {
|
|
s.scrollbar.set();
|
|
if (s.params.scrollbarDraggable) {
|
|
s.scrollbar.enableDraggable();
|
|
}
|
|
}
|
|
if (s.params.effect !== 'slide' && s.effects[s.params.effect]) {
|
|
if (!s.params.loop) s.updateProgress();
|
|
s.effects[s.params.effect].setTranslate();
|
|
}
|
|
if (s.params.loop) {
|
|
s.slideTo(s.params.initialSlide + s.loopedSlides, 0, s.params.runCallbacksOnInit);
|
|
}
|
|
else {
|
|
s.slideTo(s.params.initialSlide, 0, s.params.runCallbacksOnInit);
|
|
if (s.params.initialSlide === 0) {
|
|
if (s.parallax && s.params.parallax) s.parallax.setTranslate();
|
|
if (s.lazy && s.params.lazyLoading) {
|
|
s.lazy.load();
|
|
s.lazy.initialImageLoaded = true;
|
|
}
|
|
}
|
|
}
|
|
s.attachEvents();
|
|
if (s.params.observer && s.support.observer) {
|
|
s.initObservers();
|
|
}
|
|
if (s.params.preloadImages && !s.params.lazyLoading) {
|
|
s.preloadImages();
|
|
}
|
|
if (s.params.autoplay) {
|
|
s.startAutoplay();
|
|
}
|
|
if (s.params.keyboardControl) {
|
|
if (s.enableKeyboardControl) s.enableKeyboardControl();
|
|
}
|
|
if (s.params.mousewheelControl) {
|
|
if (s.enableMousewheelControl) s.enableMousewheelControl();
|
|
}
|
|
if (s.params.hashnav) {
|
|
if (s.hashnav) s.hashnav.init();
|
|
}
|
|
if (s.params.a11y && s.a11y) s.a11y.init();
|
|
s.emit('onInit', s);
|
|
};
|
|
|
|
// Cleanup dynamic styles
|
|
s.cleanupStyles = function () {
|
|
// Container
|
|
s.container.removeClass(s.classNames.join(' ')).removeAttr('style');
|
|
|
|
// Wrapper
|
|
s.wrapper.removeAttr('style');
|
|
|
|
// Slides
|
|
if (s.slides && s.slides.length) {
|
|
s.slides
|
|
.removeClass([
|
|
s.params.slideVisibleClass,
|
|
s.params.slideActiveClass,
|
|
s.params.slideNextClass,
|
|
s.params.slidePrevClass
|
|
].join(' '))
|
|
.removeAttr('style')
|
|
.removeAttr('data-swiper-column')
|
|
.removeAttr('data-swiper-row');
|
|
}
|
|
|
|
// Pagination/Bullets
|
|
if (s.paginationContainer && s.paginationContainer.length) {
|
|
s.paginationContainer.removeClass(s.params.paginationHiddenClass);
|
|
}
|
|
if (s.bullets && s.bullets.length) {
|
|
s.bullets.removeClass(s.params.bulletActiveClass);
|
|
}
|
|
|
|
// Buttons
|
|
if (s.params.prevButton) $(s.params.prevButton).removeClass(s.params.buttonDisabledClass);
|
|
if (s.params.nextButton) $(s.params.nextButton).removeClass(s.params.buttonDisabledClass);
|
|
|
|
// Scrollbar
|
|
if (s.params.scrollbar && s.scrollbar) {
|
|
if (s.scrollbar.track && s.scrollbar.track.length) s.scrollbar.track.removeAttr('style');
|
|
if (s.scrollbar.drag && s.scrollbar.drag.length) s.scrollbar.drag.removeAttr('style');
|
|
}
|
|
};
|
|
|
|
// Destroy
|
|
s.destroy = function (deleteInstance, cleanupStyles) {
|
|
// Detach evebts
|
|
s.detachEvents();
|
|
// Stop autoplay
|
|
s.stopAutoplay();
|
|
// Disable draggable
|
|
if (s.params.scrollbar && s.scrollbar) {
|
|
if (s.params.scrollbarDraggable) {
|
|
s.scrollbar.disableDraggable();
|
|
}
|
|
}
|
|
// Destroy loop
|
|
if (s.params.loop) {
|
|
s.destroyLoop();
|
|
}
|
|
// Cleanup styles
|
|
if (cleanupStyles) {
|
|
s.cleanupStyles();
|
|
}
|
|
// Disconnect observer
|
|
s.disconnectObservers();
|
|
// Disable keyboard/mousewheel
|
|
if (s.params.keyboardControl) {
|
|
if (s.disableKeyboardControl) s.disableKeyboardControl();
|
|
}
|
|
if (s.params.mousewheelControl) {
|
|
if (s.disableMousewheelControl) s.disableMousewheelControl();
|
|
}
|
|
// Disable a11y
|
|
if (s.params.a11y && s.a11y) s.a11y.destroy();
|
|
// Destroy callback
|
|
s.emit('onDestroy');
|
|
// Delete instance
|
|
if (deleteInstance !== false) s = null;
|
|
};
|
|
|
|
s.init();
|
|
|
|
|
|
|
|
// Return swiper instance
|
|
return s;
|
|
};
|
|
|
|
|
|
/*==================================================
|
|
Prototype
|
|
====================================================*/
|
|
Swiper.prototype = {
|
|
isSafari: (function () {
|
|
var ua = navigator.userAgent.toLowerCase();
|
|
return (ua.indexOf('safari') >= 0 && ua.indexOf('chrome') < 0 && ua.indexOf('android') < 0);
|
|
})(),
|
|
isUiWebView: /(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/i.test(navigator.userAgent),
|
|
isArray: function (arr) {
|
|
return Object.prototype.toString.apply(arr) === '[object Array]';
|
|
},
|
|
/*==================================================
|
|
Browser
|
|
====================================================*/
|
|
browser: {
|
|
ie: window.navigator.pointerEnabled || window.navigator.msPointerEnabled,
|
|
ieTouch: (window.navigator.msPointerEnabled && window.navigator.msMaxTouchPoints > 1) || (window.navigator.pointerEnabled && window.navigator.maxTouchPoints > 1)
|
|
},
|
|
/*==================================================
|
|
Devices
|
|
====================================================*/
|
|
device: (function () {
|
|
var ua = navigator.userAgent;
|
|
var android = ua.match(/(Android);?[\s\/]+([\d.]+)?/);
|
|
var ipad = ua.match(/(iPad).*OS\s([\d_]+)/);
|
|
var ipod = ua.match(/(iPod)(.*OS\s([\d_]+))?/);
|
|
var iphone = !ipad && ua.match(/(iPhone\sOS)\s([\d_]+)/);
|
|
return {
|
|
ios: ipad || iphone || ipod,
|
|
android: android
|
|
};
|
|
})(),
|
|
/*==================================================
|
|
Feature Detection
|
|
====================================================*/
|
|
support: {
|
|
touch : (window.Modernizr && Modernizr.touch === true) || (function () {
|
|
return !!(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch);
|
|
})(),
|
|
|
|
transforms3d : (window.Modernizr && Modernizr.csstransforms3d === true) || (function () {
|
|
var div = document.createElement('div').style;
|
|
return ('webkitPerspective' in div || 'MozPerspective' in div || 'OPerspective' in div || 'MsPerspective' in div || 'perspective' in div);
|
|
})(),
|
|
|
|
flexbox: (function () {
|
|
var div = document.createElement('div').style;
|
|
var styles = ('alignItems webkitAlignItems webkitBoxAlign msFlexAlign mozBoxAlign webkitFlexDirection msFlexDirection mozBoxDirection mozBoxOrient webkitBoxDirection webkitBoxOrient').split(' ');
|
|
for (var i = 0; i < styles.length; i++) {
|
|
if (styles[i] in div) return true;
|
|
}
|
|
})(),
|
|
|
|
observer: (function () {
|
|
return ('MutationObserver' in window || 'WebkitMutationObserver' in window);
|
|
})()
|
|
},
|
|
/*==================================================
|
|
Plugins
|
|
====================================================*/
|
|
plugins: {}
|
|
};
|
|
|
|
|
|
/*===========================
|
|
Dom7 Library
|
|
===========================*/
|
|
var Dom7 = (function () {
|
|
var Dom7 = function (arr) {
|
|
var _this = this, i = 0;
|
|
// Create array-like object
|
|
for (i = 0; i < arr.length; i++) {
|
|
_this[i] = arr[i];
|
|
}
|
|
_this.length = arr.length;
|
|
// Return collection with methods
|
|
return this;
|
|
};
|
|
var $ = function (selector, context) {
|
|
var arr = [], i = 0;
|
|
if (selector && !context) {
|
|
if (selector instanceof Dom7) {
|
|
return selector;
|
|
}
|
|
}
|
|
if (selector) {
|
|
// String
|
|
if (typeof selector === 'string') {
|
|
var els, tempParent, html = selector.trim();
|
|
if (html.indexOf('<') >= 0 && html.indexOf('>') >= 0) {
|
|
var toCreate = 'div';
|
|
if (html.indexOf('<li') === 0) toCreate = 'ul';
|
|
if (html.indexOf('<tr') === 0) toCreate = 'tbody';
|
|
if (html.indexOf('<td') === 0 || html.indexOf('<th') === 0) toCreate = 'tr';
|
|
if (html.indexOf('<tbody') === 0) toCreate = 'table';
|
|
if (html.indexOf('<option') === 0) toCreate = 'select';
|
|
tempParent = document.createElement(toCreate);
|
|
tempParent.innerHTML = selector;
|
|
for (i = 0; i < tempParent.childNodes.length; i++) {
|
|
arr.push(tempParent.childNodes[i]);
|
|
}
|
|
}
|
|
else {
|
|
if (!context && selector[0] === '#' && !selector.match(/[ .<>:~]/)) {
|
|
// Pure ID selector
|
|
els = [document.getElementById(selector.split('#')[1])];
|
|
}
|
|
else {
|
|
// Other selectors
|
|
els = (context || document).querySelectorAll(selector);
|
|
}
|
|
for (i = 0; i < els.length; i++) {
|
|
if (els[i]) arr.push(els[i]);
|
|
}
|
|
}
|
|
}
|
|
// Node/element
|
|
else if (selector.nodeType || selector === window || selector === document) {
|
|
arr.push(selector);
|
|
}
|
|
//Array of elements or instance of Dom
|
|
else if (selector.length > 0 && selector[0].nodeType) {
|
|
for (i = 0; i < selector.length; i++) {
|
|
arr.push(selector[i]);
|
|
}
|
|
}
|
|
}
|
|
return new Dom7(arr);
|
|
};
|
|
Dom7.prototype = {
|
|
// Classes and attriutes
|
|
addClass: function (className) {
|
|
if (typeof className === 'undefined') {
|
|
return this;
|
|
}
|
|
var classes = className.split(' ');
|
|
for (var i = 0; i < classes.length; i++) {
|
|
for (var j = 0; j < this.length; j++) {
|
|
this[j].classList.add(classes[i]);
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
removeClass: function (className) {
|
|
var classes = className.split(' ');
|
|
for (var i = 0; i < classes.length; i++) {
|
|
for (var j = 0; j < this.length; j++) {
|
|
this[j].classList.remove(classes[i]);
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
hasClass: function (className) {
|
|
if (!this[0]) return false;
|
|
else return this[0].classList.contains(className);
|
|
},
|
|
toggleClass: function (className) {
|
|
var classes = className.split(' ');
|
|
for (var i = 0; i < classes.length; i++) {
|
|
for (var j = 0; j < this.length; j++) {
|
|
this[j].classList.toggle(classes[i]);
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
attr: function (attrs, value) {
|
|
if (arguments.length === 1 && typeof attrs === 'string') {
|
|
// Get attr
|
|
if (this[0]) return this[0].getAttribute(attrs);
|
|
else return undefined;
|
|
}
|
|
else {
|
|
// Set attrs
|
|
for (var i = 0; i < this.length; i++) {
|
|
if (arguments.length === 2) {
|
|
// String
|
|
this[i].setAttribute(attrs, value);
|
|
}
|
|
else {
|
|
// Object
|
|
for (var attrName in attrs) {
|
|
this[i][attrName] = attrs[attrName];
|
|
this[i].setAttribute(attrName, attrs[attrName]);
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
},
|
|
removeAttr: function (attr) {
|
|
for (var i = 0; i < this.length; i++) {
|
|
this[i].removeAttribute(attr);
|
|
}
|
|
return this;
|
|
},
|
|
data: function (key, value) {
|
|
if (typeof value === 'undefined') {
|
|
// Get value
|
|
if (this[0]) {
|
|
var dataKey = this[0].getAttribute('data-' + key);
|
|
if (dataKey) return dataKey;
|
|
else if (this[0].dom7ElementDataStorage && (key in this[0].dom7ElementDataStorage)) return this[0].dom7ElementDataStorage[key];
|
|
else return undefined;
|
|
}
|
|
else return undefined;
|
|
}
|
|
else {
|
|
// Set value
|
|
for (var i = 0; i < this.length; i++) {
|
|
var el = this[i];
|
|
if (!el.dom7ElementDataStorage) el.dom7ElementDataStorage = {};
|
|
el.dom7ElementDataStorage[key] = value;
|
|
}
|
|
return this;
|
|
}
|
|
},
|
|
// Transforms
|
|
transform : function (transform) {
|
|
for (var i = 0; i < this.length; i++) {
|
|
var elStyle = this[i].style;
|
|
elStyle.webkitTransform = elStyle.MsTransform = elStyle.msTransform = elStyle.MozTransform = elStyle.OTransform = elStyle.transform = transform;
|
|
}
|
|
return this;
|
|
},
|
|
transition: function (duration) {
|
|
if (typeof duration !== 'string') {
|
|
duration = duration + 'ms';
|
|
}
|
|
for (var i = 0; i < this.length; i++) {
|
|
var elStyle = this[i].style;
|
|
elStyle.webkitTransitionDuration = elStyle.MsTransitionDuration = elStyle.msTransitionDuration = elStyle.MozTransitionDuration = elStyle.OTransitionDuration = elStyle.transitionDuration = duration;
|
|
}
|
|
return this;
|
|
},
|
|
//Events
|
|
on: function (eventName, targetSelector, listener, capture) {
|
|
function handleLiveEvent(e) {
|
|
var target = e.target;
|
|
if ($(target).is(targetSelector)) listener.call(target, e);
|
|
else {
|
|
var parents = $(target).parents();
|
|
for (var k = 0; k < parents.length; k++) {
|
|
if ($(parents[k]).is(targetSelector)) listener.call(parents[k], e);
|
|
}
|
|
}
|
|
}
|
|
var events = eventName.split(' ');
|
|
var i, j;
|
|
for (i = 0; i < this.length; i++) {
|
|
if (typeof targetSelector === 'function' || targetSelector === false) {
|
|
// Usual events
|
|
if (typeof targetSelector === 'function') {
|
|
listener = arguments[1];
|
|
capture = arguments[2] || false;
|
|
}
|
|
for (j = 0; j < events.length; j++) {
|
|
this[i].addEventListener(events[j], listener, capture);
|
|
}
|
|
}
|
|
else {
|
|
//Live events
|
|
for (j = 0; j < events.length; j++) {
|
|
if (!this[i].dom7LiveListeners) this[i].dom7LiveListeners = [];
|
|
this[i].dom7LiveListeners.push({listener: listener, liveListener: handleLiveEvent});
|
|
this[i].addEventListener(events[j], handleLiveEvent, capture);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
off: function (eventName, targetSelector, listener, capture) {
|
|
var events = eventName.split(' ');
|
|
for (var i = 0; i < events.length; i++) {
|
|
for (var j = 0; j < this.length; j++) {
|
|
if (typeof targetSelector === 'function' || targetSelector === false) {
|
|
// Usual events
|
|
if (typeof targetSelector === 'function') {
|
|
listener = arguments[1];
|
|
capture = arguments[2] || false;
|
|
}
|
|
this[j].removeEventListener(events[i], listener, capture);
|
|
}
|
|
else {
|
|
// Live event
|
|
if (this[j].dom7LiveListeners) {
|
|
for (var k = 0; k < this[j].dom7LiveListeners.length; k++) {
|
|
if (this[j].dom7LiveListeners[k].listener === listener) {
|
|
this[j].removeEventListener(events[i], this[j].dom7LiveListeners[k].liveListener, capture);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
once: function (eventName, targetSelector, listener, capture) {
|
|
var dom = this;
|
|
if (typeof targetSelector === 'function') {
|
|
targetSelector = false;
|
|
listener = arguments[1];
|
|
capture = arguments[2];
|
|
}
|
|
function proxy(e) {
|
|
listener(e);
|
|
dom.off(eventName, targetSelector, proxy, capture);
|
|
}
|
|
dom.on(eventName, targetSelector, proxy, capture);
|
|
},
|
|
trigger: function (eventName, eventData) {
|
|
for (var i = 0; i < this.length; i++) {
|
|
var evt;
|
|
try {
|
|
evt = new window.CustomEvent(eventName, {detail: eventData, bubbles: true, cancelable: true});
|
|
}
|
|
catch (e) {
|
|
evt = document.createEvent('Event');
|
|
evt.initEvent(eventName, true, true);
|
|
evt.detail = eventData;
|
|
}
|
|
this[i].dispatchEvent(evt);
|
|
}
|
|
return this;
|
|
},
|
|
transitionEnd: function (callback) {
|
|
var events = ['webkitTransitionEnd', 'transitionend', 'oTransitionEnd', 'MSTransitionEnd', 'msTransitionEnd'],
|
|
i, j, dom = this;
|
|
function fireCallBack(e) {
|
|
/*jshint validthis:true */
|
|
if (e.target !== this) return;
|
|
callback.call(this, e);
|
|
for (i = 0; i < events.length; i++) {
|
|
dom.off(events[i], fireCallBack);
|
|
}
|
|
}
|
|
if (callback) {
|
|
for (i = 0; i < events.length; i++) {
|
|
dom.on(events[i], fireCallBack);
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
// Sizing/Styles
|
|
width: function () {
|
|
if (this[0] === window) {
|
|
return window.innerWidth;
|
|
}
|
|
else {
|
|
if (this.length > 0) {
|
|
return parseFloat(this.css('width'));
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
},
|
|
outerWidth: function (includeMargins) {
|
|
if (this.length > 0) {
|
|
if (includeMargins)
|
|
return this[0].offsetWidth + parseFloat(this.css('margin-right')) + parseFloat(this.css('margin-left'));
|
|
else
|
|
return this[0].offsetWidth;
|
|
}
|
|
else return null;
|
|
},
|
|
height: function () {
|
|
if (this[0] === window) {
|
|
return window.innerHeight;
|
|
}
|
|
else {
|
|
if (this.length > 0) {
|
|
return parseFloat(this.css('height'));
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
},
|
|
outerHeight: function (includeMargins) {
|
|
if (this.length > 0) {
|
|
if (includeMargins)
|
|
return this[0].offsetHeight + parseFloat(this.css('margin-top')) + parseFloat(this.css('margin-bottom'));
|
|
else
|
|
return this[0].offsetHeight;
|
|
}
|
|
else return null;
|
|
},
|
|
offset: function () {
|
|
if (this.length > 0) {
|
|
var el = this[0];
|
|
var box = el.getBoundingClientRect();
|
|
var body = document.body;
|
|
var clientTop = el.clientTop || body.clientTop || 0;
|
|
var clientLeft = el.clientLeft || body.clientLeft || 0;
|
|
var scrollTop = window.pageYOffset || el.scrollTop;
|
|
var scrollLeft = window.pageXOffset || el.scrollLeft;
|
|
return {
|
|
top: box.top + scrollTop - clientTop,
|
|
left: box.left + scrollLeft - clientLeft
|
|
};
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
},
|
|
css: function (props, value) {
|
|
var i;
|
|
if (arguments.length === 1) {
|
|
if (typeof props === 'string') {
|
|
if (this[0]) return window.getComputedStyle(this[0], null).getPropertyValue(props);
|
|
}
|
|
else {
|
|
for (i = 0; i < this.length; i++) {
|
|
for (var prop in props) {
|
|
this[i].style[prop] = props[prop];
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
if (arguments.length === 2 && typeof props === 'string') {
|
|
for (i = 0; i < this.length; i++) {
|
|
this[i].style[props] = value;
|
|
}
|
|
return this;
|
|
}
|
|
return this;
|
|
},
|
|
|
|
//Dom manipulation
|
|
each: function (callback) {
|
|
for (var i = 0; i < this.length; i++) {
|
|
callback.call(this[i], i, this[i]);
|
|
}
|
|
return this;
|
|
},
|
|
html: function (html) {
|
|
if (typeof html === 'undefined') {
|
|
return this[0] ? this[0].innerHTML : undefined;
|
|
}
|
|
else {
|
|
for (var i = 0; i < this.length; i++) {
|
|
this[i].innerHTML = html;
|
|
}
|
|
return this;
|
|
}
|
|
},
|
|
is: function (selector) {
|
|
if (!this[0]) return false;
|
|
var compareWith, i;
|
|
if (typeof selector === 'string') {
|
|
var el = this[0];
|
|
if (el === document) return selector === document;
|
|
if (el === window) return selector === window;
|
|
|
|
if (el.matches) return el.matches(selector);
|
|
else if (el.webkitMatchesSelector) return el.webkitMatchesSelector(selector);
|
|
else if (el.mozMatchesSelector) return el.mozMatchesSelector(selector);
|
|
else if (el.msMatchesSelector) return el.msMatchesSelector(selector);
|
|
else {
|
|
compareWith = $(selector);
|
|
for (i = 0; i < compareWith.length; i++) {
|
|
if (compareWith[i] === this[0]) return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
else if (selector === document) return this[0] === document;
|
|
else if (selector === window) return this[0] === window;
|
|
else {
|
|
if (selector.nodeType || selector instanceof Dom7) {
|
|
compareWith = selector.nodeType ? [selector] : selector;
|
|
for (i = 0; i < compareWith.length; i++) {
|
|
if (compareWith[i] === this[0]) return true;
|
|
}
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
},
|
|
index: function () {
|
|
if (this[0]) {
|
|
var child = this[0];
|
|
var i = 0;
|
|
while ((child = child.previousSibling) !== null) {
|
|
if (child.nodeType === 1) i++;
|
|
}
|
|
return i;
|
|
}
|
|
else return undefined;
|
|
},
|
|
eq: function (index) {
|
|
if (typeof index === 'undefined') return this;
|
|
var length = this.length;
|
|
var returnIndex;
|
|
if (index > length - 1) {
|
|
return new Dom7([]);
|
|
}
|
|
if (index < 0) {
|
|
returnIndex = length + index;
|
|
if (returnIndex < 0) return new Dom7([]);
|
|
else return new Dom7([this[returnIndex]]);
|
|
}
|
|
return new Dom7([this[index]]);
|
|
},
|
|
append: function (newChild) {
|
|
var i, j;
|
|
for (i = 0; i < this.length; i++) {
|
|
if (typeof newChild === 'string') {
|
|
var tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = newChild;
|
|
while (tempDiv.firstChild) {
|
|
this[i].appendChild(tempDiv.firstChild);
|
|
}
|
|
}
|
|
else if (newChild instanceof Dom7) {
|
|
for (j = 0; j < newChild.length; j++) {
|
|
this[i].appendChild(newChild[j]);
|
|
}
|
|
}
|
|
else {
|
|
this[i].appendChild(newChild);
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
prepend: function (newChild) {
|
|
var i, j;
|
|
for (i = 0; i < this.length; i++) {
|
|
if (typeof newChild === 'string') {
|
|
var tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = newChild;
|
|
for (j = tempDiv.childNodes.length - 1; j >= 0; j--) {
|
|
this[i].insertBefore(tempDiv.childNodes[j], this[i].childNodes[0]);
|
|
}
|
|
// this[i].insertAdjacentHTML('afterbegin', newChild);
|
|
}
|
|
else if (newChild instanceof Dom7) {
|
|
for (j = 0; j < newChild.length; j++) {
|
|
this[i].insertBefore(newChild[j], this[i].childNodes[0]);
|
|
}
|
|
}
|
|
else {
|
|
this[i].insertBefore(newChild, this[i].childNodes[0]);
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
insertBefore: function (selector) {
|
|
var before = $(selector);
|
|
for (var i = 0; i < this.length; i++) {
|
|
if (before.length === 1) {
|
|
before[0].parentNode.insertBefore(this[i], before[0]);
|
|
}
|
|
else if (before.length > 1) {
|
|
for (var j = 0; j < before.length; j++) {
|
|
before[j].parentNode.insertBefore(this[i].cloneNode(true), before[j]);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
insertAfter: function (selector) {
|
|
var after = $(selector);
|
|
for (var i = 0; i < this.length; i++) {
|
|
if (after.length === 1) {
|
|
after[0].parentNode.insertBefore(this[i], after[0].nextSibling);
|
|
}
|
|
else if (after.length > 1) {
|
|
for (var j = 0; j < after.length; j++) {
|
|
after[j].parentNode.insertBefore(this[i].cloneNode(true), after[j].nextSibling);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
next: function (selector) {
|
|
if (this.length > 0) {
|
|
if (selector) {
|
|
if (this[0].nextElementSibling && $(this[0].nextElementSibling).is(selector)) return new Dom7([this[0].nextElementSibling]);
|
|
else return new Dom7([]);
|
|
}
|
|
else {
|
|
if (this[0].nextElementSibling) return new Dom7([this[0].nextElementSibling]);
|
|
else return new Dom7([]);
|
|
}
|
|
}
|
|
else return new Dom7([]);
|
|
},
|
|
nextAll: function (selector) {
|
|
var nextEls = [];
|
|
var el = this[0];
|
|
if (!el) return new Dom7([]);
|
|
while (el.nextElementSibling) {
|
|
var next = el.nextElementSibling;
|
|
if (selector) {
|
|
if($(next).is(selector)) nextEls.push(next);
|
|
}
|
|
else nextEls.push(next);
|
|
el = next;
|
|
}
|
|
return new Dom7(nextEls);
|
|
},
|
|
prev: function (selector) {
|
|
if (this.length > 0) {
|
|
if (selector) {
|
|
if (this[0].previousElementSibling && $(this[0].previousElementSibling).is(selector)) return new Dom7([this[0].previousElementSibling]);
|
|
else return new Dom7([]);
|
|
}
|
|
else {
|
|
if (this[0].previousElementSibling) return new Dom7([this[0].previousElementSibling]);
|
|
else return new Dom7([]);
|
|
}
|
|
}
|
|
else return new Dom7([]);
|
|
},
|
|
prevAll: function (selector) {
|
|
var prevEls = [];
|
|
var el = this[0];
|
|
if (!el) return new Dom7([]);
|
|
while (el.previousElementSibling) {
|
|
var prev = el.previousElementSibling;
|
|
if (selector) {
|
|
if($(prev).is(selector)) prevEls.push(prev);
|
|
}
|
|
else prevEls.push(prev);
|
|
el = prev;
|
|
}
|
|
return new Dom7(prevEls);
|
|
},
|
|
parent: function (selector) {
|
|
var parents = [];
|
|
for (var i = 0; i < this.length; i++) {
|
|
if (selector) {
|
|
if ($(this[i].parentNode).is(selector)) parents.push(this[i].parentNode);
|
|
}
|
|
else {
|
|
parents.push(this[i].parentNode);
|
|
}
|
|
}
|
|
return $($.unique(parents));
|
|
},
|
|
parents: function (selector) {
|
|
var parents = [];
|
|
for (var i = 0; i < this.length; i++) {
|
|
var parent = this[i].parentNode;
|
|
while (parent) {
|
|
if (selector) {
|
|
if ($(parent).is(selector)) parents.push(parent);
|
|
}
|
|
else {
|
|
parents.push(parent);
|
|
}
|
|
parent = parent.parentNode;
|
|
}
|
|
}
|
|
return $($.unique(parents));
|
|
},
|
|
find : function (selector) {
|
|
var foundElements = [];
|
|
for (var i = 0; i < this.length; i++) {
|
|
var found = this[i].querySelectorAll(selector);
|
|
for (var j = 0; j < found.length; j++) {
|
|
foundElements.push(found[j]);
|
|
}
|
|
}
|
|
return new Dom7(foundElements);
|
|
},
|
|
children: function (selector) {
|
|
var children = [];
|
|
for (var i = 0; i < this.length; i++) {
|
|
var childNodes = this[i].childNodes;
|
|
|
|
for (var j = 0; j < childNodes.length; j++) {
|
|
if (!selector) {
|
|
if (childNodes[j].nodeType === 1) children.push(childNodes[j]);
|
|
}
|
|
else {
|
|
if (childNodes[j].nodeType === 1 && $(childNodes[j]).is(selector)) children.push(childNodes[j]);
|
|
}
|
|
}
|
|
}
|
|
return new Dom7($.unique(children));
|
|
},
|
|
remove: function () {
|
|
for (var i = 0; i < this.length; i++) {
|
|
if (this[i].parentNode) this[i].parentNode.removeChild(this[i]);
|
|
}
|
|
return this;
|
|
},
|
|
add: function () {
|
|
var dom = this;
|
|
var i, j;
|
|
for (i = 0; i < arguments.length; i++) {
|
|
var toAdd = $(arguments[i]);
|
|
for (j = 0; j < toAdd.length; j++) {
|
|
dom[dom.length] = toAdd[j];
|
|
dom.length++;
|
|
}
|
|
}
|
|
return dom;
|
|
}
|
|
};
|
|
$.fn = Dom7.prototype;
|
|
$.unique = function (arr) {
|
|
var unique = [];
|
|
for (var i = 0; i < arr.length; i++) {
|
|
if (unique.indexOf(arr[i]) === -1) unique.push(arr[i]);
|
|
}
|
|
return unique;
|
|
};
|
|
|
|
return $;
|
|
})();
|
|
|
|
|
|
/*===========================
|
|
Get Dom libraries
|
|
===========================*/
|
|
var swiperDomPlugins = ['jQuery', 'Zepto', 'Dom7'];
|
|
for (var i = 0; i < swiperDomPlugins.length; i++) {
|
|
if (window[swiperDomPlugins[i]]) {
|
|
addLibraryPlugin(window[swiperDomPlugins[i]]);
|
|
}
|
|
}
|
|
// Required DOM Plugins
|
|
var domLib;
|
|
if (typeof Dom7 === 'undefined') {
|
|
domLib = window.Dom7 || window.Zepto || window.jQuery;
|
|
}
|
|
else {
|
|
domLib = Dom7;
|
|
}
|
|
|
|
/*===========================
|
|
Add .swiper plugin from Dom libraries
|
|
===========================*/
|
|
function addLibraryPlugin(lib) {
|
|
lib.fn.swiper = function (params) {
|
|
var firstInstance;
|
|
lib(this).each(function () {
|
|
var s = new Swiper(this, params);
|
|
if (!firstInstance) firstInstance = s;
|
|
});
|
|
return firstInstance;
|
|
};
|
|
}
|
|
|
|
if (domLib) {
|
|
if (!('transitionEnd' in domLib.fn)) {
|
|
domLib.fn.transitionEnd = function (callback) {
|
|
var events = ['webkitTransitionEnd', 'transitionend', 'oTransitionEnd', 'MSTransitionEnd', 'msTransitionEnd'],
|
|
i, j, dom = this;
|
|
function fireCallBack(e) {
|
|
/*jshint validthis:true */
|
|
if (e.target !== this) return;
|
|
callback.call(this, e);
|
|
for (i = 0; i < events.length; i++) {
|
|
dom.off(events[i], fireCallBack);
|
|
}
|
|
}
|
|
if (callback) {
|
|
for (i = 0; i < events.length; i++) {
|
|
dom.on(events[i], fireCallBack);
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
}
|
|
if (!('transform' in domLib.fn)) {
|
|
domLib.fn.transform = function (transform) {
|
|
for (var i = 0; i < this.length; i++) {
|
|
var elStyle = this[i].style;
|
|
elStyle.webkitTransform = elStyle.MsTransform = elStyle.msTransform = elStyle.MozTransform = elStyle.OTransform = elStyle.transform = transform;
|
|
}
|
|
return this;
|
|
};
|
|
}
|
|
if (!('transition' in domLib.fn)) {
|
|
domLib.fn.transition = function (duration) {
|
|
if (typeof duration !== 'string') {
|
|
duration = duration + 'ms';
|
|
}
|
|
for (var i = 0; i < this.length; i++) {
|
|
var elStyle = this[i].style;
|
|
elStyle.webkitTransitionDuration = elStyle.MsTransitionDuration = elStyle.msTransitionDuration = elStyle.MozTransitionDuration = elStyle.OTransitionDuration = elStyle.transitionDuration = duration;
|
|
}
|
|
return this;
|
|
};
|
|
}
|
|
}
|
|
|
|
ionic.views.Swiper = Swiper;
|
|
})();
|
|
|
|
(function(ionic) {
|
|
'use strict';
|
|
|
|
ionic.views.Toggle = ionic.views.View.inherit({
|
|
initialize: function(opts) {
|
|
var self = this;
|
|
|
|
this.el = opts.el;
|
|
this.checkbox = opts.checkbox;
|
|
this.track = opts.track;
|
|
this.handle = opts.handle;
|
|
this.openPercent = -1;
|
|
this.onChange = opts.onChange || function() {};
|
|
|
|
this.triggerThreshold = opts.triggerThreshold || 20;
|
|
|
|
this.dragStartHandler = function(e) {
|
|
self.dragStart(e);
|
|
};
|
|
this.dragHandler = function(e) {
|
|
self.drag(e);
|
|
};
|
|
this.holdHandler = function(e) {
|
|
self.hold(e);
|
|
};
|
|
this.releaseHandler = function(e) {
|
|
self.release(e);
|
|
};
|
|
|
|
this.dragStartGesture = ionic.onGesture('dragstart', this.dragStartHandler, this.el);
|
|
this.dragGesture = ionic.onGesture('drag', this.dragHandler, this.el);
|
|
this.dragHoldGesture = ionic.onGesture('hold', this.holdHandler, this.el);
|
|
this.dragReleaseGesture = ionic.onGesture('release', this.releaseHandler, this.el);
|
|
},
|
|
|
|
destroy: function() {
|
|
ionic.offGesture(this.dragStartGesture, 'dragstart', this.dragStartGesture);
|
|
ionic.offGesture(this.dragGesture, 'drag', this.dragGesture);
|
|
ionic.offGesture(this.dragHoldGesture, 'hold', this.holdHandler);
|
|
ionic.offGesture(this.dragReleaseGesture, 'release', this.releaseHandler);
|
|
},
|
|
|
|
tap: function() {
|
|
if(this.el.getAttribute('disabled') !== 'disabled') {
|
|
this.val( !this.checkbox.checked );
|
|
}
|
|
},
|
|
|
|
dragStart: function(e) {
|
|
if(this.checkbox.disabled) return;
|
|
|
|
this._dragInfo = {
|
|
width: this.el.offsetWidth,
|
|
left: this.el.offsetLeft,
|
|
right: this.el.offsetLeft + this.el.offsetWidth,
|
|
triggerX: this.el.offsetWidth / 2,
|
|
initialState: this.checkbox.checked
|
|
};
|
|
|
|
// Stop any parent dragging
|
|
e.gesture.srcEvent.preventDefault();
|
|
|
|
// Trigger hold styles
|
|
this.hold(e);
|
|
},
|
|
|
|
drag: function(e) {
|
|
var self = this;
|
|
if(!this._dragInfo) { return; }
|
|
|
|
// Stop any parent dragging
|
|
e.gesture.srcEvent.preventDefault();
|
|
|
|
ionic.requestAnimationFrame(function () {
|
|
if (!self._dragInfo) { return; }
|
|
|
|
var px = e.gesture.touches[0].pageX - self._dragInfo.left;
|
|
var mx = self._dragInfo.width - self.triggerThreshold;
|
|
|
|
// The initial state was on, so "tend towards" on
|
|
if(self._dragInfo.initialState) {
|
|
if(px < self.triggerThreshold) {
|
|
self.setOpenPercent(0);
|
|
} else if(px > self._dragInfo.triggerX) {
|
|
self.setOpenPercent(100);
|
|
}
|
|
} else {
|
|
// The initial state was off, so "tend towards" off
|
|
if(px < self._dragInfo.triggerX) {
|
|
self.setOpenPercent(0);
|
|
} else if(px > mx) {
|
|
self.setOpenPercent(100);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
endDrag: function() {
|
|
this._dragInfo = null;
|
|
},
|
|
|
|
hold: function() {
|
|
this.el.classList.add('dragging');
|
|
},
|
|
release: function(e) {
|
|
this.el.classList.remove('dragging');
|
|
this.endDrag(e);
|
|
},
|
|
|
|
|
|
setOpenPercent: function(openPercent) {
|
|
// only make a change if the new open percent has changed
|
|
if(this.openPercent < 0 || (openPercent < (this.openPercent - 3) || openPercent > (this.openPercent + 3) ) ) {
|
|
this.openPercent = openPercent;
|
|
|
|
if(openPercent === 0) {
|
|
this.val(false);
|
|
} else if(openPercent === 100) {
|
|
this.val(true);
|
|
} else {
|
|
var openPixel = Math.round( (openPercent / 100) * this.track.offsetWidth - (this.handle.offsetWidth) );
|
|
openPixel = (openPixel < 1 ? 0 : openPixel);
|
|
this.handle.style[ionic.CSS.TRANSFORM] = 'translate3d(' + openPixel + 'px,0,0)';
|
|
}
|
|
}
|
|
},
|
|
|
|
val: function(value) {
|
|
if(value === true || value === false) {
|
|
if(this.handle.style[ionic.CSS.TRANSFORM] !== "") {
|
|
this.handle.style[ionic.CSS.TRANSFORM] = "";
|
|
}
|
|
this.checkbox.checked = value;
|
|
this.openPercent = (value ? 100 : 0);
|
|
this.onChange && this.onChange();
|
|
}
|
|
return this.checkbox.checked;
|
|
}
|
|
|
|
});
|
|
|
|
})(ionic);
|
|
|
|
})();
|