diff options
Diffstat (limited to 'www/lib/ionic/js/ionic.js')
| -rw-r--r-- | www/lib/ionic/js/ionic.js | 8622 |
1 files changed, 8622 insertions, 0 deletions
diff --git a/www/lib/ionic/js/ionic.js b/www/lib/ionic/js/ionic.js new file mode 100644 index 00000000..4e4d926a --- /dev/null +++ b/www/lib/ionic/js/ionic.js @@ -0,0 +1,8622 @@ +/*! + * Copyright 2014 Drifty Co. + * http://drifty.com/ + * + * Ionic, v1.0.0-rc.4 + * 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.0.0-rc.4'; + +(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 + }; + }, + + /** + * @ngdoc method + * @name ionic.DomUtil#ready + * @description + * Call a function when the DOM is ready, or if it is already ready + * call the function immediately. + * @param {function} callback The function to be called. + */ + ready: function(cb) { + if (isDomReady) { + ionic.requestAnimationFrame(cb); + } else { + readyCallbacks.push(cb); + } + }, + + /** + * @ngdoc method + * @name ionic.DomUtil#getTextBounds + * @description + * Get a rect representing the bounds of the given textNode. + * @param {DOMElement} textNode The textNode to find the bounds of. + * @returns {object} An object representing the bounds of the node. Properties: + * - `{number}` `left` The left position of the textNode. + * - `{number}` `right` The right position of the textNode. + * - `{number}` `top` The top position of the textNode. + * - `{number}` `bottom` The bottom position of the textNode. + * - `{number}` `width` The width of the textNode. + * - `{number}` `height` The height of the textNode. + */ + getTextBounds: function(textNode) { + if (document.createRange) { + var range = document.createRange(); + range.selectNodeContents(textNode); + if (range.getBoundingClientRect) { + var rect = range.getBoundingClientRect(); + if (rect) { + var sx = window.scrollX; + var sy = window.scrollY; + + return { + top: rect.top + sy, + left: rect.left + sx, + right: rect.left + sx + rect.width, + bottom: rect.top + sy + rect.height, + width: rect.width, + height: rect.height + }; + } + } + } + return null; + }, + + /** + * @ngdoc method + * @name ionic.DomUtil#getChildIndex + * @description + * Get the first index of a child node within the given element of the + * specified type. + * @param {DOMElement} element The element to find the index of. + * @param {string} type The nodeName to match children of element against. + * @returns {number} The index, or -1, of a child with nodeName matching type. + */ + getChildIndex: function(element, type) { + if (type) { + var ch = element.parentNode.children; + var c; + for (var i = 0, k = 0, j = ch.length; i < j; i++) { + c = ch[i]; + if (c.nodeName && c.nodeName.toLowerCase() == type) { + if (c == element) { + return k; + } + k++; + } + } + } + return Array.prototype.slice.call(element.parentNode.children).indexOf(element); + }, + + /** + * @private + */ + swapNodes: function(src, dest) { + dest.parentNode.insertBefore(src, dest); + }, + + elementIsDescendant: function(el, parent, stopAt) { + var current = el; + do { + if (current === parent) return true; + current = current.parentNode; + } while (current && current !== stopAt); + return false; + }, + + /** + * @ngdoc method + * @name ionic.DomUtil#getParentWithClass + * @param {DOMElement} element + * @param {string} className + * @returns {DOMElement} The closest parent of element matching the + * className, or null. + */ + getParentWithClass: function(e, className, depth) { + depth = depth || 10; + while (e.parentNode && depth--) { + if (e.parentNode.classList && e.parentNode.classList.contains(className)) { + return e.parentNode; + } + e = e.parentNode; + } + return null; + }, + /** + * @ngdoc method + * @name ionic.DomUtil#getParentOrSelfWithClass + * @param {DOMElement} element + * @param {string} className + * @returns {DOMElement} The closest parent or self matching the + * className, or null. + */ + getParentOrSelfWithClass: function(e, className, depth) { + depth = depth || 10; + while (e && depth--) { + if (e.classList && e.classList.contains(className)) { + return e; + } + e = e.parentNode; + } + return null; + }, + + /** + * @ngdoc method + * @name ionic.DomUtil#rectContains + * @param {number} x + * @param {number} y + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @returns {boolean} Whether {x,y} fits within the rectangle defined by + * {x1,y1,x2,y2}. + */ + rectContains: function(x, y, x1, y1, x2, y2) { + if (x < x1 || x > x2) return false; + if (y < y1 || y > y2) return false; + return true; + }, + + /** + * @ngdoc method + * @name ionic.DomUtil#blurAll + * @description + * Blurs any currently focused input element + * @returns {DOMElement} The element blurred or null + */ + blurAll: function() { + if (document.activeElement && document.activeElement != document.body) { + document.activeElement.blur(); + return document.activeElement; + } + return null; + }, + + cachedAttr: function(ele, key, value) { + ele = ele && ele.length && ele[0] || ele; + if (ele && ele.setAttribute) { + var dataKey = '$attr-' + key; + if (arguments.length > 2) { + if (ele[dataKey] !== value) { + ele.setAttribute(key, value); + ele[dataKey] = value; + } + } else if (typeof ele[dataKey] == 'undefined') { + ele[dataKey] = ele.getAttribute(key); + } + return ele[dataKey]; + } + }, + + cachedStyles: function(ele, styles) { + ele = ele && ele.length && ele[0] || ele; + if (ele && ele.style) { + for (var prop in styles) { + if (ele['$style-' + prop] !== styles[prop]) { + ele.style[prop] = ele['$style-' + prop] = styles[prop]; + } + } + } + } + + }; + + //Shortcuts + ionic.requestAnimationFrame = ionic.DomUtil.requestAnimationFrame; + ionic.cancelAnimationFrame = ionic.DomUtil.cancelAnimationFrame; + ionic.animationFrameThrottle = ionic.DomUtil.animationFrameThrottle; + +})(window, document, ionic); + +/** + * ion-events.js + * + * Author: Max Lynch <max@drifty.com> + * + * Framework events handles various mobile browser events, and + * detects special events like tap/swipe/etc. and emits them + * as custom events that can be used in an app. + * + * Portions lovingly adapted from github.com/maker/ratchet and github.com/alexgibson/tap.js - thanks guys! + */ + +(function(ionic) { + + // Custom event polyfill + ionic.CustomEvent = (function() { + if( typeof window.CustomEvent === 'function' ) return CustomEvent; + + var customEvent = function(event, params) { + var evt; + params = params || { + bubbles: false, + cancelable: false, + detail: undefined + }; + try { + evt = document.createEvent("CustomEvent"); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + } catch (error) { + // fallback for browsers that don't support createEvent('CustomEvent') + evt = document.createEvent("Event"); + for (var param in params) { + evt[param] = params[param]; + } + evt.initEvent(event, params.bubbles, params.cancelable); + } + return evt; + }; + customEvent.prototype = window.Event.prototype; + return customEvent; + })(); + + + /** + * @ngdoc utility + * @name ionic.EventController + * @module ionic + */ + ionic.EventController = { + VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'], + + /** + * @ngdoc method + * @name ionic.EventController#trigger + * @alias ionic.trigger + * @param {string} eventType The event to trigger. + * @param {object} data The data for the event. Hint: pass in + * `{target: targetElement}` + * @param {boolean=} bubbles Whether the event should bubble up the DOM. + * @param {boolean=} cancelable Whether the event should be cancelable. + */ + // Trigger a new event + trigger: function(eventType, data, bubbles, cancelable) { + var event = new ionic.CustomEvent(eventType, { + detail: data, + bubbles: !!bubbles, + cancelable: !!cancelable + }); + + // Make sure to trigger the event on the given target, or dispatch it from + // the window if we don't have an event target + data && data.target && data.target.dispatchEvent && data.target.dispatchEvent(event) || window.dispatchEvent(event); + }, + + /** + * @ngdoc method + * @name ionic.EventController#on + * @alias ionic.on + * @description Listen to an event on an element. + * @param {string} type The event to listen for. + * @param {function} callback The listener to be called. + * @param {DOMElement} element The element to listen for the event on. + */ + on: function(type, callback, element) { + var e = element || window; + + // Bind a gesture if it's a virtual event + for(var i = 0, j = this.VIRTUALIZED_EVENTS.length; i < j; i++) { + if(type == this.VIRTUALIZED_EVENTS[i]) { + var gesture = new ionic.Gesture(element); + gesture.on(type, callback); + return gesture; + } + } + + // Otherwise bind a normal event + e.addEventListener(type, callback); + }, + + /** + * @ngdoc method + * @name ionic.EventController#off + * @alias ionic.off + * @description Remove an event listener. + * @param {string} type + * @param {function} callback + * @param {DOMElement} element + */ + off: function(type, callback, element) { + element.removeEventListener(type, callback); + }, + + /** + * @ngdoc method + * @name ionic.EventController#onGesture + * @alias ionic.onGesture + * @description Add an event listener for a gesture on an element. + * + * Available eventTypes (from [hammer.js](http://eightmedia.github.io/hammer.js/)): + * + * `hold`, `tap`, `doubletap`, `drag`, `dragstart`, `dragend`, `dragup`, `dragdown`, <br/> + * `dragleft`, `dragright`, `swipe`, `swipeup`, `swipedown`, `swipeleft`, `swiperight`, <br/> + * `transform`, `transformstart`, `transformend`, `rotate`, `pinch`, `pinchin`, `pinchout`, </br> + * `touch`, `release` + * + * @param {string} eventType The gesture event to listen for. + * @param {function(e)} callback The function to call when the gesture + * happens. + * @param {DOMElement} element The angular element to listen for the event on. + * @param {object} options object. + * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on). + */ + onGesture: function(type, callback, element, options) { + var gesture = new ionic.Gesture(element, options); + gesture.on(type, callback); + return gesture; + }, + + /** + * @ngdoc method + * @name ionic.EventController#offGesture + * @alias ionic.offGesture + * @description Remove an event listener for a gesture created on an element. + * @param {ionic.Gesture} gesture The gesture that should be removed. + * @param {string} eventType The gesture event to remove the listener for. + * @param {function(e)} callback The listener to remove. + + */ + offGesture: function(gesture, type, callback) { + gesture && gesture.off(type, callback); + }, + + handlePopState: function() {} + }; + + + // Map some convenient top-level functions for event handling + ionic.on = function() { ionic.EventController.on.apply(ionic.EventController, arguments); }; + ionic.off = function() { ionic.EventController.off.apply(ionic.EventController, arguments); }; + ionic.trigger = ionic.EventController.trigger;//function() { ionic.EventController.trigger.apply(ionic.EventController.trigger, arguments); }; + ionic.onGesture = function() { return ionic.EventController.onGesture.apply(ionic.EventController.onGesture, arguments); }; + ionic.offGesture = function() { return ionic.EventController.offGesture.apply(ionic.EventController.offGesture, arguments); }; + +})(window.ionic); + +/* eslint camelcase:0 */ +/** + * Simple gesture controllers with some common gestures that emit + * gesture events. + * + * Ported from github.com/EightMedia/hammer.js Gestures - thanks! + */ +(function(ionic) { + + /** + * ionic.Gestures + * use this to create instances + * @param {HTMLElement} element + * @param {Object} options + * @returns {ionic.Gestures.Instance} + * @constructor + */ + ionic.Gesture = function(element, options) { + return new ionic.Gestures.Instance(element, options || {}); + }; + + ionic.Gestures = {}; + + // default settings + ionic.Gestures.defaults = { + // add css to the element to prevent the browser from doing + // its native behavior. this doesnt prevent the scrolling, + // but cancels the contextmenu, tap highlighting etc + // set to false to disable this + stop_browser_behavior: 'disable-user-behavior' + }; + + // detect touchevents + ionic.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled; + ionic.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window); + + // dont use mouseevents on mobile devices + ionic.Gestures.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i; + ionic.Gestures.NO_MOUSEEVENTS = ionic.Gestures.HAS_TOUCHEVENTS && window.navigator.userAgent.match(ionic.Gestures.MOBILE_REGEX); + + // eventtypes per touchevent (start, move, end) + // are filled by ionic.Gestures.event.determineEventTypes on setup + ionic.Gestures.EVENT_TYPES = {}; + + // direction defines + ionic.Gestures.DIRECTION_DOWN = 'down'; + ionic.Gestures.DIRECTION_LEFT = 'left'; + ionic.Gestures.DIRECTION_UP = 'up'; + ionic.Gestures.DIRECTION_RIGHT = 'right'; + + // pointer type + ionic.Gestures.POINTER_MOUSE = 'mouse'; + ionic.Gestures.POINTER_TOUCH = 'touch'; + ionic.Gestures.POINTER_PEN = 'pen'; + + // touch event defines + ionic.Gestures.EVENT_START = 'start'; + ionic.Gestures.EVENT_MOVE = 'move'; + ionic.Gestures.EVENT_END = 'end'; + + // hammer document where the base events are added at + ionic.Gestures.DOCUMENT = window.document; + + // plugins namespace + ionic.Gestures.plugins = {}; + + // if the window events are set... + ionic.Gestures.READY = false; + + /** + * setup events to detect gestures on the document + */ + function setup() { + if(ionic.Gestures.READY) { + return; + } + + // find what eventtypes we add listeners to + ionic.Gestures.event.determineEventTypes(); + + // Register all gestures inside ionic.Gestures.gestures + for(var name in ionic.Gestures.gestures) { + if(ionic.Gestures.gestures.hasOwnProperty(name)) { + ionic.Gestures.detection.register(ionic.Gestures.gestures[name]); + } + } + + // Add touch events on the document + ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_MOVE, ionic.Gestures.detection.detect); + ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_END, ionic.Gestures.detection.detect); + + // ionic.Gestures is ready...! + ionic.Gestures.READY = true; + } + + /** + * create new hammer instance + * all methods should return the instance itself, so it is chainable. + * @param {HTMLElement} element + * @param {Object} [options={}] + * @returns {ionic.Gestures.Instance} + * @name Gesture.Instance + * @constructor + */ + ionic.Gestures.Instance = function(element, options) { + var self = this; + + // A null element was passed into the instance, which means + // whatever lookup was done to find this element failed to find it + // so we can't listen for events on it. + if(element === null) { + void 0; + return this; + } + + // setup ionic.GesturesJS window events and register all gestures + // this also sets up the default options + setup(); + + this.element = element; + + // start/stop detection option + this.enabled = true; + + // merge options + this.options = ionic.Gestures.utils.extend( + ionic.Gestures.utils.extend({}, ionic.Gestures.defaults), + options || {}); + + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.stop_browser_behavior) { + ionic.Gestures.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); + } + + // start detection on touchstart + ionic.Gestures.event.onTouch(element, ionic.Gestures.EVENT_START, function(ev) { + if(self.enabled) { + ionic.Gestures.detection.startDetect(self, ev); + } + }); + + // return instance + return this; + }; + + + ionic.Gestures.Instance.prototype = { + /** + * bind events to the instance + * @param {String} gesture + * @param {Function} handler + * @returns {ionic.Gestures.Instance} + */ + on: function onEvent(gesture, handler){ + var gestures = gesture.split(' '); + for(var t = 0; t < gestures.length; t++) { + this.element.addEventListener(gestures[t], handler, false); + } + return this; + }, + + + /** + * unbind events to the instance + * @param {String} gesture + * @param {Function} handler + * @returns {ionic.Gestures.Instance} + */ + off: function offEvent(gesture, handler){ + var gestures = gesture.split(' '); + for(var t = 0; t < gestures.length; t++) { + this.element.removeEventListener(gestures[t], handler, false); + } + return this; + }, + + + /** + * trigger gesture event + * @param {String} gesture + * @param {Object} eventData + * @returns {ionic.Gestures.Instance} + */ + trigger: function triggerEvent(gesture, eventData){ + // create DOM event + var event = ionic.Gestures.DOCUMENT.createEvent('Event'); + event.initEvent(gesture, true, true); + event.gesture = eventData; + + // trigger on the target if it is in the instance element, + // this is for event delegation tricks + var element = this.element; + if(ionic.Gestures.utils.hasParent(eventData.target, element)) { + element = eventData.target; + } + + element.dispatchEvent(event); + return this; + }, + + + /** + * enable of disable hammer.js detection + * @param {Boolean} state + * @returns {ionic.Gestures.Instance} + */ + enable: function enable(state) { + this.enabled = state; + return this; + } + }; + + /** + * this holds the last move event, + * used to fix empty touchend issue + * see the onTouch event for an explanation + * type {Object} + */ + var last_move_event = null; + + + /** + * when the mouse is hold down, this is true + * type {Boolean} + */ + var enable_detect = false; + + + /** + * when touch events have been fired, this is true + * type {Boolean} + */ + var touch_triggered = false; + + + ionic.Gestures.event = { + /** + * simple addEventListener + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + */ + bindDom: function(element, type, handler) { + var types = type.split(' '); + for(var t = 0; t < types.length; t++) { + element.addEventListener(types[t], handler, false); + } + }, + + + /** + * touch events with mouse fallback + * @param {HTMLElement} element + * @param {String} eventType like ionic.Gestures.EVENT_MOVE + * @param {Function} handler + */ + onTouch: function onTouch(element, eventType, handler) { + var self = this; + + this.bindDom(element, ionic.Gestures.EVENT_TYPES[eventType], function bindDomOnTouch(ev) { + var sourceEventType = ev.type.toLowerCase(); + + // onmouseup, but when touchend has been fired we do nothing. + // this is for touchdevices which also fire a mouseup on touchend + if(sourceEventType.match(/mouse/) && touch_triggered) { + return; + } + + // mousebutton must be down or a touch event + else if( sourceEventType.match(/touch/) || // touch events are always on screen + sourceEventType.match(/pointerdown/) || // pointerevents touch + (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed + ){ + enable_detect = true; + } + + // mouse isn't pressed + else if(sourceEventType.match(/mouse/) && ev.which !== 1) { + enable_detect = false; + } + + + // we are in a touch event, set the touch triggered bool to true, + // this for the conflicts that may occur on ios and android + if(sourceEventType.match(/touch|pointer/)) { + touch_triggered = true; + } + + // count the total touches on the screen + var count_touches = 0; + + // when touch has been triggered in this detection session + // and we are now handling a mouse event, we stop that to prevent conflicts + if(enable_detect) { + // update pointerevent + if(ionic.Gestures.HAS_POINTEREVENTS && eventType != ionic.Gestures.EVENT_END) { + count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev); + } + // touch + else if(sourceEventType.match(/touch/)) { + count_touches = ev.touches.length; + } + // mouse + else if(!touch_triggered) { + count_touches = sourceEventType.match(/up/) ? 0 : 1; + } + + // if we are in a end event, but when we remove one touch and + // we still have enough, set eventType to move + if(count_touches > 0 && eventType == ionic.Gestures.EVENT_END) { + eventType = ionic.Gestures.EVENT_MOVE; + } + // no touches, force the end event + else if(!count_touches) { + eventType = ionic.Gestures.EVENT_END; + } + + // store the last move event + if(count_touches || last_move_event === null) { + last_move_event = ev; + } + + // trigger the handler + handler.call(ionic.Gestures.detection, self.collectEventData(element, eventType, self.getTouchList(last_move_event, eventType), ev)); + + // remove pointerevent from list + if(ionic.Gestures.HAS_POINTEREVENTS && eventType == ionic.Gestures.EVENT_END) { + count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev); + } + } + + //debug(sourceEventType +" "+ eventType); + + // on the end we reset everything + if(!count_touches) { + last_move_event = null; + enable_detect = false; + touch_triggered = false; + ionic.Gestures.PointerEvent.reset(); + } + }); + }, + + + /** + * we have different events for each device/browser + * determine what we need and set them in the ionic.Gestures.EVENT_TYPES constant + */ + determineEventTypes: function determineEventTypes() { + // determine the eventtype we want to set + var types; + + // pointerEvents magic + if(ionic.Gestures.HAS_POINTEREVENTS) { + types = ionic.Gestures.PointerEvent.getEvents(); + } + // on Android, iOS, blackberry, windows mobile we dont want any mouseevents + else if(ionic.Gestures.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel']; + } + // for non pointer events browsers and mixed browsers, + // like chrome on windows8 touch laptop + else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup']; + } + + ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_START] = types[0]; + ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_MOVE] = types[1]; + ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_END] = types[2]; + }, + + + /** + * create touchlist depending on the event + * @param {Object} ev + * @param {String} eventType used by the fakemultitouch plugin + */ + getTouchList: function getTouchList(ev/*, eventType*/) { + // get the fake pointerEvent touchlist + if(ionic.Gestures.HAS_POINTEREVENTS) { + return ionic.Gestures.PointerEvent.getTouchList(); + } + // get the touchlist + else if(ev.touches) { + return ev.touches; + } + // make fake touchlist from mouse position + else { + ev.identifier = 1; + return [ev]; + } + }, + + + /** + * collect event data for ionic.Gestures js + * @param {HTMLElement} element + * @param {String} eventType like ionic.Gestures.EVENT_MOVE + * @param {Object} eventData + */ + collectEventData: function collectEventData(element, eventType, touches, ev) { + + // find out pointerType + var pointerType = ionic.Gestures.POINTER_TOUCH; + if(ev.type.match(/mouse/) || ionic.Gestures.PointerEvent.matchType(ionic.Gestures.POINTER_MOUSE, ev)) { + pointerType = ionic.Gestures.POINTER_MOUSE; + } + + return { + center: ionic.Gestures.utils.getCenter(touches), + timeStamp: new Date().getTime(), + target: ev.target, + touches: touches, + eventType: eventType, + pointerType: pointerType, + srcEvent: ev, + + /** + * prevent the browser default actions + * mostly used to disable scrolling of the browser + */ + preventDefault: function() { + if(this.srcEvent.preventManipulation) { + this.srcEvent.preventManipulation(); + } + + if(this.srcEvent.preventDefault) { + // this.srcEvent.preventDefault(); + } + }, + + /** + * stop bubbling the event up to its parents + */ + stopPropagation: function() { + this.srcEvent.stopPropagation(); + }, + + /** + * immediately stop gesture detection + * might be useful after a swipe was detected + * @return {*} + */ + stopDetect: function() { + return ionic.Gestures.detection.stopDetect(); + } + }; + } + }; + + ionic.Gestures.PointerEvent = { + /** + * holds all pointers + * type {Object} + */ + pointers: {}, + + /** + * get a list of pointers + * @returns {Array} touchlist + */ + getTouchList: function() { + var self = this; + var touchlist = []; + + // we can use forEach since pointerEvents only is in IE10 + Object.keys(self.pointers).sort().forEach(function(id) { + touchlist.push(self.pointers[id]); + }); + return touchlist; + }, + + /** + * update the position of a pointer + * @param {String} type ionic.Gestures.EVENT_END + * @param {Object} pointerEvent + */ + updatePointer: function(type, pointerEvent) { + if(type == ionic.Gestures.EVENT_END) { + this.pointers = {}; + } + else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } + + return Object.keys(this.pointers).length; + }, + + /** + * check if ev matches pointertype + * @param {String} pointerType ionic.Gestures.POINTER_MOUSE + * @param {PointerEvent} ev + */ + matchType: function(pointerType, ev) { + if(!ev.pointerType) { + return false; + } + + var types = {}; + types[ionic.Gestures.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == ionic.Gestures.POINTER_MOUSE); + types[ionic.Gestures.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == ionic.Gestures.POINTER_TOUCH); + types[ionic.Gestures.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == ionic.Gestures.POINTER_PEN); + return types[pointerType]; + }, + + + /** + * get events + */ + getEvents: function() { + return [ + 'pointerdown MSPointerDown', + 'pointermove MSPointerMove', + 'pointerup pointercancel MSPointerUp MSPointerCancel' + ]; + }, + + /** + * reset the list + */ + reset: function() { + this.pointers = {}; + } + }; + + + ionic.Gestures.utils = { + /** + * extend method, + * also used for cloning when dest is an empty object + * @param {Object} dest + * @param {Object} src + * @param {Boolean} merge do a merge + * @returns {Object} dest + */ + extend: function extend(dest, src, merge) { + for (var key in src) { + if(dest[key] !== undefined && merge) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, + + + /** + * find if a node is in the given parent + * used for event delegation tricks + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @returns {boolean} has_parent + */ + hasParent: function(node, parent) { + while(node){ + if(node == parent) { + return true; + } + node = node.parentNode; + } + return false; + }, + + + /** + * get the center of all the touches + * @param {Array} touches + * @returns {Object} center + */ + getCenter: function getCenter(touches) { + var valuesX = [], valuesY = []; + + for(var t = 0, len = touches.length; t < len; t++) { + valuesX.push(touches[t].pageX); + valuesY.push(touches[t].pageY); + } + + return { + pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2), + pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2) + }; + }, + + + /** + * calculate the velocity between two points + * @param {Number} delta_time + * @param {Number} delta_x + * @param {Number} delta_y + * @returns {Object} velocity + */ + getVelocity: function getVelocity(delta_time, delta_x, delta_y) { + return { + x: Math.abs(delta_x / delta_time) || 0, + y: Math.abs(delta_y / delta_time) || 0 + }; + }, + + + /** + * calculate the angle between two coordinates + * @param {Touch} touch1 + * @param {Touch} touch2 + * @returns {Number} angle + */ + getAngle: function getAngle(touch1, touch2) { + var y = touch2.pageY - touch1.pageY, + x = touch2.pageX - touch1.pageX; + return Math.atan2(y, x) * 180 / Math.PI; + }, + + + /** + * angle to direction define + * @param {Touch} touch1 + * @param {Touch} touch2 + * @returns {String} direction constant, like ionic.Gestures.DIRECTION_LEFT + */ + getDirection: function getDirection(touch1, touch2) { + var x = Math.abs(touch1.pageX - touch2.pageX), + y = Math.abs(touch1.pageY - touch2.pageY); + + if(x >= y) { + return touch1.pageX - touch2.pageX > 0 ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT; + } + else { + return touch1.pageY - touch2.pageY > 0 ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN; + } + }, + + + /** + * calculate the distance between two touches + * @param {Touch} touch1 + * @param {Touch} touch2 + * @returns {Number} distance + */ + getDistance: function getDistance(touch1, touch2) { + var x = touch2.pageX - touch1.pageX, + y = touch2.pageY - touch1.pageY; + return Math.sqrt((x * x) + (y * y)); + }, + + + /** + * calculate the scale factor between two touchLists (fingers) + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start + * @param {Array} end + * @returns {Number} scale + */ + getScale: function getScale(start, end) { + // need two fingers... + if(start.length >= 2 && end.length >= 2) { + return this.getDistance(end[0], end[1]) / + this.getDistance(start[0], start[1]); + } + return 1; + }, + + + /** + * calculate the rotation degrees between two touchLists (fingers) + * @param {Array} start + * @param {Array} end + * @returns {Number} rotation + */ + getRotation: function getRotation(start, end) { + // need two fingers + if(start.length >= 2 && end.length >= 2) { + return this.getAngle(end[1], end[0]) - + this.getAngle(start[1], start[0]); + } + return 0; + }, + + + /** + * boolean if the direction is vertical + * @param {String} direction + * @returns {Boolean} is_vertical + */ + isVertical: function isVertical(direction) { + return (direction == ionic.Gestures.DIRECTION_UP || direction == ionic.Gestures.DIRECTION_DOWN); + }, + + + /** + * stop browser default behavior with css class + * @param {HtmlElement} element + * @param {Object} css_class + */ + stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_class) { + // changed from making many style changes to just adding a preset classname + // less DOM manipulations, less code, and easier to control in the CSS side of things + // hammer.js doesn't come with CSS, but ionic does, which is why we prefer this method + if(element && element.classList) { + element.classList.add(css_class); + element.onselectstart = function() { + return false; + }; + } + } + }; + + + ionic.Gestures.detection = { + // contains all registred ionic.Gestures.gestures in the correct order + gestures: [], + + // data of the current ionic.Gestures.gesture detection session + current: null, + + // the previous ionic.Gestures.gesture session data + // is a full clone of the previous gesture.current object + previous: null, + + // when this becomes true, no gestures are fired + stopped: false, + + + /** + * start ionic.Gestures.gesture detection + * @param {ionic.Gestures.Instance} inst + * @param {Object} eventData + */ + startDetect: function startDetect(inst, eventData) { + // already busy with a ionic.Gestures.gesture detection on an element + if(this.current) { + return; + } + + this.stopped = false; + + this.current = { + inst: inst, // reference to ionic.GesturesInstance we're working for + startEvent: ionic.Gestures.utils.extend({}, eventData), // start eventData for distances, timing etc + lastEvent: false, // last eventData + name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc + }; + + this.detect(eventData); + }, + + + /** + * ionic.Gestures.gesture detection + * @param {Object} eventData + */ + detect: function detect(eventData) { + if(!this.current || this.stopped) { + return null; + } + + // extend event data with calculations about scale, distance etc + eventData = this.extendEventData(eventData); + + // instance options + var inst_options = this.current.inst.options; + + // call ionic.Gestures.gesture handlers + for(var g = 0, len = this.gestures.length; g < len; g++) { + var gesture = this.gestures[g]; + + // only when the instance options have enabled this gesture + if(!this.stopped && inst_options[gesture.name] !== false) { + // if a handler returns false, we stop with the detection + if(gesture.handler.call(gesture, eventData, this.current.inst) === false) { + this.stopDetect(); + break; + } + } + } + + // store as previous event event + if(this.current) { + this.current.lastEvent = eventData; + } + + // endevent, but not the last touch, so dont stop + if(eventData.eventType == ionic.Gestures.EVENT_END && !eventData.touches.length - 1) { + this.stopDetect(); + } + + return eventData; + }, + + + /** + * clear the ionic.Gestures.gesture vars + * this is called on endDetect, but can also be used when a final ionic.Gestures.gesture has been detected + * to stop other ionic.Gestures.gestures from being fired + */ + stopDetect: function stopDetect() { + // clone current data to the store as the previous gesture + // used for the double tap gesture, since this is an other gesture detect session + this.previous = ionic.Gestures.utils.extend({}, this.current); + + // reset the current + this.current = null; + + // stopped! + this.stopped = true; + }, + + + /** + * extend eventData for ionic.Gestures.gestures + * @param {Object} ev + * @returns {Object} ev + */ + extendEventData: function extendEventData(ev) { + var startEv = this.current.startEvent; + + // if the touches change, set the new touches over the startEvent touches + // this because touchevents don't have all the touches on touchstart, or the + // user must place his fingers at the EXACT same time on the screen, which is not realistic + // but, sometimes it happens that both fingers are touching at the EXACT same time + if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) { + // extend 1 level deep to get the touchlist with the touch objects + startEv.touches = []; + for(var i = 0, len = ev.touches.length; i < len; i++) { + startEv.touches.push(ionic.Gestures.utils.extend({}, ev.touches[i])); + } + } + + var delta_time = ev.timeStamp - startEv.timeStamp, + delta_x = ev.center.pageX - startEv.center.pageX, + delta_y = ev.center.pageY - startEv.center.pageY, + velocity = ionic.Gestures.utils.getVelocity(delta_time, delta_x, delta_y); + + ionic.Gestures.utils.extend(ev, { + deltaTime: delta_time, + deltaX: delta_x, + deltaY: delta_y, + + velocityX: velocity.x, + velocityY: velocity.y, + + distance: ionic.Gestures.utils.getDistance(startEv.center, ev.center), + angle: ionic.Gestures.utils.getAngle(startEv.center, ev.center), + direction: ionic.Gestures.utils.getDirection(startEv.center, ev.center), + + scale: ionic.Gestures.utils.getScale(startEv.touches, ev.touches), + rotation: ionic.Gestures.utils.getRotation(startEv.touches, ev.touches), + + startEvent: startEv + }); + + return ev; + }, + + + /** + * register new gesture + * @param {Object} gesture object, see gestures.js for documentation + * @returns {Array} gestures + */ + register: function register(gesture) { + // add an enable gesture options if there is no given + var options = gesture.defaults || {}; + if(options[gesture.name] === undefined) { + options[gesture.name] = true; + } + + // extend ionic.Gestures default options with the ionic.Gestures.gesture options + ionic.Gestures.utils.extend(ionic.Gestures.defaults, options, true); + + // set its index + gesture.index = gesture.index || 1000; + + // add ionic.Gestures.gesture to the list + this.gestures.push(gesture); + + // sort the list by index + this.gestures.sort(function(a, b) { + if (a.index < b.index) { + return -1; + } + if (a.index > b.index) { + return 1; + } + return 0; + }); + + return this.gestures; + } + }; + + + ionic.Gestures.gestures = ionic.Gestures.gestures || {}; + + /** + * Custom gestures + * ============================== + * + * Gesture object + * -------------------- + * The object structure of a gesture: + * + * { name: 'mygesture', + * index: 1337, + * defaults: { + * mygesture_option: true + * } + * handler: function(type, ev, inst) { + * // trigger gesture event + * inst.trigger(this.name, ev); + * } + * } + + * @param {String} name + * this should be the name of the gesture, lowercase + * it is also being used to disable/enable the gesture per instance config. + * + * @param {Number} [index=1000] + * the index of the gesture, where it is going to be in the stack of gestures detection + * like when you build an gesture that depends on the drag gesture, it is a good + * idea to place it after the index of the drag gesture. + * + * @param {Object} [defaults={}] + * the default settings of the gesture. these are added to the instance settings, + * and can be overruled per instance. you can also add the name of the gesture, + * but this is also added by default (and set to true). + * + * @param {Function} handler + * this handles the gesture detection of your custom gesture and receives the + * following arguments: + * + * @param {Object} eventData + * event data containing the following properties: + * timeStamp {Number} time the event occurred + * target {HTMLElement} target element + * touches {Array} touches (fingers, pointers, mouse) on the screen + * pointerType {String} kind of pointer that was used. matches ionic.Gestures.POINTER_MOUSE|TOUCH + * center {Object} center position of the touches. contains pageX and pageY + * deltaTime {Number} the total time of the touches in the screen + * deltaX {Number} the delta on x axis we haved moved + * deltaY {Number} the delta on y axis we haved moved + * velocityX {Number} the velocity on the x + * velocityY {Number} the velocity on y + * angle {Number} the angle we are moving + * direction {String} the direction we are moving. matches ionic.Gestures.DIRECTION_UP|DOWN|LEFT|RIGHT + * distance {Number} the distance we haved moved + * scale {Number} scaling of the touches, needs 2 touches + * rotation {Number} rotation of the touches, needs 2 touches * + * eventType {String} matches ionic.Gestures.EVENT_START|MOVE|END + * srcEvent {Object} the source event, like TouchStart or MouseDown * + * startEvent {Object} contains the same properties as above, + * but from the first touch. this is used to calculate + * distances, deltaTime, scaling etc + * + * @param {ionic.Gestures.Instance} inst + * the instance we are doing the detection for. you can get the options from + * the inst.options object and trigger the gesture event by calling inst.trigger + * + * + * Handle gestures + * -------------------- + * inside the handler you can get/set ionic.Gestures.detectionic.current. This is the current + * detection sessionic. It has the following properties + * @param {String} name + * contains the name of the gesture we have detected. it has not a real function, + * only to check in other gestures if something is detected. + * like in the drag gesture we set it to 'drag' and in the swipe gesture we can + * check if the current gesture is 'drag' by accessing ionic.Gestures.detectionic.current.name + * + * readonly + * @param {ionic.Gestures.Instance} inst + * the instance we do the detection for + * + * readonly + * @param {Object} startEvent + * contains the properties of the first gesture detection in this sessionic. + * Used for calculations about timing, distance, etc. + * + * readonly + * @param {Object} lastEvent + * contains all the properties of the last gesture detect in this sessionic. + * + * after the gesture detection session has been completed (user has released the screen) + * the ionic.Gestures.detectionic.current object is copied into ionic.Gestures.detectionic.previous, + * this is usefull for gestures like doubletap, where you need to know if the + * previous gesture was a tap + * + * options that have been set by the instance can be received by calling inst.options + * + * You can trigger a gesture event by calling inst.trigger("mygesture", event). + * The first param is the name of your gesture, the second the event argument + * + * + * Register gestures + * -------------------- + * When an gesture is added to the ionic.Gestures.gestures object, it is auto registered + * at the setup of the first ionic.Gestures instance. You can also call ionic.Gestures.detectionic.register + * manually and pass your gesture object as a param + * + */ + + /** + * Hold + * Touch stays at the same place for x time + * events hold + */ + ionic.Gestures.gestures.Hold = { + name: 'hold', + index: 10, + defaults: { + hold_timeout: 500, + hold_threshold: 1 + }, + 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') { + if (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 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, + 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 (window.forge) { + 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); + }, + /** + * @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#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('Android') > 0) { + platformName = ANDROID; + } else if (/iPhone|iPad|iPod/.test(self.ua)) { + platformName = IOS; + } else if (self.ua.indexOf('Windows Phone') > -1) { + platformName = WINDOWS_PHONE; + } 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]); + } + } + }, + + // Check if the platform is the one detected by cordova + is: function(type) { + type = type.toLowerCase(); + // check if it has an array of platforms + if (self.platforms) { + for (var x = 0; x < self.platforms.length; x++) { + if (self.platforms[x] === type) return true; + } + } + // exact match + var pName = self.platform(); + if (pName) { + return pName === type.toLowerCase(); + } + + // A quick hack for to check userAgent + return self.ua.toLowerCase().indexOf(type) >= 0; + }, + + /** + * @ngdoc method + * @name ionic.Platform#exitApp + * @description Exit the app. + */ + exitApp: function() { + self.ready(function() { + navigator.app && navigator.app.exitApp && navigator.app.exitApp(); + }); + }, + + /** + * @ngdoc method + * @name ionic.Platform#showStatusBar + * @description Shows or hides the device status bar (in Cordova). Requires `cordova plugin add org.apache.cordova.statusbar` + * @param {boolean} shouldShow Whether or not to show the status bar. + */ + showStatusBar: function(val) { + // Only useful when run within cordova + self._showStatusBar = val; + self.ready(function() { + // run this only when or if the platform (cordova) is ready + requestAnimationFrame(function() { + if (self._showStatusBar) { + // they do not want it to be full screen + window.StatusBar && window.StatusBar.show(); + document.body.classList.remove('status-bar-hide'); + } else { + // it should be full screen + window.StatusBar && window.StatusBar.hide(); + document.body.classList.add('status-bar-hide'); + } + }); + }); + }, + + /** + * @ngdoc method + * @name ionic.Platform#fullScreen + * @description + * Sets whether the app is fullscreen or not (in Cordova). + * @param {boolean=} showFullScreen Whether or not to set the app to fullscreen. Defaults to true. Requires `cordova plugin add org.apache.cordova.statusbar` + * @param {boolean=} showStatusBar Whether or not to show the device's status bar. Defaults to false. + */ + fullScreen: function(showFullScreen, showStatusBar) { + // showFullScreen: default is true if no param provided + self.isFullScreen = (showFullScreen !== false); + + // add/remove the fullscreen classname to the body + ionic.DomUtil.ready(function() { + // run this only when or if the DOM is ready + requestAnimationFrame(function() { + if (self.isFullScreen) { + document.body.classList.add('fullscreen'); + } else { + document.body.classList.remove('fullscreen'); + } + }); + // showStatusBar: default is false if no param provided + self.showStatusBar((showStatusBar === true)); + }); + } + + }; + + var platformName = null, // just the name, like iOS or Android + platformVersion = null, // a float of the major and minor, like 7.1 + readyCallbacks = [], + windowLoadListenderAttached; + + // 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'); + }); + } + +})(this, document, ionic); + +(function(document, ionic) { + 'use strict'; + + // Ionic CSS polyfills + ionic.CSS = {}; + + (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; + } + } + + // 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'; + })(); + + // classList polyfill for them older Androids + // https://gist.github.com/devongovett/1381839 + if (!("classList" in document.documentElement) && Object.defineProperty && typeof HTMLElement !== 'undefined') { + Object.defineProperty(HTMLElement.prototype, 'classList', { + get: function() { + var self = this; + function update(fn) { + return function() { + var x, classes = self.className.split(/\s+/); + + for (x = 0; x < arguments.length; x++) { + fn(classes, classes.indexOf(arguments[x]), arguments[x]); + } + + self.className = classes.join(" "); + }; + } + + return { + add: update(function(classes, index, value) { + ~index || classes.push(value); + }), + + remove: update(function(classes, index) { + ~index && classes.splice(index, 1); + }), + + toggle: update(function(classes, index, value) { + ~index ? classes.splice(index, 1) : classes.push(value); + }), + + contains: function(value) { + return !!~self.className.split(/\s+/).indexOf(value); + }, + + item: function(i) { + return self.className.split(/\s+/)[i] || null; + } + }; + + } + }); + } + +})(document, ionic); + + +/** + * @ngdoc page + * @name tap + * @module ionic + * @description + * On touch devices such as a phone or tablet, some browsers implement a 300ms delay between + * the time the user stops touching the display and the moment the browser executes the + * click. This delay was initially introduced so the browser can know whether the user wants to + * double-tap to zoom in on the webpage. Basically, the browser waits roughly 300ms to see if + * the user is double-tapping, or just tapping on the display once. + * + * Out of the box, Ionic automatically removes the 300ms delay in order to make Ionic apps + * feel more "native" like. Resultingly, other solutions such as + * [fastclick](https://github.com/ftlabs/fastclick) and Angular's + * [ngTouch](https://docs.angularjs.org/api/ngTouch) should not be included, to avoid conflicts. + * + * Some browsers already remove the delay with certain settings, such as the CSS property + * `touch-events: none` or with specific meta tag viewport values. However, each of these + * browsers still handle clicks differently, such as when to fire off or cancel the event + * (like scrolling when the target is a button, or holding a button down). + * For browsers that already remove the 300ms delay, consider Ionic's tap system as a way to + * normalize how clicks are handled across the various devices so there's an expected response + * no matter what the device, platform or version. Additionally, Ionic will prevent + * ghostclicks which even browsers that remove the delay still experience. + * + * In some cases, third-party libraries may also be working with touch events which can interfere + * with the tap system. For example, mapping libraries like Google or Leaflet Maps often implement + * a touch detection system which conflicts with Ionic's tap system. + * + * ### Disabling the tap system + * + * To disable the tap for an element and all of its children elements, + * add the attribute `data-tap-disabled="true"`. + * + * ```html + * <div data-tap-disabled="true"> + * <div id="google-map"></div> + * </div> + * ``` + * + * ### Additional Notes: + * + * - Ionic tap works with Ionic's JavaScript scrolling + * - Elements can come and go from the DOM and Ionic tap doesn't keep adding and removing + * listeners + * - No "tap delay" after the first "tap" (you can tap as fast as you want, they all click) + * - Minimal events listeners, only being added to document + * - Correct focus in/out on each input type (select, textearea, range) on each platform/device + * - Shows and hides virtual keyboard correctly for each platform/device + * - Works with labels surrounding inputs + * - Does not fire off a click if the user moves the pointer too far + * - Adds and removes an 'activated' css class + * - Multiple [unit tests](https://github.com/driftyco/ionic/blob/master/test/unit/utils/tap.unit.js) for each scenario + * + */ +/* + + IONIC TAP + --------------- + - Both touch and mouse events are added to the document.body on DOM ready + - If a touch event happens, it does not use mouse event listeners + - On touchend, if the distance between start and end was small, trigger a click + - In the triggered click event, add a 'isIonicTap' property + - The triggered click receives the same x,y coordinates as as the end event + - On document.body click listener (with useCapture=true), only allow clicks with 'isIonicTap' + - Triggering clicks with mouse events work the same as touch, except with mousedown/mouseup + - Tapping inputs is disabled during scrolling +*/ + +var tapDoc; // the element which the listeners are on (document.body) +var tapActiveEle; // the element which is active (probably has focus) +var tapEnabledTouchEvents; +var tapMouseResetTimer; +var tapPointerMoved; +var tapPointerStart; +var tapTouchFocusedInput; +var tapLastTouchTarget; +var tapTouchMoveListener = 'touchmove'; + +// how much the coordinates can be off between start/end, but still a click +var TAP_RELEASE_TOLERANCE = 12; // default tolerance +var TAP_RELEASE_BUTTON_TOLERANCE = 50; // button elements should have a larger tolerance + +var tapEventListeners = { + 'click': tapClickGateKeeper, + + 'mousedown': tapMouseDown, + 'mouseup': tapMouseUp, + 'mousemove': tapMouseMove, + + 'touchstart': tapTouchStart, + 'touchend': tapTouchEnd, + 'touchcancel': tapTouchCancel, + 'touchmove': tapTouchMove, + + 'pointerdown': tapTouchStart, + 'pointerup': tapTouchEnd, + 'pointercancel': tapTouchCancel, + 'pointermove': tapTouchMove, + + 'MSPointerDown': tapTouchStart, + 'MSPointerUp': tapTouchEnd, + 'MSPointerCancel': tapTouchCancel, + 'MSPointerMove': tapTouchMove, + + 'focusin': tapFocusIn, + 'focusout': tapFocusOut +}; + +ionic.tap = { + + register: function(ele) { + tapDoc = ele; + + tapEventListener('click', true, true); + tapEventListener('mouseup'); + tapEventListener('mousedown'); + + if (window.navigator.pointerEnabled) { + tapEventListener('pointerdown'); + tapEventListener('pointerup'); + tapEventListener('pointcancel'); + 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)); + }, + + 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)) { + 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 (!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.dataset ? element.dataset.tapDisabled : 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) { + void 0; + e.stopPropagation(); + + if ((!ionic.tap.isTextInput(e.target) || tapLastTouchTarget !== e.target) && !(/^(select|option)$/i).test(e.target.tagName)) { + // 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. + 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) || (/^(select|option)$/i).test(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 + e.preventDefault(); + } + } +} + +function tapTouchEnd(e) { + //console.log('touchend ' + Date.now()); + if (tapIgnoreEvent(e)) return; + + tapEnableTouchEvents(); + if (!tapHasPointerMoved(e)) { + tapClick(e); + + if ((/^(select|option)$/i).test(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.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)$/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)) { + void 0; + 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 + void 0; + 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; +} + +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.contains('button')) { + eleToActivate = ele; + break; + } + // no sense climbing past these + if (ele.tagName == 'ION-CONTENT' || (ele.classList && ele.classList.contains('pane')) || ele.tagName == 'BODY') { + break; + } + ele = ele.parentElement; + } + + if (eleToActivate) { + // queue that this element should be set to active + queueElements[keyId] = eleToActivate; + + // on the next frame, set the queued elements to active + ionic.requestAnimationFrame(activateElements); + + keyId = (keyId > 29 ? 0 : keyId + 1); + } + + }); + }, + + end: function() { + // clear out any active/queued elements after XX milliseconds + setTimeout(clear, 200); + } + + }; + + function clear() { + // clear out any elements that are queued to be set to active + queueElements = {}; + + // in the next frame, remove the active class from all active elements + ionic.requestAnimationFrame(deactivateElements); + } + + function activateElements() { + // activate all elements in the queue + for (var key in queueElements) { + if (queueElements[key]) { + queueElements[key].classList.add(ACTIVATED_CLASS); + activeElements[key] = queueElements[key]; + } + } + queueElements = {}; + } + + function deactivateElements() { + if (ionic.transition && ionic.transition.isActive) { + setTimeout(deactivateElements, 400); + return; + } + + for (var key in activeElements) { + if (activeElements[key]) { + activeElements[key].classList.remove(ACTIVATED_CLASS); + delete activeElements[key]; + } + } + } + +})(document, ionic); + +(function(ionic) { + /* for nextUid function below */ + var nextId = 0; + + /** + * Various utilities used throughout Ionic + * + * Some of these are adopted from underscore.js and backbone.js, both also MIT licensed. + */ + ionic.Utils = { + + arrayMove: function(arr, oldIndex, newIndex) { + if (newIndex >= arr.length) { + var k = newIndex - arr.length; + while ((k--) + 1) { + arr.push(undefined); + } + } + arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0]); + return arr; + }, + + /** + * Return a function that will be called with the given context + */ + proxy: function(func, context) { + var args = Array.prototype.slice.call(arguments, 2); + return function() { + return func.apply(context, args.concat(Array.prototype.slice.call(arguments))); + }; + }, + + /** + * Only call a function once in the given interval. + * + * @param func {Function} the function to call + * @param wait {int} how long to wait before/after to allow function calls + * @param immediate {boolean} whether to call immediately or after the wait interval + */ + debounce: function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + return function() { + context = this; + args = arguments; + timestamp = new Date(); + var later = function() { + var last = (new Date()) - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) result = func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) result = func.apply(context, args); + return result; + }; + }, + + /** + * Throttle the given fun, only allowing it to be + * called at most every `wait` ms. + */ + throttle: function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + options || (options = {}); + var later = function() { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = Date.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + // Borrowed from Backbone.js's extend + // Helper function to correctly set up the prototype chain, for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // class properties to be extended. + inherit: function(protoProps, staticProps) { + var parent = this; + var child; + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent's constructor. + if (protoProps && protoProps.hasOwnProperty('constructor')) { + child = protoProps.constructor; + } else { + child = function() { return parent.apply(this, arguments); }; + } + + // Add static properties to the constructor function, if supplied. + ionic.extend(child, parent, staticProps); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function. + var Surrogate = function() { this.constructor = child; }; + Surrogate.prototype = parent.prototype; + child.prototype = new Surrogate(); + + // Add prototype properties (instance properties) to the subclass, + // if supplied. + if (protoProps) ionic.extend(child.prototype, protoProps); + + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + + return child; + }, + + // Extend adapted from Underscore.js + extend: function(obj) { + var args = Array.prototype.slice.call(arguments, 1); + for (var i = 0; i < args.length; i++) { + var source = args[i]; + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + } + return obj; + }, + + nextUid: function() { + return 'ion' + (nextId++); + }, + + disconnectScope: function disconnectScope(scope) { + if (!scope) return; + + if (scope.$root === scope) { + return; // we can't disconnect the root node; + } + var parent = scope.$parent; + scope.$$disconnected = true; + scope.$broadcast('$ionic.disconnectScope', scope); + + // See Scope.$destroy + if (parent.$$childHead === scope) { + parent.$$childHead = scope.$$nextSibling; + } + if (parent.$$childTail === scope) { + parent.$$childTail = scope.$$prevSibling; + } + if (scope.$$prevSibling) { + scope.$$prevSibling.$$nextSibling = scope.$$nextSibling; + } + if (scope.$$nextSibling) { + scope.$$nextSibling.$$prevSibling = scope.$$prevSibling; + } + scope.$$nextSibling = scope.$$prevSibling = null; + }, + + reconnectScope: function reconnectScope(scope) { + if (!scope) return; + + if (scope.$root === scope) { + return; // we can't disconnect the root node; + } + if (!scope.$$disconnected) { + return; + } + var parent = scope.$parent; + scope.$$disconnected = false; + scope.$broadcast('$ionic.reconnectScope', scope); + // See Scope.$new for this logic... + scope.$$prevSibling = parent.$$childTail; + if (parent.$$childHead) { + parent.$$childTail.$$nextSibling = scope; + parent.$$childTail = scope; + } else { + parent.$$childHead = parent.$$childTail = scope; + } + }, + + isScopeDisconnected: function(scope) { + var climbScope = scope; + while (climbScope) { + if (climbScope.$$disconnected) return true; + climbScope = climbScope.$parent; + } + return false; + } + }; + + // Bind a few of the most useful functions to the ionic scope + ionic.inherit = ionic.Utils.inherit; + ionic.extend = ionic.Utils.extend; + ionic.throttle = ionic.Utils.throttle; + ionic.proxy = ionic.Utils.proxy; + ionic.debounce = ionic.Utils.debounce; + +})(window.ionic); + +/** + * @ngdoc page + * @name keyboard + * @module ionic + * @description + * On both Android and iOS, Ionic will attempt to prevent the keyboard from + * obscuring inputs and focusable elements when it appears by scrolling them + * into view. In order for this to work, any focusable elements must be within + * a [Scroll View](http://ionicframework.com/docs/api/directive/ionScroll/) + * or a directive such as [Content](http://ionicframework.com/docs/api/directive/ionContent/) + * that has a Scroll View. + * + * It will also attempt to prevent the native overflow scrolling on focus, + * which can cause layout issues such as pushing headers up and out of view. + * + * The keyboard fixes work best in conjunction with the + * [Ionic Keyboard Plugin](https://github.com/driftyco/ionic-plugins-keyboard), + * although it will perform reasonably well without. However, if you are using + * Cordova there is no reason not to use the plugin. + * + * ### Hide when keyboard shows + * + * To hide an element when the keyboard is open, add the class `hide-on-keyboard-open`. + * + * ```html + * <div class="hide-on-keyboard-open"> + * <div id="google-map"></div> + * </div> + * ``` + * ---------- + * + * ### Plugin Usage + * Information on using the plugin can be found at + * [https://github.com/driftyco/ionic-plugins-keyboard](https://github.com/driftyco/ionic-plugins-keyboard). + * + * ---------- + * + * ### Android Notes + * - If your app is running in fullscreen, i.e. you have + * `<preference name="Fullscreen" value="true" />` in your `config.xml` file + * you will need to set `ionic.Platform.isFullScreen = true` manually. + * + * - You can configure the behavior of the web view when the keyboard shows by setting + * [android:windowSoftInputMode](http://developer.android.com/reference/android/R.attr.html#windowSoftInputMode) + * to either `adjustPan`, `adjustResize` or `adjustNothing` in your app's + * activity in `AndroidManifest.xml`. `adjustResize` is the recommended setting + * for Ionic, but if for some reason you do use `adjustPan` you will need to + * set `ionic.Platform.isFullScreen = true`. + * + * ```xml + * <activity android:windowSoftInputMode="adjustResize"> + * + * ``` + * + * ### iOS Notes + * - If the content of your app (including the header) is being pushed up and + * out of view on input focus, try setting `cordova.plugins.Keyboard.disableScroll(true)`. + * This does **not** disable scrolling in the Ionic scroll view, rather it + * disables the native overflow scrolling that happens automatically as a + * result of focusing on inputs below the keyboard. + * + */ + +/** + * The current viewport height. + */ +var keyboardCurrentViewportHeight = 0; + +/** + * The viewport height when in portrait orientation. + */ +var keyboardPortraitViewportHeight = 0; + +/** + * The viewport height when in landscape orientation. + */ +var keyboardLandscapeViewportHeight = 0; + +/** + * The currently focused input. + */ +var keyboardActiveElement; + +/** + * The 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'; + +/** + * 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, + + /** + * 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(); + } + } +}; + +// 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. + */ +function keyboardInit() { + var debouncedKeyboardFocusIn = ionic.debounce(keyboardFocusIn, 200, true); + + if (keyboardHasPlugin()) { + window.addEventListener('native.keyboardshow', ionic.debounce(keyboardNativeShow, 100, true)); + 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); + } +} + +/** + * 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 = inputScrollView(e.target))) { + return; + } + + keyboardActiveElement = e.target; + + // if using JS scrolling, undo the effects of native overflow scroll so the + // scroll view is positioned correctly + document.body.scrollTop = 0; + scrollView.scrollTop = 0; + ionic.requestAnimationFrame(function(){ + document.body.scrollTop = 0; + scrollView.scrollTop = 0; + }); + + 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); + + // 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 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; + + ionic.trigger('resetScrollView', { + target: keyboardActiveElement + }, 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(); + } +} + +/** + * 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() { + var elementBounds = keyboardActiveElement.getBoundingClientRect(); + var details = { + target: keyboardActiveElement, + elementTop: Math.round(elementBounds.top), + elementBottom: Math.round(elementBounds.bottom), + keyboardHeight: keyboardGetHeight(), + viewportHeight: keyboardCurrentViewportHeight + }; + + 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); + + ionic.keyboard.isOpen = true; + ionic.keyboard.isOpening = false; + + // send event so the scroll view adjusts + ionic.trigger('scrollChildIntoView', details, true); + + setTimeout(function(){ + document.body.classList.add(KEYBOARD_OPEN_CSS); + }, 400); + + return details; +} + +/* 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; + } + + 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 inputScrollView(ele) { + while(ele) { + if (ele.classList.contains(SCROLL_CONTAINER_CSS)) { + return ele; + } + ele = ele.parentElement; + } + return null; +} + +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; + } + }; +})(this); + +/* + * 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; + }; + + 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); + + } 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.touchCancel); + + container.removeEventListener("pointerdown", self.touchStart); + container.removeEventListener("pointermove", self.touchMoveBubble); + document.removeEventListener("pointermove", self.touchMove); + document.removeEventListener("pointerup", self.touchEnd); + document.removeEventListener("pointercancel", self.touchEnd); + + container.removeEventListener("MSPointerDown", self.touchStart); + container.removeEventListener("MSPointerMove", self.touchMoveBubble); + document.removeEventListener("MSPointerMove", self.touchMove); + document.removeEventListener("MSPointerUp", self.touchEnd); + document.removeEventListener("MSPointerCancel", self.touchEnd); + + container.removeEventListener("mousedown", self.mouseDown); + container.removeEventListener("mousemove", self.mouseMoveBubble); + document.removeEventListener("mousemove", self.mouseMove); + document.removeEventListener("mouseup", self.mouseUp); + document.removeEventListener('mousewheel', self.mouseWheel); + document.removeEventListener('wheel', self.mouseWheel); + + container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView); + document.removeEventListener('resetScrollView', self.resetScrollView); + + ionic.tap.removeClonedInputs(container, self); + + delete self.__container; + delete self.__content; + delete self.__indicatorX; + delete self.__indicatorY; + delete self.options.el; + + self.__callback = self.scrollChildIntoView = self.resetScrollView = NOOP; + + self.mouseMove = self.mouseDown = self.mouseUp = self.mouseWheel = + self.touchStart = self.touchMove = self.touchEnd = self.touchCancel = NOOP; + + self.resize = self.scrollTo = self.zoomTo = + self.__scrollingComplete = NOOP; + container = null; + }, + + /** Create a scroll bar div with the given direction **/ + __createScrollbar: function(direction) { + var bar = document.createElement('div'), + indicator = document.createElement('div'); + + indicator.className = 'scroll-bar-indicator scroll-bar-fade-out'; + + if (direction == 'h') { + bar.className = 'scroll-bar scroll-bar-h'; + } else { + bar.className = 'scroll-bar scroll-bar-v'; + } + + bar.appendChild(indicator); + return bar; + }, + + __createScrollbars: function() { + var self = this; + var indicatorX, indicatorY; + + if (self.options.scrollingX) { + indicatorX = { + el: self.__createScrollbar('h'), + sizeRatio: 1 + }; + indicatorX.indicator = indicatorX.el.children[0]; + + if (self.options.scrollbarX) { + self.__container.appendChild(indicatorX.el); + } + self.__indicatorX = indicatorX; + } + + if (self.options.scrollingY) { + indicatorY = { + el: self.__createScrollbar('v'), + sizeRatio: 1 + }; + indicatorY.indicator = indicatorY.el.children[0]; + + if (self.options.scrollbarY) { + self.__container.appendChild(indicatorY.el); + } + self.__indicatorY = indicatorY; + } + }, + + __resizeScrollbars: function() { + var self = this; + + // Update horiz bar + if (self.__indicatorX) { + var width = Math.max(Math.round(self.__clientWidth * self.__clientWidth / (self.__contentWidth)), 20); + if (width > self.__contentWidth) { + width = 0; + } + if (width !== self.__indicatorX.size) { + ionic.requestAnimationFrame(function(){ + self.__indicatorX.indicator.style.width = width + 'px'; + }); + } + self.__indicatorX.size = width; + self.__indicatorX.minScale = self.options.minScrollbarSizeX / width; + self.__indicatorX.maxPos = self.__clientWidth - width; + self.__indicatorX.sizeRatio = self.__maxScrollLeft ? self.__indicatorX.maxPos / self.__maxScrollLeft : 1; + } + + // Update vert bar + if (self.__indicatorY) { + var height = Math.max(Math.round(self.__clientHeight * self.__clientHeight / (self.__contentHeight)), 20); + if (height > self.__contentHeight) { + height = 0; + } + if (height !== self.__indicatorY.size) { + ionic.requestAnimationFrame(function(){ + self.__indicatorY && (self.__indicatorY.indicator.style.height = height + 'px'); + }); + } + self.__indicatorY.size = height; + self.__indicatorY.minScale = self.options.minScrollbarSizeY / height; + self.__indicatorY.maxPos = self.__clientHeight - height; + self.__indicatorY.sizeRatio = self.__maxScrollTop ? self.__indicatorY.maxPos / self.__maxScrollTop : 1; + } + }, + + /** + * Move and scale the scrollbars as the page scrolls. + */ + __repositionScrollbars: function() { + var self = this, + heightScale, widthScale, + widthDiff, heightDiff, + x, y, + xstop = 0, ystop = 0; + + if (self.__indicatorX) { + // Handle the X scrollbar + + // Don't go all the way to the right if we have a vertical scrollbar as well + if (self.__indicatorY) xstop = 10; + + x = Math.round(self.__indicatorX.sizeRatio * self.__scrollLeft) || 0; + + // The the difference between the last content X position, and our overscrolled one + widthDiff = self.__scrollLeft - (self.__maxScrollLeft - xstop); + + if (self.__scrollLeft < 0) { + + widthScale = Math.max(self.__indicatorX.minScale, + (self.__indicatorX.size - Math.abs(self.__scrollLeft)) / self.__indicatorX.size); + + // Stay at left + x = 0; + + // Make sure scale is transformed from the left/center origin point + self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'left center'; + } else if (widthDiff > 0) { + + widthScale = Math.max(self.__indicatorX.minScale, + (self.__indicatorX.size - widthDiff) / self.__indicatorX.size); + + // Stay at the furthest x for the scrollable viewport + x = self.__indicatorX.maxPos - xstop; + + // Make sure scale is transformed from the right/center origin point + self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'right center'; + + } else { + + // Normal motion + x = Math.min(self.__maxScrollLeft, Math.max(0, x)); + widthScale = 1; + + } + + var translate3dX = 'translate3d(' + x + 'px, 0, 0) scaleX(' + widthScale + ')'; + if (self.__indicatorX.transformProp !== translate3dX) { + self.__indicatorX.indicator.style[self.__transformProperty] = translate3dX; + self.__indicatorX.transformProp = translate3dX; + } + } + + if (self.__indicatorY) { + + y = Math.round(self.__indicatorY.sizeRatio * self.__scrollTop) || 0; + + // Don't go all the way to the right if we have a vertical scrollbar as well + if (self.__indicatorX) ystop = 10; + + heightDiff = self.__scrollTop - (self.__maxScrollTop - ystop); + + if (self.__scrollTop < 0) { + + heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - Math.abs(self.__scrollTop)) / self.__indicatorY.size); + + // Stay at top + y = 0; + + // Make sure scale is transformed from the center/top origin point + if (self.__indicatorY.originProp !== 'center top') { + self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center top'; + self.__indicatorY.originProp = 'center top'; + } + + } else if (heightDiff > 0) { + + heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - heightDiff) / self.__indicatorY.size); + + // Stay at bottom of scrollable viewport + y = self.__indicatorY.maxPos - ystop; + + // Make sure scale is transformed from the center/bottom origin point + if (self.__indicatorY.originProp !== 'center bottom') { + self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center bottom'; + self.__indicatorY.originProp = 'center bottom'; + } + + } else { + + // Normal motion + y = Math.min(self.__maxScrollTop, Math.max(0, y)); + heightScale = 1; + + } + + var translate3dY = 'translate3d(0,' + y + 'px, 0) scaleY(' + heightScale + ')'; + if (self.__indicatorY.transformProp !== translate3dY) { + self.__indicatorY.indicator.style[self.__transformProperty] = translate3dY; + self.__indicatorY.transformProp = translate3dY; + } + } + }, + + __fadeScrollbars: function(direction, delay) { + var self = this; + + if (!self.options.scrollbarsFade) { + return; + } + + var className = 'scroll-bar-fade-out'; + + if (self.options.scrollbarsFade === true) { + clearTimeout(self.__scrollbarFadeTimeout); + + if (direction == 'in') { + if (self.__indicatorX) { self.__indicatorX.indicator.classList.remove(className); } + if (self.__indicatorY) { self.__indicatorY.indicator.classList.remove(className); } + } else { + self.__scrollbarFadeTimeout = setTimeout(function() { + if (self.__indicatorX) { self.__indicatorX.indicator.classList.add(className); } + if (self.__indicatorY) { self.__indicatorY.indicator.classList.add(className); } + }, delay || self.options.scrollbarFadeDelay); + } + } + }, + + __scrollingComplete: function() { + this.options.scrollingComplete(); + ionic.tap.removeClonedInputs(this.__container, this); + this.__fadeScrollbars('out'); + }, + + resize: function(continueScrolling) { + var self = this; + if (!self.__container || !self.options) return; + + // Update Scroller dimensions for changed content + // Add padding to bottom of content + self.setDimensions( + self.__container.clientWidth, + self.__container.clientHeight, + self.options.getContentWidth(), + self.options.getContentHeight(), + continueScrolling + ); + }, + /* + --------------------------------------------------------------------------- + PUBLIC API + --------------------------------------------------------------------------- + */ + + getRenderFn: function() { + var self = this; + + var content = self.__content; + + var docStyle = document.documentElement.style; + + var engine; + if ('MozAppearance' in docStyle) { + engine = 'gecko'; + } else if ('WebkitAppearance' in docStyle) { + engine = 'webkit'; + } else if (typeof navigator.cpuClass === 'string') { + engine = 'trident'; + } + + var vendorPrefix = { + trident: 'ms', + gecko: 'Moz', + webkit: 'Webkit', + presto: 'O' + }[engine]; + + var helperElem = document.createElement("div"); + var undef; + + var perspectiveProperty = vendorPrefix + "Perspective"; + var transformProperty = vendorPrefix + "Transform"; + var transformOriginProperty = vendorPrefix + 'TransformOrigin'; + + self.__perspectiveProperty = transformProperty; + self.__transformProperty = transformProperty; + self.__transformOriginProperty = transformOriginProperty; + + if (helperElem.style[perspectiveProperty] !== undef) { + + return function(left, top, zoom, wasResize) { + var translate3d = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0) scale(' + zoom + ')'; + if (translate3d !== self.contentTransform) { + content.style[transformProperty] = translate3d; + self.contentTransform = translate3d; + } + self.__repositionScrollbars(); + if (!wasResize) { + self.triggerScrollEvent(); + } + }; + + } else if (helperElem.style[transformProperty] !== undef) { + + return function(left, top, zoom, wasResize) { + content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px) scale(' + zoom + ')'; + self.__repositionScrollbars(); + if (!wasResize) { + self.triggerScrollEvent(); + } + }; + + } else { + + return function(left, top, zoom, wasResize) { + content.style.marginLeft = left ? (-left / zoom) + 'px' : ''; + content.style.marginTop = top ? (-top / zoom) + 'px' : ''; + content.style.zoom = zoom || ''; + self.__repositionScrollbars(); + if (!wasResize) { + self.triggerScrollEvent(); + } + }; + + } + }, + + + /** + * Configures the dimensions of the client (outer) and content (inner) elements. + * Requires the available space for the outer element and the outer size of the inner element. + * All values which are falsy (null or zero etc.) are ignored and the old value is kept. + * + * @param clientWidth {Integer} Inner width of outer element + * @param clientHeight {Integer} Inner height of outer element + * @param contentWidth {Integer} Outer width of inner element + * @param contentHeight {Integer} Outer height of inner element + */ + setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight, continueScrolling) { + var self = this; + + if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) { + // this scrollview isn't rendered, don't bother + return; + } + + // Only update values which are defined + if (clientWidth === +clientWidth) { + self.__clientWidth = clientWidth; + } + + if (clientHeight === +clientHeight) { + self.__clientHeight = clientHeight; + } + + if (contentWidth === +contentWidth) { + self.__contentWidth = contentWidth; + } + + if (contentHeight === +contentHeight) { + self.__contentHeight = contentHeight; + } + + // Refresh maximums + self.__computeScrollMax(); + self.__resizeScrollbars(); + + // Refresh scroll position + if (!continueScrolling) { + self.scrollTo(self.__scrollLeft, self.__scrollTop, true, null, true); + } + + }, + + + /** + * Sets the client coordinates in relation to the document. + * + * @param left {Integer} Left position of outer element + * @param top {Integer} Top position of outer element + */ + setPosition: function(left, top) { + this.__clientLeft = left || 0; + this.__clientTop = top || 0; + }, + + + /** + * Configures the snapping (when snapping is active) + * + * @param width {Integer} Snapping width + * @param height {Integer} Snapping height + */ + setSnapSize: function(width, height) { + this.__snapWidth = width; + this.__snapHeight = height; + }, + + + /** + * Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever + * the user event is released during visibility of this zone. This was introduced by some apps on iOS like + * the official Twitter client. + * + * @param height {Integer} Height of pull-to-refresh zone on top of rendered list + * @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release. + * @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled. + * @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh. + * @param showCallback {Function} Callback to execute when the refresher should be shown. This is for showing the refresher during a negative scrollTop. + * @param hideCallback {Function} Callback to execute when the refresher should be hidden. This is for hiding the refresher when it's behind the nav bar. + * @param tailCallback {Function} Callback to execute just before the refresher returns to it's original state. This is for zooming out the refresher. + * @param pullProgressCallback Callback to state the progress while pulling to refresh + */ + activatePullToRefresh: function(height, refresherMethods) { + var self = this; + + self.__refreshHeight = height; + self.__refreshActivate = function() { ionic.requestAnimationFrame(refresherMethods.activate); }; + self.__refreshDeactivate = function() { ionic.requestAnimationFrame(refresherMethods.deactivate); }; + self.__refreshStart = function() { ionic.requestAnimationFrame(refresherMethods.start); }; + self.__refreshShow = function() { ionic.requestAnimationFrame(refresherMethods.show); }; + self.__refreshHide = function() { ionic.requestAnimationFrame(refresherMethods.hide); }; + self.__refreshTail = function() { ionic.requestAnimationFrame(refresherMethods.tail); }; + self.__refreshTailTime = 100; + self.__minSpinTime = 600; + }, + + + /** + * Starts pull-to-refresh manually. + */ + triggerPullToRefresh: function() { + // Use publish instead of scrollTo to allow scrolling to out of boundary position + // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled + this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true); + + var d = new Date(); + this.refreshStartTime = d.getTime(); + + if (this.__refreshStart) { + this.__refreshStart(); + } + }, + + + /** + * Signalizes that pull-to-refresh is finished. + */ + finishPullToRefresh: function() { + var self = this; + // delay to make sure the spinner has a chance to spin for a split second before it's dismissed + var d = new Date(); + var delay = 0; + if (self.refreshStartTime + self.__minSpinTime > d.getTime()) { + delay = self.refreshStartTime + self.__minSpinTime - d.getTime(); + } + setTimeout(function() { + if (self.__refreshTail) { + self.__refreshTail(); + } + setTimeout(function() { + self.__refreshActive = false; + if (self.__refreshDeactivate) { + self.__refreshDeactivate(); + } + if (self.__refreshHide) { + self.__refreshHide(); + } + + self.scrollTo(self.__scrollLeft, self.__scrollTop, true); + }, self.__refreshTailTime); + }, delay); + }, + + + /** + * Returns the scroll position and zooming values + * + * @return {Map} `left` and `top` scroll position and `zoom` level + */ + getValues: function() { + return { + left: this.__scrollLeft, + top: this.__scrollTop, + zoom: this.__zoomLevel + }; + }, + + + /** + * Returns the maximum scroll values + * + * @return {Map} `left` and `top` maximum scroll values + */ + getScrollMax: function() { + return { + left: this.__maxScrollLeft, + top: this.__maxScrollTop + }; + }, + + + /** + * Zooms to the given level. Supports optional animation. Zooms + * the center when no coordinates are given. + * + * @param level {Number} Level to zoom to + * @param animate {Boolean} Whether to use animation + * @param originLeft {Number} Zoom in at given left coordinate + * @param originTop {Number} Zoom in at given top coordinate + */ + zoomTo: function(level, animate, originLeft, originTop) { + var self = this; + + if (!self.options.zooming) { + throw new Error("Zooming is not enabled!"); + } + + // Stop deceleration + if (self.__isDecelerating) { + zyngaCore.effect.Animate.stop(self.__isDecelerating); + self.__isDecelerating = false; + } + + var oldLevel = self.__zoomLevel; + + // Normalize input origin to center of viewport if not defined + if (originLeft == null) { + originLeft = self.__clientWidth / 2; + } + + if (originTop == null) { + originTop = self.__clientHeight / 2; + } + + // Limit level according to configuration + level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom); + + // Recompute maximum values while temporary tweaking maximum scroll ranges + self.__computeScrollMax(level); + + // Recompute left and top coordinates based on new zoom level + var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft; + var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop; + + // Limit x-axis + if (left > self.__maxScrollLeft) { + left = self.__maxScrollLeft; + } else if (left < 0) { + left = 0; + } + + // Limit y-axis + if (top > self.__maxScrollTop) { + top = self.__maxScrollTop; + } else if (top < 0) { + top = 0; + } + + // Push values out + self.__publish(left, top, level, animate); + + }, + + + /** + * Zooms the content by the given factor. + * + * @param factor {Number} Zoom by given factor + * @param animate {Boolean} Whether to use animation + * @param originLeft {Number} Zoom in at given left coordinate + * @param originTop {Number} Zoom in at given top coordinate + */ + zoomBy: function(factor, animate, originLeft, originTop) { + this.zoomTo(this.__zoomLevel * factor, animate, originLeft, originTop); + }, + + + /** + * Scrolls to the given position. Respect limitations and snapping automatically. + * + * @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code> + * @param top {Number} Vertical scroll position, keeps current if value is <code>null</code> + * @param animate {Boolean} Whether the scrolling should happen using an animation + * @param zoom {Number} Zoom level to go to + */ + scrollTo: function(left, top, animate, zoom, wasResize) { + var self = this; + + // Stop deceleration + if (self.__isDecelerating) { + zyngaCore.effect.Animate.stop(self.__isDecelerating); + self.__isDecelerating = false; + } + + // Correct coordinates based on new zoom level + if (zoom != null && zoom !== self.__zoomLevel) { + + if (!self.options.zooming) { + throw new Error("Zooming is not enabled!"); + } + + left *= zoom; + top *= zoom; + + // Recompute maximum values while temporary tweaking maximum scroll ranges + self.__computeScrollMax(zoom); + + } else { + + // Keep zoom when not defined + zoom = self.__zoomLevel; + + } + + if (!self.options.scrollingX) { + + left = self.__scrollLeft; + + } else { + + if (self.options.paging) { + left = Math.round(left / self.__clientWidth) * self.__clientWidth; + } else if (self.options.snapping) { + left = Math.round(left / self.__snapWidth) * self.__snapWidth; + } + + } + + if (!self.options.scrollingY) { + + top = self.__scrollTop; + + } else { + + if (self.options.paging) { + top = Math.round(top / self.__clientHeight) * self.__clientHeight; + } else if (self.options.snapping) { + top = Math.round(top / self.__snapHeight) * self.__snapHeight; + } + + } + + // Limit for allowed ranges + left = Math.max(Math.min(self.__maxScrollLeft, left), 0); + top = Math.max(Math.min(self.__maxScrollTop, top), 0); + + // Don't animate when no change detected, still call publish to make sure + // that rendered position is really in-sync with internal data + if (left === self.__scrollLeft && top === self.__scrollTop) { + animate = false; + } + + // Publish new values + self.__publish(left, top, zoom, animate, wasResize); + + }, + + + /** + * Scroll by the given offset + * + * @param left {Number} Scroll x-axis by given offset + * @param top {Number} Scroll y-axis by given offset + * @param animate {Boolean} Whether to animate the given change + */ + scrollBy: function(left, top, animate) { + var self = this; + + var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft; + var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop; + + self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate); + }, + + + + /* + --------------------------------------------------------------------------- + EVENT CALLBACKS + --------------------------------------------------------------------------- + */ + + /** + * Mouse wheel handler for zooming support + */ + doMouseZoom: function(wheelDelta, timeStamp, pageX, pageY) { + var change = wheelDelta > 0 ? 0.97 : 1.03; + return this.zoomTo(this.__zoomLevel * change, false, pageX - this.__clientLeft, pageY - this.__clientTop); + }, + + /** + * Touch start handler for scrolling support + */ + doTouchStart: function(touches, timeStamp) { + var self = this; + + // remember if the deceleration was just stopped + self.__decStopped = !!(self.__isDecelerating || self.__isAnimating); + + self.hintResize(); + + if (timeStamp instanceof Date) { + timeStamp = timeStamp.valueOf(); + } + if (typeof timeStamp !== "number") { + timeStamp = Date.now(); + } + + // Reset interruptedAnimation flag + self.__interruptedAnimation = true; + + // Stop deceleration + if (self.__isDecelerating) { + zyngaCore.effect.Animate.stop(self.__isDecelerating); + self.__isDecelerating = false; + self.__interruptedAnimation = true; + } + + // Stop animation + if (self.__isAnimating) { + zyngaCore.effect.Animate.stop(self.__isAnimating); + self.__isAnimating = false; + self.__interruptedAnimation = true; + } + + // Use center point when dealing with two fingers + var currentTouchLeft, currentTouchTop; + var isSingleTouch = touches.length === 1; + if (isSingleTouch) { + currentTouchLeft = touches[0].pageX; + currentTouchTop = touches[0].pageY; + } else { + currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2; + currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2; + } + + // Store initial positions + self.__initialTouchLeft = currentTouchLeft; + self.__initialTouchTop = currentTouchTop; + + // Store initial touchList for scale calculation + self.__initialTouches = touches; + + // Store current zoom level + self.__zoomLevelStart = self.__zoomLevel; + + // Store initial touch positions + self.__lastTouchLeft = currentTouchLeft; + self.__lastTouchTop = currentTouchTop; + + // Store initial move time stamp + self.__lastTouchMove = timeStamp; + + // Reset initial scale + self.__lastScale = 1; + + // Reset locking flags + self.__enableScrollX = !isSingleTouch && self.options.scrollingX; + self.__enableScrollY = !isSingleTouch && self.options.scrollingY; + + // Reset tracking flag + self.__isTracking = true; + + // Reset deceleration complete flag + self.__didDecelerationComplete = false; + + // Dragging starts directly with two fingers, otherwise lazy with an offset + self.__isDragging = !isSingleTouch; + + // Some features are disabled in multi touch scenarios + self.__isSingleTouch = isSingleTouch; + + // Clearing data structure + self.__positions = []; + + }, + + + /** + * Touch move handler for scrolling support + */ + doTouchMove: function(touches, timeStamp, scale) { + if (timeStamp instanceof Date) { + timeStamp = timeStamp.valueOf(); + } + if (typeof timeStamp !== "number") { + timeStamp = Date.now(); + } + + var self = this; + + // Ignore event when tracking is not enabled (event might be outside of element) + if (!self.__isTracking) { + return; + } + + var currentTouchLeft, currentTouchTop; + + // Compute move based around of center of fingers + if (touches.length === 2) { + currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2; + currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2; + + // Calculate scale when not present and only when touches are used + if (!scale && self.options.zooming) { + scale = self.__getScale(self.__initialTouches, touches); + } + } else { + currentTouchLeft = touches[0].pageX; + currentTouchTop = touches[0].pageY; + } + + var positions = self.__positions; + + // Are we already is dragging mode? + if (self.__isDragging) { + self.__decStopped = false; + + // Compute move distance + var moveX = currentTouchLeft - self.__lastTouchLeft; + var moveY = currentTouchTop - self.__lastTouchTop; + + // Read previous scroll position and zooming + var scrollLeft = self.__scrollLeft; + var scrollTop = self.__scrollTop; + var level = self.__zoomLevel; + + // Work with scaling + if (scale != null && self.options.zooming) { + + var oldLevel = level; + + // Recompute level based on previous scale and new scale + level = level / self.__lastScale * scale; + + // Limit level according to configuration + level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom); + + // Only do further compution when change happened + if (oldLevel !== level) { + + // Compute relative event position to container + var currentTouchLeftRel = currentTouchLeft - self.__clientLeft; + var currentTouchTopRel = currentTouchTop - self.__clientTop; + + // Recompute left and top coordinates based on new zoom level + scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel; + scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel; + + // Recompute max scroll values + self.__computeScrollMax(level); + + } + } + + if (self.__enableScrollX) { + + scrollLeft -= moveX * self.options.speedMultiplier; + var maxScrollLeft = self.__maxScrollLeft; + + if (scrollLeft > maxScrollLeft || scrollLeft < 0) { + + // Slow down on the edges + if (self.options.bouncing) { + + scrollLeft += (moveX / 2 * self.options.speedMultiplier); + + } else if (scrollLeft > maxScrollLeft) { + + scrollLeft = maxScrollLeft; + + } else { + + scrollLeft = 0; + + } + } + } + + // Compute new vertical scroll position + if (self.__enableScrollY) { + + scrollTop -= moveY * self.options.speedMultiplier; + var maxScrollTop = self.__maxScrollTop; + + if (scrollTop > maxScrollTop || scrollTop < 0) { + + // Slow down on the edges + if (self.options.bouncing || (self.__refreshHeight && scrollTop < 0)) { + + scrollTop += (moveY / 2 * self.options.speedMultiplier); + + // Support pull-to-refresh (only when only y is scrollable) + if (!self.__enableScrollX && self.__refreshHeight != null) { + + // hide the refresher when it's behind the header bar in case of header transparency + if (scrollTop < 0) { + self.__refreshHidden = false; + self.__refreshShow(); + } else { + self.__refreshHide(); + self.__refreshHidden = true; + } + + if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) { + + self.__refreshActive = true; + if (self.__refreshActivate) { + self.__refreshActivate(); + } + + } else if (self.__refreshActive && scrollTop > -self.__refreshHeight) { + + self.__refreshActive = false; + if (self.__refreshDeactivate) { + self.__refreshDeactivate(); + } + + } + } + + } else if (scrollTop > maxScrollTop) { + + scrollTop = maxScrollTop; + + } else { + + scrollTop = 0; + + } + } else if (self.__refreshHeight && !self.__refreshHidden) { + // if a positive scroll value and the refresher is still not hidden, hide it + self.__refreshHide(); + self.__refreshHidden = true; + } + } + + // Keep list from growing infinitely (holding min 10, max 20 measure points) + if (positions.length > 60) { + positions.splice(0, 30); + } + + // Track scroll movement for decleration + positions.push(scrollLeft, scrollTop, timeStamp); + + // Sync scroll position + self.__publish(scrollLeft, scrollTop, level); + + // Otherwise figure out whether we are switching into dragging mode now. + } else { + + var minimumTrackingForScroll = self.options.locking ? 3 : 0; + var minimumTrackingForDrag = 5; + + var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft); + var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop); + + self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll; + self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll; + + positions.push(self.__scrollLeft, self.__scrollTop, timeStamp); + + self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag); + if (self.__isDragging) { + self.__interruptedAnimation = false; + self.__fadeScrollbars('in'); + } + + } + + // Update last touch positions and time stamp for next event + self.__lastTouchLeft = currentTouchLeft; + self.__lastTouchTop = currentTouchTop; + self.__lastTouchMove = timeStamp; + self.__lastScale = scale; + + }, + + + /** + * Touch end handler for scrolling support + */ + doTouchEnd: function(e, timeStamp) { + if (timeStamp instanceof Date) { + timeStamp = timeStamp.valueOf(); + } + if (typeof timeStamp !== "number") { + timeStamp = Date.now(); + } + + var self = this; + + // Ignore event when tracking is not enabled (no touchstart event on element) + // This is required as this listener ('touchmove') sits on the document and not on the element itself. + if (!self.__isTracking) { + return; + } + + // Not touching anymore (when two finger hit the screen there are two touch end events) + self.__isTracking = false; + + // Be sure to reset the dragging flag now. Here we also detect whether + // the finger has moved fast enough to switch into a deceleration animation. + if (self.__isDragging) { + + // Reset dragging flag + self.__isDragging = false; + + // Start deceleration + // Verify that the last move detected was in some relevant time frame + if (self.__isSingleTouch && self.options.animating && (timeStamp - self.__lastTouchMove) <= 100) { + + // Then figure out what the scroll position was about 100ms ago + var positions = self.__positions; + var endPos = positions.length - 1; + var startPos = endPos; + + // Move pointer to position measured 100ms ago + for (var i = endPos; i > 0 && positions[i] > (self.__lastTouchMove - 100); i -= 3) { + startPos = i; + } + + // If start and stop position is identical in a 100ms timeframe, + // we cannot compute any useful deceleration. + if (startPos !== endPos) { + + // Compute relative movement between these two points + var timeOffset = positions[endPos] - positions[startPos]; + var movedLeft = self.__scrollLeft - positions[startPos - 2]; + var movedTop = self.__scrollTop - positions[startPos - 1]; + + // Based on 50ms compute the movement to apply for each render step + self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60); + self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60); + + // How much velocity is required to start the deceleration + var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? self.options.decelVelocityThresholdPaging : self.options.decelVelocityThreshold; + + // Verify that we have enough velocity to start deceleration + if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) { + + // Deactivate pull-to-refresh when decelerating + if (!self.__refreshActive) { + self.__startDeceleration(timeStamp); + } + } + } else { + self.__scrollingComplete(); + } + } else if ((timeStamp - self.__lastTouchMove) > 100) { + self.__scrollingComplete(); + } + + } else if (self.__decStopped) { + // the deceleration was stopped + // user flicked the scroll fast, and stop dragging, then did a touchstart to stop the srolling + // tell the touchend event code to do nothing, we don't want to actually send a click + e.isTapHandled = true; + self.__decStopped = false; + } + + // If this was a slower move it is per default non decelerated, but this + // still means that we want snap back to the bounds which is done here. + // This is placed outside the condition above to improve edge case stability + // e.g. touchend fired without enabled dragging. This should normally do not + // have modified the scroll positions or even showed the scrollbars though. + if (!self.__isDecelerating) { + + if (self.__refreshActive && self.__refreshStart) { + + // Use publish instead of scrollTo to allow scrolling to out of boundary position + // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled + self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true); + + var d = new Date(); + self.refreshStartTime = d.getTime(); + + if (self.__refreshStart) { + self.__refreshStart(); + } + // for iOS-ey style scrolling + if (!ionic.Platform.isAndroid())self.__startDeceleration(); + } else { + + if (self.__interruptedAnimation || self.__isDragging) { + self.__scrollingComplete(); + } + self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel); + + // Directly signalize deactivation (nothing todo on refresh?) + if (self.__refreshActive) { + + self.__refreshActive = false; + if (self.__refreshDeactivate) { + self.__refreshDeactivate(); + } + + } + } + } + + // Fully cleanup list + self.__positions.length = 0; + + }, + + + + /* + --------------------------------------------------------------------------- + PRIVATE API + --------------------------------------------------------------------------- + */ + + /** + * Applies the scroll position to the content element + * + * @param left {Number} Left scroll position + * @param top {Number} Top scroll position + * @param animate {Boolean} Whether animation should be used to move to the new coordinates + */ + __publish: function(left, top, zoom, animate, wasResize) { + + var self = this; + + // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation + var wasAnimating = self.__isAnimating; + if (wasAnimating) { + zyngaCore.effect.Animate.stop(wasAnimating); + self.__isAnimating = false; + } + + if (animate && self.options.animating) { + + // Keep scheduled positions for scrollBy/zoomBy functionality + self.__scheduledLeft = left; + self.__scheduledTop = top; + self.__scheduledZoom = zoom; + + var oldLeft = self.__scrollLeft; + var oldTop = self.__scrollTop; + var oldZoom = self.__zoomLevel; + + var diffLeft = left - oldLeft; + var diffTop = top - oldTop; + var diffZoom = zoom - oldZoom; + + var step = function(percent, now, render) { + + if (render) { + + self.__scrollLeft = oldLeft + (diffLeft * percent); + self.__scrollTop = oldTop + (diffTop * percent); + self.__zoomLevel = oldZoom + (diffZoom * percent); + + // Push values out + if (self.__callback) { + self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel, wasResize); + } + + } + }; + + var verify = function(id) { + return self.__isAnimating === id; + }; + + var completed = function(renderedFramesPerSecond, animationId, wasFinished) { + if (animationId === self.__isAnimating) { + self.__isAnimating = false; + } + if (self.__didDecelerationComplete || wasFinished) { + self.__scrollingComplete(); + } + + if (self.options.zooming) { + self.__computeScrollMax(); + } + }; + + // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out + self.__isAnimating = zyngaCore.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic); + + } else { + + self.__scheduledLeft = self.__scrollLeft = left; + self.__scheduledTop = self.__scrollTop = top; + self.__scheduledZoom = self.__zoomLevel = zoom; + + // Push values out + if (self.__callback) { + self.__callback(left, top, zoom, wasResize); + } + + // Fix max scroll ranges + if (self.options.zooming) { + self.__computeScrollMax(); + } + } + }, + + + /** + * Recomputes scroll minimum values based on client dimensions and content dimensions. + */ + __computeScrollMax: function(zoomLevel) { + var self = this; + + if (zoomLevel == null) { + zoomLevel = self.__zoomLevel; + } + + self.__maxScrollLeft = Math.max((self.__contentWidth * zoomLevel) - self.__clientWidth, 0); + self.__maxScrollTop = Math.max((self.__contentHeight * zoomLevel) - self.__clientHeight, 0); + + if (!self.__didWaitForSize && !self.__maxScrollLeft && !self.__maxScrollTop) { + self.__didWaitForSize = true; + self.__waitForSize(); + } + }, + + + /** + * If the scroll view isn't sized correctly on start, wait until we have at least some size + */ + __waitForSize: function() { + var self = this; + + clearTimeout(self.__sizerTimeout); + + var sizer = function() { + self.resize(true); + }; + + sizer(); + self.__sizerTimeout = setTimeout(sizer, 500); + }, + + /* + --------------------------------------------------------------------------- + ANIMATION (DECELERATION) SUPPORT + --------------------------------------------------------------------------- + */ + + /** + * Called when a touch sequence end and the speed of the finger was high enough + * to switch into deceleration mode. + */ + __startDeceleration: function() { + var self = this; + + if (self.options.paging) { + + var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0); + var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0); + var clientWidth = self.__clientWidth; + var clientHeight = self.__clientHeight; + + // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area. + // Each page should have exactly the size of the client area. + self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth; + self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight; + self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth; + self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight; + + } else { + + self.__minDecelerationScrollLeft = 0; + self.__minDecelerationScrollTop = 0; + self.__maxDecelerationScrollLeft = self.__maxScrollLeft; + self.__maxDecelerationScrollTop = self.__maxScrollTop; + if (self.__refreshActive) self.__minDecelerationScrollTop = self.__refreshHeight * -1; + } + + // Wrap class method + var step = function(percent, now, render) { + self.__stepThroughDeceleration(render); + }; + + // How much velocity is required to keep the deceleration running + self.__minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1; + + // Detect whether it's still worth to continue animating steps + // If we are already slow enough to not being user perceivable anymore, we stop the whole process here. + var verify = function() { + var shouldContinue = Math.abs(self.__decelerationVelocityX) >= self.__minVelocityToKeepDecelerating || + Math.abs(self.__decelerationVelocityY) >= self.__minVelocityToKeepDecelerating; + if (!shouldContinue) { + self.__didDecelerationComplete = true; + + //Make sure the scroll values are within the boundaries after a bounce, + //not below 0 or above maximum + if (self.options.bouncing && !self.__refreshActive) { + self.scrollTo( + Math.min( Math.max(self.__scrollLeft, 0), self.__maxScrollLeft ), + Math.min( Math.max(self.__scrollTop, 0), self.__maxScrollTop ), + self.__refreshActive + ); + } + } + return shouldContinue; + }; + + var completed = function() { + self.__isDecelerating = false; + if (self.__didDecelerationComplete) { + self.__scrollingComplete(); + } + + // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions + if (self.options.paging) { + self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping); + } + }; + + // Start animation and switch on flag + self.__isDecelerating = zyngaCore.effect.Animate.start(step, verify, completed); + + }, + + + /** + * Called on every step of the animation + * + * @param inMemory {Boolean} Whether to not render the current step, but keep it in memory only. Used internally only! + */ + __stepThroughDeceleration: function(render) { + var self = this; + + + // + // COMPUTE NEXT SCROLL POSITION + // + + // Add deceleration to scroll position + var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;// * self.options.deceleration); + var scrollTop = self.__scrollTop + self.__decelerationVelocityY;// * self.options.deceleration); + + + // + // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE + // + + if (!self.options.bouncing) { + + var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft); + if (scrollLeftFixed !== scrollLeft) { + scrollLeft = scrollLeftFixed; + self.__decelerationVelocityX = 0; + } + + var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop); + if (scrollTopFixed !== scrollTop) { + scrollTop = scrollTopFixed; + self.__decelerationVelocityY = 0; + } + + } + + + // + // UPDATE SCROLL POSITION + // + + if (render) { + + self.__publish(scrollLeft, scrollTop, self.__zoomLevel); + + } else { + + self.__scrollLeft = scrollLeft; + self.__scrollTop = scrollTop; + + } + + + // + // SLOW DOWN + // + + // Slow down velocity on every iteration + if (!self.options.paging) { + + // This is the factor applied to every iteration of the animation + // to slow down the process. This should emulate natural behavior where + // objects slow down when the initiator of the movement is removed + var frictionFactor = self.options.deceleration; + + self.__decelerationVelocityX *= frictionFactor; + self.__decelerationVelocityY *= frictionFactor; + + } + + + // + // BOUNCING SUPPORT + // + + if (self.options.bouncing) { + + var scrollOutsideX = 0; + var scrollOutsideY = 0; + + // This configures the amount of change applied to deceleration/acceleration when reaching boundaries + var penetrationDeceleration = self.options.penetrationDeceleration; + var penetrationAcceleration = self.options.penetrationAcceleration; + + // Check limits + if (scrollLeft < self.__minDecelerationScrollLeft) { + scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft; + } else if (scrollLeft > self.__maxDecelerationScrollLeft) { + scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft; + } + + if (scrollTop < self.__minDecelerationScrollTop) { + scrollOutsideY = self.__minDecelerationScrollTop - scrollTop; + } else if (scrollTop > self.__maxDecelerationScrollTop) { + scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop; + } + + // Slow down until slow enough, then flip back to snap position + if (scrollOutsideX !== 0) { + var isHeadingOutwardsX = scrollOutsideX * self.__decelerationVelocityX <= self.__minDecelerationScrollLeft; + if (isHeadingOutwardsX) { + self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration; + } + var isStoppedX = Math.abs(self.__decelerationVelocityX) <= self.__minVelocityToKeepDecelerating; + //If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds + if (!isHeadingOutwardsX || isStoppedX) { + self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration; + } + } + + if (scrollOutsideY !== 0) { + var isHeadingOutwardsY = scrollOutsideY * self.__decelerationVelocityY <= self.__minDecelerationScrollTop; + if (isHeadingOutwardsY) { + self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration; + } + var isStoppedY = Math.abs(self.__decelerationVelocityY) <= self.__minVelocityToKeepDecelerating; + //If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds + if (!isHeadingOutwardsY || isStoppedY) { + self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration; + } + } + } + }, + + + /** + * calculate the distance between two touches + * @param {Touch} touch1 + * @param {Touch} touch2 + * @returns {Number} distance + */ + __getDistance: function getDistance(touch1, touch2) { + var x = touch2.pageX - touch1.pageX, + y = touch2.pageY - touch1.pageY; + return Math.sqrt((x * x) + (y * y)); + }, + + + /** + * calculate the scale factor between two touchLists (fingers) + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start + * @param {Array} end + * @returns {Number} scale + */ + __getScale: function getScale(start, end) { + // need two fingers... + if (start.length >= 2 && end.length >= 2) { + return this.__getDistance(end[0], end[1]) / + this.__getDistance(start[0], start[1]); + } + return 1; + } +}); + +ionic.scroll = { + isScrolling: false, + lastTop: 0 +}; + +})(ionic); + +(function(ionic) { + var NOOP = function() {}; + var depreciated = 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; + 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); + + 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 = NOOP; + + self.__initEventHandlers(); + }, + + /** Methods not used in native scrolling */ + __callback: function() { depreciated('__callback'); }, + zoomTo: function() { depreciated('zoomTo'); }, + zoomBy: function() { depreciated('zoomBy'); }, + activatePullToRefresh: function() { depreciated('activatePullToRefresh'); }, + + /** + * Returns the scroll position and zooming values + * + * @return {Map} `left` and `top` scroll position and `zoom` level + */ + resize: function(continueScrolling) { + var self = this; + if (!self.__container || !self.options) return; + + // Update Scroller dimensions for changed content + // Add padding to bottom of content + self.setDimensions( + self.__container.clientWidth, + self.__container.clientHeight, + self.options.getContentWidth(), + self.options.getContentHeight(), + continueScrolling + ); + }, + + /** + * Initialize the scrollview + * In native scrolling, this only means we need to gather size information + */ + run: function() { + this.resize(); + }, + + /** + * Returns the scroll position and zooming values + * + * @return {Map} `left` and `top` scroll position and `zoom` level + */ + getValues: function() { + var self = this; + self.update(); + return { + left: self.__scrollLeft, + top: self.__scrollTop, + zoom: 1 + }; + }, + + /** + * Updates the __scrollLeft and __scrollTop values to el's current value + */ + update: function() { + var self = this; + self.__scrollLeft = self.el.scrollLeft; + self.__scrollTop = self.el.scrollTop; + }, + + /** + * Configures the dimensions of the client (outer) and content (inner) elements. + * Requires the available space for the outer element and the outer size of the inner element. + * All values which are falsy (null or zero etc.) are ignored and the old value is kept. + * + * @param clientWidth {Integer} Inner width of outer element + * @param clientHeight {Integer} Inner height of outer element + * @param contentWidth {Integer} Outer width of inner element + * @param contentHeight {Integer} Outer height of inner element + */ + setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight) { + var self = this; + + if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) { + // this scrollview isn't rendered, don't bother + return; + } + + // Only update values which are defined + if (clientWidth === +clientWidth) { + self.__clientWidth = clientWidth; + } + + if (clientHeight === +clientHeight) { + self.__clientHeight = clientHeight; + } + + if (contentWidth === +contentWidth) { + self.__contentWidth = contentWidth; + } + + if (contentHeight === +contentHeight) { + self.__contentHeight = contentHeight; + } + + // Refresh maximums + self.__computeScrollMax(); + }, + + /** + * Returns the maximum scroll values + * + * @return {Map} `left` and `top` maximum scroll values + */ + getScrollMax: function() { + return { + left: this.__maxScrollLeft, + top: this.__maxScrollTop + }; + }, + + /** + * Scrolls by the given amount in px. + * + * @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code> + * @param top {Number} Vertical scroll position, keeps current if value is <code>null</code> + * @param animate {Boolean} Whether the scrolling should happen using an animation + */ + + scrollBy: function(left, top, animate) { + var self = this; + + // update scroll vars before refferencing them + self.update(); + + var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft; + var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop; + + self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate); + }, + + /** + * Scrolls to the given position in px. + * + * @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code> + * @param top {Number} Vertical scroll position, keeps current if value is <code>null</code> + * @param animate {Boolean} Whether the scrolling should happen using an animation + */ + scrollTo: function(left, top, animate) { + var self = this; + if (!animate) { + self.el.scrollTop = top; + self.el.scrollLeft = left; + self.resize(); + return; + } + animateScroll(top, left); + + function animateScroll(Y, X) { + // scroll animation loop w/ easing + // credit https://gist.github.com/dezinezync/5487119 + var start = Date.now(), + duration = 1000, //milliseconds + fromY = self.el.scrollTop, + fromX = self.el.scrollLeft; + + if (fromY === Y && fromX === X) { + 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 + 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; + + // should be unnecessary in native scrolling, but keep in case bugs show up + self.scrollChildIntoView = NOOP; + + self.resetScrollView = function() { + //return scrollview to original height once keyboard has hidden + if (self.isScrolledIntoView) { + self.isScrolledIntoView = false; + container.style.height = ""; + container.style.overflow = ""; + self.resize(); + ionic.scroll.isScrolling = false; + } + }; + + container.addEventListener('resetScrollView', self.resetScrollView); + 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('resetScrollView', self.resetScrollView); + }, + + __cleanup: function() { + var self = this; + var container = self.__container; + + container.removeEventListener('resetScrollView', self.resetScrollView); + container.removeEventListener('scroll', self.onScroll); + + container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView); + container.removeEventListener('resetScrollView', self.resetScrollView); + + 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; + 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; + + // 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, + touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch, + 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 (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.type == 'mousedown' || event.type == 'mouseup' || event.type == 'mousemove') { + event.touches = [{ + pageX: event.pageX, + pageY: event.pageY + }]; + } + + switch (event.type) { + case 'mousedown': this.start(event); break; + case 'touchstart': this.start(event); break; + case 'touchmove': this.touchmove(event); break; + case 'mousemove': this.touchmove(event); break; + case 'touchend': offloadFn(this.end(event)); break; + case 'mouseup': 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) { + + 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 + if(browser.touch) { + element.addEventListener('touchmove', this, false); + element.addEventListener('touchend', this, false); + } else { + element.addEventListener('mousemove', this, false); + element.addEventListener('mouseup', this, false); + document.addEventListener('mouseup', this, false); + } + }, + touchmove: function(event) { + + // ensure swiping with one touch and not pinching + // ensure sliding is enabled + if (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 { + + 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 + + // 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 + if(browser.touch) { + element.removeEventListener('touchmove', events, false); + element.removeEventListener('touchend', events, false); + } else { + element.removeEventListener('mousemove', events, false); + element.removeEventListener('mouseup', events, false); + document.removeEventListener('mouseup', 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.length = 0); + + // removed event listeners + if (browser.addEventListener) { + + // remove current event listeners + element.removeEventListener('touchstart', 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 + if (browser.touch) { + element.addEventListener('touchstart', events, false); + } else { + element.addEventListener('mousedown', 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); + +(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); + +})();
\ No newline at end of file |
