/*! * Copyright 2015 Drifty Co. * http://drifty.com/ * * Ionic, v1.3.3 * 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.3'; (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 * * 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`,
* `dragleft`, `dragright`, `swipe`, `swipeup`, `swipedown`, `swipeleft`, `swiperight`,
* `transform`, `transformstart`, `transformend`, `rotate`, `pinch`, `pinchin`, `pinchout`,
* `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 `ionic plugin add cordova-plugin-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 `ionic plugin add cordova-plugin-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 *
*
*
* ``` * * ### 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/1.x/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) && !e.target.isContentEditable && !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(e.target.tagName == 'SELECT') { 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 *
*
*
* ``` * * 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 * `` 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 * * * ``` * * ### 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 null * @param top {Number} Vertical scroll position, keeps current if value is null * @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.__originalContainerHeight = self.el.getBoundingClientRect().height; 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 null * @param top {Number} Vertical scroll position, keeps current if value is null * @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 null * @param top {Number} Vertical scroll position, keeps current if value is null * @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('scroll', self.onScroll); container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView); container.removeEventListener(ionic.EVENTS.touchstart, self.handleTouchMove); container.removeEventListener(ionic.EVENTS.touchmove, self.handleTouchMove); 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.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.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 = $('
'); s.wrapper.append(cubeShadow); } cubeShadow.css({height: s.width + 'px'}); } else { cubeShadow = s.container.find('.swiper-cube-shadow'); if (cubeShadow.length === 0) { cubeShadow = $('
'); 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 = $('
'); slide.append(shadowBefore); } if (shadowAfter.length === 0) { shadowAfter = $('
'); 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 = $('
'); slide.append(shadowBefore); } if (shadowAfter.length === 0) { shadowAfter = $('
'); 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 = $('
'); 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: $(''), 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(':~]/)) { // 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); })();