diff --git a/.versions b/.versions
index 75675c64..6a20a259 100644
--- a/.versions
+++ b/.versions
@@ -1,4 +1,4 @@
+angularui:angular-ui-router@0.2.13
meteor@1.1.4
-mquandalle:bower@0.1.11
underscore@1.0.2
-urigo:ionic@0.2.1
+urigo:ionic@0.3.0
diff --git a/dependencies/angular-animate.js b/dependencies/angular-animate.js
new file mode 100644
index 00000000..98e39bc1
--- /dev/null
+++ b/dependencies/angular-animate.js
@@ -0,0 +1,2137 @@
+/**
+ * @license AngularJS v1.3.12
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+/* jshint maxlen: false */
+
+/**
+ * @ngdoc module
+ * @name ngAnimate
+ * @description
+ *
+ * The `ngAnimate` module provides support for JavaScript, CSS3 transition and CSS3 keyframe animation hooks within existing core and custom directives.
+ *
+ *
+ *
+ * # Usage
+ *
+ * To see animations in action, all that is required is to define the appropriate CSS classes
+ * or to register a JavaScript animation via the `myModule.animation()` function. The directives that support animation automatically are:
+ * `ngRepeat`, `ngInclude`, `ngIf`, `ngSwitch`, `ngShow`, `ngHide`, `ngView` and `ngClass`. Custom directives can take advantage of animation
+ * by using the `$animate` service.
+ *
+ * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives:
+ *
+ * | Directive | Supported Animations |
+ * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
+ * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
+ * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
+ * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
+ * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
+ * | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
+ * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) |
+ * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) |
+ * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
+ * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) |
+ * | {@link module:ngMessages#animations ngMessage} | enter and leave |
+ *
+ * You can find out more information about animations upon visiting each directive page.
+ *
+ * Below is an example of how to apply animations to a directive that supports animation hooks:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Keep in mind that, by default, if an animation is running, any child elements cannot be animated
+ * until the parent element's animation has completed. This blocking feature can be overridden by
+ * placing the `ng-animate-children` attribute on a parent container tag.
+ *
+ * ```html
+ *
+ *
+ *
+ * ...
+ *
+ *
+ *
+ * ```
+ *
+ * When the `on` expression value changes and an animation is triggered then each of the elements within
+ * will all animate without the block being applied to child elements.
+ *
+ * ## Are animations run when the application starts?
+ * No they are not. When an application is bootstrapped Angular will disable animations from running to avoid
+ * a frenzy of animations from being triggered as soon as the browser has rendered the screen. For this to work,
+ * Angular will wait for two digest cycles until enabling animations. From there on, any animation-triggering
+ * layout changes in the application will trigger animations as normal.
+ *
+ * In addition, upon bootstrap, if the routing system or any directives or load remote data (via $http) then Angular
+ * will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests
+ * are complete.
+ *
+ * ## CSS-defined Animations
+ * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
+ * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported
+ * and can be used to play along with this naming structure.
+ *
+ * The following code below demonstrates how to perform animations using **CSS transitions** with Angular:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * The following code below demonstrates how to perform animations using **CSS animations** with Angular:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing.
+ *
+ * Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add
+ * the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically
+ * detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be
+ * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end
+ * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element
+ * has no CSS transition/animation classes applied to it.
+ *
+ * ### Structural transition animations
+ *
+ * Structural transitions (such as enter, leave and move) will always apply a `0s none` transition
+ * value to force the browser into rendering the styles defined in the setup (`.ng-enter`, `.ng-leave`
+ * or `.ng-move`) class. This means that any active transition animations operating on the element
+ * will be cut off to make way for the enter, leave or move animation.
+ *
+ * ### Class-based transition animations
+ *
+ * Class-based transitions refer to transition animations that are triggered when a CSS class is
+ * added to or removed from the element (via `$animate.addClass`, `$animate.removeClass`,
+ * `$animate.setClass`, or by directives such as `ngClass`, `ngModel` and `form`).
+ * They are different when compared to structural animations since they **do not cancel existing
+ * animations** nor do they **block successive transitions** from rendering on the same element.
+ * This distinction allows for **multiple class-based transitions** to be performed on the same element.
+ *
+ * In addition to ngAnimate supporting the default (natural) functionality of class-based transition
+ * animations, ngAnimate also decorates the element with starting and ending CSS classes to aid the
+ * developer in further styling the element throughout the transition animation. Earlier versions
+ * of ngAnimate may have caused natural CSS transitions to break and not render properly due to
+ * $animate temporarily blocking transitions using `0s none` in order to allow the setup CSS class
+ * (the `-add` or `-remove` class) to be applied without triggering an animation. However, as of
+ * **version 1.3**, this workaround has been removed with ngAnimate and all non-ngAnimate CSS
+ * class transitions are compatible with ngAnimate.
+ *
+ * There is, however, one special case when dealing with class-based transitions in ngAnimate.
+ * When rendering class-based transitions that make use of the setup and active CSS classes
+ * (e.g. `.fade-add` and `.fade-add-active` for when `.fade` is added) be sure to define
+ * the transition value **on the active CSS class** and not the setup class.
+ *
+ * ```css
+ * .fade-add {
+ * /* remember to place a 0s transition here
+ * to ensure that the styles are applied instantly
+ * even if the element already has a transition style */
+ * transition:0s linear all;
+ *
+ * /* starting CSS styles */
+ * opacity:1;
+ * }
+ * .fade-add.fade-add-active {
+ * /* this will be the length of the animation */
+ * transition:1s linear all;
+ * opacity:0;
+ * }
+ * ```
+ *
+ * The setup CSS class (in this case `.fade-add`) also has a transition style property, however, it
+ * has a duration of zero. This may not be required, however, incase the browser is unable to render
+ * the styling present in this CSS class instantly then it could be that the browser is attempting
+ * to perform an unnecessary transition.
+ *
+ * This workaround, however, does not apply to standard class-based transitions that are rendered
+ * when a CSS class containing a transition is applied to an element:
+ *
+ * ```css
+ * /* this works as expected */
+ * .fade {
+ * transition:1s linear all;
+ * opacity:0;
+ * }
+ * ```
+ *
+ * Please keep this in mind when coding the CSS markup that will be used within class-based transitions.
+ * Also, try not to mix the two class-based animation flavors together since the CSS code may become
+ * overly complex.
+ *
+ *
+ * ### Preventing Collisions With Third Party Libraries
+ *
+ * Some third-party frameworks place animation duration defaults across many element or className
+ * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which
+ * is expecting actual animations on these elements and has to wait for their completion.
+ *
+ * You can prevent this unwanted behavior by using a prefix on all your animation classes:
+ *
+ * ```css
+ * /* prefixed with animate- */
+ * .animate-fade-add.animate-fade-add-active {
+ * transition:1s linear all;
+ * opacity:0;
+ * }
+ * ```
+ *
+ * You then configure `$animate` to enforce this prefix:
+ *
+ * ```js
+ * $animateProvider.classNameFilter(/animate-/);
+ * ```
+ *
+ *
+ * ### CSS Staggering Animations
+ * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a
+ * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be
+ * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for
+ * the animation. The style property expected within the stagger class can either be a **transition-delay** or an
+ * **animation-delay** property (or both if your animation contains both transitions and keyframe animations).
+ *
+ * ```css
+ * .my-animation.ng-enter {
+ * /* standard transition code */
+ * -webkit-transition: 1s linear all;
+ * transition: 1s linear all;
+ * opacity:0;
+ * }
+ * .my-animation.ng-enter-stagger {
+ * /* this will have a 100ms delay between each successive leave animation */
+ * -webkit-transition-delay: 0.1s;
+ * transition-delay: 0.1s;
+ *
+ * /* in case the stagger doesn't work then these two values
+ * must be set to 0 to avoid an accidental CSS inheritance */
+ * -webkit-transition-duration: 0s;
+ * transition-duration: 0s;
+ * }
+ * .my-animation.ng-enter.ng-enter-active {
+ * /* standard transition styles */
+ * opacity:1;
+ * }
+ * ```
+ *
+ * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations
+ * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this
+ * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation
+ * will also be reset if more than 10ms has passed after the last animation has been fired.
+ *
+ * The following code will issue the **ng-leave-stagger** event on the element provided:
+ *
+ * ```js
+ * var kids = parent.children();
+ *
+ * $animate.leave(kids[0]); //stagger index=0
+ * $animate.leave(kids[1]); //stagger index=1
+ * $animate.leave(kids[2]); //stagger index=2
+ * $animate.leave(kids[3]); //stagger index=3
+ * $animate.leave(kids[4]); //stagger index=4
+ *
+ * $timeout(function() {
+ * //stagger has reset itself
+ * $animate.leave(kids[5]); //stagger index=0
+ * $animate.leave(kids[6]); //stagger index=1
+ * }, 100, false);
+ * ```
+ *
+ * Stagger animations are currently only supported within CSS-defined animations.
+ *
+ * ## JavaScript-defined Animations
+ * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not
+ * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module.
+ *
+ * ```js
+ * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application.
+ * var ngModule = angular.module('YourApp', ['ngAnimate']);
+ * ngModule.animation('.my-crazy-animation', function() {
+ * return {
+ * enter: function(element, done) {
+ * //run the animation here and call done when the animation is complete
+ * return function(cancelled) {
+ * //this (optional) function will be called when the animation
+ * //completes or when the animation is cancelled (the cancelled
+ * //flag will be set to true if cancelled).
+ * };
+ * },
+ * leave: function(element, done) { },
+ * move: function(element, done) { },
+ *
+ * //animation that can be triggered before the class is added
+ * beforeAddClass: function(element, className, done) { },
+ *
+ * //animation that can be triggered after the class is added
+ * addClass: function(element, className, done) { },
+ *
+ * //animation that can be triggered before the class is removed
+ * beforeRemoveClass: function(element, className, done) { },
+ *
+ * //animation that can be triggered after the class is removed
+ * removeClass: function(element, className, done) { }
+ * };
+ * });
+ * ```
+ *
+ * JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run
+ * a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits
+ * the element's CSS class attribute value and then run the matching animation event function (if found).
+ * In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function will
+ * be executed. It should be also noted that only simple, single class selectors are allowed (compound class selectors are not supported).
+ *
+ * Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned.
+ * As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run,
+ * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation
+ * or transition code that is defined via a stylesheet).
+ *
+ *
+ * ### Applying Directive-specific Styles to an Animation
+ * In some cases a directive or service may want to provide `$animate` with extra details that the animation will
+ * include into its animation. Let's say for example we wanted to render an animation that animates an element
+ * towards the mouse coordinates as to where the user clicked last. By collecting the X/Y coordinates of the click
+ * (via the event parameter) we can set the `top` and `left` styles into an object and pass that into our function
+ * call to `$animate.addClass`.
+ *
+ * ```js
+ * canvas.on('click', function(e) {
+ * $animate.addClass(element, 'on', {
+ * to: {
+ * left : e.client.x + 'px',
+ * top : e.client.y + 'px'
+ * }
+ * }):
+ * });
+ * ```
+ *
+ * Now when the animation runs, and a transition or keyframe animation is picked up, then the animation itself will
+ * also include and transition the styling of the `left` and `top` properties into its running animation. If we want
+ * to provide some starting animation values then we can do so by placing the starting animations styles into an object
+ * called `from` in the same object as the `to` animations.
+ *
+ * ```js
+ * canvas.on('click', function(e) {
+ * $animate.addClass(element, 'on', {
+ * from: {
+ * position: 'absolute',
+ * left: '0px',
+ * top: '0px'
+ * },
+ * to: {
+ * left : e.client.x + 'px',
+ * top : e.client.y + 'px'
+ * }
+ * }):
+ * });
+ * ```
+ *
+ * Once the animation is complete or cancelled then the union of both the before and after styles are applied to the
+ * element. If `ngAnimate` is not present then the styles will be applied immediately.
+ *
+ */
+
+angular.module('ngAnimate', ['ng'])
+
+ /**
+ * @ngdoc provider
+ * @name $animateProvider
+ * @description
+ *
+ * The `$animateProvider` allows developers to register JavaScript animation event handlers directly inside of a module.
+ * When an animation is triggered, the $animate service will query the $animate service to find any animations that match
+ * the provided name value.
+ *
+ * Requires the {@link ngAnimate `ngAnimate`} module to be installed.
+ *
+ * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
+ *
+ */
+ .directive('ngAnimateChildren', function() {
+ var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
+ return function(scope, element, attrs) {
+ var val = attrs.ngAnimateChildren;
+ if (angular.isString(val) && val.length === 0) { //empty attribute
+ element.data(NG_ANIMATE_CHILDREN, true);
+ } else {
+ scope.$watch(val, function(value) {
+ element.data(NG_ANIMATE_CHILDREN, !!value);
+ });
+ }
+ };
+ })
+
+ //this private service is only used within CSS-enabled animations
+ //IE8 + IE9 do not support rAF natively, but that is fine since they
+ //also don't support transitions and keyframes which means that the code
+ //below will never be used by the two browsers.
+ .factory('$$animateReflow', ['$$rAF', '$document', function($$rAF, $document) {
+ var bod = $document[0].body;
+ return function(fn) {
+ //the returned function acts as the cancellation function
+ return $$rAF(function() {
+ //the line below will force the browser to perform a repaint
+ //so that all the animated elements within the animation frame
+ //will be properly updated and drawn on screen. This is
+ //required to perform multi-class CSS based animations with
+ //Firefox. DO NOT REMOVE THIS LINE.
+ var a = bod.offsetWidth + 1;
+ fn();
+ });
+ };
+ }])
+
+ .config(['$provide', '$animateProvider', function($provide, $animateProvider) {
+ var noop = angular.noop;
+ var forEach = angular.forEach;
+ var selectors = $animateProvider.$$selectors;
+ var isArray = angular.isArray;
+ var isString = angular.isString;
+ var isObject = angular.isObject;
+
+ var ELEMENT_NODE = 1;
+ var NG_ANIMATE_STATE = '$$ngAnimateState';
+ var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
+ var NG_ANIMATE_CLASS_NAME = 'ng-animate';
+ var rootAnimateState = {running: true};
+
+ function extractElementNode(element) {
+ for (var i = 0; i < element.length; i++) {
+ var elm = element[i];
+ if (elm.nodeType == ELEMENT_NODE) {
+ return elm;
+ }
+ }
+ }
+
+ function prepareElement(element) {
+ return element && angular.element(element);
+ }
+
+ function stripCommentsFromElement(element) {
+ return angular.element(extractElementNode(element));
+ }
+
+ function isMatchingElement(elm1, elm2) {
+ return extractElementNode(elm1) == extractElementNode(elm2);
+ }
+ var $$jqLite;
+ $provide.decorator('$animate',
+ ['$delegate', '$$q', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', '$templateRequest', '$$jqLite',
+ function($delegate, $$q, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document, $templateRequest, $$$jqLite) {
+
+ $$jqLite = $$$jqLite;
+ $rootElement.data(NG_ANIMATE_STATE, rootAnimateState);
+
+ // Wait until all directive and route-related templates are downloaded and
+ // compiled. The $templateRequest.totalPendingRequests variable keeps track of
+ // all of the remote templates being currently downloaded. If there are no
+ // templates currently downloading then the watcher will still fire anyway.
+ var deregisterWatch = $rootScope.$watch(
+ function() { return $templateRequest.totalPendingRequests; },
+ function(val, oldVal) {
+ if (val !== 0) return;
+ deregisterWatch();
+
+ // Now that all templates have been downloaded, $animate will wait until
+ // the post digest queue is empty before enabling animations. By having two
+ // calls to $postDigest calls we can ensure that the flag is enabled at the
+ // very end of the post digest queue. Since all of the animations in $animate
+ // use $postDigest, it's important that the code below executes at the end.
+ // This basically means that the page is fully downloaded and compiled before
+ // any animations are triggered.
+ $rootScope.$$postDigest(function() {
+ $rootScope.$$postDigest(function() {
+ rootAnimateState.running = false;
+ });
+ });
+ }
+ );
+
+ var globalAnimationCounter = 0;
+ var classNameFilter = $animateProvider.classNameFilter();
+ var isAnimatableClassName = !classNameFilter
+ ? function() { return true; }
+ : function(className) {
+ return classNameFilter.test(className);
+ };
+
+ function classBasedAnimationsBlocked(element, setter) {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+ if (setter) {
+ data.running = true;
+ data.structural = true;
+ element.data(NG_ANIMATE_STATE, data);
+ }
+ return data.disabled || (data.running && data.structural);
+ }
+
+ function runAnimationPostDigest(fn) {
+ var cancelFn, defer = $$q.defer();
+ defer.promise.$$cancelFn = function() {
+ cancelFn && cancelFn();
+ };
+ $rootScope.$$postDigest(function() {
+ cancelFn = fn(function() {
+ defer.resolve();
+ });
+ });
+ return defer.promise;
+ }
+
+ function parseAnimateOptions(options) {
+ // some plugin code may still be passing in the callback
+ // function as the last param for the $animate methods so
+ // it's best to only allow string or array values for now
+ if (isObject(options)) {
+ if (options.tempClasses && isString(options.tempClasses)) {
+ options.tempClasses = options.tempClasses.split(/\s+/);
+ }
+ return options;
+ }
+ }
+
+ function resolveElementClasses(element, cache, runningAnimations) {
+ runningAnimations = runningAnimations || {};
+
+ var lookup = {};
+ forEach(runningAnimations, function(data, selector) {
+ forEach(selector.split(' '), function(s) {
+ lookup[s]=data;
+ });
+ });
+
+ var hasClasses = Object.create(null);
+ forEach((element.attr('class') || '').split(/\s+/), function(className) {
+ hasClasses[className] = true;
+ });
+
+ var toAdd = [], toRemove = [];
+ forEach((cache && cache.classes) || [], function(status, className) {
+ var hasClass = hasClasses[className];
+ var matchingAnimation = lookup[className] || {};
+
+ // When addClass and removeClass is called then $animate will check to
+ // see if addClass and removeClass cancel each other out. When there are
+ // more calls to removeClass than addClass then the count falls below 0
+ // and then the removeClass animation will be allowed. Otherwise if the
+ // count is above 0 then that means an addClass animation will commence.
+ // Once an animation is allowed then the code will also check to see if
+ // there exists any on-going animation that is already adding or remvoing
+ // the matching CSS class.
+ if (status === false) {
+ //does it have the class or will it have the class
+ if (hasClass || matchingAnimation.event == 'addClass') {
+ toRemove.push(className);
+ }
+ } else if (status === true) {
+ //is the class missing or will it be removed?
+ if (!hasClass || matchingAnimation.event == 'removeClass') {
+ toAdd.push(className);
+ }
+ }
+ });
+
+ return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')];
+ }
+
+ function lookup(name) {
+ if (name) {
+ var matches = [],
+ flagMap = {},
+ classes = name.substr(1).split('.');
+
+ //the empty string value is the default animation
+ //operation which performs CSS transition and keyframe
+ //animations sniffing. This is always included for each
+ //element animation procedure if the browser supports
+ //transitions and/or keyframe animations. The default
+ //animation is added to the top of the list to prevent
+ //any previous animations from affecting the element styling
+ //prior to the element being animated.
+ if ($sniffer.transitions || $sniffer.animations) {
+ matches.push($injector.get(selectors['']));
+ }
+
+ for (var i=0; i < classes.length; i++) {
+ var klass = classes[i],
+ selectorFactoryName = selectors[klass];
+ if (selectorFactoryName && !flagMap[klass]) {
+ matches.push($injector.get(selectorFactoryName));
+ flagMap[klass] = true;
+ }
+ }
+ return matches;
+ }
+ }
+
+ function animationRunner(element, animationEvent, className, options) {
+ //transcluded directives may sometimes fire an animation using only comment nodes
+ //best to catch this early on to prevent any animation operations from occurring
+ var node = element[0];
+ if (!node) {
+ return;
+ }
+
+ if (options) {
+ options.to = options.to || {};
+ options.from = options.from || {};
+ }
+
+ var classNameAdd;
+ var classNameRemove;
+ if (isArray(className)) {
+ classNameAdd = className[0];
+ classNameRemove = className[1];
+ if (!classNameAdd) {
+ className = classNameRemove;
+ animationEvent = 'removeClass';
+ } else if (!classNameRemove) {
+ className = classNameAdd;
+ animationEvent = 'addClass';
+ } else {
+ className = classNameAdd + ' ' + classNameRemove;
+ }
+ }
+
+ var isSetClassOperation = animationEvent == 'setClass';
+ var isClassBased = isSetClassOperation
+ || animationEvent == 'addClass'
+ || animationEvent == 'removeClass'
+ || animationEvent == 'animate';
+
+ var currentClassName = element.attr('class');
+ var classes = currentClassName + ' ' + className;
+ if (!isAnimatableClassName(classes)) {
+ return;
+ }
+
+ var beforeComplete = noop,
+ beforeCancel = [],
+ before = [],
+ afterComplete = noop,
+ afterCancel = [],
+ after = [];
+
+ var animationLookup = (' ' + classes).replace(/\s+/g,'.');
+ forEach(lookup(animationLookup), function(animationFactory) {
+ var created = registerAnimation(animationFactory, animationEvent);
+ if (!created && isSetClassOperation) {
+ registerAnimation(animationFactory, 'addClass');
+ registerAnimation(animationFactory, 'removeClass');
+ }
+ });
+
+ function registerAnimation(animationFactory, event) {
+ var afterFn = animationFactory[event];
+ var beforeFn = animationFactory['before' + event.charAt(0).toUpperCase() + event.substr(1)];
+ if (afterFn || beforeFn) {
+ if (event == 'leave') {
+ beforeFn = afterFn;
+ //when set as null then animation knows to skip this phase
+ afterFn = null;
+ }
+ after.push({
+ event: event, fn: afterFn
+ });
+ before.push({
+ event: event, fn: beforeFn
+ });
+ return true;
+ }
+ }
+
+ function run(fns, cancellations, allCompleteFn) {
+ var animations = [];
+ forEach(fns, function(animation) {
+ animation.fn && animations.push(animation);
+ });
+
+ var count = 0;
+ function afterAnimationComplete(index) {
+ if (cancellations) {
+ (cancellations[index] || noop)();
+ if (++count < animations.length) return;
+ cancellations = null;
+ }
+ allCompleteFn();
+ }
+
+ //The code below adds directly to the array in order to work with
+ //both sync and async animations. Sync animations are when the done()
+ //operation is called right away. DO NOT REFACTOR!
+ forEach(animations, function(animation, index) {
+ var progress = function() {
+ afterAnimationComplete(index);
+ };
+ switch (animation.event) {
+ case 'setClass':
+ cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress, options));
+ break;
+ case 'animate':
+ cancellations.push(animation.fn(element, className, options.from, options.to, progress));
+ break;
+ case 'addClass':
+ cancellations.push(animation.fn(element, classNameAdd || className, progress, options));
+ break;
+ case 'removeClass':
+ cancellations.push(animation.fn(element, classNameRemove || className, progress, options));
+ break;
+ default:
+ cancellations.push(animation.fn(element, progress, options));
+ break;
+ }
+ });
+
+ if (cancellations && cancellations.length === 0) {
+ allCompleteFn();
+ }
+ }
+
+ return {
+ node: node,
+ event: animationEvent,
+ className: className,
+ isClassBased: isClassBased,
+ isSetClassOperation: isSetClassOperation,
+ applyStyles: function() {
+ if (options) {
+ element.css(angular.extend(options.from || {}, options.to || {}));
+ }
+ },
+ before: function(allCompleteFn) {
+ beforeComplete = allCompleteFn;
+ run(before, beforeCancel, function() {
+ beforeComplete = noop;
+ allCompleteFn();
+ });
+ },
+ after: function(allCompleteFn) {
+ afterComplete = allCompleteFn;
+ run(after, afterCancel, function() {
+ afterComplete = noop;
+ allCompleteFn();
+ });
+ },
+ cancel: function() {
+ if (beforeCancel) {
+ forEach(beforeCancel, function(cancelFn) {
+ (cancelFn || noop)(true);
+ });
+ beforeComplete(true);
+ }
+ if (afterCancel) {
+ forEach(afterCancel, function(cancelFn) {
+ (cancelFn || noop)(true);
+ });
+ afterComplete(true);
+ }
+ }
+ };
+ }
+
+ /**
+ * @ngdoc service
+ * @name $animate
+ * @kind object
+ *
+ * @description
+ * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) as well as during addClass and removeClass operations.
+ * When any of these operations are run, the $animate service
+ * will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object)
+ * as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run.
+ *
+ * The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives
+ * will work out of the box without any extra configuration.
+ *
+ * Requires the {@link ngAnimate `ngAnimate`} module to be installed.
+ *
+ * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
+ * ## Callback Promises
+ * With AngularJS 1.3, each of the animation methods, on the `$animate` service, return a promise when called. The
+ * promise itself is then resolved once the animation has completed itself, has been cancelled or has been
+ * skipped due to animations being disabled. (Note that even if the animation is cancelled it will still
+ * call the resolve function of the animation.)
+ *
+ * ```js
+ * $animate.enter(element, container).then(function() {
+ * //...this is called once the animation is complete...
+ * });
+ * ```
+ *
+ * Also note that, due to the nature of the callback promise, if any Angular-specific code (like changing the scope,
+ * location of the page, etc...) is executed within the callback promise then be sure to wrap the code using
+ * `$scope.$apply(...)`;
+ *
+ * ```js
+ * $animate.leave(element).then(function() {
+ * $scope.$apply(function() {
+ * $location.path('/new-page');
+ * });
+ * });
+ * ```
+ *
+ * An animation can also be cancelled by calling the `$animate.cancel(promise)` method with the provided
+ * promise that was returned when the animation was started.
+ *
+ * ```js
+ * var promise = $animate.addClass(element, 'super-long-animation');
+ * promise.then(function() {
+ * //this will still be called even if cancelled
+ * });
+ *
+ * element.on('click', function() {
+ * //tooo lazy to wait for the animation to end
+ * $animate.cancel(promise);
+ * });
+ * ```
+ *
+ * (Keep in mind that the promise cancellation is unique to `$animate` since promises in
+ * general cannot be cancelled.)
+ *
+ */
+ return {
+ /**
+ * @ngdoc method
+ * @name $animate#animate
+ * @kind function
+ *
+ * @description
+ * Performs an inline animation on the element which applies the provided `to` and `from` CSS styles to the element.
+ * If any detected CSS transition, keyframe or JavaScript matches the provided `className` value then the animation
+ * will take on the provided styles. For example, if a transition animation is set for the given className then the
+ * provided `from` and `to` styles will be applied alongside the given transition. If a JavaScript animation is
+ * detected then the provided styles will be given in as function paramters.
+ *
+ * ```js
+ * ngModule.animation('.my-inline-animation', function() {
+ * return {
+ * animate : function(element, className, from, to, done) {
+ * //styles
+ * }
+ * }
+ * });
+ * ```
+ *
+ * Below is a breakdown of each step that occurs during the `animate` animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------|
+ * | 1. `$animate.animate(...)` is called | `class="my-animation"` |
+ * | 2. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
+ * | 3. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 4. the `className` class value is added to the element | `class="my-animation ng-animate className"` |
+ * | 5. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate className"` |
+ * | 6. `$animate` blocks all CSS transitions on the element to ensure the `.className` class styling is applied right away| `class="my-animation ng-animate className"` |
+ * | 7. `$animate` applies the provided collection of `from` CSS styles to the element | `class="my-animation ng-animate className"` |
+ * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate className"` |
+ * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate className"` |
+ * | 10. the `className-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate className className-active"` |
+ * | 11. `$animate` applies the collection of `to` CSS styles to the element which are then handled by the transition | `class="my-animation ng-animate className className-active"` |
+ * | 12. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate className className-active"` |
+ * | 13. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 14. The returned promise is resolved. | `class="my-animation"` |
+ *
+ * @param {DOMElement} element the element that will be the focus of the enter animation
+ * @param {object} from a collection of CSS styles that will be applied to the element at the start of the animation
+ * @param {object} to a collection of CSS styles that the element will animate towards
+ * @param {string=} className an optional CSS class that will be added to the element for the duration of the animation (the default class is `ng-inline-animate`)
+ * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ animate: function(element, from, to, className, options) {
+ className = className || 'ng-inline-animate';
+ options = parseAnimateOptions(options) || {};
+ options.from = to ? from : null;
+ options.to = to ? to : from;
+
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('animate', className, stripCommentsFromElement(element), null, null, noop, options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#enter
+ * @kind function
+ *
+ * @description
+ * Appends the element to the parentElement element that resides in the document and then runs the enter animation. Once
+ * the animation is started, the following CSS classes will be present on the element for the duration of the animation:
+ *
+ * Below is a breakdown of each step that occurs during enter animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
+ * | 1. `$animate.enter(...)` is called | `class="my-animation"` |
+ * | 2. element is inserted into the `parentElement` element or beside the `afterElement` element | `class="my-animation"` |
+ * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
+ * | 4. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 5. the `.ng-enter` class is added to the element | `class="my-animation ng-animate ng-enter"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-enter"` |
+ * | 7. `$animate` blocks all CSS transitions on the element to ensure the `.ng-enter` class styling is applied right away | `class="my-animation ng-animate ng-enter"` |
+ * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-enter"` |
+ * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-enter"` |
+ * | 10. the `.ng-enter-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-enter ng-enter-active"` |
+ * | 11. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-enter ng-enter-active"` |
+ * | 12. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 13. The returned promise is resolved. | `class="my-animation"` |
+ *
+ * @param {DOMElement} element the element that will be the focus of the enter animation
+ * @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation
+ * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation
+ * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ enter: function(element, parentElement, afterElement, options) {
+ options = parseAnimateOptions(options);
+ element = angular.element(element);
+ parentElement = prepareElement(parentElement);
+ afterElement = prepareElement(afterElement);
+
+ classBasedAnimationsBlocked(element, true);
+ $delegate.enter(element, parentElement, afterElement);
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#leave
+ * @kind function
+ *
+ * @description
+ * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once
+ * the animation is started, the following CSS classes will be added for the duration of the animation:
+ *
+ * Below is a breakdown of each step that occurs during leave animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
+ * | 1. `$animate.leave(...)` is called | `class="my-animation"` |
+ * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
+ * | 4. the `.ng-leave` class is added to the element | `class="my-animation ng-animate ng-leave"` |
+ * | 5. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-leave"` |
+ * | 6. `$animate` blocks all CSS transitions on the element to ensure the `.ng-leave` class styling is applied right away | `class="my-animation ng-animate ng-leave"` |
+ * | 7. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-leave"` |
+ * | 8. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-leave"` |
+ * | 9. the `.ng-leave-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-leave ng-leave-active"` |
+ * | 10. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-leave ng-leave-active"` |
+ * | 11. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 12. The element is removed from the DOM | ... |
+ * | 13. The returned promise is resolved. | ... |
+ *
+ * @param {DOMElement} element the element that will be the focus of the leave animation
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ leave: function(element, options) {
+ options = parseAnimateOptions(options);
+ element = angular.element(element);
+
+ cancelChildAnimations(element);
+ classBasedAnimationsBlocked(element, true);
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() {
+ $delegate.leave(element);
+ }, options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#move
+ * @kind function
+ *
+ * @description
+ * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parentElement container or
+ * add the element directly after the afterElement element if present. Then the move animation will be run. Once
+ * the animation is started, the following CSS classes will be added for the duration of the animation:
+ *
+ * Below is a breakdown of each step that occurs during move animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |----------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
+ * | 1. `$animate.move(...)` is called | `class="my-animation"` |
+ * | 2. element is moved into the parentElement element or beside the afterElement element | `class="my-animation"` |
+ * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
+ * | 4. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 5. the `.ng-move` class is added to the element | `class="my-animation ng-animate ng-move"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-move"` |
+ * | 7. `$animate` blocks all CSS transitions on the element to ensure the `.ng-move` class styling is applied right away | `class="my-animation ng-animate ng-move"` |
+ * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-move"` |
+ * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-move"` |
+ * | 10. the `.ng-move-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-move ng-move-active"` |
+ * | 11. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-move ng-move-active"` |
+ * | 12. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 13. The returned promise is resolved. | `class="my-animation"` |
+ *
+ * @param {DOMElement} element the element that will be the focus of the move animation
+ * @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation
+ * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ move: function(element, parentElement, afterElement, options) {
+ options = parseAnimateOptions(options);
+ element = angular.element(element);
+ parentElement = prepareElement(parentElement);
+ afterElement = prepareElement(afterElement);
+
+ cancelChildAnimations(element);
+ classBasedAnimationsBlocked(element, true);
+ $delegate.move(element, parentElement, afterElement);
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#addClass
+ *
+ * @description
+ * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class.
+ * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide
+ * the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions
+ * or keyframes are defined on the -add-active or base CSS class).
+ *
+ * Below is a breakdown of each step that occurs during addClass animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
+ * | 1. `$animate.addClass(element, 'super')` is called | `class="my-animation"` |
+ * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 3. the `.super-add` class is added to the element | `class="my-animation ng-animate super-add"` |
+ * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate super-add"` |
+ * | 5. the `.super` and `.super-add-active` classes are added (this triggers the CSS transition/animation) | `class="my-animation ng-animate super super-add super-add-active"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate super super-add super-add-active"` |
+ * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate super super-add super-add-active"` |
+ * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation super"` |
+ * | 9. The super class is kept on the element | `class="my-animation super"` |
+ * | 10. The returned promise is resolved. | `class="my-animation super"` |
+ *
+ * @param {DOMElement} element the element that will be animated
+ * @param {string} className the CSS class that will be added to the element and then animated
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ addClass: function(element, className, options) {
+ return this.setClass(element, className, [], options);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#removeClass
+ *
+ * @description
+ * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value
+ * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in
+ * order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if
+ * no CSS transitions or keyframes are defined on the -remove or base CSS classes).
+ *
+ * Below is a breakdown of each step that occurs during removeClass animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
+ * | 1. `$animate.removeClass(element, 'super')` is called | `class="my-animation super"` |
+ * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation super ng-animate"` |
+ * | 3. the `.super-remove` class is added to the element | `class="my-animation super ng-animate super-remove"` |
+ * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation super ng-animate super-remove"` |
+ * | 5. the `.super-remove-active` classes are added and `.super` is removed (this triggers the CSS transition/animation) | `class="my-animation ng-animate super-remove super-remove-active"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate super-remove super-remove-active"` |
+ * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate super-remove super-remove-active"` |
+ * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 9. The returned promise is resolved. | `class="my-animation"` |
+ *
+ *
+ * @param {DOMElement} element the element that will be animated
+ * @param {string} className the CSS class that will be animated and then removed from the element
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ removeClass: function(element, className, options) {
+ return this.setClass(element, [], className, options);
+ },
+
+ /**
+ *
+ * @ngdoc method
+ * @name $animate#setClass
+ *
+ * @description Adds and/or removes the given CSS classes to and from the element.
+ * Once complete, the `done()` callback will be fired (if provided).
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |----------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|
+ * | 1. `$animate.setClass(element, 'on', 'off')` is called | `class="my-animation off"` |
+ * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate off"` |
+ * | 3. the `.on-add` and `.off-remove` classes are added to the element | `class="my-animation ng-animate on-add off-remove off"` |
+ * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate on-add off-remove off"` |
+ * | 5. the `.on`, `.on-add-active` and `.off-remove-active` classes are added and `.off` is removed (this triggers the CSS transition/animation) | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` |
+ * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` |
+ * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation on"` |
+ * | 9. The returned promise is resolved. | `class="my-animation on"` |
+ *
+ * @param {DOMElement} element the element which will have its CSS classes changed
+ * removed from it
+ * @param {string} add the CSS classes which will be added to the element
+ * @param {string} remove the CSS class which will be removed from the element
+ * CSS classes have been set on the element
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ setClass: function(element, add, remove, options) {
+ options = parseAnimateOptions(options);
+
+ var STORAGE_KEY = '$$animateClasses';
+ element = angular.element(element);
+ element = stripCommentsFromElement(element);
+
+ if (classBasedAnimationsBlocked(element)) {
+ return $delegate.$$setClassImmediately(element, add, remove, options);
+ }
+
+ // we're using a combined array for both the add and remove
+ // operations since the ORDER OF addClass and removeClass matters
+ var classes, cache = element.data(STORAGE_KEY);
+ var hasCache = !!cache;
+ if (!cache) {
+ cache = {};
+ cache.classes = {};
+ }
+ classes = cache.classes;
+
+ add = isArray(add) ? add : add.split(' ');
+ forEach(add, function(c) {
+ if (c && c.length) {
+ classes[c] = true;
+ }
+ });
+
+ remove = isArray(remove) ? remove : remove.split(' ');
+ forEach(remove, function(c) {
+ if (c && c.length) {
+ classes[c] = false;
+ }
+ });
+
+ if (hasCache) {
+ if (options && cache.options) {
+ cache.options = angular.extend(cache.options || {}, options);
+ }
+
+ //the digest cycle will combine all the animations into one function
+ return cache.promise;
+ } else {
+ element.data(STORAGE_KEY, cache = {
+ classes: classes,
+ options: options
+ });
+ }
+
+ return cache.promise = runAnimationPostDigest(function(done) {
+ var parentElement = element.parent();
+ var elementNode = extractElementNode(element);
+ var parentNode = elementNode.parentNode;
+ // TODO(matsko): move this code into the animationsDisabled() function once #8092 is fixed
+ if (!parentNode || parentNode['$$NG_REMOVED'] || elementNode['$$NG_REMOVED']) {
+ done();
+ return;
+ }
+
+ var cache = element.data(STORAGE_KEY);
+ element.removeData(STORAGE_KEY);
+
+ var state = element.data(NG_ANIMATE_STATE) || {};
+ var classes = resolveElementClasses(element, cache, state.active);
+ return !classes
+ ? done()
+ : performAnimation('setClass', classes, element, parentElement, null, function() {
+ if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]);
+ if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]);
+ }, cache.options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#cancel
+ * @kind function
+ *
+ * @param {Promise} animationPromise The animation promise that is returned when an animation is started.
+ *
+ * @description
+ * Cancels the provided animation.
+ */
+ cancel: function(promise) {
+ promise.$$cancelFn();
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#enabled
+ * @kind function
+ *
+ * @param {boolean=} value If provided then set the animation on or off.
+ * @param {DOMElement=} element If provided then the element will be used to represent the enable/disable operation
+ * @return {boolean} Current animation state.
+ *
+ * @description
+ * Globally enables/disables animations.
+ *
+ */
+ enabled: function(value, element) {
+ switch (arguments.length) {
+ case 2:
+ if (value) {
+ cleanup(element);
+ } else {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+ data.disabled = true;
+ element.data(NG_ANIMATE_STATE, data);
+ }
+ break;
+
+ case 1:
+ rootAnimateState.disabled = !value;
+ break;
+
+ default:
+ value = !rootAnimateState.disabled;
+ break;
+ }
+ return !!value;
+ }
+ };
+
+ /*
+ all animations call this shared animation triggering function internally.
+ The animationEvent variable refers to the JavaScript animation event that will be triggered
+ and the className value is the name of the animation that will be applied within the
+ CSS code. Element, `parentElement` and `afterElement` are provided DOM elements for the animation
+ and the onComplete callback will be fired once the animation is fully complete.
+ */
+ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, options, doneCallback) {
+ var noopCancel = noop;
+ var runner = animationRunner(element, animationEvent, className, options);
+ if (!runner) {
+ fireDOMOperation();
+ fireBeforeCallbackAsync();
+ fireAfterCallbackAsync();
+ closeAnimation();
+ return noopCancel;
+ }
+
+ animationEvent = runner.event;
+ className = runner.className;
+ var elementEvents = angular.element._data(runner.node);
+ elementEvents = elementEvents && elementEvents.events;
+
+ if (!parentElement) {
+ parentElement = afterElement ? afterElement.parent() : element.parent();
+ }
+
+ //skip the animation if animations are disabled, a parent is already being animated,
+ //the element is not currently attached to the document body or then completely close
+ //the animation if any matching animations are not found at all.
+ //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found.
+ if (animationsDisabled(element, parentElement)) {
+ fireDOMOperation();
+ fireBeforeCallbackAsync();
+ fireAfterCallbackAsync();
+ closeAnimation();
+ return noopCancel;
+ }
+
+ var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
+ var runningAnimations = ngAnimateState.active || {};
+ var totalActiveAnimations = ngAnimateState.totalActive || 0;
+ var lastAnimation = ngAnimateState.last;
+ var skipAnimation = false;
+
+ if (totalActiveAnimations > 0) {
+ var animationsToCancel = [];
+ if (!runner.isClassBased) {
+ if (animationEvent == 'leave' && runningAnimations['ng-leave']) {
+ skipAnimation = true;
+ } else {
+ //cancel all animations when a structural animation takes place
+ for (var klass in runningAnimations) {
+ animationsToCancel.push(runningAnimations[klass]);
+ }
+ ngAnimateState = {};
+ cleanup(element, true);
+ }
+ } else if (lastAnimation.event == 'setClass') {
+ animationsToCancel.push(lastAnimation);
+ cleanup(element, className);
+ } else if (runningAnimations[className]) {
+ var current = runningAnimations[className];
+ if (current.event == animationEvent) {
+ skipAnimation = true;
+ } else {
+ animationsToCancel.push(current);
+ cleanup(element, className);
+ }
+ }
+
+ if (animationsToCancel.length > 0) {
+ forEach(animationsToCancel, function(operation) {
+ operation.cancel();
+ });
+ }
+ }
+
+ if (runner.isClassBased
+ && !runner.isSetClassOperation
+ && animationEvent != 'animate'
+ && !skipAnimation) {
+ skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR
+ }
+
+ if (skipAnimation) {
+ fireDOMOperation();
+ fireBeforeCallbackAsync();
+ fireAfterCallbackAsync();
+ fireDoneCallbackAsync();
+ return noopCancel;
+ }
+
+ runningAnimations = ngAnimateState.active || {};
+ totalActiveAnimations = ngAnimateState.totalActive || 0;
+
+ if (animationEvent == 'leave') {
+ //there's no need to ever remove the listener since the element
+ //will be removed (destroyed) after the leave animation ends or
+ //is cancelled midway
+ element.one('$destroy', function(e) {
+ var element = angular.element(this);
+ var state = element.data(NG_ANIMATE_STATE);
+ if (state) {
+ var activeLeaveAnimation = state.active['ng-leave'];
+ if (activeLeaveAnimation) {
+ activeLeaveAnimation.cancel();
+ cleanup(element, 'ng-leave');
+ }
+ }
+ });
+ }
+
+ //the ng-animate class does nothing, but it's here to allow for
+ //parent animations to find and cancel child animations when needed
+ $$jqLite.addClass(element, NG_ANIMATE_CLASS_NAME);
+ if (options && options.tempClasses) {
+ forEach(options.tempClasses, function(className) {
+ $$jqLite.addClass(element, className);
+ });
+ }
+
+ var localAnimationCount = globalAnimationCounter++;
+ totalActiveAnimations++;
+ runningAnimations[className] = runner;
+
+ element.data(NG_ANIMATE_STATE, {
+ last: runner,
+ active: runningAnimations,
+ index: localAnimationCount,
+ totalActive: totalActiveAnimations
+ });
+
+ //first we run the before animations and when all of those are complete
+ //then we perform the DOM operation and run the next set of animations
+ fireBeforeCallbackAsync();
+ runner.before(function(cancelled) {
+ var data = element.data(NG_ANIMATE_STATE);
+ cancelled = cancelled ||
+ !data || !data.active[className] ||
+ (runner.isClassBased && data.active[className].event != animationEvent);
+
+ fireDOMOperation();
+ if (cancelled === true) {
+ closeAnimation();
+ } else {
+ fireAfterCallbackAsync();
+ runner.after(closeAnimation);
+ }
+ });
+
+ return runner.cancel;
+
+ function fireDOMCallback(animationPhase) {
+ var eventName = '$animate:' + animationPhase;
+ if (elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) {
+ $$asyncCallback(function() {
+ element.triggerHandler(eventName, {
+ event: animationEvent,
+ className: className
+ });
+ });
+ }
+ }
+
+ function fireBeforeCallbackAsync() {
+ fireDOMCallback('before');
+ }
+
+ function fireAfterCallbackAsync() {
+ fireDOMCallback('after');
+ }
+
+ function fireDoneCallbackAsync() {
+ fireDOMCallback('close');
+ doneCallback();
+ }
+
+ //it is less complicated to use a flag than managing and canceling
+ //timeouts containing multiple callbacks.
+ function fireDOMOperation() {
+ if (!fireDOMOperation.hasBeenRun) {
+ fireDOMOperation.hasBeenRun = true;
+ domOperation();
+ }
+ }
+
+ function closeAnimation() {
+ if (!closeAnimation.hasBeenRun) {
+ if (runner) { //the runner doesn't exist if it fails to instantiate
+ runner.applyStyles();
+ }
+
+ closeAnimation.hasBeenRun = true;
+ if (options && options.tempClasses) {
+ forEach(options.tempClasses, function(className) {
+ $$jqLite.removeClass(element, className);
+ });
+ }
+
+ var data = element.data(NG_ANIMATE_STATE);
+ if (data) {
+
+ /* only structural animations wait for reflow before removing an
+ animation, but class-based animations don't. An example of this
+ failing would be when a parent HTML tag has a ng-class attribute
+ causing ALL directives below to skip animations during the digest */
+ if (runner && runner.isClassBased) {
+ cleanup(element, className);
+ } else {
+ $$asyncCallback(function() {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+ if (localAnimationCount == data.index) {
+ cleanup(element, className, animationEvent);
+ }
+ });
+ element.data(NG_ANIMATE_STATE, data);
+ }
+ }
+ fireDoneCallbackAsync();
+ }
+ }
+ }
+
+ function cancelChildAnimations(element) {
+ var node = extractElementNode(element);
+ if (node) {
+ var nodes = angular.isFunction(node.getElementsByClassName) ?
+ node.getElementsByClassName(NG_ANIMATE_CLASS_NAME) :
+ node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME);
+ forEach(nodes, function(element) {
+ element = angular.element(element);
+ var data = element.data(NG_ANIMATE_STATE);
+ if (data && data.active) {
+ forEach(data.active, function(runner) {
+ runner.cancel();
+ });
+ }
+ });
+ }
+ }
+
+ function cleanup(element, className) {
+ if (isMatchingElement(element, $rootElement)) {
+ if (!rootAnimateState.disabled) {
+ rootAnimateState.running = false;
+ rootAnimateState.structural = false;
+ }
+ } else if (className) {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+
+ var removeAnimations = className === true;
+ if (!removeAnimations && data.active && data.active[className]) {
+ data.totalActive--;
+ delete data.active[className];
+ }
+
+ if (removeAnimations || !data.totalActive) {
+ $$jqLite.removeClass(element, NG_ANIMATE_CLASS_NAME);
+ element.removeData(NG_ANIMATE_STATE);
+ }
+ }
+ }
+
+ function animationsDisabled(element, parentElement) {
+ if (rootAnimateState.disabled) {
+ return true;
+ }
+
+ if (isMatchingElement(element, $rootElement)) {
+ return rootAnimateState.running;
+ }
+
+ var allowChildAnimations, parentRunningAnimation, hasParent;
+ do {
+ //the element did not reach the root element which means that it
+ //is not apart of the DOM. Therefore there is no reason to do
+ //any animations on it
+ if (parentElement.length === 0) break;
+
+ var isRoot = isMatchingElement(parentElement, $rootElement);
+ var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {});
+ if (state.disabled) {
+ return true;
+ }
+
+ //no matter what, for an animation to work it must reach the root element
+ //this implies that the element is attached to the DOM when the animation is run
+ if (isRoot) {
+ hasParent = true;
+ }
+
+ //once a flag is found that is strictly false then everything before
+ //it will be discarded and all child animations will be restricted
+ if (allowChildAnimations !== false) {
+ var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN);
+ if (angular.isDefined(animateChildrenFlag)) {
+ allowChildAnimations = animateChildrenFlag;
+ }
+ }
+
+ parentRunningAnimation = parentRunningAnimation ||
+ state.running ||
+ (state.last && !state.last.isClassBased);
+ }
+ while (parentElement = parentElement.parent());
+
+ return !hasParent || (!allowChildAnimations && parentRunningAnimation);
+ }
+ }]);
+
+ $animateProvider.register('', ['$window', '$sniffer', '$timeout', '$$animateReflow',
+ function($window, $sniffer, $timeout, $$animateReflow) {
+ // Detect proper transitionend/animationend event names.
+ var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT;
+
+ // If unprefixed events are not supported but webkit-prefixed are, use the latter.
+ // Otherwise, just use W3C names, browsers not supporting them at all will just ignore them.
+ // Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend`
+ // but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`.
+ // Register both events in case `window.onanimationend` is not supported because of that,
+ // do the same for `transitionend` as Safari is likely to exhibit similar behavior.
+ // Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
+ // therefore there is no reason to test anymore for other vendor prefixes: http://caniuse.com/#search=transition
+ if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
+ CSS_PREFIX = '-webkit-';
+ TRANSITION_PROP = 'WebkitTransition';
+ TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
+ } else {
+ TRANSITION_PROP = 'transition';
+ TRANSITIONEND_EVENT = 'transitionend';
+ }
+
+ if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
+ CSS_PREFIX = '-webkit-';
+ ANIMATION_PROP = 'WebkitAnimation';
+ ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend';
+ } else {
+ ANIMATION_PROP = 'animation';
+ ANIMATIONEND_EVENT = 'animationend';
+ }
+
+ var DURATION_KEY = 'Duration';
+ var PROPERTY_KEY = 'Property';
+ var DELAY_KEY = 'Delay';
+ var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
+ var ANIMATION_PLAYSTATE_KEY = 'PlayState';
+ var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey';
+ var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data';
+ var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
+ var CLOSING_TIME_BUFFER = 1.5;
+ var ONE_SECOND = 1000;
+
+ var lookupCache = {};
+ var parentCounter = 0;
+ var animationReflowQueue = [];
+ var cancelAnimationReflow;
+ function clearCacheAfterReflow() {
+ if (!cancelAnimationReflow) {
+ cancelAnimationReflow = $$animateReflow(function() {
+ animationReflowQueue = [];
+ cancelAnimationReflow = null;
+ lookupCache = {};
+ });
+ }
+ }
+
+ function afterReflow(element, callback) {
+ if (cancelAnimationReflow) {
+ cancelAnimationReflow();
+ }
+ animationReflowQueue.push(callback);
+ cancelAnimationReflow = $$animateReflow(function() {
+ forEach(animationReflowQueue, function(fn) {
+ fn();
+ });
+
+ animationReflowQueue = [];
+ cancelAnimationReflow = null;
+ lookupCache = {};
+ });
+ }
+
+ var closingTimer = null;
+ var closingTimestamp = 0;
+ var animationElementQueue = [];
+ function animationCloseHandler(element, totalTime) {
+ var node = extractElementNode(element);
+ element = angular.element(node);
+
+ //this item will be garbage collected by the closing
+ //animation timeout
+ animationElementQueue.push(element);
+
+ //but it may not need to cancel out the existing timeout
+ //if the timestamp is less than the previous one
+ var futureTimestamp = Date.now() + totalTime;
+ if (futureTimestamp <= closingTimestamp) {
+ return;
+ }
+
+ $timeout.cancel(closingTimer);
+
+ closingTimestamp = futureTimestamp;
+ closingTimer = $timeout(function() {
+ closeAllAnimations(animationElementQueue);
+ animationElementQueue = [];
+ }, totalTime, false);
+ }
+
+ function closeAllAnimations(elements) {
+ forEach(elements, function(element) {
+ var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
+ if (elementData) {
+ forEach(elementData.closeAnimationFns, function(fn) {
+ fn();
+ });
+ }
+ });
+ }
+
+ function getElementAnimationDetails(element, cacheKey) {
+ var data = cacheKey ? lookupCache[cacheKey] : null;
+ if (!data) {
+ var transitionDuration = 0;
+ var transitionDelay = 0;
+ var animationDuration = 0;
+ var animationDelay = 0;
+
+ //we want all the styles defined before and after
+ forEach(element, function(element) {
+ if (element.nodeType == ELEMENT_NODE) {
+ var elementStyles = $window.getComputedStyle(element) || {};
+
+ var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY];
+ transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration);
+
+ var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY];
+ transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay);
+
+ var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY];
+ animationDelay = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), animationDelay);
+
+ var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]);
+
+ if (aDuration > 0) {
+ aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1;
+ }
+ animationDuration = Math.max(aDuration, animationDuration);
+ }
+ });
+ data = {
+ total: 0,
+ transitionDelay: transitionDelay,
+ transitionDuration: transitionDuration,
+ animationDelay: animationDelay,
+ animationDuration: animationDuration
+ };
+ if (cacheKey) {
+ lookupCache[cacheKey] = data;
+ }
+ }
+ return data;
+ }
+
+ function parseMaxTime(str) {
+ var maxValue = 0;
+ var values = isString(str) ?
+ str.split(/\s*,\s*/) :
+ [];
+ forEach(values, function(value) {
+ maxValue = Math.max(parseFloat(value) || 0, maxValue);
+ });
+ return maxValue;
+ }
+
+ function getCacheKey(element) {
+ var parentElement = element.parent();
+ var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY);
+ if (!parentID) {
+ parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter);
+ parentID = parentCounter;
+ }
+ return parentID + '-' + extractElementNode(element).getAttribute('class');
+ }
+
+ function animateSetup(animationEvent, element, className, styles) {
+ var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0;
+
+ var cacheKey = getCacheKey(element);
+ var eventCacheKey = cacheKey + ' ' + className;
+ var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0;
+
+ var stagger = {};
+ if (itemIndex > 0) {
+ var staggerClassName = className + '-stagger';
+ var staggerCacheKey = cacheKey + ' ' + staggerClassName;
+ var applyClasses = !lookupCache[staggerCacheKey];
+
+ applyClasses && $$jqLite.addClass(element, staggerClassName);
+
+ stagger = getElementAnimationDetails(element, staggerCacheKey);
+
+ applyClasses && $$jqLite.removeClass(element, staggerClassName);
+ }
+
+ $$jqLite.addClass(element, className);
+
+ var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {};
+ var timings = getElementAnimationDetails(element, eventCacheKey);
+ var transitionDuration = timings.transitionDuration;
+ var animationDuration = timings.animationDuration;
+
+ if (structural && transitionDuration === 0 && animationDuration === 0) {
+ $$jqLite.removeClass(element, className);
+ return false;
+ }
+
+ var blockTransition = styles || (structural && transitionDuration > 0);
+ var blockAnimation = animationDuration > 0 &&
+ stagger.animationDelay > 0 &&
+ stagger.animationDuration === 0;
+
+ var closeAnimationFns = formerData.closeAnimationFns || [];
+ element.data(NG_ANIMATE_CSS_DATA_KEY, {
+ stagger: stagger,
+ cacheKey: eventCacheKey,
+ running: formerData.running || 0,
+ itemIndex: itemIndex,
+ blockTransition: blockTransition,
+ closeAnimationFns: closeAnimationFns
+ });
+
+ var node = extractElementNode(element);
+
+ if (blockTransition) {
+ blockTransitions(node, true);
+ if (styles) {
+ element.css(styles);
+ }
+ }
+
+ if (blockAnimation) {
+ blockAnimations(node, true);
+ }
+
+ return true;
+ }
+
+ function animateRun(animationEvent, element, className, activeAnimationComplete, styles) {
+ var node = extractElementNode(element);
+ var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
+ if (node.getAttribute('class').indexOf(className) == -1 || !elementData) {
+ activeAnimationComplete();
+ return;
+ }
+
+ var activeClassName = '';
+ var pendingClassName = '';
+ forEach(className.split(' '), function(klass, i) {
+ var prefix = (i > 0 ? ' ' : '') + klass;
+ activeClassName += prefix + '-active';
+ pendingClassName += prefix + '-pending';
+ });
+
+ var style = '';
+ var appliedStyles = [];
+ var itemIndex = elementData.itemIndex;
+ var stagger = elementData.stagger;
+ var staggerTime = 0;
+ if (itemIndex > 0) {
+ var transitionStaggerDelay = 0;
+ if (stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
+ transitionStaggerDelay = stagger.transitionDelay * itemIndex;
+ }
+
+ var animationStaggerDelay = 0;
+ if (stagger.animationDelay > 0 && stagger.animationDuration === 0) {
+ animationStaggerDelay = stagger.animationDelay * itemIndex;
+ appliedStyles.push(CSS_PREFIX + 'animation-play-state');
+ }
+
+ staggerTime = Math.round(Math.max(transitionStaggerDelay, animationStaggerDelay) * 100) / 100;
+ }
+
+ if (!staggerTime) {
+ $$jqLite.addClass(element, activeClassName);
+ if (elementData.blockTransition) {
+ blockTransitions(node, false);
+ }
+ }
+
+ var eventCacheKey = elementData.cacheKey + ' ' + activeClassName;
+ var timings = getElementAnimationDetails(element, eventCacheKey);
+ var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
+ if (maxDuration === 0) {
+ $$jqLite.removeClass(element, activeClassName);
+ animateClose(element, className);
+ activeAnimationComplete();
+ return;
+ }
+
+ if (!staggerTime && styles && Object.keys(styles).length > 0) {
+ if (!timings.transitionDuration) {
+ element.css('transition', timings.animationDuration + 's linear all');
+ appliedStyles.push('transition');
+ }
+ element.css(styles);
+ }
+
+ var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
+ var maxDelayTime = maxDelay * ONE_SECOND;
+
+ if (appliedStyles.length > 0) {
+ //the element being animated may sometimes contain comment nodes in
+ //the jqLite object, so we're safe to use a single variable to house
+ //the styles since there is always only one element being animated
+ var oldStyle = node.getAttribute('style') || '';
+ if (oldStyle.charAt(oldStyle.length - 1) !== ';') {
+ oldStyle += ';';
+ }
+ node.setAttribute('style', oldStyle + ' ' + style);
+ }
+
+ var startTime = Date.now();
+ var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;
+ var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER;
+ var totalTime = (staggerTime + animationTime) * ONE_SECOND;
+
+ var staggerTimeout;
+ if (staggerTime > 0) {
+ $$jqLite.addClass(element, pendingClassName);
+ staggerTimeout = $timeout(function() {
+ staggerTimeout = null;
+
+ if (timings.transitionDuration > 0) {
+ blockTransitions(node, false);
+ }
+ if (timings.animationDuration > 0) {
+ blockAnimations(node, false);
+ }
+
+ $$jqLite.addClass(element, activeClassName);
+ $$jqLite.removeClass(element, pendingClassName);
+
+ if (styles) {
+ if (timings.transitionDuration === 0) {
+ element.css('transition', timings.animationDuration + 's linear all');
+ }
+ element.css(styles);
+ appliedStyles.push('transition');
+ }
+ }, staggerTime * ONE_SECOND, false);
+ }
+
+ element.on(css3AnimationEvents, onAnimationProgress);
+ elementData.closeAnimationFns.push(function() {
+ onEnd();
+ activeAnimationComplete();
+ });
+
+ elementData.running++;
+ animationCloseHandler(element, totalTime);
+ return onEnd;
+
+ // This will automatically be called by $animate so
+ // there is no need to attach this internally to the
+ // timeout done method.
+ function onEnd() {
+ element.off(css3AnimationEvents, onAnimationProgress);
+ $$jqLite.removeClass(element, activeClassName);
+ $$jqLite.removeClass(element, pendingClassName);
+ if (staggerTimeout) {
+ $timeout.cancel(staggerTimeout);
+ }
+ animateClose(element, className);
+ var node = extractElementNode(element);
+ for (var i in appliedStyles) {
+ node.style.removeProperty(appliedStyles[i]);
+ }
+ }
+
+ function onAnimationProgress(event) {
+ event.stopPropagation();
+ var ev = event.originalEvent || event;
+ var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now();
+
+ /* Firefox (or possibly just Gecko) likes to not round values up
+ * when a ms measurement is used for the animation */
+ var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES));
+
+ /* $manualTimeStamp is a mocked timeStamp value which is set
+ * within browserTrigger(). This is only here so that tests can
+ * mock animations properly. Real events fallback to event.timeStamp,
+ * or, if they don't, then a timeStamp is automatically created for them.
+ * We're checking to see if the timeStamp surpasses the expected delay,
+ * but we're using elapsedTime instead of the timeStamp on the 2nd
+ * pre-condition since animations sometimes close off early */
+ if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
+ activeAnimationComplete();
+ }
+ }
+ }
+
+ function blockTransitions(node, bool) {
+ node.style[TRANSITION_PROP + PROPERTY_KEY] = bool ? 'none' : '';
+ }
+
+ function blockAnimations(node, bool) {
+ node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : '';
+ }
+
+ function animateBefore(animationEvent, element, className, styles) {
+ if (animateSetup(animationEvent, element, className, styles)) {
+ return function(cancelled) {
+ cancelled && animateClose(element, className);
+ };
+ }
+ }
+
+ function animateAfter(animationEvent, element, className, afterAnimationComplete, styles) {
+ if (element.data(NG_ANIMATE_CSS_DATA_KEY)) {
+ return animateRun(animationEvent, element, className, afterAnimationComplete, styles);
+ } else {
+ animateClose(element, className);
+ afterAnimationComplete();
+ }
+ }
+
+ function animate(animationEvent, element, className, animationComplete, options) {
+ //If the animateSetup function doesn't bother returning a
+ //cancellation function then it means that there is no animation
+ //to perform at all
+ var preReflowCancellation = animateBefore(animationEvent, element, className, options.from);
+ if (!preReflowCancellation) {
+ clearCacheAfterReflow();
+ animationComplete();
+ return;
+ }
+
+ //There are two cancellation functions: one is before the first
+ //reflow animation and the second is during the active state
+ //animation. The first function will take care of removing the
+ //data from the element which will not make the 2nd animation
+ //happen in the first place
+ var cancel = preReflowCancellation;
+ afterReflow(element, function() {
+ //once the reflow is complete then we point cancel to
+ //the new cancellation function which will remove all of the
+ //animation properties from the active animation
+ cancel = animateAfter(animationEvent, element, className, animationComplete, options.to);
+ });
+
+ return function(cancelled) {
+ (cancel || noop)(cancelled);
+ };
+ }
+
+ function animateClose(element, className) {
+ $$jqLite.removeClass(element, className);
+ var data = element.data(NG_ANIMATE_CSS_DATA_KEY);
+ if (data) {
+ if (data.running) {
+ data.running--;
+ }
+ if (!data.running || data.running === 0) {
+ element.removeData(NG_ANIMATE_CSS_DATA_KEY);
+ }
+ }
+ }
+
+ return {
+ animate: function(element, className, from, to, animationCompleted, options) {
+ options = options || {};
+ options.from = from;
+ options.to = to;
+ return animate('animate', element, className, animationCompleted, options);
+ },
+
+ enter: function(element, animationCompleted, options) {
+ options = options || {};
+ return animate('enter', element, 'ng-enter', animationCompleted, options);
+ },
+
+ leave: function(element, animationCompleted, options) {
+ options = options || {};
+ return animate('leave', element, 'ng-leave', animationCompleted, options);
+ },
+
+ move: function(element, animationCompleted, options) {
+ options = options || {};
+ return animate('move', element, 'ng-move', animationCompleted, options);
+ },
+
+ beforeSetClass: function(element, add, remove, animationCompleted, options) {
+ options = options || {};
+ var className = suffixClasses(remove, '-remove') + ' ' +
+ suffixClasses(add, '-add');
+ var cancellationMethod = animateBefore('setClass', element, className, options.from);
+ if (cancellationMethod) {
+ afterReflow(element, animationCompleted);
+ return cancellationMethod;
+ }
+ clearCacheAfterReflow();
+ animationCompleted();
+ },
+
+ beforeAddClass: function(element, className, animationCompleted, options) {
+ options = options || {};
+ var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), options.from);
+ if (cancellationMethod) {
+ afterReflow(element, animationCompleted);
+ return cancellationMethod;
+ }
+ clearCacheAfterReflow();
+ animationCompleted();
+ },
+
+ beforeRemoveClass: function(element, className, animationCompleted, options) {
+ options = options || {};
+ var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), options.from);
+ if (cancellationMethod) {
+ afterReflow(element, animationCompleted);
+ return cancellationMethod;
+ }
+ clearCacheAfterReflow();
+ animationCompleted();
+ },
+
+ setClass: function(element, add, remove, animationCompleted, options) {
+ options = options || {};
+ remove = suffixClasses(remove, '-remove');
+ add = suffixClasses(add, '-add');
+ var className = remove + ' ' + add;
+ return animateAfter('setClass', element, className, animationCompleted, options.to);
+ },
+
+ addClass: function(element, className, animationCompleted, options) {
+ options = options || {};
+ return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted, options.to);
+ },
+
+ removeClass: function(element, className, animationCompleted, options) {
+ options = options || {};
+ return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted, options.to);
+ }
+ };
+
+ function suffixClasses(classes, suffix) {
+ var className = '';
+ classes = isArray(classes) ? classes : classes.split(/\s+/);
+ forEach(classes, function(klass, i) {
+ if (klass && klass.length > 0) {
+ className += (i > 0 ? ' ' : '') + klass + suffix;
+ }
+ });
+ return className;
+ }
+ }]);
+ }]);
+
+
+})(window, window.angular);
diff --git a/dependencies/angular-sanitize.js b/dependencies/angular-sanitize.js
new file mode 100644
index 00000000..d0366949
--- /dev/null
+++ b/dependencies/angular-sanitize.js
@@ -0,0 +1,680 @@
+/**
+ * @license AngularJS v1.3.12
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+var $sanitizeMinErr = angular.$$minErr('$sanitize');
+
+/**
+ * @ngdoc module
+ * @name ngSanitize
+ * @description
+ *
+ * # ngSanitize
+ *
+ * The `ngSanitize` module provides functionality to sanitize HTML.
+ *
+ *
+ *
+ *
+ * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
+ */
+
+/*
+ * HTML Parser By Misko Hevery (misko@hevery.com)
+ * based on: HTML Parser By John Resig (ejohn.org)
+ * Original code by Erik Arvidsson, Mozilla Public License
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
+ *
+ * // Use like so:
+ * htmlParser(htmlString, {
+ * start: function(tag, attrs, unary) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * });
+ *
+ */
+
+
+/**
+ * @ngdoc service
+ * @name $sanitize
+ * @kind function
+ *
+ * @description
+ * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
+ * then serialized back to properly escaped html string. This means that no unsafe input can make
+ * it into the returned string, however, since our parser is more strict than a typical browser
+ * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
+ * browser, won't make it through the sanitizer. The input may also contain SVG markup.
+ * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
+ * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
+ *
+ * @param {string} html HTML input.
+ * @returns {string} Sanitized HTML.
+ *
+ * @example
+
+
+
+
+ Snippet:
+
+
+
Directive
+
How
+
Source
+
Rendered
+
+
+
ng-bind-html
+
Automatically uses $sanitize
+
<div ng-bind-html="snippet"> </div>
+
+
+
+
ng-bind-html
+
Bypass $sanitize by explicitly trusting the dangerous value
');
+ var backdropHolds = 0;
+
+ $document[0].body.appendChild(el[0]);
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicBackdrop#retain
+ * @description Retains the backdrop.
+ */
+ retain: retain,
+ /**
+ * @ngdoc method
+ * @name $ionicBackdrop#release
+ * @description
+ * Releases the backdrop.
+ */
+ release: release,
+
+ getElement: getElement,
+
+ // exposed for testing
+ _element: el
+ };
+
+ function retain() {
+ if ((++backdropHolds) === 1) {
+ el.addClass('visible');
+ ionic.requestAnimationFrame(function() {
+ backdropHolds && el.addClass('active');
+ });
+ }
+ }
+ function release() {
+ if ((--backdropHolds) === 0) {
+ el.removeClass('active');
+ $timeout(function() {
+ !backdropHolds && el.removeClass('visible');
+ }, 400, false);
+ }
+ }
+
+ function getElement() {
+ return el;
+ }
+
+}]);
+
+/**
+ * @private
+ */
+IonicModule
+.factory('$ionicBind', ['$parse', '$interpolate', function($parse, $interpolate) {
+ var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
+ return function(scope, attrs, bindDefinition) {
+ forEach(bindDefinition || {}, function (definition, scopeName) {
+ //Adapted from angular.js $compile
+ var match = definition.match(LOCAL_REGEXP) || [],
+ attrName = match[3] || scopeName,
+ mode = match[1], // @, =, or &
+ parentGet,
+ unwatch;
+
+ switch(mode) {
+ case '@':
+ if (!attrs[attrName]) {
+ return;
+ }
+ attrs.$observe(attrName, function(value) {
+ scope[scopeName] = value;
+ });
+ // we trigger an interpolation to ensure
+ // the value is there for use immediately
+ if (attrs[attrName]) {
+ scope[scopeName] = $interpolate(attrs[attrName])(scope);
+ }
+ break;
+
+ case '=':
+ if (!attrs[attrName]) {
+ return;
+ }
+ unwatch = scope.$watch(attrs[attrName], function(value) {
+ scope[scopeName] = value;
+ });
+ //Destroy parent scope watcher when this scope is destroyed
+ scope.$on('$destroy', unwatch);
+ break;
+
+ case '&':
+ /* jshint -W044 */
+ if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) {
+ throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' +
+ attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.');
+ }
+ parentGet = $parse(attrs[attrName]);
+ scope[scopeName] = function(locals) {
+ return parentGet(scope, locals);
+ };
+ break;
+ }
+ });
+ };
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicBody
+ * @module ionic
+ * @description An angular utility service to easily and efficiently
+ * add and remove CSS classes from the document's body element.
+ */
+IonicModule
+.factory('$ionicBody', ['$document', function($document) {
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicBody#add
+ * @description Add a class to the document's body element.
+ * @param {string} class Each argument will be added to the body element.
+ * @returns {$ionicBody} The $ionicBody service so methods can be chained.
+ */
+ addClass: function() {
+ for (var x = 0; x < arguments.length; x++) {
+ $document[0].body.classList.add(arguments[x]);
+ }
+ return this;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicBody#removeClass
+ * @description Remove a class from the document's body element.
+ * @param {string} class Each argument will be removed from the body element.
+ * @returns {$ionicBody} The $ionicBody service so methods can be chained.
+ */
+ removeClass: function() {
+ for (var x = 0; x < arguments.length; x++) {
+ $document[0].body.classList.remove(arguments[x]);
+ }
+ return this;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicBody#enableClass
+ * @description Similar to the `add` method, except the first parameter accepts a boolean
+ * value determining if the class should be added or removed. Rather than writing user code,
+ * such as "if true then add the class, else then remove the class", this method can be
+ * given a true or false value which reduces redundant code.
+ * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed.
+ * @param {string} class Each remaining argument would be added or removed depending on
+ * the first argument.
+ * @returns {$ionicBody} The $ionicBody service so methods can be chained.
+ */
+ enableClass: function(shouldEnableClass) {
+ var args = Array.prototype.slice.call(arguments).slice(1);
+ if (shouldEnableClass) {
+ this.addClass.apply(this, args);
+ } else {
+ this.removeClass.apply(this, args);
+ }
+ return this;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicBody#append
+ * @description Append a child to the document's body.
+ * @param {element} element The element to be appended to the body. The passed in element
+ * can be either a jqLite element, or a DOM element.
+ * @returns {$ionicBody} The $ionicBody service so methods can be chained.
+ */
+ append: function(ele) {
+ $document[0].body.appendChild(ele.length ? ele[0] : ele);
+ return this;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicBody#get
+ * @description Get the document's body element.
+ * @returns {element} Returns the document's body element.
+ */
+ get: function() {
+ return $document[0].body;
+ }
+ };
+}]);
+
+IonicModule
+.factory('$ionicClickBlock', [
+ '$document',
+ '$ionicBody',
+ '$timeout',
+function($document, $ionicBody, $timeout) {
+ var CSS_HIDE = 'click-block-hide';
+ var cbEle, fallbackTimer, pendingShow;
+
+ function addClickBlock() {
+ if (pendingShow) {
+ if (cbEle) {
+ cbEle.classList.remove(CSS_HIDE);
+ } else {
+ cbEle = $document[0].createElement('div');
+ cbEle.className = 'click-block';
+ $ionicBody.append(cbEle);
+ }
+ pendingShow = false;
+ }
+ }
+
+ function removeClickBlock() {
+ cbEle && cbEle.classList.add(CSS_HIDE);
+ }
+
+ return {
+ show: function(autoExpire) {
+ pendingShow = true;
+ $timeout.cancel(fallbackTimer);
+ fallbackTimer = $timeout(this.hide, autoExpire || 310);
+ ionic.requestAnimationFrame(addClickBlock);
+ },
+ hide: function() {
+ pendingShow = false;
+ $timeout.cancel(fallbackTimer);
+ ionic.requestAnimationFrame(removeClickBlock);
+ }
+ };
+}]);
+
+IonicModule
+.factory('$collectionDataSource', [
+ '$cacheFactory',
+ '$parse',
+ '$rootScope',
+function($cacheFactory, $parse, $rootScope) {
+ function hideWithTransform(element) {
+ element.css(ionic.CSS.TRANSFORM, 'translate3d(-2000px,-2000px,0)');
+ }
+
+ function CollectionRepeatDataSource(options) {
+ var self = this;
+ this.scope = options.scope;
+ this.transcludeFn = options.transcludeFn;
+ this.transcludeParent = options.transcludeParent;
+ this.element = options.element;
+
+ this.keyExpr = options.keyExpr;
+ this.listExpr = options.listExpr;
+ this.trackByExpr = options.trackByExpr;
+
+ this.heightGetter = options.heightGetter;
+ this.widthGetter = options.widthGetter;
+
+ this.dimensions = [];
+ this.data = [];
+
+ this.attachedItems = {};
+ this.BACKUP_ITEMS_LENGTH = 20;
+ this.backupItemsArray = [];
+ }
+ CollectionRepeatDataSource.prototype = {
+ setup: function() {
+ if (this.isSetup) return;
+ this.isSetup = true;
+ for (var i = 0; i < this.BACKUP_ITEMS_LENGTH; i++) {
+ this.detachItem(this.createItem());
+ }
+ },
+ destroy: function() {
+ this.dimensions.length = 0;
+ this.data = null;
+ this.backupItemsArray.length = 0;
+ this.attachedItems = {};
+ },
+ calculateDataDimensions: function() {
+ var locals = {};
+ this.dimensions = this.data.map(function(value, index) {
+ locals[this.keyExpr] = value;
+ locals.$index = index;
+ return {
+ width: this.widthGetter(this.scope, locals),
+ height: this.heightGetter(this.scope, locals)
+ };
+ }, this);
+ this.dimensions = this.beforeSiblings.concat(this.dimensions).concat(this.afterSiblings);
+ this.dataStartIndex = this.beforeSiblings.length;
+ },
+ createItem: function() {
+ var item = {};
+
+ item.scope = this.scope.$new();
+ this.transcludeFn(item.scope, function(clone) {
+ clone.css('position', 'absolute');
+ item.element = clone;
+ });
+ this.transcludeParent.append(item.element);
+
+ return item;
+ },
+ getItem: function(index) {
+ var item;
+ if ( (item = this.attachedItems[index]) ) {
+ //do nothing, the item is good
+ } else if ( (item = this.backupItemsArray.pop()) ) {
+ ionic.Utils.reconnectScope(item.scope);
+ } else {
+ item = this.createItem();
+ }
+ return item;
+ },
+ attachItemAtIndex: function(index) {
+ if (index < this.dataStartIndex) {
+ return this.beforeSiblings[index];
+ }
+ // Subtract so we start at the beginning of this.data, after
+ // this.beforeSiblings.
+ index -= this.dataStartIndex;
+
+ if (index > this.data.length - 1) {
+ return this.afterSiblings[index - this.dataStartIndex];
+ }
+
+ var item = this.getItem(index);
+ var value = this.data[index];
+
+ if (item.index !== index || item.scope[this.keyExpr] !== value) {
+ item.index = item.scope.$index = index;
+ item.scope[this.keyExpr] = value;
+ item.scope.$first = (index === 0);
+ item.scope.$last = (index === (this.getLength() - 1));
+ item.scope.$middle = !(item.scope.$first || item.scope.$last);
+ item.scope.$odd = !(item.scope.$even = (index&1) === 0);
+
+ //We changed the scope, so digest if needed
+ if (!$rootScope.$$phase) {
+ item.scope.$digest();
+ }
+ }
+ this.attachedItems[index] = item;
+
+ return item;
+ },
+ destroyItem: function(item) {
+ item.element.remove();
+ item.scope.$destroy();
+ item.scope = null;
+ item.element = null;
+ },
+ detachItem: function(item) {
+ delete this.attachedItems[item.index];
+
+ //If it's an outside item, only hide it. These items aren't part of collection
+ //repeat's list, only sit outside
+ if (item.isOutside) {
+ hideWithTransform(item.element);
+ // If we are at the limit of backup items, just get rid of the this element
+ } else if (this.backupItemsArray.length >= this.BACKUP_ITEMS_LENGTH) {
+ this.destroyItem(item);
+ // Otherwise, add it to our backup items
+ } else {
+ this.backupItemsArray.push(item);
+ hideWithTransform(item.element);
+ //Don't .$destroy(), just stop watchers and events firing
+ ionic.Utils.disconnectScope(item.scope);
+ }
+
+ },
+ getLength: function() {
+ return this.dimensions && this.dimensions.length || 0;
+ },
+ setData: function(value, beforeSiblings, afterSiblings) {
+ this.data = value || [];
+ this.beforeSiblings = beforeSiblings || [];
+ this.afterSiblings = afterSiblings || [];
+ this.calculateDataDimensions();
+
+ this.afterSiblings.forEach(function(item) {
+ item.element.css({position: 'absolute', top: '0', left: '0' });
+ hideWithTransform(item.element);
+ });
+ },
+ };
+
+ return CollectionRepeatDataSource;
+}]);
+
+
+IonicModule
+.factory('$collectionRepeatManager', [
+ '$rootScope',
+ '$timeout',
+function($rootScope, $timeout) {
+ /**
+ * Vocabulary: "primary" and "secondary" size/direction/position mean
+ * "y" and "x" for vertical scrolling, or "x" and "y" for horizontal scrolling.
+ */
+ function CollectionRepeatManager(options) {
+ var self = this;
+ this.dataSource = options.dataSource;
+ this.element = options.element;
+ this.scrollView = options.scrollView;
+
+ this.isVertical = !!this.scrollView.options.scrollingY;
+ this.renderedItems = {};
+ this.dimensions = [];
+ this.setCurrentIndex(0);
+
+ //Override scrollview's render callback
+ this.scrollView.__$callback = this.scrollView.__callback;
+ this.scrollView.__callback = angular.bind(this, this.renderScroll);
+
+ function getViewportSize() { return self.viewportSize; }
+ //Set getters and setters to match whether this scrollview is vertical or not
+ if (this.isVertical) {
+ this.scrollView.options.getContentHeight = getViewportSize;
+
+ this.scrollValue = function() {
+ return this.scrollView.__scrollTop;
+ };
+ this.scrollMaxValue = function() {
+ return this.scrollView.__maxScrollTop;
+ };
+ this.scrollSize = function() {
+ return this.scrollView.__clientHeight;
+ };
+ this.secondaryScrollSize = function() {
+ return this.scrollView.__clientWidth;
+ };
+ this.transformString = function(y, x) {
+ return 'translate3d('+x+'px,'+y+'px,0)';
+ };
+ this.primaryDimension = function(dim) {
+ return dim.height;
+ };
+ this.secondaryDimension = function(dim) {
+ return dim.width;
+ };
+ } else {
+ this.scrollView.options.getContentWidth = getViewportSize;
+
+ this.scrollValue = function() {
+ return this.scrollView.__scrollLeft;
+ };
+ this.scrollMaxValue = function() {
+ return this.scrollView.__maxScrollLeft;
+ };
+ this.scrollSize = function() {
+ return this.scrollView.__clientWidth;
+ };
+ this.secondaryScrollSize = function() {
+ return this.scrollView.__clientHeight;
+ };
+ this.transformString = function(x, y) {
+ return 'translate3d('+x+'px,'+y+'px,0)';
+ };
+ this.primaryDimension = function(dim) {
+ return dim.width;
+ };
+ this.secondaryDimension = function(dim) {
+ return dim.height;
+ };
+ }
+ }
+
+ CollectionRepeatManager.prototype = {
+ destroy: function() {
+ this.renderedItems = {};
+ this.render = angular.noop;
+ this.calculateDimensions = angular.noop;
+ this.dimensions = [];
+ },
+
+ /*
+ * Pre-calculate the position of all items in the data list.
+ * Do this using the provided width and height (primarySize and secondarySize)
+ * provided by the dataSource.
+ */
+ calculateDimensions: function() {
+ /*
+ * For the sake of explanations below, we're going to pretend we are scrolling
+ * vertically: Items are laid out with primarySize being height,
+ * secondarySize being width.
+ */
+ var primaryPos = 0;
+ var secondaryPos = 0;
+ var secondaryScrollSize = this.secondaryScrollSize();
+ var previousItem;
+
+ this.dataSource.beforeSiblings && this.dataSource.beforeSiblings.forEach(calculateSize, this);
+ var beforeSize = primaryPos + (previousItem ? previousItem.primarySize : 0);
+
+ primaryPos = secondaryPos = 0;
+ previousItem = null;
+
+ var dimensions = this.dataSource.dimensions.map(calculateSize, this);
+ var totalSize = primaryPos + (previousItem ? previousItem.primarySize : 0);
+
+ return {
+ beforeSize: beforeSize,
+ totalSize: totalSize,
+ dimensions: dimensions
+ };
+
+ function calculateSize(dim) {
+
+ //Each dimension is an object {width: Number, height: Number} provided by
+ //the dataSource
+ var rect = {
+ //Get the height out of the dimension object
+ primarySize: this.primaryDimension(dim),
+ //Max out the item's width to the width of the scrollview
+ secondarySize: Math.min(this.secondaryDimension(dim), secondaryScrollSize)
+ };
+
+ //If this isn't the first item
+ if (previousItem) {
+ //Move the item's x position over by the width of the previous item
+ secondaryPos += previousItem.secondarySize;
+ //If the y position is the same as the previous item and
+ //the x position is bigger than the scroller's width
+ if (previousItem.primaryPos === primaryPos &&
+ secondaryPos + rect.secondarySize > secondaryScrollSize) {
+ //Then go to the next row, with x position 0
+ secondaryPos = 0;
+ primaryPos += previousItem.primarySize;
+ }
+ }
+
+ rect.primaryPos = primaryPos;
+ rect.secondaryPos = secondaryPos;
+
+ previousItem = rect;
+ return rect;
+ }
+ },
+ resize: function() {
+ var result = this.calculateDimensions();
+ this.dimensions = result.dimensions;
+ this.viewportSize = result.totalSize;
+ this.beforeSize = result.beforeSize;
+ this.setCurrentIndex(0);
+ this.render(true);
+ this.dataSource.setup();
+ },
+ /*
+ * setCurrentIndex sets the index in the list that matches the scroller's position.
+ * Also save the position in the scroller for next and previous items (if they exist)
+ */
+ setCurrentIndex: function(index, height) {
+ var currentPos = (this.dimensions[index] || {}).primaryPos || 0;
+ this.currentIndex = index;
+
+ this.hasPrevIndex = index > 0;
+ if (this.hasPrevIndex) {
+ this.previousPos = Math.max(
+ currentPos - this.dimensions[index - 1].primarySize,
+ this.dimensions[index - 1].primaryPos
+ );
+ }
+ this.hasNextIndex = index + 1 < this.dataSource.getLength();
+ if (this.hasNextIndex) {
+ this.nextPos = Math.min(
+ currentPos + this.dimensions[index + 1].primarySize,
+ this.dimensions[index + 1].primaryPos
+ );
+ }
+ },
+ /**
+ * override the scroller's render callback to check if we need to
+ * re-render our collection
+ */
+ renderScroll: ionic.animationFrameThrottle(function(transformLeft, transformTop, zoom, wasResize) {
+ if (this.isVertical) {
+ this.renderIfNeeded(transformTop);
+ } else {
+ this.renderIfNeeded(transformLeft);
+ }
+ return this.scrollView.__$callback(transformLeft, transformTop, zoom, wasResize);
+ }),
+
+ renderIfNeeded: function(scrollPos) {
+ if ((this.hasNextIndex && scrollPos >= this.nextPos) ||
+ (this.hasPrevIndex && scrollPos < this.previousPos)) {
+ // Math.abs(transformPos - this.lastRenderScrollValue) > 100) {
+ this.render();
+ }
+ },
+ /*
+ * getIndexForScrollValue: Given the most recent data index and a new scrollValue,
+ * find the data index that matches that scrollValue.
+ *
+ * Strategy (if we are scrolling down): keep going forward in the dimensions list,
+ * starting at the given index, until an item with height matching the new scrollValue
+ * is found.
+ *
+ * This is a while loop. In the worst case it will have to go through the whole list
+ * (eg to scroll from top to bottom). The most common case is to scroll
+ * down 1-3 items at a time.
+ *
+ * While this is not as efficient as it could be, optimizing it gives no noticeable
+ * benefit. We would have to use a new memory-intensive data structure for dimensions
+ * to fully optimize it.
+ */
+ getIndexForScrollValue: function(i, scrollValue) {
+ var rect;
+ //Scrolling up
+ if (scrollValue <= this.dimensions[i].primaryPos) {
+ while ( (rect = this.dimensions[i - 1]) && rect.primaryPos > scrollValue) {
+ i--;
+ }
+ //Scrolling down
+ } else {
+ while ( (rect = this.dimensions[i + 1]) && rect.primaryPos < scrollValue) {
+ i++;
+ }
+ }
+ return i;
+ },
+ /*
+ * render: Figure out the scroll position, the index matching it, and then tell
+ * the data source to render the correct items into the DOM.
+ */
+ render: function(shouldRedrawAll) {
+ var self = this;
+ var i;
+ var isOutOfBounds = ( this.currentIndex >= this.dataSource.getLength() );
+ // We want to remove all the items and redraw everything if we're out of bounds
+ // or a flag is passed in.
+ if (isOutOfBounds || shouldRedrawAll) {
+ for (i in this.renderedItems) {
+ this.removeItem(i);
+ }
+ // Just don't render anything if we're out of bounds
+ if (isOutOfBounds) return;
+ }
+
+ var rect;
+ var scrollValue = this.scrollValue();
+ // Scroll size = how many pixels are visible in the scroller at one time
+ var scrollSize = this.scrollSize();
+ // We take the current scroll value and add it to the scrollSize to get
+ // what scrollValue the current visible scroll area ends at.
+ var scrollSizeEnd = scrollSize + scrollValue;
+ // Get the new start index for scrolling, based on the current scrollValue and
+ // the most recent known index
+ var startIndex = this.getIndexForScrollValue(this.currentIndex, scrollValue);
+
+ // If we aren't on the first item, add one row of items before so that when the user is
+ // scrolling up he sees the previous item
+ var renderStartIndex = Math.max(startIndex - 1, 0);
+ // Keep adding items to the 'extra row above' until we get to a new row.
+ // This is for the case where there are multiple items on one row above
+ // the current item; we want to keep adding items above until
+ // a new row is reached.
+ while (renderStartIndex > 0 &&
+ (rect = this.dimensions[renderStartIndex]) &&
+ rect.primaryPos === this.dimensions[startIndex - 1].primaryPos) {
+ renderStartIndex--;
+ }
+
+ // Keep rendering items, adding them until we are past the end of the visible scroll area
+ i = renderStartIndex;
+ while ((rect = this.dimensions[i]) && (rect.primaryPos - rect.primarySize < scrollSizeEnd)) {
+ doRender(i, rect);
+ i++;
+ }
+
+ // Render two extra items at the end as a buffer
+ if (self.dimensions[i]) {
+ doRender(i, self.dimensions[i]);
+ i++;
+ }
+ if (self.dimensions[i]) {
+ doRender(i, self.dimensions[i]);
+ }
+ var renderEndIndex = i;
+
+ // Remove any items that were rendered and aren't visible anymore
+ for (var renderIndex in this.renderedItems) {
+ if (renderIndex < renderStartIndex || renderIndex > renderEndIndex) {
+ this.removeItem(renderIndex);
+ }
+ }
+
+ this.setCurrentIndex(startIndex);
+
+ function doRender(dataIndex, rect) {
+ if (dataIndex < self.dataSource.dataStartIndex) {
+ // do nothing
+ } else {
+ self.renderItem(dataIndex, rect.primaryPos - self.beforeSize, rect.secondaryPos);
+ }
+ }
+ },
+ renderItem: function(dataIndex, primaryPos, secondaryPos) {
+ // Attach an item, and set its transform position to the required value
+ var item = this.dataSource.attachItemAtIndex(dataIndex);
+ //console.log(dataIndex, item);
+ if (item && item.element) {
+ if (item.primaryPos !== primaryPos || item.secondaryPos !== secondaryPos) {
+ item.element.css(ionic.CSS.TRANSFORM, this.transformString(
+ primaryPos, secondaryPos
+ ));
+ item.primaryPos = primaryPos;
+ item.secondaryPos = secondaryPos;
+ }
+ // Save the item in rendered items
+ this.renderedItems[dataIndex] = item;
+ } else {
+ // If an item at this index doesn't exist anymore, be sure to delete
+ // it from rendered items
+ delete this.renderedItems[dataIndex];
+ }
+ },
+ removeItem: function(dataIndex) {
+ // Detach a given item
+ var item = this.renderedItems[dataIndex];
+ if (item) {
+ item.primaryPos = item.secondaryPos = null;
+ this.dataSource.detachItem(item);
+ delete this.renderedItems[dataIndex];
+ }
+ }
+ };
+
+ return CollectionRepeatManager;
+}]);
+
+
+/**
+ * @ngdoc service
+ * @name $ionicGesture
+ * @module ionic
+ * @description An angular service exposing ionic
+ * {@link ionic.utility:ionic.EventController}'s gestures.
+ */
+IonicModule
+.factory('$ionicGesture', [function() {
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicGesture#on
+ * @description Add an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#onGesture}.
+ * @param {string} eventType The gesture event to listen for.
+ * @param {function(e)} callback The function to call when the gesture
+ * happens.
+ * @param {element} $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).
+ */
+ on: function(eventType, cb, $element, options) {
+ return window.ionic.onGesture(eventType, cb, $element[0], options);
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicGesture#off
+ * @description Remove an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#offGesture}.
+ * @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.
+ */
+ off: function(gesture, eventType, cb) {
+ return window.ionic.offGesture(gesture, eventType, cb);
+ }
+ };
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicHistory
+ * @module ionic
+ * @description
+ * $ionicHistory keeps track of views as the user navigates through an app. Similar to the way a
+ * browser behaves, an Ionic app is able to keep track of the previous view, the current view, and
+ * the forward view (if there is one). However, a typical web browser only keeps track of one
+ * history stack in a linear fashion.
+ *
+ * Unlike a traditional browser environment, apps and webapps have parallel independent histories,
+ * such as with tabs. Should a user navigate few pages deep on one tab, and then switch to a new
+ * tab and back, the back button relates not to the previous tab, but to the previous pages
+ * visited within _that_ tab.
+ *
+ * `$ionicHistory` facilitates this parallel history architecture.
+ */
+
+IonicModule
+.factory('$ionicHistory', [
+ '$rootScope',
+ '$state',
+ '$location',
+ '$window',
+ '$timeout',
+ '$ionicViewSwitcher',
+ '$ionicNavViewDelegate',
+function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ionicNavViewDelegate) {
+
+ // history actions while navigating views
+ var ACTION_INITIAL_VIEW = 'initialView';
+ var ACTION_NEW_VIEW = 'newView';
+ var ACTION_MOVE_BACK = 'moveBack';
+ var ACTION_MOVE_FORWARD = 'moveForward';
+
+ // direction of navigation
+ var DIRECTION_BACK = 'back';
+ var DIRECTION_FORWARD = 'forward';
+ var DIRECTION_ENTER = 'enter';
+ var DIRECTION_EXIT = 'exit';
+ var DIRECTION_SWAP = 'swap';
+ var DIRECTION_NONE = 'none';
+
+ var stateChangeCounter = 0;
+ var lastStateId, nextViewOptions, nextViewExpireTimer, forcedNav;
+
+ var viewHistory = {
+ histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1 } },
+ views: {},
+ backView: null,
+ forwardView: null,
+ currentView: null
+ };
+
+ var View = function() {};
+ View.prototype.initialize = function(data) {
+ if (data) {
+ for (var name in data) this[name] = data[name];
+ return this;
+ }
+ return null;
+ };
+ View.prototype.go = function() {
+
+ if (this.stateName) {
+ return $state.go(this.stateName, this.stateParams);
+ }
+
+ if (this.url && this.url !== $location.url()) {
+
+ if (viewHistory.backView === this) {
+ return $window.history.go(-1);
+ } else if (viewHistory.forwardView === this) {
+ return $window.history.go(1);
+ }
+
+ $location.url(this.url);
+ return;
+ }
+
+ return null;
+ };
+ View.prototype.destroy = function() {
+ if (this.scope) {
+ this.scope.$destroy && this.scope.$destroy();
+ this.scope = null;
+ }
+ };
+
+
+ function getViewById(viewId) {
+ return (viewId ? viewHistory.views[ viewId ] : null);
+ }
+
+ function getBackView(view) {
+ return (view ? getViewById(view.backViewId) : null);
+ }
+
+ function getForwardView(view) {
+ return (view ? getViewById(view.forwardViewId) : null);
+ }
+
+ function getHistoryById(historyId) {
+ return (historyId ? viewHistory.histories[ historyId ] : null);
+ }
+
+ function getHistory(scope) {
+ var histObj = getParentHistoryObj(scope);
+
+ if (!viewHistory.histories[ histObj.historyId ]) {
+ // this history object exists in parent scope, but doesn't
+ // exist in the history data yet
+ viewHistory.histories[ histObj.historyId ] = {
+ historyId: histObj.historyId,
+ parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId,
+ stack: [],
+ cursor: -1
+ };
+ }
+ return getHistoryById(histObj.historyId);
+ }
+
+ function getParentHistoryObj(scope) {
+ var parentScope = scope;
+ while (parentScope) {
+ if (parentScope.hasOwnProperty('$historyId')) {
+ // this parent scope has a historyId
+ return { historyId: parentScope.$historyId, scope: parentScope };
+ }
+ // nothing found keep climbing up
+ parentScope = parentScope.$parent;
+ }
+ // no history for the parent, use the root
+ return { historyId: 'root', scope: $rootScope };
+ }
+
+ function setNavViews(viewId) {
+ viewHistory.currentView = getViewById(viewId);
+ viewHistory.backView = getBackView(viewHistory.currentView);
+ viewHistory.forwardView = getForwardView(viewHistory.currentView);
+ }
+
+ function getCurrentStateId() {
+ var id;
+ if ($state && $state.current && $state.current.name) {
+ id = $state.current.name;
+ if ($state.params) {
+ for (var key in $state.params) {
+ if ($state.params.hasOwnProperty(key) && $state.params[key]) {
+ id += "_" + key + "=" + $state.params[key];
+ }
+ }
+ }
+ return id;
+ }
+ // if something goes wrong make sure its got a unique stateId
+ return ionic.Utils.nextUid();
+ }
+
+ function getCurrentStateParams() {
+ var rtn;
+ if ($state && $state.params) {
+ for (var key in $state.params) {
+ if ($state.params.hasOwnProperty(key)) {
+ rtn = rtn || {};
+ rtn[key] = $state.params[key];
+ }
+ }
+ }
+ return rtn;
+ }
+
+
+ return {
+
+ register: function(parentScope, viewLocals) {
+
+ var currentStateId = getCurrentStateId(),
+ hist = getHistory(parentScope),
+ currentView = viewHistory.currentView,
+ backView = viewHistory.backView,
+ forwardView = viewHistory.forwardView,
+ viewId = null,
+ action = null,
+ direction = DIRECTION_NONE,
+ historyId = hist.historyId,
+ url = $location.url(),
+ tmp, x, ele;
+
+ if (lastStateId !== currentStateId) {
+ lastStateId = currentStateId;
+ stateChangeCounter++;
+ }
+
+ if (forcedNav) {
+ // we've previously set exactly what to do
+ viewId = forcedNav.viewId;
+ action = forcedNav.action;
+ direction = forcedNav.direction;
+ forcedNav = null;
+
+ } else if (backView && backView.stateId === currentStateId) {
+ // they went back one, set the old current view as a forward view
+ viewId = backView.viewId;
+ historyId = backView.historyId;
+ action = ACTION_MOVE_BACK;
+ if (backView.historyId === currentView.historyId) {
+ // went back in the same history
+ direction = DIRECTION_BACK;
+
+ } else if (currentView) {
+ direction = DIRECTION_EXIT;
+
+ tmp = getHistoryById(backView.historyId);
+ if (tmp && tmp.parentHistoryId === currentView.historyId) {
+ direction = DIRECTION_ENTER;
+
+ } else {
+ tmp = getHistoryById(currentView.historyId);
+ if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
+ direction = DIRECTION_SWAP;
+ }
+ }
+ }
+
+ } else if (forwardView && forwardView.stateId === currentStateId) {
+ // they went to the forward one, set the forward view to no longer a forward view
+ viewId = forwardView.viewId;
+ historyId = forwardView.historyId;
+ action = ACTION_MOVE_FORWARD;
+ if (forwardView.historyId === currentView.historyId) {
+ direction = DIRECTION_FORWARD;
+
+ } else if (currentView) {
+ direction = DIRECTION_EXIT;
+
+ if (currentView.historyId === hist.parentHistoryId) {
+ direction = DIRECTION_ENTER;
+
+ } else {
+ tmp = getHistoryById(currentView.historyId);
+ if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
+ direction = DIRECTION_SWAP;
+ }
+ }
+ }
+
+ tmp = getParentHistoryObj(parentScope);
+ if (forwardView.historyId && tmp.scope) {
+ // if a history has already been created by the forward view then make sure it stays the same
+ tmp.scope.$historyId = forwardView.historyId;
+ historyId = forwardView.historyId;
+ }
+
+ } else if (currentView && currentView.historyId !== historyId &&
+ hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length &&
+ hist.stack[hist.cursor].stateId === currentStateId) {
+ // they just changed to a different history and the history already has views in it
+ var switchToView = hist.stack[hist.cursor];
+ viewId = switchToView.viewId;
+ historyId = switchToView.historyId;
+ action = ACTION_MOVE_BACK;
+ direction = DIRECTION_SWAP;
+
+ tmp = getHistoryById(currentView.historyId);
+ if (tmp && tmp.parentHistoryId === historyId) {
+ direction = DIRECTION_EXIT;
+
+ } else {
+ tmp = getHistoryById(historyId);
+ if (tmp && tmp.parentHistoryId === currentView.historyId) {
+ direction = DIRECTION_ENTER;
+ }
+ }
+
+ // if switching to a different history, and the history of the view we're switching
+ // to has an existing back view from a different history than itself, then
+ // it's back view would be better represented using the current view as its back view
+ tmp = getViewById(switchToView.backViewId);
+ if (tmp && switchToView.historyId !== tmp.historyId) {
+ hist.stack[hist.cursor].backViewId = currentView.viewId;
+ }
+
+ } else {
+
+ // create an element from the viewLocals template
+ ele = $ionicViewSwitcher.createViewEle(viewLocals);
+ if (this.isAbstractEle(ele, viewLocals)) {
+ void 0;
+ return {
+ action: 'abstractView',
+ direction: DIRECTION_NONE,
+ ele: ele
+ };
+ }
+
+ // set a new unique viewId
+ viewId = ionic.Utils.nextUid();
+
+ if (currentView) {
+ // set the forward view if there is a current view (ie: if its not the first view)
+ currentView.forwardViewId = viewId;
+
+ action = ACTION_NEW_VIEW;
+
+ // check if there is a new forward view within the same history
+ if (forwardView && currentView.stateId !== forwardView.stateId &&
+ currentView.historyId === forwardView.historyId) {
+ // they navigated to a new view but the stack already has a forward view
+ // since its a new view remove any forwards that existed
+ tmp = getHistoryById(forwardView.historyId);
+ if (tmp) {
+ // the forward has a history
+ for (x = tmp.stack.length - 1; x >= forwardView.index; x--) {
+ // starting from the end destroy all forwards in this history from this point
+ tmp.stack[x].destroy();
+ tmp.stack.splice(x);
+ }
+ historyId = forwardView.historyId;
+ }
+ }
+
+ // its only moving forward if its in the same history
+ if (hist.historyId === currentView.historyId) {
+ direction = DIRECTION_FORWARD;
+
+ } else if (currentView.historyId !== hist.historyId) {
+ direction = DIRECTION_ENTER;
+
+ tmp = getHistoryById(currentView.historyId);
+ if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
+ direction = DIRECTION_SWAP;
+
+ } else {
+ tmp = getHistoryById(tmp.parentHistoryId);
+ if (tmp && tmp.historyId === hist.historyId) {
+ direction = DIRECTION_EXIT;
+ }
+ }
+ }
+
+ } else {
+ // there's no current view, so this must be the initial view
+ action = ACTION_INITIAL_VIEW;
+ }
+
+ if (stateChangeCounter < 2) {
+ // views that were spun up on the first load should not animate
+ direction = DIRECTION_NONE;
+ }
+
+ // add the new view
+ viewHistory.views[viewId] = this.createView({
+ viewId: viewId,
+ index: hist.stack.length,
+ historyId: hist.historyId,
+ backViewId: (currentView && currentView.viewId ? currentView.viewId : null),
+ forwardViewId: null,
+ stateId: currentStateId,
+ stateName: this.currentStateName(),
+ stateParams: getCurrentStateParams(),
+ url: url
+ });
+
+ // add the new view to this history's stack
+ hist.stack.push(viewHistory.views[viewId]);
+ }
+
+ $timeout.cancel(nextViewExpireTimer);
+ if (nextViewOptions) {
+ if (nextViewOptions.disableAnimate) direction = DIRECTION_NONE;
+ if (nextViewOptions.disableBack) viewHistory.views[viewId].backViewId = null;
+ if (nextViewOptions.historyRoot) {
+ for (x = 0; x < hist.stack.length; x++) {
+ if (hist.stack[x].viewId === viewId) {
+ hist.stack[x].index = 0;
+ hist.stack[x].backViewId = hist.stack[x].forwardViewId = null;
+ } else {
+ delete viewHistory.views[hist.stack[x].viewId];
+ }
+ }
+ hist.stack = [viewHistory.views[viewId]];
+ }
+ nextViewOptions = null;
+ }
+
+ setNavViews(viewId);
+
+ if (viewHistory.backView && historyId == viewHistory.backView.historyId && currentStateId == viewHistory.backView.stateId && url == viewHistory.backView.url) {
+ for (x = 0; x < hist.stack.length; x++) {
+ if (hist.stack[x].viewId == viewId) {
+ action = 'dupNav';
+ direction = DIRECTION_NONE;
+ hist.stack[x - 1].forwardViewId = viewHistory.forwardView = null;
+ viewHistory.currentView.index = viewHistory.backView.index;
+ viewHistory.currentView.backViewId = viewHistory.backView.backViewId;
+ viewHistory.backView = getBackView(viewHistory.backView);
+ hist.stack.splice(x, 1);
+ break;
+ }
+ }
+ }
+
+ void 0;
+
+ hist.cursor = viewHistory.currentView.index;
+
+ return {
+ viewId: viewId,
+ action: action,
+ direction: direction,
+ historyId: historyId,
+ enableBack: !!(viewHistory.backView && viewHistory.backView.historyId === viewHistory.currentView.historyId),
+ isHistoryRoot: (viewHistory.currentView.index === 0),
+ ele: ele
+ };
+ },
+
+ registerHistory: function(scope) {
+ scope.$historyId = ionic.Utils.nextUid();
+ },
+
+ createView: function(data) {
+ var newView = new View();
+ return newView.initialize(data);
+ },
+
+ getViewById: getViewById,
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#viewHistory
+ * @description The app's view history data, such as all the views and histories, along
+ * with how they are ordered and linked together within the navigation stack.
+ * @returns {object} Returns an object containing the apps view history data.
+ */
+ viewHistory: function() {
+ return viewHistory;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#currentView
+ * @description The app's current view.
+ * @returns {object} Returns the current view.
+ */
+ currentView: function(view) {
+ if (arguments.length) {
+ viewHistory.currentView = view;
+ }
+ return viewHistory.currentView;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#currentHistoryId
+ * @description The ID of the history stack which is the parent container of the current view.
+ * @returns {string} Returns the current history ID.
+ */
+ currentHistoryId: function() {
+ return viewHistory.currentView ? viewHistory.currentView.historyId : null;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#currentTitle
+ * @description Gets and sets the current view's title.
+ * @param {string=} val The title to update the current view with.
+ * @returns {string} Returns the current view's title.
+ */
+ currentTitle: function(val) {
+ if (viewHistory.currentView) {
+ if (arguments.length) {
+ viewHistory.currentView.title = val;
+ }
+ return viewHistory.currentView.title;
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#backView
+ * @description Returns the view that was before the current view in the history stack.
+ * If the user navigated from View A to View B, then View A would be the back view, and
+ * View B would be the current view.
+ * @returns {object} Returns the back view.
+ */
+ backView: function(view) {
+ if (arguments.length) {
+ viewHistory.backView = view;
+ }
+ return viewHistory.backView;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#backTitle
+ * @description Gets the back view's title.
+ * @returns {string} Returns the back view's title.
+ */
+ backTitle: function() {
+ if (viewHistory.backView) {
+ return viewHistory.backView.title;
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#forwardView
+ * @description Returns the view that was in front of the current view in the history stack.
+ * A forward view would exist if the user navigated from View A to View B, then
+ * navigated back to View A. At this point then View B would be the forward view, and View
+ * A would be the current view.
+ * @returns {object} Returns the forward view.
+ */
+ forwardView: function(view) {
+ if (arguments.length) {
+ viewHistory.forwardView = view;
+ }
+ return viewHistory.forwardView;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#currentStateName
+ * @description Returns the current state name.
+ * @returns {string}
+ */
+ currentStateName: function() {
+ return ($state && $state.current ? $state.current.name : null);
+ },
+
+ isCurrentStateNavView: function(navView) {
+ return !!($state && $state.current && $state.current.views && $state.current.views[navView]);
+ },
+
+ goToHistoryRoot: function(historyId) {
+ if (historyId) {
+ var hist = getHistoryById(historyId);
+ if (hist && hist.stack.length) {
+ if (viewHistory.currentView && viewHistory.currentView.viewId === hist.stack[0].viewId) {
+ return;
+ }
+ forcedNav = {
+ viewId: hist.stack[0].viewId,
+ action: ACTION_MOVE_BACK,
+ direction: DIRECTION_BACK
+ };
+ hist.stack[0].go();
+ }
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#goBack
+ * @description Navigates the app to the back view, if a back view exists.
+ */
+ goBack: function() {
+ viewHistory.backView && viewHistory.backView.go();
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#clearHistory
+ * @description Clears out the app's entire history, except for the current view.
+ */
+ clearHistory: function() {
+ var
+ histories = viewHistory.histories,
+ currentView = viewHistory.currentView;
+
+ if (histories) {
+ for (var historyId in histories) {
+
+ if (histories[historyId].stack) {
+ histories[historyId].stack = [];
+ histories[historyId].cursor = -1;
+ }
+
+ if (currentView && currentView.historyId === historyId) {
+ currentView.backViewId = currentView.forwardViewId = null;
+ histories[historyId].stack.push(currentView);
+ } else if (histories[historyId].destroy) {
+ histories[historyId].destroy();
+ }
+
+ }
+ }
+
+ for (var viewId in viewHistory.views) {
+ if (viewId !== currentView.viewId) {
+ delete viewHistory.views[viewId];
+ }
+ }
+
+ if (currentView) {
+ setNavViews(currentView.viewId);
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#clearCache
+ * @description Removes all cached views within every {@link ionic.directive:ionNavView}.
+ * This both removes the view element from the DOM, and destroy it's scope.
+ */
+ clearCache: function() {
+ $ionicNavViewDelegate._instances.forEach(function(instance) {
+ instance.clearCache();
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#nextViewOptions
+ * @description Sets options for the next view. This method can be useful to override
+ * certain view/transition defaults right before a view transition happens. For example,
+ * the {@link ionic.directive:menuClose} directive uses this method internally to ensure
+ * an animated view transition does not happen when a side menu is open, and also sets
+ * the next view as the root of its history stack. After the transition these options
+ * are set back to null.
+ *
+ * Available options:
+ *
+ * * `disableAnimate`: Do not animate the next transition.
+ * * `disableBack`: The next view should forget its back view, and set it to null.
+ * * `historyRoot`: The next view should become the root view in its history stack.
+ *
+ * ```js
+ * $ionicHistory.nextViewOptions({
+ * disableAnimate: true,
+ * disableBack: true
+ * });
+ * ```
+ */
+ nextViewOptions: function(opts) {
+ if (arguments.length) {
+ $timeout.cancel(nextViewExpireTimer);
+ if (opts === null) {
+ nextViewOptions = opts;
+ } else {
+ nextViewOptions = nextViewOptions || {};
+ extend(nextViewOptions, opts);
+ if (nextViewOptions.expire) {
+ nextViewExpireTimer = $timeout(function(){
+ nextViewOptions = null;
+ }, nextViewOptions.expire);
+ }
+ }
+ }
+ return nextViewOptions;
+ },
+
+ isAbstractEle: function(ele, viewLocals) {
+ if (viewLocals && viewLocals.$$state && viewLocals.$$state.self.abstract) {
+ return true;
+ }
+ return !!(ele && (isAbstractTag(ele) || isAbstractTag(ele.children())));
+ },
+
+ isActiveScope: function(scope) {
+ if (!scope) return false;
+
+ var climbScope = scope;
+ var currentHistoryId = this.currentHistoryId();
+ var foundHistoryId;
+
+ while (climbScope) {
+ if (climbScope.$$disconnected) {
+ return false;
+ }
+
+ if (!foundHistoryId && climbScope.hasOwnProperty('$historyId')) {
+ foundHistoryId = true;
+ }
+
+ if (currentHistoryId) {
+ if (climbScope.hasOwnProperty('$historyId') && currentHistoryId == climbScope.$historyId) {
+ return true;
+ }
+ if (climbScope.hasOwnProperty('$activeHistoryId')) {
+ if (currentHistoryId == climbScope.$activeHistoryId) {
+ if (climbScope.hasOwnProperty('$historyId')) {
+ return true;
+ }
+ if (!foundHistoryId) {
+ return true;
+ }
+ }
+ }
+ }
+
+ if (foundHistoryId && climbScope.hasOwnProperty('$activeHistoryId')) {
+ foundHistoryId = false;
+ }
+
+ climbScope = climbScope.$parent;
+ }
+
+ return currentHistoryId ? currentHistoryId == 'root' : true;
+ }
+
+ };
+
+ function isAbstractTag(ele) {
+ return ele && ele.length && /ion-side-menus|ion-tabs/i.test(ele[0].tagName);
+ }
+
+}])
+
+.run([
+ '$rootScope',
+ '$state',
+ '$location',
+ '$document',
+ '$ionicPlatform',
+ '$ionicHistory',
+function($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory) {
+
+ // always reset the keyboard state when change stage
+ $rootScope.$on('$ionicView.beforeEnter', function() {
+ ionic.keyboard && ionic.keyboard.hide && ionic.keyboard.hide();
+ });
+
+ $rootScope.$on('$ionicHistory.change', function(e, data) {
+ if (!data) return;
+
+ var viewHistory = $ionicHistory.viewHistory();
+
+ var hist = (data.historyId ? viewHistory.histories[ data.historyId ] : null);
+ if (hist && hist.cursor > -1 && hist.cursor < hist.stack.length) {
+ // the history they're going to already exists
+ // go to it's last view in its stack
+ var view = hist.stack[ hist.cursor ];
+ return view.go(data);
+ }
+
+ // this history does not have a URL, but it does have a uiSref
+ // figure out its URL from the uiSref
+ if (!data.url && data.uiSref) {
+ data.url = $state.href(data.uiSref);
+ }
+
+ if (data.url) {
+ // don't let it start with a #, messes with $location.url()
+ if (data.url.indexOf('#') === 0) {
+ data.url = data.url.replace('#', '');
+ }
+ if (data.url !== $location.url()) {
+ // we've got a good URL, ready GO!
+ $location.url(data.url);
+ }
+ }
+ });
+
+ $rootScope.$ionicGoBack = function() {
+ $ionicHistory.goBack();
+ };
+
+ // Set the document title when a new view is shown
+ $rootScope.$on('$ionicView.afterEnter', function(ev, data) {
+ if (data && data.title) {
+ $document[0].title = data.title;
+ }
+ });
+
+ // Triggered when devices with a hardware back button (Android) is clicked by the user
+ // This is a Cordova/Phonegap platform specifc method
+ function onHardwareBackButton(e) {
+ var backView = $ionicHistory.backView();
+ if (backView) {
+ // there is a back view, go to it
+ backView.go();
+ } else {
+ // there is no back view, so close the app instead
+ ionic.Platform.exitApp();
+ }
+ e.preventDefault();
+ return false;
+ }
+ $ionicPlatform.registerBackButtonAction(
+ onHardwareBackButton,
+ PLATFORM_BACK_BUTTON_PRIORITY_VIEW
+ );
+
+}]);
+
+/**
+ * @ngdoc provider
+ * @name $ionicConfigProvider
+ * @module ionic
+ * @description
+ * Ionic automatically takes platform configurations into account to adjust things like what
+ * transition style to use and whether tab icons should show on the top or bottom. For example,
+ * iOS will move forward by transitioning the entering view from right to center and the leaving
+ * view from center to left. However, Android will transition with the entering view going from
+ * bottom to center, covering the previous view, which remains stationary. It should be noted
+ * that when a platform is not iOS or Android, then it'll default to iOS. So if you are
+ * developing on a desktop browser, it's going to take on iOS default configs.
+ *
+ * These configs can be changed using the `$ionicConfigProvider` during the configuration phase
+ * of your app. Additionally, `$ionicConfig` can also set and get config values during the run
+ * phase and within the app itself.
+ *
+ * By default, all base config variables are set to `'platform'`, which means it'll take on the
+ * default config of the platform on which it's running. Config variables can be set at this
+ * level so all platforms follow the same setting, rather than its platform config.
+ * The following code would set the same config variable for all platforms:
+ *
+ * ```js
+ * $ionicConfigProvider.views.maxCache(10);
+ * ```
+ *
+ * Additionally, each platform can have it's own config within the `$ionicConfigProvider.platform`
+ * property. The config below would only apply to Android devices.
+ *
+ * ```js
+ * $ionicConfigProvider.platform.android.views.maxCache(5);
+ * ```
+ *
+ * @usage
+ * ```js
+ * var myApp = angular.module('reallyCoolApp', ['ionic']);
+ *
+ * myApp.config(function($ionicConfigProvider) {
+ * $ionicConfigProvider.views.maxCache(5);
+ *
+ * // note that you can also chain configs
+ * $ionicConfigProvider.backButton.text('Go Back').icon('ion-chevron-left');
+ * });
+ * ```
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#views.transition
+ * @description Animation style when transitioning between views. Default `platform`.
+ *
+ * @param {string} transition Which style of view transitioning to use.
+ *
+ * * `platform`: Dynamically choose the correct transition style depending on the platform
+ * the app is running from. If the platform is not `ios` or `android` then it will default
+ * to `ios`.
+ * * `ios`: iOS style transition.
+ * * `android`: Android style transition.
+ * * `none`: Do not preform animated transitions.
+ *
+ * @returns {string} value
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#views.maxCache
+ * @description Maximum number of view elements to cache in the DOM. When the max number is
+ * exceeded, the view with the longest time period since it was accessed is removed. Views that
+ * stay in the DOM cache the view's scope, current state, and scroll position. The scope is
+ * disconnected from the `$watch` cycle when it is cached and reconnected when it enters again.
+ * When the maximum cache is `0`, the leaving view's element will be removed from the DOM after
+ * each view transition, and the next time the same view is shown, it will have to re-compile,
+ * attach to the DOM, and link the element again. This disables caching, in effect.
+ * @param {number} maxNumber Maximum number of views to retain. Default `10`.
+ * @returns {number} How many views Ionic will hold onto until the a view is removed.
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#views.forwardCache
+ * @description By default, when navigating, views that were recently visited are cached, and
+ * the same instance data and DOM elements are referenced when navigating back. However, when
+ * navigating back in the history, the "forward" views are removed from the cache. If you
+ * navigate forward to the same view again, it'll create a new DOM element and controller
+ * instance. Basically, any forward views are reset each time. Set this config to `true` to have
+ * forward views cached and not reset on each load.
+ * @param {boolean} value
+ * @returns {boolean}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#backButton.icon
+ * @description Back button icon.
+ * @param {string} value
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#backButton.text
+ * @description Back button text.
+ * @param {string} value Defaults to `Back`.
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#backButton.previousTitleText
+ * @description If the previous title text should become the back button text. This
+ * is the default for iOS.
+ * @param {boolean} value
+ * @returns {boolean}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#tabs.style
+ * @description Tab style. Android defaults to `striped` and iOS defaults to `standard`.
+ * @param {string} value Available values include `striped` and `standard`.
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#tabs.position
+ * @description Tab position. Android defaults to `top` and iOS defaults to `bottom`.
+ * @param {string} value Available values include `top` and `bottom`.
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#templates.maxPrefetch
+ * @description Sets the maximum number of templates to prefetch from the templateUrls defined in
+ * $stateProvider.state. If set to `0`, the user will have to wait
+ * for a template to be fetched the first time when navigating to a new page. Default `30`.
+ * @param {integer} value Max number of template to prefetch from the templateUrls defined in
+ * `$stateProvider.state()`.
+ * @returns {integer}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#navBar.alignTitle
+ * @description Which side of the navBar to align the title. Default `center`.
+ *
+ * @param {string} value side of the navBar to align the title.
+ *
+ * * `platform`: Dynamically choose the correct title style depending on the platform
+ * the app is running from. If the platform is `ios`, it will default to `center`.
+ * If the platform is `android`, it will default to `left`. If the platform is not
+ * `ios` or `android`, it will default to `center`.
+ *
+ * * `left`: Left align the title in the navBar
+ * * `center`: Center align the title in the navBar
+ * * `right`: Right align the title in the navBar.
+ *
+ * @returns {string} value
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#navBar.positionPrimaryButtons
+ * @description Which side of the navBar to align the primary navBar buttons. Default `left`.
+ *
+ * @param {string} value side of the navBar to align the primary navBar buttons.
+ *
+ * * `platform`: Dynamically choose the correct title style depending on the platform
+ * the app is running from. If the platform is `ios`, it will default to `left`.
+ * If the platform is `android`, it will default to `right`. If the platform is not
+ * `ios` or `android`, it will default to `left`.
+ *
+ * * `left`: Left align the primary navBar buttons in the navBar
+ * * `right`: Right align the primary navBar buttons in the navBar.
+ *
+ * @returns {string} value
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#navBar.positionSecondaryButtons
+ * @description Which side of the navBar to align the secondary navBar buttons. Default `right`.
+ *
+ * @param {string} value side of the navBar to align the secondary navBar buttons.
+ *
+ * * `platform`: Dynamically choose the correct title style depending on the platform
+ * the app is running from. If the platform is `ios`, it will default to `right`.
+ * If the platform is `android`, it will default to `right`. If the platform is not
+ * `ios` or `android`, it will default to `right`.
+ *
+ * * `left`: Left align the secondary navBar buttons in the navBar
+ * * `right`: Right align the secondary navBar buttons in the navBar.
+ *
+ * @returns {string} value
+ */
+
+IonicModule
+.provider('$ionicConfig', function() {
+
+ var provider = this;
+ provider.platform = {};
+ var PLATFORM = 'platform';
+
+ var configProperties = {
+ views: {
+ maxCache: PLATFORM,
+ forwardCache: PLATFORM,
+ transition: PLATFORM
+ },
+ navBar: {
+ alignTitle: PLATFORM,
+ positionPrimaryButtons: PLATFORM,
+ positionSecondaryButtons: PLATFORM,
+ transition: PLATFORM
+ },
+ backButton: {
+ icon: PLATFORM,
+ text: PLATFORM,
+ previousTitleText: PLATFORM
+ },
+ form: {
+ checkbox: PLATFORM
+ },
+ tabs: {
+ style: PLATFORM,
+ position: PLATFORM
+ },
+ templates: {
+ maxPrefetch: PLATFORM
+ },
+ platform: {}
+ };
+ createConfig(configProperties, provider, '');
+
+
+
+ // Default
+ // -------------------------
+ setPlatformConfig('default', {
+
+ views: {
+ maxCache: 10,
+ forwardCache: false,
+ transition: 'ios'
+ },
+
+ navBar: {
+ alignTitle: 'center',
+ positionPrimaryButtons: 'left',
+ positionSecondaryButtons: 'right',
+ transition: 'view'
+ },
+
+ backButton: {
+ icon: 'ion-ios7-arrow-back',
+ text: 'Back',
+ previousTitleText: true
+ },
+
+ form: {
+ checkbox: 'circle'
+ },
+
+ tabs: {
+ style: 'standard',
+ position: 'bottom'
+ },
+
+ templates: {
+ maxPrefetch: 30
+ }
+
+ });
+
+
+
+ // iOS (it is the default already)
+ // -------------------------
+ setPlatformConfig('ios', {});
+
+
+
+ // Android
+ // -------------------------
+ setPlatformConfig('android', {
+
+ views: {
+ transition: 'android'
+ },
+
+ navBar: {
+ alignTitle: 'left',
+ positionPrimaryButtons: 'right',
+ positionSecondaryButtons: 'right'
+ },
+
+ backButton: {
+ icon: 'ion-arrow-left-c',
+ text: false,
+ previousTitleText: false
+ },
+
+ form: {
+ checkbox: 'square'
+ },
+
+ tabs: {
+ style: 'striped',
+ position: 'top'
+ }
+
+ });
+
+
+ provider.transitions = {
+ views: {},
+ navBar: {}
+ };
+
+
+ // iOS Transitions
+ // -----------------------
+ provider.transitions.views.ios = function(enteringEle, leavingEle, direction, shouldAnimate) {
+ shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back');
+
+ function setStyles(ele, opacity, x) {
+ var css = {};
+ css[ionic.CSS.TRANSITION_DURATION] = shouldAnimate ? '' : 0;
+ css.opacity = opacity;
+ css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
+ ionic.DomUtil.cachedStyles(ele, css);
+ }
+
+ return {
+ run: function(step) {
+ if (direction == 'forward') {
+ setStyles(enteringEle, 1, (1 - step) * 99); // starting at 98% prevents a flicker
+ setStyles(leavingEle, (1 - 0.1 * step), step * -33);
+
+ } else if (direction == 'back') {
+ setStyles(enteringEle, (1 - 0.1 * (1 - step)), (1 - step) * -33);
+ setStyles(leavingEle, 1, step * 100);
+
+ } else {
+ // swap, enter, exit
+ setStyles(enteringEle, 1, 0);
+ setStyles(leavingEle, 0, 0);
+ }
+ },
+ shouldAnimate: shouldAnimate
+ };
+ };
+
+ provider.transitions.navBar.ios = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {
+ shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back');
+
+ function setStyles(ctrl, opacity, titleX, backTextX) {
+ var css = {};
+ css[ionic.CSS.TRANSITION_DURATION] = shouldAnimate ? '' : 0;
+ css.opacity = opacity === 1 ? '' : opacity;
+
+ ctrl.setCss('buttons-left', css);
+ ctrl.setCss('buttons-right', css);
+ ctrl.setCss('back-button', css);
+
+ css[ionic.CSS.TRANSFORM] = 'translate3d(' + backTextX + 'px,0,0)';
+ ctrl.setCss('back-text', css);
+
+ css[ionic.CSS.TRANSFORM] = 'translate3d(' + titleX + 'px,0,0)';
+ ctrl.setCss('title', css);
+ }
+
+ function enter(ctrlA, ctrlB, step) {
+ if (!ctrlA) return;
+ var titleX = (ctrlA.titleTextX() + ctrlA.titleWidth()) * (1 - step);
+ var backTextX = (ctrlB && (ctrlB.titleTextX() - ctrlA.backButtonTextLeft()) * (1 - step)) || 0;
+ setStyles(ctrlA, step, titleX, backTextX);
+ }
+
+ function leave(ctrlA, ctrlB, step) {
+ if (!ctrlA) return;
+ var titleX = (-(ctrlA.titleTextX() - ctrlB.backButtonTextLeft()) - (ctrlA.titleLeftRight())) * step;
+ setStyles(ctrlA, 1 - step, titleX, 0);
+ }
+
+ return {
+ run: function(step) {
+ var enteringHeaderCtrl = enteringHeaderBar.controller();
+ var leavingHeaderCtrl = leavingHeaderBar && leavingHeaderBar.controller();
+ if (direction == 'back') {
+ leave(enteringHeaderCtrl, leavingHeaderCtrl, 1 - step);
+ enter(leavingHeaderCtrl, enteringHeaderCtrl, 1 - step);
+ } else {
+ enter(enteringHeaderCtrl, leavingHeaderCtrl, step);
+ leave(leavingHeaderCtrl, enteringHeaderCtrl, step);
+ }
+ },
+ shouldAnimate: shouldAnimate
+ };
+ };
+
+
+ // Android Transitions
+ // -----------------------
+
+ provider.transitions.views.android = function(enteringEle, leavingEle, direction, shouldAnimate) {
+ shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back');
+
+ function setStyles(ele, x) {
+ var css = {};
+ css[ionic.CSS.TRANSITION_DURATION] = shouldAnimate ? '' : 0;
+ css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
+ ionic.DomUtil.cachedStyles(ele, css);
+ }
+
+ return {
+ run: function(step) {
+ if (direction == 'forward') {
+ setStyles(enteringEle, (1 - step) * 99); // starting at 98% prevents a flicker
+ setStyles(leavingEle, step * -100);
+
+ } else if (direction == 'back') {
+ setStyles(enteringEle, (1 - step) * -100);
+ setStyles(leavingEle, step * 100);
+
+ } else {
+ // swap, enter, exit
+ setStyles(enteringEle, 0);
+ setStyles(leavingEle, 0);
+ }
+ },
+ shouldAnimate: shouldAnimate
+ };
+ };
+
+ provider.transitions.navBar.android = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {
+ shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back');
+
+ function setStyles(ctrl, opacity) {
+ if (!ctrl) return;
+ var css = {};
+ css.opacity = opacity === 1 ? '' : opacity;
+
+ ctrl.setCss('buttons-left', css);
+ ctrl.setCss('buttons-right', css);
+ ctrl.setCss('back-button', css);
+ ctrl.setCss('back-text', css);
+ ctrl.setCss('title', css);
+ }
+
+ return {
+ run: function(step) {
+ setStyles(enteringHeaderBar.controller(), step);
+ setStyles(leavingHeaderBar && leavingHeaderBar.controller(), 1 - step);
+ },
+ shouldAnimate: true
+ };
+ };
+
+
+ // No Transition
+ // -----------------------
+
+ provider.transitions.views.none = function(enteringEle, leavingEle) {
+ return {
+ run: function(step) {
+ provider.transitions.views.android(enteringEle, leavingEle, false, false).run(step);
+ }
+ };
+ };
+
+ provider.transitions.navBar.none = function(enteringHeaderBar, leavingHeaderBar) {
+ return {
+ run: function(step) {
+ provider.transitions.navBar.ios(enteringHeaderBar, leavingHeaderBar, false, false).run(step);
+ provider.transitions.navBar.android(enteringHeaderBar, leavingHeaderBar, false, false).run(step);
+ }
+ };
+ };
+
+
+ // private: used to set platform configs
+ function setPlatformConfig(platformName, platformConfigs) {
+ configProperties.platform[platformName] = platformConfigs;
+ provider.platform[platformName] = {};
+
+ addConfig(configProperties, configProperties.platform[platformName]);
+
+ createConfig(configProperties.platform[platformName], provider.platform[platformName], '');
+ }
+
+
+ // private: used to recursively add new platform configs
+ function addConfig(configObj, platformObj) {
+ for (var n in configObj) {
+ if (n != PLATFORM && configObj.hasOwnProperty(n)) {
+ if (angular.isObject(configObj[n])) {
+ if (!isDefined(platformObj[n])) {
+ platformObj[n] = {};
+ }
+ addConfig(configObj[n], platformObj[n]);
+
+ } else if (!isDefined(platformObj[n])) {
+ platformObj[n] = null;
+ }
+ }
+ }
+ }
+
+
+ // private: create methods for each config to get/set
+ function createConfig(configObj, providerObj, platformPath) {
+ forEach(configObj, function(value, namespace) {
+
+ if (angular.isObject(configObj[namespace])) {
+ // recursively drill down the config object so we can create a method for each one
+ providerObj[namespace] = {};
+ createConfig(configObj[namespace], providerObj[namespace], platformPath + '.' + namespace);
+
+ } else {
+ // create a method for the provider/config methods that will be exposed
+ providerObj[namespace] = function(newValue) {
+ if (arguments.length) {
+ configObj[namespace] = newValue;
+ return providerObj;
+ }
+ if (configObj[namespace] == PLATFORM) {
+ // if the config is set to 'platform', then get this config's platform value
+ var platformConfig = stringObj(configProperties.platform, ionic.Platform.platform() + platformPath + '.' + namespace);
+ if (platformConfig || platformConfig === false) {
+ return platformConfig;
+ }
+ // didnt find a specific platform config, now try the default
+ return stringObj(configProperties.platform, 'default' + platformPath + '.' + namespace);
+ }
+ return configObj[namespace];
+ };
+ }
+
+ });
+ }
+
+ function stringObj(obj, str) {
+ str = str.split(".");
+ for (var i = 0; i < str.length; i++) {
+ if (obj && isDefined(obj[str[i]])) {
+ obj = obj[str[i]];
+ } else {
+ return null;
+ }
+ }
+ return obj;
+ }
+
+ provider.setPlatformConfig = setPlatformConfig;
+
+
+ // private: Service definition for internal Ionic use
+ /**
+ * @ngdoc service
+ * @name $ionicConfig
+ * @module ionic
+ * @private
+ */
+ provider.$get = function() {
+ return provider;
+ };
+});
+
+
+var LOADING_TPL =
+ '
' +
+ '
' +
+ '
' +
+ '
';
+
+var LOADING_HIDE_DEPRECATED = '$ionicLoading instance.hide() has been deprecated. Use $ionicLoading.hide().';
+var LOADING_SHOW_DEPRECATED = '$ionicLoading instance.show() has been deprecated. Use $ionicLoading.show().';
+var LOADING_SET_DEPRECATED = '$ionicLoading instance.setContent() has been deprecated. Use $ionicLoading.show({ template: \'my content\' }).';
+
+/**
+ * @ngdoc service
+ * @name $ionicLoading
+ * @module ionic
+ * @description
+ * An overlay that can be used to indicate activity while blocking user
+ * interaction.
+ *
+ * @usage
+ * ```js
+ * angular.module('LoadingApp', ['ionic'])
+ * .controller('LoadingCtrl', function($scope, $ionicLoading) {
+ * $scope.show = function() {
+ * $ionicLoading.show({
+ * template: 'Loading...'
+ * });
+ * };
+ * $scope.hide = function(){
+ * $ionicLoading.hide();
+ * };
+ * });
+ * ```
+ */
+/**
+ * @ngdoc object
+ * @name $ionicLoadingConfig
+ * @module ionic
+ * @description
+ * Set the default options to be passed to the {@link ionic.service:$ionicLoading} service.
+ *
+ * @usage
+ * ```js
+ * var app = angular.module('myApp', ['ionic'])
+ * app.constant('$ionicLoadingConfig', {
+ * template: 'Default Loading Template...'
+ * });
+ * app.controller('AppCtrl', function($scope, $ionicLoading) {
+ * $scope.showLoading = function() {
+ * $ionicLoading.show(); //options default to values in $ionicLoadingConfig
+ * };
+ * });
+ * ```
+ */
+IonicModule
+.constant('$ionicLoadingConfig', {
+ template: ''
+})
+.factory('$ionicLoading', [
+ '$ionicLoadingConfig',
+ '$ionicBody',
+ '$ionicTemplateLoader',
+ '$ionicBackdrop',
+ '$timeout',
+ '$q',
+ '$log',
+ '$compile',
+ '$ionicPlatform',
+ '$rootScope',
+function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform, $rootScope) {
+
+ var loaderInstance;
+ //default values
+ var deregisterBackAction = angular.noop;
+ var deregisterStateListener = angular.noop;
+ var loadingShowDelay = $q.when();
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicLoading#show
+ * @description Shows a loading indicator. If the indicator is already shown,
+ * it will set the options given and keep the indicator shown.
+ * @param {object} opts The options for the loading indicator. Available properties:
+ * - `{string=}` `template` The html content of the indicator.
+ * - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator.
+ * - `{object=}` `scope` The scope to be a child of. Default: creates a child of $rootScope.
+ * - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown.
+ * - `{boolean=}` `hideOnStateChange` Whether to hide the loading spinner when navigating
+ * to a new state. Default false.
+ * - `{number=}` `delay` How many milliseconds to delay showing the indicator. By default there is no delay.
+ * - `{number=}` `duration` How many milliseconds to wait until automatically
+ * hiding the indicator. By default, the indicator will be shown until `.hide()` is called.
+ */
+ show: showLoader,
+ /**
+ * @ngdoc method
+ * @name $ionicLoading#hide
+ * @description Hides the loading indicator, if shown.
+ */
+ hide: hideLoader,
+ /**
+ * @private for testing
+ */
+ _getLoader: getLoader
+ };
+
+ function getLoader() {
+ if (!loaderInstance) {
+ loaderInstance = $ionicTemplateLoader.compile({
+ template: LOADING_TPL,
+ appendTo: $ionicBody.get()
+ })
+ .then(function(loader) {
+ var self = loader;
+
+ loader.show = function(options) {
+ var templatePromise = options.templateUrl ?
+ $ionicTemplateLoader.load(options.templateUrl) :
+ //options.content: deprecated
+ $q.when(options.template || options.content || '');
+
+ self.scope = options.scope || self.scope;
+
+ if (!this.isShown) {
+ //options.showBackdrop: deprecated
+ this.hasBackdrop = !options.noBackdrop && options.showBackdrop !== false;
+ if (this.hasBackdrop) {
+ $ionicBackdrop.retain();
+ $ionicBackdrop.getElement().addClass('backdrop-loading');
+ }
+ }
+
+ if (options.duration) {
+ $timeout.cancel(this.durationTimeout);
+ this.durationTimeout = $timeout(
+ angular.bind(this, this.hide),
+ +options.duration
+ );
+ }
+
+ deregisterBackAction();
+ //Disable hardware back button while loading
+ deregisterBackAction = $ionicPlatform.registerBackButtonAction(
+ angular.noop,
+ PLATFORM_BACK_BUTTON_PRIORITY_LOADING
+ );
+
+ templatePromise.then(function(html) {
+ if (html) {
+ var loading = self.element.children();
+ loading.html(html);
+ $compile(loading.contents())(self.scope);
+ }
+
+ //Don't show until template changes
+ if (self.isShown) {
+ self.element.addClass('visible');
+ ionic.requestAnimationFrame(function() {
+ if(self.isShown) {
+ self.element.addClass('active');
+ $ionicBody.addClass('loading-active');
+ }
+ });
+ }
+ });
+
+ this.isShown = true;
+ };
+ loader.hide = function() {
+
+ deregisterBackAction();
+ if (this.isShown) {
+ if (this.hasBackdrop) {
+ $ionicBackdrop.release();
+ $ionicBackdrop.getElement().removeClass('backdrop-loading');
+ }
+ self.element.removeClass('active');
+ $ionicBody.removeClass('loading-active');
+ setTimeout(function() {
+ !self.isShown && self.element.removeClass('visible');
+ }, 200);
+ }
+ $timeout.cancel(this.durationTimeout);
+ this.isShown = false;
+ };
+
+ return loader;
+ });
+ }
+ return loaderInstance;
+ }
+
+ function showLoader(options) {
+ options = extend({}, $ionicLoadingConfig || {}, options || {});
+ var delay = options.delay || options.showDelay || 0;
+
+ //If loading.show() was called previously, cancel it and show with our new options
+ loadingShowDelay && $timeout.cancel(loadingShowDelay);
+ loadingShowDelay = $timeout(angular.noop, delay);
+
+ loadingShowDelay.then(getLoader).then(function(loader) {
+ if (options.hideOnStateChange) {
+ deregisterStateListener = $rootScope.$on('$stateChangeSuccess', hideLoader);
+ }
+ return loader.show(options);
+ });
+
+ return {
+ hide: deprecated.method(LOADING_HIDE_DEPRECATED, $log.error, hideLoader),
+ show: deprecated.method(LOADING_SHOW_DEPRECATED, $log.error, function() {
+ showLoader(options);
+ }),
+ setContent: deprecated.method(LOADING_SET_DEPRECATED, $log.error, function(content) {
+ getLoader().then(function(loader) {
+ loader.show({ template: content });
+ });
+ })
+ };
+ }
+
+ function hideLoader() {
+ deregisterStateListener();
+ $timeout.cancel(loadingShowDelay);
+ getLoader().then(function(loader) {
+ loader.hide();
+ });
+ }
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicModal
+ * @module ionic
+ * @description
+ *
+ * Related: {@link ionic.controller:ionicModal ionicModal controller}.
+ *
+ * The Modal is a content pane that can go over the user's main view
+ * temporarily. Usually used for making a choice or editing an item.
+ *
+ * Put the content of the modal inside of an `` element.
+ *
+ * **Notes:**
+ * - A modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
+ * scope, passing in itself as an event argument. Both the modal.removed and modal.hidden events are
+ * called when the modal is removed.
+ *
+ * - This example assumes your modal is in your main index file or another template file. If it is in its own
+ * template file, remove the script tags and call it by file name.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ * ```js
+ * angular.module('testApp', ['ionic'])
+ * .controller('MyController', function($scope, $ionicModal) {
+ * $ionicModal.fromTemplateUrl('my-modal.html', {
+ * scope: $scope,
+ * animation: 'slide-in-up'
+ * }).then(function(modal) {
+ * $scope.modal = modal;
+ * });
+ * $scope.openModal = function() {
+ * $scope.modal.show();
+ * };
+ * $scope.closeModal = function() {
+ * $scope.modal.hide();
+ * };
+ * //Cleanup the modal when we're done with it!
+ * $scope.$on('$destroy', function() {
+ * $scope.modal.remove();
+ * });
+ * // Execute action on hide modal
+ * $scope.$on('modal.hidden', function() {
+ * // Execute action
+ * });
+ * // Execute action on remove modal
+ * $scope.$on('modal.removed', function() {
+ * // Execute action
+ * });
+ * });
+ * ```
+ */
+IonicModule
+.factory('$ionicModal', [
+ '$rootScope',
+ '$ionicBody',
+ '$compile',
+ '$timeout',
+ '$ionicPlatform',
+ '$ionicTemplateLoader',
+ '$q',
+ '$log',
+function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $q, $log) {
+
+ /**
+ * @ngdoc controller
+ * @name ionicModal
+ * @module ionic
+ * @description
+ * Instantiated by the {@link ionic.service:$ionicModal} service.
+ *
+ * Be sure to call [remove()](#remove) when you are done with each modal
+ * to clean it up and avoid memory leaks.
+ *
+ * Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
+ * scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are
+ * called when the modal is removed.
+ */
+ var ModalView = ionic.views.Modal.inherit({
+ /**
+ * @ngdoc method
+ * @name ionicModal#initialize
+ * @description Creates a new modal controller instance.
+ * @param {object} options An options object with the following properties:
+ * - `{object=}` `scope` The scope to be a child of.
+ * Default: creates a child of $rootScope.
+ * - `{string=}` `animation` The animation to show & hide with.
+ * Default: 'slide-in-up'
+ * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
+ * the modal when shown. Default: false.
+ * - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop.
+ * Default: true.
+ * - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware
+ * back button on Android and similar devices. Default: true.
+ */
+ initialize: function(opts) {
+ ionic.views.Modal.prototype.initialize.call(this, opts);
+ this.animation = opts.animation || 'slide-in-up';
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionicModal#show
+ * @description Show this modal instance.
+ * @returns {promise} A promise which is resolved when the modal is finished animating in.
+ */
+ show: function(target) {
+ var self = this;
+
+ if (self.scope.$$destroyed) {
+ $log.error('Cannot call ' + self.viewType + '.show() after remove(). Please create a new ' + self.viewType + ' instance.');
+ return;
+ }
+
+ var modalEl = jqLite(self.modalEl);
+
+ self.el.classList.remove('hide');
+ $timeout(function() {
+ $ionicBody.addClass(self.viewType + '-open');
+ }, 400);
+
+ if (!self.el.parentElement) {
+ modalEl.addClass(self.animation);
+ $ionicBody.append(self.el);
+ }
+
+ if (target && self.positionView) {
+ self.positionView(target, modalEl);
+ // set up a listener for in case the window size changes
+ ionic.on('resize',function() {
+ ionic.off('resize',null,window);
+ self.positionView(target,modalEl);
+ },window);
+ }
+
+ modalEl.addClass('ng-enter active')
+ .removeClass('ng-leave ng-leave-active');
+
+ self._isShown = true;
+ self._deregisterBackButton = $ionicPlatform.registerBackButtonAction(
+ self.hardwareBackButtonClose ? angular.bind(self, self.hide) : angular.noop,
+ PLATFORM_BACK_BUTTON_PRIORITY_MODAL
+ );
+
+ self._isOpenPromise = $q.defer();
+
+ ionic.views.Modal.prototype.show.call(self);
+
+ $timeout(function() {
+ modalEl.addClass('ng-enter-active');
+ ionic.trigger('resize');
+ self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self);
+ self.el.classList.add('active');
+ self.scope.$broadcast('$ionicHeader.align');
+ }, 20);
+
+ return $timeout(function() {
+ //After animating in, allow hide on backdrop click
+ self.$el.on('click', function(e) {
+ if (self.backdropClickToClose && e.target === self.el) {
+ self.hide();
+ }
+ });
+ }, 400);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionicModal#hide
+ * @description Hide this modal instance.
+ * @returns {promise} A promise which is resolved when the modal is finished animating out.
+ */
+ hide: function() {
+ var self = this;
+ var modalEl = jqLite(self.modalEl);
+
+ self.el.classList.remove('active');
+ modalEl.addClass('ng-leave');
+
+ $timeout(function() {
+ modalEl.addClass('ng-leave-active')
+ .removeClass('ng-enter ng-enter-active active');
+ }, 20);
+
+ self.$el.off('click');
+ self._isShown = false;
+ self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self);
+ self._deregisterBackButton && self._deregisterBackButton();
+
+ ionic.views.Modal.prototype.hide.call(self);
+
+ // clean up event listeners
+ if (self.positionView) {
+ ionic.off('resize',null,window);
+ }
+
+ return $timeout(function() {
+ $ionicBody.removeClass(self.viewType + '-open');
+ self.el.classList.add('hide');
+ }, self.hideDelay || 320);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionicModal#remove
+ * @description Remove this modal instance from the DOM and clean up.
+ * @returns {promise} A promise which is resolved when the modal is finished animating out.
+ */
+ remove: function() {
+ var self = this;
+ self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self);
+
+ return self.hide().then(function() {
+ self.scope.$destroy();
+ self.$el.remove();
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionicModal#isShown
+ * @returns boolean Whether this modal is currently shown.
+ */
+ isShown: function() {
+ return !!this._isShown;
+ }
+ });
+
+ var createModal = function(templateString, options) {
+ // Create a new scope for the modal
+ var scope = options.scope && options.scope.$new() || $rootScope.$new(true);
+
+ options.viewType = options.viewType || 'modal';
+
+ extend(scope, {
+ $hasHeader: false,
+ $hasSubheader: false,
+ $hasFooter: false,
+ $hasSubfooter: false,
+ $hasTabs: false,
+ $hasTabsTop: false
+ });
+
+ // Compile the template
+ var element = $compile('' + templateString + '')(scope);
+
+ options.$el = element;
+ options.el = element[0];
+ options.modalEl = options.el.querySelector('.' + options.viewType);
+ var modal = new ModalView(options);
+
+ modal.scope = scope;
+
+ // If this wasn't a defined scope, we can assign the viewType to the isolated scope
+ // we created
+ if (!options.scope) {
+ scope[ options.viewType ] = modal;
+ }
+
+ return modal;
+ };
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicModal#fromTemplate
+ * @param {string} templateString The template string to use as the modal's
+ * content.
+ * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
+ * @returns {object} An instance of an {@link ionic.controller:ionicModal}
+ * controller.
+ */
+ fromTemplate: function(templateString, options) {
+ var modal = createModal(templateString, options || {});
+ return modal;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicModal#fromTemplateUrl
+ * @param {string} templateUrl The url to load the template from.
+ * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
+ * options object.
+ * @returns {promise} A promise that will be resolved with an instance of
+ * an {@link ionic.controller:ionicModal} controller.
+ */
+ fromTemplateUrl: function(url, options, _) {
+ var cb;
+ //Deprecated: allow a callback as second parameter. Now we return a promise.
+ if (angular.isFunction(options)) {
+ cb = options;
+ options = _;
+ }
+ return $ionicTemplateLoader.load(url).then(function(templateString) {
+ var modal = createModal(templateString, options || {});
+ cb && cb(modal);
+ return modal;
+ });
+ }
+ };
+}]);
+
+
+/**
+ * @ngdoc service
+ * @name $ionicNavBarDelegate
+ * @module ionic
+ * @description
+ * Delegate for controlling the {@link ionic.directive:ionNavBar} directive.
+ *
+ * @usage
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ * ```js
+ * function MyCtrl($scope, $ionicNavBarDelegate) {
+ * $scope.setNavTitle = function(title) {
+ * $ionicNavBarDelegate.title(title);
+ * }
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicNavBarDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicNavBarDelegate#align
+ * @description Aligns the title with the buttons in a given direction.
+ * @param {string=} direction The direction to the align the title text towards.
+ * Available: 'left', 'right', 'center'. Default: 'center'.
+ */
+ 'align',
+ /**
+ * @ngdoc method
+ * @name $ionicNavBarDelegate#showBackButton
+ * @description
+ * Set/get whether the {@link ionic.directive:ionNavBackButton} is shown
+ * (if it exists and there is a previous view that can be navigated to).
+ * @param {boolean=} show Whether to show the back button.
+ * @returns {boolean} Whether the back button is shown.
+ */
+ 'showBackButton',
+ /**
+ * @ngdoc method
+ * @name $ionicNavBarDelegate#showBar
+ * @description
+ * Set/get whether the {@link ionic.directive:ionNavBar} is shown.
+ * @param {boolean} show Whether to show the bar.
+ * @returns {boolean} Whether the bar is shown.
+ */
+ 'showBar',
+ /**
+ * @ngdoc method
+ * @name $ionicNavBarDelegate#title
+ * @description
+ * Set the title for the {@link ionic.directive:ionNavBar}.
+ * @param {string} title The new title to show.
+ */
+ 'title',
+
+ // DEPRECATED, as of v1.0.0-beta14 -------
+ 'changeTitle',
+ 'setTitle',
+ 'getTitle',
+ 'back',
+ 'getPreviousTitle'
+ // END DEPRECATED -------
+]));
+
+
+IonicModule
+.service('$ionicNavViewDelegate', ionic.DelegateService([
+ 'clearCache'
+]));
+
+
+var PLATFORM_BACK_BUTTON_PRIORITY_VIEW = 100;
+var PLATFORM_BACK_BUTTON_PRIORITY_SIDE_MENU = 150;
+var PLATFORM_BACK_BUTTON_PRIORITY_MODAL = 200;
+var PLATFORM_BACK_BUTTON_PRIORITY_ACTION_SHEET = 300;
+var PLATFORM_BACK_BUTTON_PRIORITY_POPUP = 400;
+var PLATFORM_BACK_BUTTON_PRIORITY_LOADING = 500;
+
+/**
+ * @ngdoc service
+ * @name $ionicPlatform
+ * @module ionic
+ * @description
+ * An angular abstraction of {@link ionic.utility:ionic.Platform}.
+ *
+ * Used to detect the current platform, as well as do things like override the
+ * Android back button in PhoneGap/Cordova.
+ */
+IonicModule
+.provider('$ionicPlatform', function() {
+ return {
+ $get: ['$q', '$rootScope', function($q, $rootScope) {
+ var self = {
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#onHardwareBackButton
+ * @description
+ * Some platforms have a hardware back button, so this is one way to
+ * bind to it.
+ * @param {function} callback the callback to trigger when this event occurs
+ */
+ onHardwareBackButton: function(cb) {
+ ionic.Platform.ready(function() {
+ document.addEventListener('backbutton', cb, false);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#offHardwareBackButton
+ * @description
+ * Remove an event listener for the backbutton.
+ * @param {function} callback The listener function that was
+ * originally bound.
+ */
+ offHardwareBackButton: function(fn) {
+ ionic.Platform.ready(function() {
+ document.removeEventListener('backbutton', fn);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#registerBackButtonAction
+ * @description
+ * Register a hardware back button action. Only one action will execute
+ * when the back button is clicked, so this method decides which of
+ * the registered back button actions has the highest priority.
+ *
+ * For example, if an actionsheet is showing, the back button should
+ * close the actionsheet, but it should not also go back a page view
+ * or close a modal which may be open.
+ *
+ * @param {function} callback Called when the back button is pressed,
+ * if this listener is the highest priority.
+ * @param {number} priority Only the highest priority will execute.
+ * @param {*=} actionId The id to assign this action. Default: a
+ * random unique id.
+ * @returns {function} A function that, when called, will deregister
+ * this backButtonAction.
+ */
+ $backButtonActions: {},
+ registerBackButtonAction: function(fn, priority, actionId) {
+
+ if (!self._hasBackButtonHandler) {
+ // add a back button listener if one hasn't been setup yet
+ self.$backButtonActions = {};
+ self.onHardwareBackButton(self.hardwareBackButtonClick);
+ self._hasBackButtonHandler = true;
+ }
+
+ var action = {
+ id: (actionId ? actionId : ionic.Utils.nextUid()),
+ priority: (priority ? priority : 0),
+ fn: fn
+ };
+ self.$backButtonActions[action.id] = action;
+
+ // return a function to de-register this back button action
+ return function() {
+ delete self.$backButtonActions[action.id];
+ };
+ },
+
+ /**
+ * @private
+ */
+ hardwareBackButtonClick: function(e) {
+ // loop through all the registered back button actions
+ // and only run the last one of the highest priority
+ var priorityAction, actionId;
+ for (actionId in self.$backButtonActions) {
+ if (!priorityAction || self.$backButtonActions[actionId].priority >= priorityAction.priority) {
+ priorityAction = self.$backButtonActions[actionId];
+ }
+ }
+ if (priorityAction) {
+ priorityAction.fn(e);
+ return priorityAction;
+ }
+ },
+
+ is: function(type) {
+ return ionic.Platform.is(type);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#on
+ * @description
+ * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`,
+ * `offline`, etc. More information about available event types can be found in
+ * [Cordova's event documentation](https://cordova.apache.org/docs/en/edge/cordova_events_events.md.html#Events).
+ * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/edge/cordova_events_events.md.html#Events).
+ * @param {function} callback Called when the Cordova event is fired.
+ * @returns {function} Returns a deregistration function to remove the event listener.
+ */
+ on: function(type, cb) {
+ ionic.Platform.ready(function() {
+ document.addEventListener(type, cb, false);
+ });
+ return function() {
+ ionic.Platform.ready(function() {
+ document.removeEventListener(type, cb);
+ });
+ };
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#ready
+ * @description
+ * Trigger a callback once the device is ready,
+ * or immediately if the device is already ready.
+ * @param {function=} callback The function to call.
+ * @returns {promise} A promise which is resolved when the device is ready.
+ */
+ ready: function(cb) {
+ var q = $q.defer();
+
+ ionic.Platform.ready(function() {
+ q.resolve();
+ cb && cb();
+ });
+
+ return q.promise;
+ }
+ };
+ return self;
+ }]
+ };
+
+});
+
+/**
+ * @ngdoc service
+ * @name $ionicPopover
+ * @module ionic
+ * @description
+ *
+ * Related: {@link ionic.controller:ionicPopover ionicPopover controller}.
+ *
+ * The Popover is a view that floats above an app’s content. Popovers provide an
+ * easy way to present or gather information from the user and are
+ * commonly used in the following situations:
+ *
+ * - Show more info about the current view
+ * - Select a commonly used tool or configuration
+ * - Present a list of actions to perform inside one of your views
+ *
+ * Put the content of the popover inside of an `` element.
+ *
+ * @usage
+ * ```html
+ *
Hello! ';
+ *
+ * $scope.popover = $ionicPopover.fromTemplate(template, {
+ * scope: $scope,
+ * });
+ *
+ * // .fromTemplateUrl() method
+ * $ionicPopover.fromTemplateUrl('my-popover.html', {
+ * scope: $scope,
+ * }).then(function(popover) {
+ * $scope.popover = popover;
+ * });
+ *
+ *
+ * $scope.openPopover = function($event) {
+ * $scope.popover.show($event);
+ * };
+ * $scope.closePopover = function() {
+ * $scope.popover.hide();
+ * };
+ * //Cleanup the popover when we're done with it!
+ * $scope.$on('$destroy', function() {
+ * $scope.popover.remove();
+ * });
+ * // Execute action on hide popover
+ * $scope.$on('popover.hidden', function() {
+ * // Execute action
+ * });
+ * // Execute action on remove popover
+ * $scope.$on('popover.removed', function() {
+ * // Execute action
+ * });
+ * });
+ * ```
+ */
+
+
+IonicModule
+.factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', '$window',
+function($ionicModal, $ionicPosition, $document, $window) {
+
+ var POPOVER_BODY_PADDING = 6;
+
+ var POPOVER_OPTIONS = {
+ viewType: 'popover',
+ hideDelay: 1,
+ animation: 'none',
+ positionView: positionView
+ };
+
+ function positionView(target, popoverEle) {
+ var targetEle = angular.element(target.target || target);
+ var buttonOffset = $ionicPosition.offset(targetEle);
+ var popoverWidth = popoverEle.prop('offsetWidth');
+ var popoverHeight = popoverEle.prop('offsetHeight');
+ var bodyWidth = $document[0].body.clientWidth;
+ // clientHeight doesn't work on all platforms for body
+ var bodyHeight = $window.innerHeight;
+
+ var popoverCSS = {
+ left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2
+ };
+ var arrowEle = jqLite(popoverEle[0].querySelector('.popover-arrow'));
+
+ if (popoverCSS.left < POPOVER_BODY_PADDING) {
+ popoverCSS.left = POPOVER_BODY_PADDING;
+ } else if (popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) {
+ popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING;
+ }
+
+ // If the popover when popped down stretches past bottom of screen,
+ // make it pop up
+ if (buttonOffset.top + buttonOffset.height + popoverHeight > bodyHeight) {
+ popoverCSS.top = buttonOffset.top - popoverHeight;
+ popoverEle.addClass('popover-bottom');
+ } else {
+ popoverCSS.top = buttonOffset.top + buttonOffset.height;
+ popoverEle.removeClass('popover-bottom');
+ }
+
+ arrowEle.css({
+ left: buttonOffset.left + buttonOffset.width / 2 -
+ arrowEle.prop('offsetWidth') / 2 - popoverCSS.left + 'px'
+ });
+
+ popoverEle.css({
+ top: popoverCSS.top + 'px',
+ left: popoverCSS.left + 'px',
+ marginLeft: '0',
+ opacity: '1'
+ });
+
+ }
+
+ /**
+ * @ngdoc controller
+ * @name ionicPopover
+ * @module ionic
+ * @description
+ * Instantiated by the {@link ionic.service:$ionicPopover} service.
+ *
+ * Be sure to call [remove()](#remove) when you are done with each popover
+ * to clean it up and avoid memory leaks.
+ *
+ * Note: a popover will broadcast 'popover.shown', 'popover.hidden', and 'popover.removed' events from its originating
+ * scope, passing in itself as an event argument. Both the popover.removed and popover.hidden events are
+ * called when the popover is removed.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#initialize
+ * @description Creates a new popover controller instance.
+ * @param {object} options An options object with the following properties:
+ * - `{object=}` `scope` The scope to be a child of.
+ * Default: creates a child of $rootScope.
+ * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
+ * the popover when shown. Default: false.
+ * - `{boolean=}` `backdropClickToClose` Whether to close the popover on clicking the backdrop.
+ * Default: true.
+ * - `{boolean=}` `hardwareBackButtonClose` Whether the popover can be closed using the hardware
+ * back button on Android and similar devices. Default: true.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#show
+ * @description Show this popover instance.
+ * @param {$event} $event The $event or target element which the popover should align
+ * itself next to.
+ * @returns {promise} A promise which is resolved when the popover is finished animating in.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#hide
+ * @description Hide this popover instance.
+ * @returns {promise} A promise which is resolved when the popover is finished animating out.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#remove
+ * @description Remove this popover instance from the DOM and clean up.
+ * @returns {promise} A promise which is resolved when the popover is finished animating out.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#isShown
+ * @returns boolean Whether this popover is currently shown.
+ */
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicPopover#fromTemplate
+ * @param {string} templateString The template string to use as the popovers's
+ * content.
+ * @param {object} options Options to be passed to the initialize method.
+ * @returns {object} An instance of an {@link ionic.controller:ionicPopover}
+ * controller (ionicPopover is built on top of $ionicPopover).
+ */
+ fromTemplate: function(templateString, options) {
+ return $ionicModal.fromTemplate(templateString, ionic.Utils.extend(POPOVER_OPTIONS, options || {}));
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicPopover#fromTemplateUrl
+ * @param {string} templateUrl The url to load the template from.
+ * @param {object} options Options to be passed to the initialize method.
+ * @returns {promise} A promise that will be resolved with an instance of
+ * an {@link ionic.controller:ionicPopover} controller (ionicPopover is built on top of $ionicPopover).
+ */
+ fromTemplateUrl: function(url, options) {
+ return $ionicModal.fromTemplateUrl(url, ionic.Utils.extend(POPOVER_OPTIONS, options || {}));
+ }
+ };
+
+}]);
+
+
+var POPUP_TPL =
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '
' +
+ '
';
+
+/**
+ * @ngdoc service
+ * @name $ionicPopup
+ * @module ionic
+ * @restrict E
+ * @codepen zkmhJ
+ * @description
+ *
+ * The Ionic Popup service allows programmatically creating and showing popup
+ * windows that require the user to respond in order to continue.
+ *
+ * The popup system has support for more flexible versions of the built in `alert()`, `prompt()`,
+ * and `confirm()` functions that users are used to, in addition to allowing popups with completely
+ * custom content and look.
+ *
+ * An input can be given an `autofocus` attribute so it automatically receives focus when
+ * the popup first shows. However, depending on certain use-cases this can cause issues with
+ * the tap/click system, which is why Ionic prefers using the `autofocus` attribute as
+ * an opt-in feature and not the default.
+ *
+ * @usage
+ * A few basic examples, see below for details about all of the options available.
+ *
+ * ```js
+ *angular.module('mySuperApp', ['ionic'])
+ *.controller('PopupCtrl',function($scope, $ionicPopup, $timeout) {
+ *
+ * // Triggered on a button click, or some other target
+ * $scope.showPopup = function() {
+ * $scope.data = {}
+ *
+ * // An elaborate, custom popup
+ * var myPopup = $ionicPopup.show({
+ * template: '',
+ * title: 'Enter Wi-Fi Password',
+ * subTitle: 'Please use normal things',
+ * scope: $scope,
+ * buttons: [
+ * { text: 'Cancel' },
+ * {
+ * text: 'Save',
+ * type: 'button-positive',
+ * onTap: function(e) {
+ * if (!$scope.data.wifi) {
+ * //don't allow the user to close unless he enters wifi password
+ * e.preventDefault();
+ * } else {
+ * return $scope.data.wifi;
+ * }
+ * }
+ * }
+ * ]
+ * });
+ * myPopup.then(function(res) {
+ * console.log('Tapped!', res);
+ * });
+ * $timeout(function() {
+ * myPopup.close(); //close the popup after 3 seconds for some reason
+ * }, 3000);
+ * };
+ * // A confirm dialog
+ * $scope.showConfirm = function() {
+ * var confirmPopup = $ionicPopup.confirm({
+ * title: 'Consume Ice Cream',
+ * template: 'Are you sure you want to eat this ice cream?'
+ * });
+ * confirmPopup.then(function(res) {
+ * if(res) {
+ * console.log('You are sure');
+ * } else {
+ * console.log('You are not sure');
+ * }
+ * });
+ * };
+ *
+ * // An alert dialog
+ * $scope.showAlert = function() {
+ * var alertPopup = $ionicPopup.alert({
+ * title: 'Don\'t eat that!',
+ * template: 'It might taste good'
+ * });
+ * alertPopup.then(function(res) {
+ * console.log('Thank you for not eating my delicious ice cream cone');
+ * });
+ * };
+ *});
+ *```
+ */
+
+IonicModule
+.factory('$ionicPopup', [
+ '$ionicTemplateLoader',
+ '$ionicBackdrop',
+ '$q',
+ '$timeout',
+ '$rootScope',
+ '$ionicBody',
+ '$compile',
+ '$ionicPlatform',
+function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicBody, $compile, $ionicPlatform) {
+ //TODO allow this to be configured
+ var config = {
+ stackPushDelay: 75
+ };
+ var popupStack = [];
+ var $ionicPopup = {
+ /**
+ * @ngdoc method
+ * @description
+ * Show a complex popup. This is the master show function for all popups.
+ *
+ * A complex popup has a `buttons` array, with each button having a `text` and `type`
+ * field, in addition to an `onTap` function. The `onTap` function, called when
+ * the corresponding button on the popup is tapped, will by default close the popup
+ * and resolve the popup promise with its return value. If you wish to prevent the
+ * default and keep the popup open on button tap, call `event.preventDefault()` on the
+ * passed in tap event. Details below.
+ *
+ * @name $ionicPopup#show
+ * @param {object} options The options for the new popup, of the form:
+ *
+ * ```
+ * {
+ * title: '', // String. The title of the popup.
+ * cssClass: '', // String, The custom CSS class name
+ * subTitle: '', // String (optional). The sub-title of the popup.
+ * template: '', // String (optional). The html template to place in the popup body.
+ * templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
+ * scope: null, // Scope (optional). A scope to link to the popup content.
+ * buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer.
+ * text: 'Cancel',
+ * type: 'button-default',
+ * onTap: function(e) {
+ * // e.preventDefault() will stop the popup from closing when tapped.
+ * e.preventDefault();
+ * }
+ * }, {
+ * text: 'OK',
+ * type: 'button-positive',
+ * onTap: function(e) {
+ * // Returning a value will cause the promise to resolve with the given value.
+ * return scope.data.response;
+ * }
+ * }]
+ * }
+ * ```
+ *
+ * @returns {object} A promise which is resolved when the popup is closed. Has an additional
+ * `close` function, which can be used to programmatically close the popup.
+ */
+ show: showPopup,
+
+ /**
+ * @ngdoc method
+ * @name $ionicPopup#alert
+ * @description Show a simple alert popup with a message and one button that the user can
+ * tap to close the popup.
+ *
+ * @param {object} options The options for showing the alert, of the form:
+ *
+ * ```
+ * {
+ * title: '', // String. The title of the popup.
+ * cssClass: '', // String, The custom CSS class name
+ * subTitle: '', // String (optional). The sub-title of the popup.
+ * template: '', // String (optional). The html template to place in the popup body.
+ * templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
+ * okText: '', // String (default: 'OK'). The text of the OK button.
+ * okType: '', // String (default: 'button-positive'). The type of the OK button.
+ * }
+ * ```
+ *
+ * @returns {object} A promise which is resolved when the popup is closed. Has one additional
+ * function `close`, which can be called with any value to programmatically close the popup
+ * with the given value.
+ */
+ alert: showAlert,
+
+ /**
+ * @ngdoc method
+ * @name $ionicPopup#confirm
+ * @description
+ * Show a simple confirm popup with a Cancel and OK button.
+ *
+ * Resolves the promise with true if the user presses the OK button, and false if the
+ * user presses the Cancel button.
+ *
+ * @param {object} options The options for showing the confirm popup, of the form:
+ *
+ * ```
+ * {
+ * title: '', // String. The title of the popup.
+ * cssClass: '', // String, The custom CSS class name
+ * subTitle: '', // String (optional). The sub-title of the popup.
+ * template: '', // String (optional). The html template to place in the popup body.
+ * templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
+ * cancelText: '', // String (default: 'Cancel'). The text of the Cancel button.
+ * cancelType: '', // String (default: 'button-default'). The type of the Cancel button.
+ * okText: '', // String (default: 'OK'). The text of the OK button.
+ * okType: '', // String (default: 'button-positive'). The type of the OK button.
+ * }
+ * ```
+ *
+ * @returns {object} A promise which is resolved when the popup is closed. Has one additional
+ * function `close`, which can be called with any value to programmatically close the popup
+ * with the given value.
+ */
+ confirm: showConfirm,
+
+ /**
+ * @ngdoc method
+ * @name $ionicPopup#prompt
+ * @description Show a simple prompt popup, which has an input, OK button, and Cancel button.
+ * Resolves the promise with the value of the input if the user presses OK, and with undefined
+ * if the user presses Cancel.
+ *
+ * ```javascript
+ * $ionicPopup.prompt({
+ * title: 'Password Check',
+ * template: 'Enter your secret password',
+ * inputType: 'password',
+ * inputPlaceholder: 'Your password'
+ * }).then(function(res) {
+ * console.log('Your password is', res);
+ * });
+ * ```
+ * @param {object} options The options for showing the prompt popup, of the form:
+ *
+ * ```
+ * {
+ * title: '', // String. The title of the popup.
+ * cssClass: '', // String, The custom CSS class name
+ * subTitle: '', // String (optional). The sub-title of the popup.
+ * template: '', // String (optional). The html template to place in the popup body.
+ * templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
+ * inputType: // String (default: 'text'). The type of input to use
+ * inputPlaceholder: // String (default: ''). A placeholder to use for the input.
+ * cancelText: // String (default: 'Cancel'. The text of the Cancel button.
+ * cancelType: // String (default: 'button-default'). The type of the Cancel button.
+ * okText: // String (default: 'OK'). The text of the OK button.
+ * okType: // String (default: 'button-positive'). The type of the OK button.
+ * }
+ * ```
+ *
+ * @returns {object} A promise which is resolved when the popup is closed. Has one additional
+ * function `close`, which can be called with any value to programmatically close the popup
+ * with the given value.
+ */
+ prompt: showPrompt,
+ /**
+ * @private for testing
+ */
+ _createPopup: createPopup,
+ _popupStack: popupStack
+ };
+
+ return $ionicPopup;
+
+ function createPopup(options) {
+ options = extend({
+ scope: null,
+ title: '',
+ buttons: []
+ }, options || {});
+
+ var popupPromise = $ionicTemplateLoader.compile({
+ template: POPUP_TPL,
+ scope: options.scope && options.scope.$new(),
+ appendTo: $ionicBody.get()
+ });
+ var contentPromise = options.templateUrl ?
+ $ionicTemplateLoader.load(options.templateUrl) :
+ $q.when(options.template || options.content || '');
+
+ return $q.all([popupPromise, contentPromise])
+ .then(function(results) {
+ var self = results[0];
+ var content = results[1];
+ var responseDeferred = $q.defer();
+
+ self.responseDeferred = responseDeferred;
+
+ //Can't ng-bind-html for popup-body because it can be insecure html
+ //(eg an input in case of prompt)
+ var body = jqLite(self.element[0].querySelector('.popup-body'));
+ if (content) {
+ body.html(content);
+ $compile(body.contents())(self.scope);
+ } else {
+ body.remove();
+ }
+
+ extend(self.scope, {
+ title: options.title,
+ buttons: options.buttons,
+ subTitle: options.subTitle,
+ cssClass: options.cssClass,
+ $buttonTapped: function(button, event) {
+ var result = (button.onTap || angular.noop)(event);
+ event = event.originalEvent || event; //jquery events
+
+ if (!event.defaultPrevented) {
+ responseDeferred.resolve(result);
+ }
+ }
+ });
+
+ self.show = function() {
+ if (self.isShown) return;
+
+ self.isShown = true;
+ ionic.requestAnimationFrame(function() {
+ //if hidden while waiting for raf, don't show
+ if (!self.isShown) return;
+
+ self.element.removeClass('popup-hidden');
+ self.element.addClass('popup-showing active');
+ focusInput(self.element);
+ });
+ };
+ self.hide = function(callback) {
+ callback = callback || angular.noop;
+ if (!self.isShown) return callback();
+
+ self.isShown = false;
+ self.element.removeClass('active');
+ self.element.addClass('popup-hidden');
+ $timeout(callback, 250);
+ };
+ self.remove = function() {
+ if (self.removed) return;
+
+ self.hide(function() {
+ self.element.remove();
+ self.scope.$destroy();
+ });
+
+ self.removed = true;
+ };
+
+ return self;
+ });
+ }
+
+ function onHardwareBackButton(e) {
+ popupStack[0] && popupStack[0].responseDeferred.resolve();
+ }
+
+ function showPopup(options) {
+ var popupPromise = $ionicPopup._createPopup(options);
+ var previousPopup = popupStack[0];
+
+ if (previousPopup) {
+ previousPopup.hide();
+ }
+
+ var resultPromise = $timeout(angular.noop, previousPopup ? config.stackPushDelay : 0)
+ .then(function() { return popupPromise; })
+ .then(function(popup) {
+ if (!previousPopup) {
+ //Add popup-open & backdrop if this is first popup
+ $ionicBody.addClass('popup-open');
+ $ionicBackdrop.retain();
+ //only show the backdrop on the first popup
+ $ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction(
+ onHardwareBackButton,
+ PLATFORM_BACK_BUTTON_PRIORITY_POPUP
+ );
+ }
+ popupStack.unshift(popup);
+ popup.show();
+
+ //DEPRECATED: notify the promise with an object with a close method
+ popup.responseDeferred.notify({
+ close: resultPromise.close
+ });
+
+ return popup.responseDeferred.promise.then(function(result) {
+ var index = popupStack.indexOf(popup);
+ if (index !== -1) {
+ popupStack.splice(index, 1);
+ }
+ popup.remove();
+
+ var previousPopup = popupStack[0];
+ if (previousPopup) {
+ previousPopup.show();
+ } else {
+ //Remove popup-open & backdrop if this is last popup
+ $timeout(function() {
+ // wait to remove this due to a 300ms delay native
+ // click which would trigging whatever was underneath this
+ $ionicBody.removeClass('popup-open');
+ }, 400);
+ $timeout(function() {
+ $ionicBackdrop.release();
+ }, config.stackPushDelay || 0);
+ ($ionicPopup._backButtonActionDone || angular.noop)();
+ }
+ return result;
+ });
+ });
+
+ function close(result) {
+ popupPromise.then(function(popup) {
+ if (!popup.removed) {
+ popup.responseDeferred.resolve(result);
+ }
+ });
+ }
+ resultPromise.close = close;
+
+ return resultPromise;
+ }
+
+ function focusInput(element) {
+ var focusOn = element[0].querySelector('[autofocus]');
+ if (focusOn) {
+ focusOn.focus();
+ }
+ }
+
+ function showAlert(opts) {
+ return showPopup(extend({
+ buttons: [{
+ text: opts.okText || 'OK',
+ type: opts.okType || 'button-positive',
+ onTap: function(e) {
+ return true;
+ }
+ }]
+ }, opts || {}));
+ }
+
+ function showConfirm(opts) {
+ return showPopup(extend({
+ buttons: [{
+ text: opts.cancelText || 'Cancel',
+ type: opts.cancelType || 'button-default',
+ onTap: function(e) { return false; }
+ }, {
+ text: opts.okText || 'OK',
+ type: opts.okType || 'button-positive',
+ onTap: function(e) { return true; }
+ }]
+ }, opts || {}));
+ }
+
+ function showPrompt(opts) {
+ var scope = $rootScope.$new(true);
+ scope.data = {};
+ var text = '';
+ if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) {
+ text = '' + opts.template + '';
+ delete opts.template;
+ }
+ return showPopup(extend({
+ template: text + '',
+ scope: scope,
+ buttons: [{
+ text: opts.cancelText || 'Cancel',
+ type: opts.cancelType || 'button-default',
+ onTap: function(e) {}
+ }, {
+ text: opts.okText || 'OK',
+ type: opts.okType || 'button-positive',
+ onTap: function(e) {
+ return scope.data.response || '';
+ }
+ }]
+ }, opts || {}));
+ }
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicPosition
+ * @module ionic
+ * @description
+ * A set of utility methods that can be use to retrieve position of DOM elements.
+ * It is meant to be used where we need to absolute-position DOM elements in
+ * relation to other, existing elements (this is the case for tooltips, popovers, etc.).
+ *
+ * Adapted from [AngularUI Bootstrap](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js),
+ * ([license](https://github.com/angular-ui/bootstrap/blob/master/LICENSE))
+ */
+IonicModule
+.factory('$ionicPosition', ['$document', '$window', function($document, $window) {
+
+ function getStyle(el, cssprop) {
+ if (el.currentStyle) { //IE
+ return el.currentStyle[cssprop];
+ } else if ($window.getComputedStyle) {
+ return $window.getComputedStyle(el)[cssprop];
+ }
+ // finally try and get inline style
+ return el.style[cssprop];
+ }
+
+ /**
+ * Checks if a given element is statically positioned
+ * @param element - raw DOM element
+ */
+ function isStaticPositioned(element) {
+ return (getStyle(element, 'position') || 'static') === 'static';
+ }
+
+ /**
+ * returns the closest, non-statically positioned parentOffset of a given element
+ * @param element
+ */
+ var parentOffsetEl = function(element) {
+ var docDomEl = $document[0];
+ var offsetParent = element.offsetParent || docDomEl;
+ while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) {
+ offsetParent = offsetParent.offsetParent;
+ }
+ return offsetParent || docDomEl;
+ };
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicPosition#position
+ * @description Get the current coordinates of the element, relative to the offset parent.
+ * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/).
+ * @param {element} element The element to get the position of.
+ * @returns {object} Returns an object containing the properties top, left, width and height.
+ */
+ position: function(element) {
+ var elBCR = this.offset(element);
+ var offsetParentBCR = { top: 0, left: 0 };
+ var offsetParentEl = parentOffsetEl(element[0]);
+ if (offsetParentEl != $document[0]) {
+ offsetParentBCR = this.offset(angular.element(offsetParentEl));
+ offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
+ offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
+ }
+
+ var boundingClientRect = element[0].getBoundingClientRect();
+ return {
+ width: boundingClientRect.width || element.prop('offsetWidth'),
+ height: boundingClientRect.height || element.prop('offsetHeight'),
+ top: elBCR.top - offsetParentBCR.top,
+ left: elBCR.left - offsetParentBCR.left
+ };
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPosition#offset
+ * @description Get the current coordinates of the element, relative to the document.
+ * Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/).
+ * @param {element} element The element to get the offset of.
+ * @returns {object} Returns an object containing the properties top, left, width and height.
+ */
+ offset: function(element) {
+ var boundingClientRect = element[0].getBoundingClientRect();
+ return {
+ width: boundingClientRect.width || element.prop('offsetWidth'),
+ height: boundingClientRect.height || element.prop('offsetHeight'),
+ top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
+ left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
+ };
+ }
+
+ };
+}]);
+
+
+/**
+ * @ngdoc service
+ * @name $ionicScrollDelegate
+ * @module ionic
+ * @description
+ * Delegate for controlling scrollViews (created by
+ * {@link ionic.directive:ionContent} and
+ * {@link ionic.directive:ionScroll} directives).
+ *
+ * Methods called directly on the $ionicScrollDelegate service will control all scroll
+ * views. Use the {@link ionic.service:$ionicScrollDelegate#$getByHandle $getByHandle}
+ * method to control specific scrollViews.
+ *
+ * @usage
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ * ```js
+ * function MainCtrl($scope, $ionicScrollDelegate) {
+ * $scope.scrollTop = function() {
+ * $ionicScrollDelegate.scrollTop();
+ * };
+ * }
+ * ```
+ *
+ * Example of advanced usage, with two scroll areas using `delegate-handle`
+ * for fine control.
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ * ```js
+ * function MainCtrl($scope, $ionicScrollDelegate) {
+ * $scope.scrollMainToTop = function() {
+ * $ionicScrollDelegate.$getByHandle('mainScroll').scrollTop();
+ * };
+ * $scope.scrollSmallToTop = function() {
+ * $ionicScrollDelegate.$getByHandle('small').scrollTop();
+ * };
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicScrollDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#resize
+ * @description Tell the scrollView to recalculate the size of its container.
+ */
+ 'resize',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#scrollTop
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'scrollTop',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#scrollBottom
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'scrollBottom',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#scrollTo
+ * @param {number} left The x-value to scroll to.
+ * @param {number} top The y-value to scroll to.
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'scrollTo',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#scrollBy
+ * @param {number} left The x-offset to scroll by.
+ * @param {number} top The y-offset to scroll by.
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'scrollBy',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#zoomTo
+ * @param {number} level Level to zoom to.
+ * @param {boolean=} animate Whether to animate the zoom.
+ * @param {number=} originLeft Zoom in at given left coordinate.
+ * @param {number=} originTop Zoom in at given top coordinate.
+ */
+ 'zoomTo',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#zoomBy
+ * @param {number} factor The factor to zoom by.
+ * @param {boolean=} animate Whether to animate the zoom.
+ * @param {number=} originLeft Zoom in at given left coordinate.
+ * @param {number=} originTop Zoom in at given top coordinate.
+ */
+ 'zoomBy',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#getScrollPosition
+ * @returns {object} The scroll position of this view, with the following properties:
+ * - `{number}` `left` The distance the user has scrolled from the left (starts at 0).
+ * - `{number}` `top` The distance the user has scrolled from the top (starts at 0).
+ */
+ 'getScrollPosition',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#anchorScroll
+ * @description Tell the scrollView to scroll to the element with an id
+ * matching window.location.hash.
+ *
+ * If no matching element is found, it will scroll to top.
+ *
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'anchorScroll',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#getScrollView
+ * @returns {object} The scrollView associated with this delegate.
+ */
+ 'getScrollView',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * scrollViews with `delegate-handle` matching the given handle.
+ *
+ * Example: `$ionicScrollDelegate.$getByHandle('my-handle').scrollTop();`
+ */
+]));
+
+
+/**
+ * @ngdoc service
+ * @name $ionicSideMenuDelegate
+ * @module ionic
+ *
+ * @description
+ * Delegate for controlling the {@link ionic.directive:ionSideMenus} directive.
+ *
+ * Methods called directly on the $ionicSideMenuDelegate service will control all side
+ * menus. Use the {@link ionic.service:$ionicSideMenuDelegate#$getByHandle $getByHandle}
+ * method to control specific ionSideMenus instances.
+ *
+ * @usage
+ *
+ * ```html
+ *
+ *
+ *
+ * Content!
+ *
+ *
+ *
+ * Left Menu!
+ *
+ *
+ *
+ * ```
+ * ```js
+ * function MainCtrl($scope, $ionicSideMenuDelegate) {
+ * $scope.toggleLeftSideMenu = function() {
+ * $ionicSideMenuDelegate.toggleLeft();
+ * };
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicSideMenuDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#toggleLeft
+ * @description Toggle the left side menu (if it exists).
+ * @param {boolean=} isOpen Whether to open or close the menu.
+ * Default: Toggles the menu.
+ */
+ 'toggleLeft',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#toggleRight
+ * @description Toggle the right side menu (if it exists).
+ * @param {boolean=} isOpen Whether to open or close the menu.
+ * Default: Toggles the menu.
+ */
+ 'toggleRight',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#getOpenRatio
+ * @description Gets the ratio of open amount over menu width. For example, a
+ * menu of width 100 that is opened by 50 pixels is 50% opened, and would return
+ * a ratio of 0.5.
+ *
+ * @returns {float} 0 if nothing is open, between 0 and 1 if left menu is
+ * opened/opening, and between 0 and -1 if right menu is opened/opening.
+ */
+ 'getOpenRatio',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#isOpen
+ * @returns {boolean} Whether either the left or right menu is currently opened.
+ */
+ 'isOpen',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#isOpenLeft
+ * @returns {boolean} Whether the left menu is currently opened.
+ */
+ 'isOpenLeft',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#isOpenRight
+ * @returns {boolean} Whether the right menu is currently opened.
+ */
+ 'isOpenRight',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#canDragContent
+ * @param {boolean=} canDrag Set whether the content can or cannot be dragged to open
+ * side menus.
+ * @returns {boolean} Whether the content can be dragged to open side menus.
+ */
+ 'canDragContent',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#edgeDragThreshold
+ * @param {boolean|number=} value Set whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Accepts three different values:
+ * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu.
+ * - If true is given, the default number of pixels (25) is used as the maximum allowed distance.
+ * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed.
+ * @returns {boolean} Whether the drag can start only from within the edge of screen threshold.
+ */
+ 'edgeDragThreshold',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * {@link ionic.directive:ionSideMenus} directives with `delegate-handle` matching
+ * the given handle.
+ *
+ * Example: `$ionicSideMenuDelegate.$getByHandle('my-handle').toggleLeft();`
+ */
+]));
+
+
+/**
+ * @ngdoc service
+ * @name $ionicSlideBoxDelegate
+ * @module ionic
+ * @description
+ * Delegate that controls the {@link ionic.directive:ionSlideBox} directive.
+ *
+ * Methods called directly on the $ionicSlideBoxDelegate service will control all slide boxes. Use the {@link ionic.service:$ionicSlideBoxDelegate#$getByHandle $getByHandle}
+ * method to control specific slide box instances.
+ *
+ * @usage
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Slide 2!
+ *
+ *
+ *
+ *
+ * ```
+ * ```js
+ * function MyCtrl($scope, $ionicSlideBoxDelegate) {
+ * $scope.nextSlide = function() {
+ * $ionicSlideBoxDelegate.next();
+ * }
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicSlideBoxDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#update
+ * @description
+ * Update the slidebox (for example if using Angular with ng-repeat,
+ * resize it for the elements inside).
+ */
+ 'update',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#slide
+ * @param {number} to The index to slide to.
+ * @param {number=} speed The number of milliseconds for the change to take.
+ */
+ 'slide',
+ 'select',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#enableSlide
+ * @param {boolean=} shouldEnable Whether to enable sliding the slidebox.
+ * @returns {boolean} Whether sliding is enabled.
+ */
+ 'enableSlide',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#previous
+ * @description Go to the previous slide. Wraps around if at the beginning.
+ */
+ 'previous',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#next
+ * @description Go to the next slide. Wraps around if at the end.
+ */
+ 'next',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#stop
+ * @description Stop sliding. The slideBox will not move again until
+ * explicitly told to do so.
+ */
+ 'stop',
+ 'autoPlay',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#start
+ * @description Start sliding again if the slideBox was stopped.
+ */
+ 'start',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#currentIndex
+ * @returns number The index of the current slide.
+ */
+ 'currentIndex',
+ 'selected',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#slidesCount
+ * @returns number The number of slides there are currently.
+ */
+ 'slidesCount',
+ 'count',
+ 'loop',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * {@link ionic.directive:ionSlideBox} directives with `delegate-handle` matching
+ * the given handle.
+ *
+ * Example: `$ionicSlideBoxDelegate.$getByHandle('my-handle').stop();`
+ */
+]));
+
+
+/**
+ * @ngdoc service
+ * @name $ionicTabsDelegate
+ * @module ionic
+ *
+ * @description
+ * Delegate for controlling the {@link ionic.directive:ionTabs} directive.
+ *
+ * Methods called directly on the $ionicTabsDelegate service will control all ionTabs
+ * directives. Use the {@link ionic.service:$ionicTabsDelegate#$getByHandle $getByHandle}
+ * method to control specific ionTabs instances.
+ *
+ * @usage
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ * Hello tab 1!
+ *
+ *
+ * Hello tab 2!
+ *
+ *
+ *
+ * ```
+ * ```js
+ * function MyCtrl($scope, $ionicTabsDelegate) {
+ * $scope.selectTabWithIndex = function(index) {
+ * $ionicTabsDelegate.select(index);
+ * }
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicTabsDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicTabsDelegate#select
+ * @description Select the tab matching the given index.
+ *
+ * @param {number} index Index of the tab to select.
+ */
+ 'select',
+ /**
+ * @ngdoc method
+ * @name $ionicTabsDelegate#selectedIndex
+ * @returns `number` The index of the selected tab, or -1.
+ */
+ 'selectedIndex'
+ /**
+ * @ngdoc method
+ * @name $ionicTabsDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * {@link ionic.directive:ionTabs} directives with `delegate-handle` matching
+ * the given handle.
+ *
+ * Example: `$ionicTabsDelegate.$getByHandle('my-handle').select(0);`
+ */
+]));
+
+
+// closure to keep things neat
+(function() {
+ var templatesToCache = [];
+
+/**
+ * @ngdoc service
+ * @name $ionicTemplateCache
+ * @module ionic
+ * @description A service that preemptively caches template files to eliminate transition flicker and boost performance.
+ * @usage
+ * State templates are cached automatically, but you can optionally cache other templates.
+ *
+ * ```js
+ * $ionicTemplateCache('myNgIncludeTemplate.html');
+ * ```
+ *
+ * Optionally disable all preemptive caching with the `$ionicConfigProvider` or individual states by setting `prefetchTemplate`
+ * in the `$state` definition
+ *
+ * ```js
+ * angular.module('myApp', ['ionic'])
+ * .config(function($stateProvider, $ionicConfigProvider) {
+ *
+ * // disable preemptive template caching globally
+ * $ionicConfigProvider.templates.prefetch(false);
+ *
+ * // disable individual states
+ * $stateProvider
+ * .state('tabs', {
+ * url: "/tab",
+ * abstract: true,
+ * prefetchTemplate: false,
+ * templateUrl: "tabs-templates/tabs.html"
+ * })
+ * .state('tabs.home', {
+ * url: "/home",
+ * views: {
+ * 'home-tab': {
+ * prefetchTemplate: false,
+ * templateUrl: "tabs-templates/home.html",
+ * controller: 'HomeTabCtrl'
+ * }
+ * }
+ * });
+ * });
+ * ```
+ */
+IonicModule
+.factory('$ionicTemplateCache', [
+'$http',
+'$templateCache',
+'$timeout',
+function($http, $templateCache, $timeout) {
+ var toCache = templatesToCache,
+ hasRun;
+
+ function $ionicTemplateCache(templates) {
+ if (typeof templates === 'undefined') {
+ return run();
+ }
+ if (isString(templates)) {
+ templates = [templates];
+ }
+ forEach(templates, function(template) {
+ toCache.push(template);
+ });
+ if (hasRun) {
+ run();
+ }
+ }
+
+ // run through methods - internal method
+ function run() {
+ $ionicTemplateCache._runCount++;
+
+ hasRun = true;
+ // ignore if race condition already zeroed out array
+ if (toCache.length === 0) return;
+
+ var i = 0;
+ while (i < 4 && (template = toCache.pop())) {
+ // note that inline templates are ignored by this request
+ if (isString(template)) $http.get(template, { cache: $templateCache });
+ i++;
+ }
+ // only preload 3 templates a second
+ if (toCache.length) {
+ $timeout(run, 1000);
+ }
+ }
+
+ // exposing for testing
+ $ionicTemplateCache._runCount = 0;
+ // default method
+ return $ionicTemplateCache;
+}])
+
+// Intercepts the $stateprovider.state() command to look for templateUrls that can be cached
+.config([
+'$stateProvider',
+'$ionicConfigProvider',
+function($stateProvider, $ionicConfigProvider) {
+ var stateProviderState = $stateProvider.state;
+ $stateProvider.state = function(stateName, definition) {
+ // don't even bother if it's disabled. note, another config may run after this, so it's not a catch-all
+ if (typeof definition === 'object') {
+ var enabled = definition.prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
+ if (enabled && isString(definition.templateUrl)) templatesToCache.push(definition.templateUrl);
+ if (angular.isObject(definition.views)) {
+ for (var key in definition.views) {
+ enabled = definition.views[key].prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
+ if (enabled && isString(definition.views[key].templateUrl)) templatesToCache.push(definition.views[key].templateUrl);
+ }
+ }
+ }
+ return stateProviderState.call($stateProvider, stateName, definition);
+ };
+}])
+
+// process the templateUrls collected by the $stateProvider, adding them to the cache
+.run(['$ionicTemplateCache', function($ionicTemplateCache) {
+ $ionicTemplateCache();
+}]);
+
+})();
+
+IonicModule
+.factory('$ionicTemplateLoader', [
+ '$compile',
+ '$controller',
+ '$http',
+ '$q',
+ '$rootScope',
+ '$templateCache',
+function($compile, $controller, $http, $q, $rootScope, $templateCache) {
+
+ return {
+ load: fetchTemplate,
+ compile: loadAndCompile
+ };
+
+ function fetchTemplate(url) {
+ return $http.get(url, {cache: $templateCache})
+ .then(function(response) {
+ return response.data && response.data.trim();
+ });
+ }
+
+ function loadAndCompile(options) {
+ options = extend({
+ template: '',
+ templateUrl: '',
+ scope: null,
+ controller: null,
+ locals: {},
+ appendTo: null
+ }, options || {});
+
+ var templatePromise = options.templateUrl ?
+ this.load(options.templateUrl) :
+ $q.when(options.template);
+
+ return templatePromise.then(function(template) {
+ var controller;
+ var scope = options.scope || $rootScope.$new();
+
+ //Incase template doesn't have just one root element, do this
+ var element = jqLite('
').html(template).contents();
+
+ if (options.controller) {
+ controller = $controller(
+ options.controller,
+ extend(options.locals, {
+ $scope: scope
+ })
+ );
+ element.children().data('$ngControllerController', controller);
+ }
+ if (options.appendTo) {
+ jqLite(options.appendTo).append(element);
+ }
+
+ $compile(element)(scope);
+
+ return {
+ element: element,
+ scope: scope
+ };
+ });
+ }
+
+}]);
+
+/**
+ * @private
+ * DEPRECATED, as of v1.0.0-beta14 -------
+ */
+IonicModule
+.factory('$ionicViewService', ['$ionicHistory', '$log', function($ionicHistory, $log) {
+
+ function warn(oldMethod, newMethod) {
+ $log.warn('$ionicViewService' + oldMethod + ' is deprecated, please use $ionicHistory' + newMethod + ' instead: http://ionicframework.com/docs/nightly/api/service/$ionicHistory/');
+ }
+
+ warn('', '');
+
+ var methodsMap = {
+ getCurrentView: 'currentView',
+ getBackView: 'backView',
+ getForwardView: 'forwardView',
+ getCurrentStateName: 'currentStateName',
+ nextViewOptions: 'nextViewOptions',
+ clearHistory: 'clearHistory'
+ };
+
+ forEach(methodsMap, function(newMethod, oldMethod) {
+ methodsMap[oldMethod] = function() {
+ warn('.' + oldMethod, '.' + newMethod);
+ return $ionicHistory[newMethod].apply(this, arguments);
+ };
+ });
+
+ return methodsMap;
+
+}]);
+
+/**
+ * @private
+ * TODO document
+ */
+
+IonicModule
+.factory('$ionicViewSwitcher',[
+ '$timeout',
+ '$document',
+ '$q',
+ '$ionicClickBlock',
+ '$ionicConfig',
+ '$ionicNavBarDelegate',
+function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDelegate) {
+
+ var TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
+ var DATA_NO_CACHE = '$noCache';
+ var DATA_DESTROY_ELE = '$destroyEle';
+ var DATA_ELE_IDENTIFIER = '$eleId';
+ var DATA_VIEW_ACCESSED = '$accessed';
+ var DATA_FALLBACK_TIMER = '$fallbackTimer';
+ var DATA_VIEW = '$viewData';
+ var NAV_VIEW_ATTR = 'nav-view';
+ var HISTORY_CURSOR_ATTR = 'history-cursor';
+ var VIEW_STATUS_ACTIVE = 'active';
+ var VIEW_STATUS_CACHED = 'cached';
+ var VIEW_STATUS_STAGED = 'stage';
+
+ var transitionCounter = 0;
+ var nextTransition, nextDirection;
+ ionic.transition = ionic.transition || {};
+ ionic.transition.isActive = false;
+ var isActiveTimer;
+ var cachedAttr = ionic.DomUtil.cachedAttr;
+ var transitionPromises = [];
+
+ var ionicViewSwitcher = {
+
+ create: function(navViewCtrl, viewLocals, enteringView, leavingView) {
+ // get a reference to an entering/leaving element if they exist
+ // loop through to see if the view is already in the navViewElement
+ var enteringEle, leavingEle;
+ var transitionId = ++transitionCounter;
+ var alreadyInDom;
+
+ var switcher = {
+
+ init: function(registerData, callback) {
+ ionicViewSwitcher.isTransitioning(true);
+
+ switcher.loadViewElements(registerData);
+
+ switcher.render(registerData, function() {
+ callback && callback();
+ });
+ },
+
+ loadViewElements: function(registerData) {
+ var viewEle, viewElements = navViewCtrl.getViewElements();
+ var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView);
+ var navViewActiveEleId = navViewCtrl.activeEleId();
+
+ for (var x = 0, l = viewElements.length; x < l; x++) {
+ viewEle = viewElements.eq(x);
+
+ if (viewEle.data(DATA_ELE_IDENTIFIER) === enteringEleIdentifier) {
+ // we found an existing element in the DOM that should be entering the view
+ if (viewEle.data(DATA_NO_CACHE)) {
+ // the existing element should not be cached, don't use it
+ viewEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier + ionic.Utils.nextUid());
+ viewEle.data(DATA_DESTROY_ELE, true);
+
+ } else {
+ enteringEle = viewEle;
+ }
+
+ } else if (viewEle.data(DATA_ELE_IDENTIFIER) === navViewActiveEleId) {
+ leavingEle = viewEle;
+ }
+
+ if (enteringEle && leavingEle) break;
+ }
+
+ alreadyInDom = !!enteringEle;
+
+ if (!alreadyInDom) {
+ // still no existing element to use
+ // create it using existing template/scope/locals
+ enteringEle = registerData.ele || ionicViewSwitcher.createViewEle(viewLocals);
+
+ // existing elements in the DOM are looked up by their state name and state id
+ enteringEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier);
+ }
+
+ navViewCtrl.activeEleId(enteringEleIdentifier);
+
+ registerData.ele = null;
+ },
+
+ render: function(registerData, callback) {
+ // disconnect the leaving scope before reconnecting or creating a scope for the entering view
+ leavingEle && ionic.Utils.disconnectScope(leavingEle.scope());
+
+ if (alreadyInDom) {
+ // it was already found in the DOM, just reconnect the scope
+ ionic.Utils.reconnectScope(enteringEle.scope());
+
+ } else {
+ // the entering element is not already in the DOM
+ // set that the entering element should be "staged" and its
+ // styles of where this element will go before it hits the DOM
+ navViewAttr(enteringEle, VIEW_STATUS_STAGED);
+
+ var enteringData = getTransitionData(viewLocals, enteringEle, registerData.direction, enteringView);
+ var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none;
+ transitionFn(enteringEle, null, enteringData.direction, true).run(0);
+
+ enteringEle.data(DATA_VIEW, {
+ viewId: enteringData.viewId,
+ historyId: enteringData.historyId,
+ stateName: enteringData.stateName,
+ stateParams: enteringData.stateParams
+ });
+
+ // if the current state has cache:false
+ // or the element has cache-view="false" attribute
+ if (viewState(viewLocals).cache === false || viewState(viewLocals).cache === 'false' ||
+ enteringEle.attr('cache-view') == 'false' || $ionicConfig.views.maxCache() === 0) {
+ enteringEle.data(DATA_NO_CACHE, true);
+ }
+
+ // append the entering element to the DOM, create a new scope and run link
+ var viewScope = navViewCtrl.appendViewElement(enteringEle, viewLocals);
+
+ delete enteringData.direction;
+ delete enteringData.transition;
+ viewScope.$emit('$ionicView.loaded', enteringData);
+ }
+
+ // update that this view was just accessed
+ enteringEle.data(DATA_VIEW_ACCESSED, Date.now());
+
+ callback && callback();
+ },
+
+ transition: function(direction, enableBack) {
+ var deferred = $q.defer();
+ transitionPromises.push(deferred.promise);
+
+ var enteringData = getTransitionData(viewLocals, enteringEle, direction, enteringView);
+ var leavingData = extend(extend({}, enteringData), getViewData(leavingView));
+ enteringData.transitionId = leavingData.transitionId = transitionId;
+ enteringData.fromCache = !!alreadyInDom;
+ enteringData.enableBack = !!enableBack;
+
+ cachedAttr(enteringEle.parent(), 'nav-view-transition', enteringData.transition);
+ cachedAttr(enteringEle.parent(), 'nav-view-direction', enteringData.direction);
+
+ // cancel any previous transition complete fallbacks
+ $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
+
+ switcher.emit('before', enteringData, leavingData);
+
+ // 1) get the transition ready and see if it'll animate
+ var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none;
+ var viewTransition = transitionFn(enteringEle, leavingEle, enteringData.direction, enteringData.shouldAnimate);
+
+ if (viewTransition.shouldAnimate) {
+ // 2) attach transitionend events (and fallback timer)
+ enteringEle.on(TRANSITIONEND_EVENT, transitionComplete);
+ enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, 1000));
+ $ionicClickBlock.show();
+ }
+
+ // 3) stage entering element, opacity 0, no transition duration
+ navViewAttr(enteringEle, VIEW_STATUS_STAGED);
+
+ // 4) place the elements in the correct step to begin
+ viewTransition.run(0);
+
+ // 5) wait a frame so the styles apply
+ $timeout(onReflow, 16);
+
+ function onReflow() {
+ // 6) remove that we're staging the entering element so it can transition
+ navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE);
+ navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED);
+
+ // 7) start the transition
+ viewTransition.run(1);
+
+ $ionicNavBarDelegate._instances.forEach(function(instance) {
+ instance.triggerTransitionStart(transitionId);
+ });
+
+ if (!viewTransition.shouldAnimate) {
+ // no animated transition
+ transitionComplete();
+ }
+ }
+
+ function transitionComplete() {
+ if (transitionComplete.x) return;
+ transitionComplete.x = true;
+
+ enteringEle.off(TRANSITIONEND_EVENT, transitionComplete);
+ $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
+ leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER));
+
+ // 8) emit that the views have finished transitioning
+ // each parent nav-view will update which views are active and cached
+ switcher.emit('after', enteringData, leavingData);
+
+ // 9) resolve that this one transition (there could be many w/ nested views)
+ deferred.resolve(navViewCtrl);
+
+ // 10) the most recent transition added has completed and all the active
+ // transition promises should be added to the services array of promises
+ if (transitionId === transitionCounter) {
+ $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd);
+ switcher.cleanup(enteringData);
+ }
+
+ $ionicNavBarDelegate._instances.forEach(function(instance) {
+ instance.triggerTransitionEnd();
+ });
+
+ // remove any references that could cause memory issues
+ nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null;
+ }
+
+ },
+
+ emit: function(step, enteringData, leavingData) {
+ var scope = enteringEle.scope();
+ if (scope) {
+ scope.$emit('$ionicView.' + step + 'Enter', enteringData);
+ if (step == 'after') {
+ scope.$emit('$ionicView.enter', enteringData);
+ }
+ }
+
+ if (leavingEle) {
+ scope = leavingEle.scope();
+ if (scope) {
+ scope.$emit('$ionicView.' + step + 'Leave', leavingData);
+ if (step == 'after') {
+ scope.$emit('$ionicView.leave', leavingData);
+ }
+ }
+ }
+ },
+
+ cleanup: function(transData) {
+ // check if any views should be removed
+ if (leavingEle && transData.direction == 'back' && !$ionicConfig.views.forwardCache()) {
+ // if they just navigated back we can destroy the forward view
+ // do not remove forward views if cacheForwardViews config is true
+ destroyViewEle(leavingEle);
+ }
+
+ var viewElements = navViewCtrl.getViewElements();
+ var viewElementsLength = viewElements.length;
+ var x, viewElement;
+ var removeOldestAccess = (viewElementsLength - 1) > $ionicConfig.views.maxCache();
+ var removableEle;
+ var oldestAccess = Date.now();
+
+ for (x = 0; x < viewElementsLength; x++) {
+ viewElement = viewElements.eq(x);
+
+ if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) {
+ // remember what was the oldest element to be accessed so it can be destroyed
+ oldestAccess = viewElement.data(DATA_VIEW_ACCESSED);
+ removableEle = viewElements.eq(x);
+
+ } else if (viewElement.data(DATA_DESTROY_ELE) && navViewAttr(viewElement) != VIEW_STATUS_ACTIVE) {
+ destroyViewEle(viewElement);
+ }
+ }
+
+ destroyViewEle(removableEle);
+
+ if (enteringEle.data(DATA_NO_CACHE)) {
+ enteringEle.data(DATA_DESTROY_ELE, true);
+ }
+ },
+
+ enteringEle: function() { return enteringEle; },
+ leavingEle: function() { return leavingEle; }
+
+ };
+
+ return switcher;
+ },
+
+ transitionEnd: function(navViewCtrls) {
+ forEach(navViewCtrls, function(navViewCtrl){
+ navViewCtrl.transitionEnd();
+ });
+
+ ionicViewSwitcher.isTransitioning(false);
+ $ionicClickBlock.hide();
+ transitionPromises = [];
+ },
+
+ nextTransition: function(val) {
+ nextTransition = val;
+ },
+
+ nextDirection: function(val) {
+ nextDirection = val;
+ },
+
+ isTransitioning: function(val) {
+ if (arguments.length) {
+ ionic.transition.isActive = !!val;
+ $timeout.cancel(isActiveTimer);
+ if (val) {
+ isActiveTimer = $timeout(function() {
+ ionicViewSwitcher.isTransitioning(false);
+ }, 999);
+ }
+ }
+ return ionic.transition.isActive;
+ },
+
+ createViewEle: function(viewLocals) {
+ var containerEle = $document[0].createElement('div');
+ if (viewLocals && viewLocals.$template) {
+ containerEle.innerHTML = viewLocals.$template;
+ if (containerEle.children.length === 1) {
+ containerEle.children[0].classList.add('pane');
+ return jqLite(containerEle.children[0]);
+ }
+ }
+ containerEle.className = "pane";
+ return jqLite(containerEle);
+ },
+
+ viewEleIsActive: function(viewEle, isActiveAttr) {
+ navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED);
+ },
+
+ getTransitionData: getTransitionData,
+ navViewAttr: navViewAttr,
+ destroyViewEle: destroyViewEle
+
+ };
+
+ return ionicViewSwitcher;
+
+
+ function getViewElementIdentifier(locals, view) {
+ if (viewState(locals).abstract) return viewState(locals).name;
+ if (view) return view.stateId || view.viewId;
+ return ionic.Utils.nextUid();
+ }
+
+ function viewState(locals) {
+ return locals && locals.$$state && locals.$$state.self || {};
+ }
+
+ function getTransitionData(viewLocals, enteringEle, direction, view) {
+ // Priority
+ // 1) attribute directive on the button/link to this view
+ // 2) entering element's attribute
+ // 3) entering view's $state config property
+ // 4) view registration data
+ // 5) global config
+ // 6) fallback value
+
+ var state = viewState(viewLocals);
+ var viewTransition = nextTransition || cachedAttr(enteringEle, 'view-transition') || state.viewTransition || $ionicConfig.views.transition() || 'ios';
+ var navBarTransition = $ionicConfig.navBar.transition();
+ direction = nextDirection || cachedAttr(enteringEle, 'view-direction') || state.viewDirection || direction || 'none';
+
+ return extend(getViewData(view), {
+ transition: viewTransition,
+ navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition,
+ direction: direction,
+ shouldAnimate: (viewTransition !== 'none' && direction !== 'none')
+ });
+ }
+
+ function getViewData(view) {
+ view = view || {};
+ return {
+ viewId: view.viewId,
+ historyId: view.historyId,
+ stateId: view.stateId,
+ stateName: view.stateName,
+ stateParams: view.stateParams
+ };
+ }
+
+ function navViewAttr(ele, value) {
+ if (arguments.length > 1) {
+ cachedAttr(ele, NAV_VIEW_ATTR, value);
+ } else {
+ return cachedAttr(ele, NAV_VIEW_ATTR);
+ }
+ }
+
+ function destroyViewEle(ele) {
+ // we found an element that should be removed
+ // destroy its scope, then remove the element
+ if (ele && ele.length) {
+ var viewScope = ele.scope();
+ if (viewScope) {
+ viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW));
+ viewScope.$destroy();
+ }
+ ele.remove();
+ }
+ }
+
+}]);
+
+/**
+ * @private
+ * Parts of Ionic requires that $scope data is attached to the element.
+ * We do not want to disable adding $scope data to the $element when
+ * $compileProvider.debugInfoEnabled(false) is used.
+ */
+IonicModule.config(['$provide', function($provide) {
+ $provide.decorator('$compile', ['$delegate', function($compile) {
+ $compile.$$addScopeInfo = function $$addScopeInfo($element, scope, isolated, noTemplate) {
+ var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope';
+ $element.data(dataName, scope);
+ };
+ return $compile;
+ }]);
+}]);
+
+/**
+ * @private
+ */
+IonicModule.config([
+ '$provide',
+function($provide) {
+ function $LocationDecorator($location, $timeout) {
+
+ $location.__hash = $location.hash;
+ //Fix: when window.location.hash is set, the scrollable area
+ //found nearest to body's scrollTop is set to scroll to an element
+ //with that ID.
+ $location.hash = function(value) {
+ if (angular.isDefined(value)) {
+ $timeout(function() {
+ var scroll = document.querySelector('.scroll-content');
+ if (scroll)
+ scroll.scrollTop = 0;
+ }, 0, false);
+ }
+ return $location.__hash(value);
+ };
+
+ return $location;
+ }
+
+ $provide.decorator('$location', ['$delegate', '$timeout', $LocationDecorator]);
+}]);
+
+IonicModule
+
+.controller('$ionicHeaderBar', [
+ '$scope',
+ '$element',
+ '$attrs',
+ '$q',
+ '$ionicConfig',
+ '$ionicHistory',
+function($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) {
+ var TITLE = 'title';
+ var BACK_TEXT = 'back-text';
+ var BACK_BUTTON = 'back-button';
+ var DEFAULT_TITLE = 'default-title';
+ var PREVIOUS_TITLE = 'previous-title';
+ var HIDE = 'hide';
+
+ var self = this;
+ var titleText = '';
+ var previousTitleText = '';
+ var titleLeft = 0;
+ var titleRight = 0;
+ var titleCss = '';
+ var isBackEnabled = false;
+ var isBackShown = true;
+ var isNavBackShown = true;
+ var isBackElementShown = false;
+ var titleTextWidth = 0;
+
+
+ self.beforeEnter = function(viewData) {
+ $scope.$broadcast('$ionicView.beforeEnter', viewData);
+ };
+
+
+ self.title = function(newTitleText) {
+ if (arguments.length && newTitleText !== titleText) {
+ getEle(TITLE).innerHTML = newTitleText;
+ titleText = newTitleText;
+ titleTextWidth = 0;
+ }
+ return titleText;
+ };
+
+
+ self.enableBack = function(shouldEnable, disableReset) {
+ // whether or not the back button show be visible, according
+ // to the navigation and history
+ if (arguments.length) {
+ isBackEnabled = shouldEnable;
+ if (!disableReset) self.updateBackButton();
+ }
+ return isBackEnabled;
+ };
+
+
+ self.showBack = function(shouldShow, disableReset) {
+ // different from enableBack() because this will always have the back
+ // visually hidden if false, even if the history says it should show
+ if (arguments.length) {
+ isBackShown = shouldShow;
+ if (!disableReset) self.updateBackButton();
+ }
+ return isBackShown;
+ };
+
+
+ self.showNavBack = function(shouldShow) {
+ // different from showBack() because this is for the entire nav bar's
+ // setting for all of it's child headers. For internal use.
+ isNavBackShown = shouldShow;
+ self.updateBackButton();
+ };
+
+
+ self.updateBackButton = function() {
+ if ((isBackShown && isNavBackShown && isBackEnabled) !== isBackElementShown) {
+ isBackElementShown = isBackShown && isNavBackShown && isBackEnabled;
+ var backBtnEle = getEle(BACK_BUTTON);
+ backBtnEle && backBtnEle.classList[ isBackElementShown ? 'remove' : 'add' ](HIDE);
+ }
+ };
+
+
+ self.titleTextWidth = function() {
+ if (!titleTextWidth) {
+ var bounds = ionic.DomUtil.getTextBounds(getEle(TITLE));
+ titleTextWidth = Math.min(bounds && bounds.width || 30);
+ }
+ return titleTextWidth;
+ };
+
+
+ self.titleWidth = function() {
+ var titleWidth = self.titleTextWidth();
+ var offsetWidth = getEle(TITLE).offsetWidth;
+ if (offsetWidth < titleWidth) {
+ titleWidth = offsetWidth + (titleLeft - titleRight - 5);
+ }
+ return titleWidth;
+ };
+
+
+ self.titleTextX = function() {
+ return ($element[0].offsetWidth / 2) - (self.titleWidth() / 2);
+ };
+
+
+ self.titleLeftRight = function() {
+ return titleLeft - titleRight;
+ };
+
+
+ self.backButtonTextLeft = function() {
+ var offsetLeft = 0;
+ var ele = getEle(BACK_TEXT);
+ while (ele) {
+ offsetLeft += ele.offsetLeft;
+ ele = ele.parentElement;
+ }
+ return offsetLeft;
+ };
+
+
+ self.resetBackButton = function() {
+ if ($ionicConfig.backButton.previousTitleText()) {
+ var previousTitleEle = getEle(PREVIOUS_TITLE);
+ if (previousTitleEle) {
+ previousTitleEle.classList.remove(HIDE);
+
+ var newPreviousTitleText = $ionicHistory.backTitle();
+
+ if (newPreviousTitleText !== previousTitleText) {
+ previousTitleText = previousTitleEle.innerHTML = newPreviousTitleText;
+ }
+ }
+ var defaultTitleEle = getEle(DEFAULT_TITLE);
+ if (defaultTitleEle) {
+ defaultTitleEle.classList.remove(HIDE);
+ }
+ }
+ };
+
+
+ self.align = function(textAlign) {
+ var titleEle = getEle(TITLE);
+
+ textAlign = textAlign || $attrs.alignTitle || $ionicConfig.navBar.alignTitle();
+
+ var widths = self.calcWidths(textAlign, false);
+
+ if (isBackShown && previousTitleText && $ionicConfig.backButton.previousTitleText()) {
+ var previousTitleWidths = self.calcWidths(textAlign, true);
+
+ var availableTitleWidth = $element[0].offsetWidth - previousTitleWidths.titleLeft - previousTitleWidths.titleRight;
+
+ if (self.titleTextWidth() <= availableTitleWidth) {
+ widths = previousTitleWidths;
+ }
+ }
+
+ return self.updatePositions(titleEle, widths.titleLeft, widths.titleRight, widths.buttonsLeft, widths.buttonsRight, widths.css, widths.showPrevTitle);
+ };
+
+
+ self.calcWidths = function(textAlign, isPreviousTitle) {
+ var titleEle = getEle(TITLE);
+ var backBtnEle = getEle(BACK_BUTTON);
+ var x, y, z, b, c, d, childSize, bounds;
+ var childNodes = $element[0].childNodes;
+ var buttonsLeft = 0;
+ var buttonsRight = 0;
+ var isCountRightOfTitle;
+ var updateTitleLeft = 0;
+ var updateTitleRight = 0;
+ var updateCss = '';
+ var backButtonWidth = 0;
+
+ // Compute how wide the left children are
+ // Skip all titles (there may still be two titles, one leaving the dom)
+ // Once we encounter a titleEle, realize we are now counting the right-buttons, not left
+ for (x = 0; x < childNodes.length; x++) {
+ c = childNodes[x];
+
+ childSize = 0;
+ if (c.nodeType == 1) {
+ // element node
+ if (c === titleEle) {
+ isCountRightOfTitle = true;
+ continue;
+ }
+
+ if (c.classList.contains(HIDE)) {
+ continue;
+ }
+
+ if (isBackShown && c === backBtnEle) {
+
+ for (y = 0; y < c.childNodes.length; y++) {
+ b = c.childNodes[y];
+
+ if (b.nodeType == 1) {
+
+ if (b.classList.contains(BACK_TEXT)) {
+ for (z = 0; z < b.children.length; z++) {
+ d = b.children[z];
+
+ if (isPreviousTitle) {
+ if (d.classList.contains(DEFAULT_TITLE)) continue;
+ backButtonWidth += d.offsetWidth;
+ } else {
+ if (d.classList.contains(PREVIOUS_TITLE)) continue;
+ backButtonWidth += d.offsetWidth;
+ }
+ }
+
+ } else {
+ backButtonWidth += b.offsetWidth;
+ }
+
+ } else if (b.nodeType == 3 && b.nodeValue.trim()) {
+ bounds = ionic.DomUtil.getTextBounds(b);
+ backButtonWidth += bounds && bounds.width || 0;
+ }
+
+ }
+ childSize = backButtonWidth || c.offsetWidth;
+
+ } else {
+ // not the title, not the back button, not a hidden element
+ childSize = c.offsetWidth;
+ }
+
+ } else if (c.nodeType == 3 && c.nodeValue.trim()) {
+ // text node
+ bounds = ionic.DomUtil.getTextBounds(c);
+ childSize = bounds && bounds.width || 0;
+ }
+
+ if (isCountRightOfTitle) {
+ buttonsRight += childSize;
+ } else {
+ buttonsLeft += childSize;
+ }
+ }
+
+ // Size and align the header titleEle based on the sizes of the left and
+ // right children, and the desired alignment mode
+ if (textAlign == 'left') {
+ updateCss = 'title-left';
+ if (buttonsLeft) {
+ updateTitleLeft = buttonsLeft + 15;
+ }
+ if (buttonsRight) {
+ updateTitleRight = buttonsRight + 15;
+ }
+
+ } else if (textAlign == 'right') {
+ updateCss = 'title-right';
+ if (buttonsLeft) {
+ updateTitleLeft = buttonsLeft + 15;
+ }
+ if (buttonsRight) {
+ updateTitleRight = buttonsRight + 15;
+ }
+
+ } else {
+ // center the default
+ var margin = Math.max(buttonsLeft, buttonsRight) + 10;
+ if (margin > 10) {
+ updateTitleLeft = updateTitleRight = margin;
+ }
+ }
+
+ return {
+ backButtonWidth: backButtonWidth,
+ buttonsLeft: buttonsLeft,
+ buttonsRight: buttonsRight,
+ titleLeft: updateTitleLeft,
+ titleRight: updateTitleRight,
+ showPrevTitle: isPreviousTitle,
+ css: updateCss
+ };
+ };
+
+
+ self.updatePositions = function(titleEle, updateTitleLeft, updateTitleRight, buttonsLeft, buttonsRight, updateCss, showPreviousTitle) {
+ var deferred = $q.defer();
+
+ // only make DOM updates when there are actual changes
+ if (titleEle) {
+ if (updateTitleLeft !== titleLeft) {
+ titleEle.style.left = updateTitleLeft ? updateTitleLeft + 'px' : '';
+ titleLeft = updateTitleLeft;
+ }
+ if (updateTitleRight !== titleRight) {
+ titleEle.style.right = updateTitleRight ? updateTitleRight + 'px' : '';
+ titleRight = updateTitleRight;
+ }
+
+ if (updateCss !== titleCss) {
+ updateCss && titleEle.classList.add(updateCss);
+ titleCss && titleEle.classList.remove(titleCss);
+ titleCss = updateCss;
+ }
+ }
+
+ if ($ionicConfig.backButton.previousTitleText()) {
+ var prevTitle = getEle(PREVIOUS_TITLE);
+ var defaultTitle = getEle(DEFAULT_TITLE);
+
+ prevTitle && prevTitle.classList[ showPreviousTitle ? 'remove' : 'add'](HIDE);
+ defaultTitle && defaultTitle.classList[ showPreviousTitle ? 'add' : 'remove'](HIDE);
+ }
+
+ ionic.requestAnimationFrame(function() {
+ if (titleEle && titleEle.offsetWidth + 10 < titleEle.scrollWidth) {
+ var minRight = buttonsRight + 5;
+ var testRight = $element[0].offsetWidth - titleLeft - self.titleTextWidth() - 20;
+ updateTitleRight = testRight < minRight ? minRight : testRight;
+ if (updateTitleRight !== titleRight) {
+ titleEle.style.right = updateTitleRight + 'px';
+ titleRight = updateTitleRight;
+ }
+ }
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ };
+
+
+ self.setCss = function(elementClassname, css) {
+ ionic.DomUtil.cachedStyles(getEle(elementClassname), css);
+ };
+
+
+ var eleCache = {};
+ function getEle(className) {
+ if (!eleCache[className]) {
+ eleCache[className] = $element[0].querySelector('.' + className);
+ }
+ return eleCache[className];
+ }
+
+
+ $scope.$on('$destroy', function() {
+ for (var n in eleCache) eleCache[n] = null;
+ });
+
+}]);
+
+
+/**
+ * @ngdoc service
+ * @name $ionicListDelegate
+ * @module ionic
+ *
+ * @description
+ * Delegate for controlling the {@link ionic.directive:ionList} directive.
+ *
+ * Methods called directly on the $ionicListDelegate service will control all lists.
+ * Use the {@link ionic.service:$ionicListDelegate#$getByHandle $getByHandle}
+ * method to control specific ionList instances.
+ *
+ * @usage
+ *
+ * ````html
+ *
+ *
+ *
+ *
+ * {% raw %}Hello, {{i}}!{% endraw %}
+ *
+ *
+ *
+ *
+ * ```
+ * ```js
+ * function MyCtrl($scope, $ionicListDelegate) {
+ * $scope.showDeleteButtons = function() {
+ * $ionicListDelegate.showDelete(true);
+ * };
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicListDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#showReorder
+ * @param {boolean=} showReorder Set whether or not this list is showing its reorder buttons.
+ * @returns {boolean} Whether the reorder buttons are shown.
+ */
+ 'showReorder',
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#showDelete
+ * @param {boolean=} showDelete Set whether or not this list is showing its delete buttons.
+ * @returns {boolean} Whether the delete buttons are shown.
+ */
+ 'showDelete',
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#canSwipeItems
+ * @param {boolean=} canSwipeItems Set whether or not this list is able to swipe to show
+ * option buttons.
+ * @returns {boolean} Whether the list is able to swipe to show option buttons.
+ */
+ 'canSwipeItems',
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#closeOptionButtons
+ * @description Closes any option buttons on the list that are swiped open.
+ */
+ 'closeOptionButtons',
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * {@link ionic.directive:ionList} directives with `delegate-handle` matching
+ * the given handle.
+ *
+ * Example: `$ionicListDelegate.$getByHandle('my-handle').showReorder(true);`
+ */
+]))
+
+.controller('$ionicList', [
+ '$scope',
+ '$attrs',
+ '$ionicListDelegate',
+ '$ionicHistory',
+function($scope, $attrs, $ionicListDelegate, $ionicHistory) {
+ var self = this;
+ var isSwipeable = true;
+ var isReorderShown = false;
+ var isDeleteShown = false;
+
+ var deregisterInstance = $ionicListDelegate._registerInstance(
+ self, $attrs.delegateHandle, function() {
+ return $ionicHistory.isActiveScope($scope);
+ }
+ );
+ $scope.$on('$destroy', deregisterInstance);
+
+ self.showReorder = function(show) {
+ if (arguments.length) {
+ isReorderShown = !!show;
+ }
+ return isReorderShown;
+ };
+
+ self.showDelete = function(show) {
+ if (arguments.length) {
+ isDeleteShown = !!show;
+ }
+ return isDeleteShown;
+ };
+
+ self.canSwipeItems = function(can) {
+ if (arguments.length) {
+ isSwipeable = !!can;
+ }
+ return isSwipeable;
+ };
+
+ self.closeOptionButtons = function() {
+ self.listView && self.listView.clearDragEffects();
+ };
+}]);
+
+IonicModule
+
+.controller('$ionicNavBar', [
+ '$scope',
+ '$element',
+ '$attrs',
+ '$compile',
+ '$timeout',
+ '$ionicNavBarDelegate',
+ '$ionicConfig',
+ '$ionicHistory',
+function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $ionicConfig, $ionicHistory) {
+
+ var CSS_HIDE = 'hide';
+ var DATA_NAV_BAR_CTRL = '$ionNavBarController';
+ var PRIMARY_BUTTONS = 'primaryButtons';
+ var SECONDARY_BUTTONS = 'secondaryButtons';
+ var BACK_BUTTON = 'backButton';
+ var ITEM_TYPES = 'primaryButtons secondaryButtons leftButtons rightButtons title'.split(' ');
+
+ var self = this;
+ var headerBars = [];
+ var navElementHtml = {};
+ var isVisible = true;
+ var queuedTransitionStart, queuedTransitionEnd, latestTransitionId;
+
+ $element.parent().data(DATA_NAV_BAR_CTRL, self);
+
+ var delegateHandle = $attrs.delegateHandle || 'navBar' + ionic.Utils.nextUid();
+
+ var deregisterInstance = $ionicNavBarDelegate._registerInstance(self, delegateHandle);
+
+
+ self.init = function() {
+ $element.addClass('nav-bar-container');
+ ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', $ionicConfig.views.transition());
+
+ // create two nav bar blocks which will trade out which one is shown
+ self.createHeaderBar(false);
+ self.createHeaderBar(true);
+
+ $scope.$emit('ionNavBar.init', delegateHandle);
+ };
+
+
+ self.createHeaderBar = function(isActive, navBarClass) {
+ var containerEle = jqLite('
');
+ ionic.DomUtil.cachedAttr(containerEle, 'nav-bar', isActive ? 'active' : 'cached');
+
+ var alignTitle = $attrs.alignTitle || $ionicConfig.navBar.alignTitle();
+ var headerBarEle = jqLite('').addClass($attrs.class).attr('align-title', alignTitle);
+ if (isDefined($attrs.noTapScroll)) headerBarEle.attr('no-tap-scroll', $attrs.noTapScroll);
+ var titleEle = jqLite('
');
+ var navEle = {};
+ var lastViewItemEle = {};
+ var leftButtonsEle, rightButtonsEle;
+
+ //navEle[BACK_BUTTON] = self.createBackButtonElement(headerBarEle);
+ navEle[BACK_BUTTON] = createNavElement(BACK_BUTTON);
+ navEle[BACK_BUTTON] && headerBarEle.append(navEle[BACK_BUTTON]);
+
+ // append title in the header, this is the rock to where buttons append
+ headerBarEle.append(titleEle);
+
+ forEach(ITEM_TYPES, function(itemType) {
+ // create default button elements
+ navEle[itemType] = createNavElement(itemType);
+ // append and position buttons
+ positionItem(navEle[itemType], itemType);
+ });
+
+ // add header-item to the root children
+ for (var x = 0; x < headerBarEle[0].children.length; x++) {
+ headerBarEle[0].children[x].classList.add('header-item');
+ }
+
+ // compile header and append to the DOM
+ containerEle.append(headerBarEle);
+ $element.append($compile(containerEle)($scope.$new()));
+
+ var headerBarCtrl = headerBarEle.data('$ionHeaderBarController');
+
+ var headerBarInstance = {
+ isActive: isActive,
+ title: function(newTitleText) {
+ headerBarCtrl.title(newTitleText);
+ },
+ setItem: function(navBarItemEle, itemType) {
+ // first make sure any exiting nav bar item has been removed
+ headerBarInstance.removeItem(itemType);
+
+ if (navBarItemEle) {
+ if (itemType === 'title') {
+ // clear out the text based title
+ headerBarInstance.title("");
+ }
+
+ // there's a custom nav bar item
+ positionItem(navBarItemEle, itemType);
+
+ if (navEle[itemType]) {
+ // make sure the default on this itemType is hidden
+ navEle[itemType].addClass(CSS_HIDE);
+ }
+ lastViewItemEle[itemType] = navBarItemEle;
+
+ } else if (navEle[itemType]) {
+ // there's a default button for this side and no view button
+ navEle[itemType].removeClass(CSS_HIDE);
+ }
+ },
+ removeItem: function(itemType) {
+ if (lastViewItemEle[itemType]) {
+ lastViewItemEle[itemType].scope().$destroy();
+ lastViewItemEle[itemType].remove();
+ lastViewItemEle[itemType] = null;
+ }
+ },
+ containerEle: function() {
+ return containerEle;
+ },
+ headerBarEle: function() {
+ return headerBarEle;
+ },
+ afterLeave: function() {
+ forEach(ITEM_TYPES, function(itemType) {
+ headerBarInstance.removeItem(itemType);
+ });
+ headerBarCtrl.resetBackButton();
+ },
+ controller: function() {
+ return headerBarCtrl;
+ },
+ destroy: function() {
+ forEach(ITEM_TYPES, function(itemType) {
+ headerBarInstance.removeItem(itemType);
+ });
+ containerEle.scope().$destroy();
+ for (var n in navEle) {
+ if (navEle[n]) {
+ navEle[n].removeData();
+ navEle[n] = null;
+ }
+ }
+ leftButtonsEle && leftButtonsEle.removeData();
+ rightButtonsEle && rightButtonsEle.removeData();
+ titleEle.removeData();
+ headerBarEle.removeData();
+ containerEle.remove();
+ containerEle = headerBarEle = titleEle = leftButtonsEle = rightButtonsEle = null;
+ }
+ };
+
+ function positionItem(ele, itemType) {
+ if (!ele) return;
+
+ if (itemType === 'title') {
+ // title element
+ titleEle.append(ele);
+
+ } else if (itemType == 'rightButtons' ||
+ (itemType == SECONDARY_BUTTONS && $ionicConfig.navBar.positionSecondaryButtons() != 'left') ||
+ (itemType == PRIMARY_BUTTONS && $ionicConfig.navBar.positionPrimaryButtons() == 'right')) {
+ // right side
+ if (!rightButtonsEle) {
+ rightButtonsEle = jqLite('
');
+ headerBarEle.append(rightButtonsEle);
+ }
+ if (itemType == SECONDARY_BUTTONS) {
+ rightButtonsEle.append(ele);
+ } else {
+ rightButtonsEle.prepend(ele);
+ }
+
+ } else {
+ // left side
+ if (!leftButtonsEle) {
+ leftButtonsEle = jqLite('
');
+ if (navEle[BACK_BUTTON]) {
+ navEle[BACK_BUTTON].after(leftButtonsEle);
+ } else {
+ headerBarEle.prepend(leftButtonsEle);
+ }
+ }
+ if (itemType == SECONDARY_BUTTONS) {
+ leftButtonsEle.append(ele);
+ } else {
+ leftButtonsEle.prepend(ele);
+ }
+ }
+
+ }
+
+ headerBars.push(headerBarInstance);
+
+ return headerBarInstance;
+ };
+
+
+ self.navElement = function(type, html) {
+ if (isDefined(html)) {
+ navElementHtml[type] = html;
+ }
+ return navElementHtml[type];
+ };
+
+
+ self.update = function(viewData) {
+ var showNavBar = !viewData.hasHeaderBar && viewData.showNavBar;
+ viewData.transition = $ionicConfig.views.transition();
+
+ if (!showNavBar) {
+ viewData.direction = 'none';
+ }
+
+ self.enable(showNavBar);
+ var enteringHeaderBar = self.isInitialized ? getOffScreenHeaderBar() : getOnScreenHeaderBar();
+ var leavingHeaderBar = self.isInitialized ? getOnScreenHeaderBar() : null;
+ var enteringHeaderCtrl = enteringHeaderBar.controller();
+
+ // update if the entering header should show the back button or not
+ enteringHeaderCtrl.enableBack(viewData.enableBack, true);
+ enteringHeaderCtrl.showBack(viewData.showBack, true);
+ enteringHeaderCtrl.updateBackButton();
+
+ // update the entering header bar's title
+ self.title(viewData.title, enteringHeaderBar);
+
+ self.showBar(showNavBar);
+
+ // update the nav bar items, depending if the view has their own or not
+ if (viewData.navBarItems) {
+ forEach(ITEM_TYPES, function(itemType) {
+ enteringHeaderBar.setItem(viewData.navBarItems[itemType], itemType);
+ });
+ }
+
+ // begin transition of entering and leaving header bars
+ self.transition(enteringHeaderBar, leavingHeaderBar, viewData);
+
+ self.isInitialized = true;
+ };
+
+
+ self.transition = function(enteringHeaderBar, leavingHeaderBar, viewData) {
+ var enteringHeaderBarCtrl = enteringHeaderBar.controller();
+ var transitionFn = $ionicConfig.transitions.navBar[viewData.navBarTransition] || $ionicConfig.transitions.navBar.none;
+ var transitionId = viewData.transitionId;
+
+ enteringHeaderBarCtrl.beforeEnter(viewData);
+
+ var navBarTransition = transitionFn(enteringHeaderBar, leavingHeaderBar, viewData.direction, viewData.shouldAnimate && self.isInitialized);
+
+ ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', viewData.navBarTransition);
+ ionic.DomUtil.cachedAttr($element, 'nav-bar-direction', viewData.direction);
+
+ if (navBarTransition.shouldAnimate) {
+ navBarAttr(enteringHeaderBar, 'stage');
+ } else {
+ navBarAttr(enteringHeaderBar, 'entering');
+ navBarAttr(leavingHeaderBar, 'leaving');
+ }
+
+ enteringHeaderBarCtrl.resetBackButton();
+
+ navBarTransition.run(0);
+
+ $timeout(enteringHeaderBarCtrl.align, 16);
+
+ queuedTransitionStart = function() {
+ if (latestTransitionId !== transitionId) return;
+
+ navBarAttr(enteringHeaderBar, 'entering');
+ navBarAttr(leavingHeaderBar, 'leaving');
+
+ navBarTransition.run(1);
+
+ queuedTransitionEnd = function() {
+ if (latestTransitionId == transitionId || !navBarTransition.shouldAnimate) {
+ for (var x = 0; x < headerBars.length; x++) {
+ headerBars[x].isActive = false;
+ }
+ enteringHeaderBar.isActive = true;
+
+ navBarAttr(enteringHeaderBar, 'active');
+ navBarAttr(leavingHeaderBar, 'cached');
+
+ queuedTransitionEnd = null;
+ }
+ };
+
+ queuedTransitionStart = null;
+ };
+
+ queuedTransitionStart();
+
+ };
+
+
+ self.triggerTransitionStart = function(triggerTransitionId) {
+ latestTransitionId = triggerTransitionId;
+ queuedTransitionStart && queuedTransitionStart();
+ };
+
+
+ self.triggerTransitionEnd = function() {
+ queuedTransitionEnd && queuedTransitionEnd();
+ };
+
+
+ self.showBar = function(shouldShow) {
+ if (arguments.length) {
+ self.visibleBar(shouldShow);
+ $scope.$parent.$hasHeader = !!shouldShow;
+ }
+ return !!$scope.$parent.$hasHeader;
+ };
+
+
+ self.visibleBar = function(shouldShow) {
+ if (shouldShow && !isVisible) {
+ $element.removeClass(CSS_HIDE);
+ } else if (!shouldShow && isVisible) {
+ $element.addClass(CSS_HIDE);
+ }
+ isVisible = shouldShow;
+ };
+
+
+ self.enable = function(val) {
+ // set primary to show first
+ self.visibleBar(val);
+
+ // set non primary to hide second
+ for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) {
+ if ($ionicNavBarDelegate._instances[x] !== self) $ionicNavBarDelegate._instances[x].visibleBar(false);
+ }
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $ionicNavBar#showBackButton
+ * @description Show/hide the nav bar back button when there is a
+ * back view. If the back button is not possible, for example, the
+ * first view in the stack, then this will not force the back button
+ * to show.
+ */
+ self.showBackButton = function(shouldShow) {
+ for (var x = 0; x < headerBars.length; x++) {
+ headerBars[x].controller().showNavBack(!!shouldShow);
+ }
+ $scope.$isBackButtonShown = !!shouldShow;
+ return $scope.$isBackButtonShown;
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $ionicNavBar#showActiveBackButton
+ * @description Show/hide only the active header bar's back button.
+ */
+ self.showActiveBackButton = function(shouldShow) {
+ var headerBar = getOnScreenHeaderBar();
+ headerBar && headerBar.controller().showBack(shouldShow);
+ };
+
+
+ self.title = function(newTitleText, headerBar) {
+ if (isDefined(newTitleText)) {
+ newTitleText = newTitleText || '';
+ headerBar = headerBar || getOnScreenHeaderBar();
+ headerBar && headerBar.title(newTitleText);
+ $scope.$title = newTitleText;
+ $ionicHistory.currentTitle(newTitleText);
+ }
+ return $scope.$title;
+ };
+
+
+ self.align = function(val, headerBar) {
+ headerBar = headerBar || getOnScreenHeaderBar();
+ headerBar && headerBar.controller().align(val);
+ };
+
+
+ // DEPRECATED, as of v1.0.0-beta14 -------
+ self.changeTitle = function(val) {
+ deprecatedWarning('changeTitle(val)', 'title(val)');
+ self.title(val);
+ };
+ self.setTitle = function(val) {
+ deprecatedWarning('setTitle(val)', 'title(val)');
+ self.title(val);
+ };
+ self.getTitle = function() {
+ deprecatedWarning('getTitle()', 'title()');
+ return self.title();
+ };
+ self.back = function() {
+ deprecatedWarning('back()', '$ionicHistory.goBack()');
+ $ionicHistory.goBack();
+ };
+ self.getPreviousTitle = function() {
+ deprecatedWarning('getPreviousTitle()', '$ionicHistory.backTitle()');
+ $ionicHistory.goBack();
+ };
+ function deprecatedWarning(oldMethod, newMethod) {
+ var warn = console.warn || console.log;
+ warn && warn('navBarController.' + oldMethod + ' is deprecated, please use ' + newMethod + ' instead');
+ }
+ // END DEPRECATED -------
+
+
+ function createNavElement(type) {
+ if (navElementHtml[type]) {
+ return jqLite(navElementHtml[type]);
+ }
+ }
+
+
+ function getOnScreenHeaderBar() {
+ for (var x = 0; x < headerBars.length; x++) {
+ if (headerBars[x].isActive) return headerBars[x];
+ }
+ }
+
+
+ function getOffScreenHeaderBar() {
+ for (var x = 0; x < headerBars.length; x++) {
+ if (!headerBars[x].isActive) return headerBars[x];
+ }
+ }
+
+
+ function navBarAttr(ctrl, val) {
+ ctrl && ionic.DomUtil.cachedAttr(ctrl.containerEle(), 'nav-bar', val);
+ }
+
+
+ $scope.$on('$destroy', function() {
+ $scope.$parent.$hasHeader = false;
+ $element.parent().removeData(DATA_NAV_BAR_CTRL);
+ for (var x = 0; x < headerBars.length; x++) {
+ headerBars[x].destroy();
+ }
+ $element.remove();
+ $element = headerBars = null;
+ deregisterInstance();
+ });
+
+}]);
+
+IonicModule
+.controller('$ionicNavView', [
+ '$scope',
+ '$element',
+ '$attrs',
+ '$compile',
+ '$controller',
+ '$ionicNavBarDelegate',
+ '$ionicNavViewDelegate',
+ '$ionicHistory',
+ '$ionicViewSwitcher',
+function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher) {
+
+ var DATA_ELE_IDENTIFIER = '$eleId';
+ var DATA_DESTROY_ELE = '$destroyEle';
+ var DATA_NO_CACHE = '$noCache';
+ var VIEW_STATUS_ACTIVE = 'active';
+ var VIEW_STATUS_CACHED = 'cached';
+
+ var self = this;
+ var direction;
+ var isPrimary = false;
+ var navBarDelegate;
+ var activeEleId;
+ var navViewAttr = $ionicViewSwitcher.navViewAttr;
+
+ self.scope = $scope;
+
+ self.init = function() {
+ var navViewName = $attrs.name || '';
+
+ // Find the details of the parent view directive (if any) and use it
+ // to derive our own qualified view name, then hang our own details
+ // off the DOM so child directives can find it.
+ var parent = $element.parent().inheritedData('$uiView');
+ var parentViewName = ((parent && parent.state) ? parent.state.name : '');
+ if (navViewName.indexOf('@') < 0) navViewName = navViewName + '@' + parentViewName;
+
+ var viewData = { name: navViewName, state: null };
+ $element.data('$uiView', viewData);
+
+ var deregisterInstance = $ionicNavViewDelegate._registerInstance(self, $attrs.delegateHandle);
+ $scope.$on('$destroy', deregisterInstance);
+
+ $scope.$on('$ionicHistory.deselect', self.cacheCleanup);
+
+ return viewData;
+ };
+
+
+ self.register = function(viewLocals) {
+ var leavingView = extend({}, $ionicHistory.currentView());
+
+ // register that a view is coming in and get info on how it should transition
+ var registerData = $ionicHistory.register($scope, viewLocals);
+
+ // update which direction
+ self.update(registerData);
+
+ // begin rendering and transitioning
+ self.render(registerData, viewLocals, leavingView);
+ };
+
+
+ self.update = function(registerData) {
+ // always reset that this is the primary navView
+ isPrimary = true;
+
+ // remember what direction this navView should use
+ // this may get updated later by a child navView
+ direction = registerData.direction;
+
+ var parentNavViewCtrl = $element.parent().inheritedData('$ionNavViewController');
+ if (parentNavViewCtrl) {
+ // this navView is nested inside another one
+ // update the parent to use this direction and not
+ // the other it originally was set to
+
+ // inform the parent navView that it is not the primary navView
+ parentNavViewCtrl.isPrimary(false);
+
+ if (direction === 'enter' || direction === 'exit') {
+ // they're entering/exiting a history
+ // find parent navViewController
+ parentNavViewCtrl.direction(direction);
+
+ if (direction === 'enter') {
+ // reset the direction so this navView doesn't animate
+ // because it's parent will
+ direction = 'none';
+ }
+ }
+ }
+ };
+
+
+ self.render = function(registerData, viewLocals, leavingView) {
+ var enteringView = $ionicHistory.getViewById(registerData.viewId) || {};
+
+ // register the view and figure out where it lives in the various
+ // histories and nav stacks, along with how views should enter/leave
+ var switcher = $ionicViewSwitcher.create(self, viewLocals, enteringView, leavingView);
+
+ // init the rendering of views for this navView directive
+ switcher.init(registerData, function() {
+ // the view is now compiled, in the dom and linked, now lets transition the views.
+ // this uses a callback incase THIS nav-view has a nested nav-view, and after the NESTED
+ // nav-view links, the NESTED nav-view would update which direction THIS nav-view should use
+ switcher.transition(self.direction(), registerData.enableBack);
+ });
+
+ };
+
+
+ self.beforeEnter = function(transitionData) {
+ if (isPrimary) {
+ // only update this nav-view's nav-bar if this is the primary nav-view
+ navBarDelegate = transitionData.navBarDelegate;
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.update(transitionData);
+ }
+ };
+
+
+ self.activeEleId = function(eleId) {
+ if (arguments.length) {
+ activeEleId = eleId;
+ }
+ return activeEleId;
+ };
+
+
+ self.transitionEnd = function() {
+ var viewElements = $element.children();
+ var x, l, viewElement;
+
+ for (x = 0, l = viewElements.length; x < l; x++) {
+ viewElement = viewElements.eq(x);
+
+ if (viewElement.data(DATA_ELE_IDENTIFIER) === activeEleId) {
+ // this is the active element
+ navViewAttr(viewElement, VIEW_STATUS_ACTIVE);
+
+ } else if (navViewAttr(viewElement) === 'leaving' || navViewAttr(viewElement) === VIEW_STATUS_ACTIVE || navViewAttr(viewElement) === VIEW_STATUS_CACHED) {
+ // this is a leaving element or was the former active element, or is an cached element
+ if (viewElement.data(DATA_DESTROY_ELE) || viewElement.data(DATA_NO_CACHE)) {
+ // this element shouldn't stay cached
+ $ionicViewSwitcher.destroyViewEle(viewElement);
+ } else {
+ // keep in the DOM, mark as cached
+ navViewAttr(viewElement, VIEW_STATUS_CACHED);
+ }
+ }
+ }
+ };
+
+
+ self.cacheCleanup = function() {
+ var viewElements = $element.children();
+ for (var x = 0, l = viewElements.length; x < l; x++) {
+ if (viewElements.eq(x).data(DATA_DESTROY_ELE)) {
+ $ionicViewSwitcher.destroyViewEle(viewElements.eq(x));
+ }
+ }
+ };
+
+
+ self.clearCache = function() {
+ var viewElements = $element.children();
+ var viewElement, viewScope;
+
+ for (var x = 0, l = viewElements.length; x < l; x++) {
+ viewElement = viewElements.eq(x);
+ if (navViewAttr(viewElement) == VIEW_STATUS_CACHED) {
+ $ionicViewSwitcher.destroyViewEle(viewElement);
+ } else if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) {
+ viewScope = viewElement.scope();
+ viewScope && viewScope.$broadcast('$ionicView.clearCache');
+ }
+ }
+
+ };
+
+
+ self.getViewElements = function() {
+ return $element.children();
+ };
+
+
+ self.appendViewElement = function(viewEle, viewLocals) {
+ // compile the entering element and get the link function
+ var linkFn = $compile(viewEle);
+
+ $element.append(viewEle);
+
+ var viewScope = $scope.$new();
+
+ if (viewLocals && viewLocals.$$controller) {
+ viewLocals.$scope = viewScope;
+ var controller = $controller(viewLocals.$$controller, viewLocals);
+ $element.children().data('$ngControllerController', controller);
+ }
+
+ linkFn(viewScope);
+
+ return viewScope;
+ };
+
+
+ self.title = function(val) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.title(val);
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $ionicNavView#enableBackButton
+ * @description Enable/disable if the back button can be shown or not. For
+ * example, the very first view in the navigation stack would not have a
+ * back view, so the back button would be disabled.
+ */
+ self.enableBackButton = function(shouldEnable) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.enableBackButton(shouldEnable);
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $ionicNavView#showBackButton
+ * @description Show/hide the nav bar active back button. If the back button
+ * is not possible this will not force the back button to show. The
+ * `enableBackButton()` method handles if a back button is even possible or not.
+ */
+ self.showBackButton = function(shouldShow) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.showActiveBackButton(shouldShow);
+ };
+
+
+ self.showBar = function(val) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.showBar(val);
+ };
+
+
+ self.isPrimary = function(val) {
+ if (arguments.length) {
+ isPrimary = val;
+ }
+ return isPrimary;
+ };
+
+
+ self.direction = function(val) {
+ if (arguments.length) {
+ direction = val;
+ }
+ return direction;
+ };
+
+
+ function getAssociatedNavBarCtrl() {
+ if (navBarDelegate) {
+ for (var x=0; x < $ionicNavBarDelegate._instances.length; x++) {
+ if ($ionicNavBarDelegate._instances[x].$$delegateHandle == navBarDelegate) {
+ return $ionicNavBarDelegate._instances[x];
+ }
+ }
+ }
+ return $element.inheritedData('$ionNavBarController');
+ }
+
+}]);
+
+/**
+ * @private
+ */
+IonicModule
+
+.controller('$ionicScroll', [
+ '$scope',
+ 'scrollViewOptions',
+ '$timeout',
+ '$window',
+ '$location',
+ '$document',
+ '$ionicScrollDelegate',
+ '$ionicHistory',
+function($scope, scrollViewOptions, $timeout, $window, $location, $document, $ionicScrollDelegate, $ionicHistory) {
+
+ var self = this;
+ // for testing
+ self.__timeout = $timeout;
+
+ self._scrollViewOptions = scrollViewOptions; //for testing
+
+ var element = self.element = scrollViewOptions.el;
+ var $element = self.$element = jqLite(element);
+ var scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions);
+
+ //Attach self to element as a controller so other directives can require this controller
+ //through `require: '$ionicScroll'
+ //Also attach to parent so that sibling elements can require this
+ ($element.parent().length ? $element.parent() : $element)
+ .data('$$ionicScrollController', self);
+
+ var deregisterInstance = $ionicScrollDelegate._registerInstance(
+ self, scrollViewOptions.delegateHandle, function() {
+ return $ionicHistory.isActiveScope($scope);
+ }
+ );
+
+ if (!angular.isDefined(scrollViewOptions.bouncing)) {
+ ionic.Platform.ready(function() {
+ if (scrollView.options) {
+ scrollView.options.bouncing = true;
+ if (ionic.Platform.isAndroid()) {
+ // No bouncing by default on Android
+ scrollView.options.bouncing = false;
+ // Faster scroll decel
+ scrollView.options.deceleration = 0.95;
+ }
+ }
+ });
+ }
+
+ var resize = angular.bind(scrollView, scrollView.resize);
+ ionic.on('resize', resize, $window);
+
+
+ var scrollFunc = function(e) {
+ var detail = (e.originalEvent || e).detail || {};
+ $scope.$onScroll && $scope.$onScroll({
+ event: e,
+ scrollTop: detail.scrollTop || 0,
+ scrollLeft: detail.scrollLeft || 0
+ });
+ };
+
+ $element.on('scroll', scrollFunc);
+
+ $scope.$on('$destroy', function() {
+ deregisterInstance();
+ //Windows: make sure the scrollView.__cleanup exists before calling it
+ if (scrollView.__cleanup) {
+ scrollView.__cleanup();
+ }
+ ionic.off('resize', resize, $window);
+ $window.removeEventListener('resize', resize);
+ scrollViewOptions = null;
+ self._scrollViewOptions.el = null;
+ self._scrollViewOptions = null;
+ $element.off('scroll', scrollFunc);
+ $element = null;
+ self.$element = null;
+ element = null;
+ self.element = null;
+ self.scrollView = null;
+ scrollView = null;
+ });
+
+ $timeout(function() {
+ scrollView && scrollView.run && scrollView.run();
+ });
+
+ self.getScrollView = function() {
+ return self.scrollView;
+ };
+
+ self.getScrollPosition = function() {
+ return self.scrollView.getValues();
+ };
+
+ self.resize = function() {
+ return $timeout(resize).then(function() {
+ $element && $element.triggerHandler('scroll.resize');
+ });
+ };
+
+ self.scrollTop = function(shouldAnimate) {
+ ionic.DomUtil.blurAll();
+ self.resize().then(function() {
+ scrollView.scrollTo(0, 0, !!shouldAnimate);
+ });
+ };
+
+ self.scrollBottom = function(shouldAnimate) {
+ ionic.DomUtil.blurAll();
+ self.resize().then(function() {
+ var max = scrollView.getScrollMax();
+ scrollView.scrollTo(max.left, max.top, !!shouldAnimate);
+ });
+ };
+
+ self.scrollTo = function(left, top, shouldAnimate) {
+ ionic.DomUtil.blurAll();
+ self.resize().then(function() {
+ scrollView.scrollTo(left, top, !!shouldAnimate);
+ });
+ };
+
+ self.zoomTo = function(zoom, shouldAnimate, originLeft, originTop) {
+ ionic.DomUtil.blurAll();
+ self.resize().then(function() {
+ scrollView.zoomTo(zoom, !!shouldAnimate, originLeft, originTop);
+ });
+ };
+
+ self.zoomBy = function(zoom, shouldAnimate, originLeft, originTop) {
+ ionic.DomUtil.blurAll();
+ self.resize().then(function() {
+ scrollView.zoomBy(zoom, !!shouldAnimate, originLeft, originTop);
+ });
+ };
+
+ self.scrollBy = function(left, top, shouldAnimate) {
+ ionic.DomUtil.blurAll();
+ self.resize().then(function() {
+ scrollView.scrollBy(left, top, !!shouldAnimate);
+ });
+ };
+
+ self.anchorScroll = function(shouldAnimate) {
+ ionic.DomUtil.blurAll();
+ self.resize().then(function() {
+ var hash = $location.hash();
+ var elm = hash && $document[0].getElementById(hash);
+ if (!(hash && elm)) {
+ scrollView.scrollTo(0,0, !!shouldAnimate);
+ return;
+ }
+ var curElm = elm;
+ var scrollLeft = 0, scrollTop = 0, levelsClimbed = 0;
+ do {
+ if (curElm !== null) scrollLeft += curElm.offsetLeft;
+ if (curElm !== null) scrollTop += curElm.offsetTop;
+ curElm = curElm.offsetParent;
+ levelsClimbed++;
+ } while (curElm.attributes != self.element.attributes && curElm.offsetParent);
+ scrollView.scrollTo(scrollLeft, scrollTop, !!shouldAnimate);
+ });
+ };
+
+
+ /**
+ * @private
+ */
+ self._setRefresher = function(refresherScope, refresherElement) {
+ var refresher = self.refresher = refresherElement;
+ var refresherHeight = self.refresher.clientHeight || 60;
+ scrollView.activatePullToRefresh(refresherHeight, function() {
+ // activateCallback
+ refresher.classList.add('active');
+ refresherScope.$onPulling();
+ }, function() {
+ refresher.classList.remove('active');
+ refresher.classList.remove('refreshing');
+ refresher.classList.remove('refreshing-tail');
+ }, function() {
+ // startCallback
+ refresher.classList.add('refreshing');
+ refresherScope.$onRefresh();
+ }, function() {
+ // showCallback
+ refresher.classList.remove('invisible');
+ }, function() {
+ // hideCallback
+ refresher.classList.add('invisible');
+ }, function() {
+ // tailCallback
+ refresher.classList.add('refreshing-tail');
+ });
+ };
+}]);
+
+IonicModule
+.controller('$ionicSideMenus', [
+ '$scope',
+ '$attrs',
+ '$ionicSideMenuDelegate',
+ '$ionicPlatform',
+ '$ionicBody',
+ '$ionicHistory',
+function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $ionicBody, $ionicHistory) {
+ var self = this;
+ var rightShowing, leftShowing, isDragging;
+ var startX, lastX, offsetX, isAsideExposed;
+ var enableMenuWithBackViews = true;
+
+ self.$scope = $scope;
+
+ self.initialize = function(options) {
+ self.left = options.left;
+ self.right = options.right;
+ self.setContent(options.content);
+ self.dragThresholdX = options.dragThresholdX || 10;
+ $ionicHistory.registerHistory(self.$scope);
+ };
+
+ /**
+ * Set the content view controller if not passed in the constructor options.
+ *
+ * @param {object} content
+ */
+ self.setContent = function(content) {
+ if (content) {
+ self.content = content;
+
+ self.content.onDrag = function(e) {
+ self._handleDrag(e);
+ };
+
+ self.content.endDrag = function(e) {
+ self._endDrag(e);
+ };
+ }
+ };
+
+ self.isOpenLeft = function() {
+ return self.getOpenAmount() > 0;
+ };
+
+ self.isOpenRight = function() {
+ return self.getOpenAmount() < 0;
+ };
+
+ /**
+ * Toggle the left menu to open 100%
+ */
+ self.toggleLeft = function(shouldOpen) {
+ if (isAsideExposed || !self.left.isEnabled) return;
+ var openAmount = self.getOpenAmount();
+ if (arguments.length === 0) {
+ shouldOpen = openAmount <= 0;
+ }
+ self.content.enableAnimation();
+ if (!shouldOpen) {
+ self.openPercentage(0);
+ } else {
+ self.openPercentage(100);
+ }
+ };
+
+ /**
+ * Toggle the right menu to open 100%
+ */
+ self.toggleRight = function(shouldOpen) {
+ if (isAsideExposed || !self.right.isEnabled) return;
+ var openAmount = self.getOpenAmount();
+ if (arguments.length === 0) {
+ shouldOpen = openAmount >= 0;
+ }
+ self.content.enableAnimation();
+ if (!shouldOpen) {
+ self.openPercentage(0);
+ } else {
+ self.openPercentage(-100);
+ }
+ };
+
+ self.toggle = function(side) {
+ if (side == 'right') {
+ self.toggleRight();
+ } else {
+ self.toggleLeft();
+ }
+ };
+
+ /**
+ * Close all menus.
+ */
+ self.close = function() {
+ self.openPercentage(0);
+ };
+
+ /**
+ * @return {float} The amount the side menu is open, either positive or negative for left (positive), or right (negative)
+ */
+ self.getOpenAmount = function() {
+ return self.content && self.content.getTranslateX() || 0;
+ };
+
+ /**
+ * @return {float} The ratio of open amount over menu width. For example, a
+ * menu of width 100 open 50 pixels would be open 50% or a ratio of 0.5. Value is negative
+ * for right menu.
+ */
+ self.getOpenRatio = function() {
+ var amount = self.getOpenAmount();
+ if (amount >= 0) {
+ return amount / self.left.width;
+ }
+ return amount / self.right.width;
+ };
+
+ self.isOpen = function() {
+ return self.getOpenAmount() !== 0;
+ };
+
+ /**
+ * @return {float} The percentage of open amount over menu width. For example, a
+ * menu of width 100 open 50 pixels would be open 50%. Value is negative
+ * for right menu.
+ */
+ self.getOpenPercentage = function() {
+ return self.getOpenRatio() * 100;
+ };
+
+ /**
+ * Open the menu with a given percentage amount.
+ * @param {float} percentage The percentage (positive or negative for left/right) to open the menu.
+ */
+ self.openPercentage = function(percentage) {
+ var p = percentage / 100;
+
+ if (self.left && percentage >= 0) {
+ self.openAmount(self.left.width * p);
+ } else if (self.right && percentage < 0) {
+ var maxRight = self.right.width;
+ self.openAmount(self.right.width * p);
+ }
+
+ // add the CSS class "menu-open" if the percentage does not
+ // equal 0, otherwise remove the class from the body element
+ $ionicBody.enableClass((percentage !== 0), 'menu-open');
+ };
+
+ /**
+ * Open the menu the given pixel amount.
+ * @param {float} amount the pixel amount to open the menu. Positive value for left menu,
+ * negative value for right menu (only one menu will be visible at a time).
+ */
+ self.openAmount = function(amount) {
+ var maxLeft = self.left && self.left.width || 0;
+ var maxRight = self.right && self.right.width || 0;
+
+ // Check if we can move to that side, depending if the left/right panel is enabled
+ if (!(self.left && self.left.isEnabled) && amount > 0) {
+ self.content.setTranslateX(0);
+ return;
+ }
+
+ if (!(self.right && self.right.isEnabled) && amount < 0) {
+ self.content.setTranslateX(0);
+ return;
+ }
+
+ if (leftShowing && amount > maxLeft) {
+ self.content.setTranslateX(maxLeft);
+ return;
+ }
+
+ if (rightShowing && amount < -maxRight) {
+ self.content.setTranslateX(-maxRight);
+ return;
+ }
+
+ self.content.setTranslateX(amount);
+
+ if (amount >= 0) {
+ leftShowing = true;
+ rightShowing = false;
+
+ if (amount > 0) {
+ // Push the z-index of the right menu down
+ self.right && self.right.pushDown && self.right.pushDown();
+ // Bring the z-index of the left menu up
+ self.left && self.left.bringUp && self.left.bringUp();
+ }
+ } else {
+ rightShowing = true;
+ leftShowing = false;
+
+ // Bring the z-index of the right menu up
+ self.right && self.right.bringUp && self.right.bringUp();
+ // Push the z-index of the left menu down
+ self.left && self.left.pushDown && self.left.pushDown();
+ }
+ };
+
+ /**
+ * Given an event object, find the final resting position of this side
+ * menu. For example, if the user "throws" the content to the right and
+ * releases the touch, the left menu should snap open (animated, of course).
+ *
+ * @param {Event} e the gesture event to use for snapping
+ */
+ self.snapToRest = function(e) {
+ // We want to animate at the end of this
+ self.content.enableAnimation();
+ isDragging = false;
+
+ // Check how much the panel is open after the drag, and
+ // what the drag velocity is
+ var ratio = self.getOpenRatio();
+
+ if (ratio === 0) {
+ // Just to be safe
+ self.openPercentage(0);
+ return;
+ }
+
+ var velocityThreshold = 0.3;
+ var velocityX = e.gesture.velocityX;
+ var direction = e.gesture.direction;
+
+ // Going right, less than half, too slow (snap back)
+ if (ratio > 0 && ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
+ self.openPercentage(0);
+ }
+
+ // Going left, more than half, too slow (snap back)
+ else if (ratio > 0.5 && direction == 'left' && velocityX < velocityThreshold) {
+ self.openPercentage(100);
+ }
+
+ // Going left, less than half, too slow (snap back)
+ else if (ratio < 0 && ratio > -0.5 && direction == 'left' && velocityX < velocityThreshold) {
+ self.openPercentage(0);
+ }
+
+ // Going right, more than half, too slow (snap back)
+ else if (ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
+ self.openPercentage(-100);
+ }
+
+ // Going right, more than half, or quickly (snap open)
+ else if (direction == 'right' && ratio >= 0 && (ratio >= 0.5 || velocityX > velocityThreshold)) {
+ self.openPercentage(100);
+ }
+
+ // Going left, more than half, or quickly (span open)
+ else if (direction == 'left' && ratio <= 0 && (ratio <= -0.5 || velocityX > velocityThreshold)) {
+ self.openPercentage(-100);
+ }
+
+ // Snap back for safety
+ else {
+ self.openPercentage(0);
+ }
+ };
+
+ self.enableMenuWithBackViews = function(val) {
+ if (arguments.length) {
+ enableMenuWithBackViews = !!val;
+ }
+ return enableMenuWithBackViews;
+ };
+
+ self.isAsideExposed = function() {
+ return !!isAsideExposed;
+ };
+
+ self.exposeAside = function(shouldExposeAside) {
+ if (!(self.left && self.left.isEnabled) && !(self.right && self.right.isEnabled)) return;
+ self.close();
+ isAsideExposed = shouldExposeAside;
+ if (self.left && self.left.isEnabled) {
+ // set the left marget width if it should be exposed
+ // otherwise set false so there's no left margin
+ self.content.setMarginLeft(isAsideExposed ? self.left.width : 0);
+ } else if (self.right && self.right.isEnabled) {
+ self.content.setMarginRight(isAsideExposed ? self.right.width : 0);
+ }
+
+ self.$scope.$emit('$ionicExposeAside', isAsideExposed);
+ };
+
+ self.activeAsideResizing = function(isResizing) {
+ $ionicBody.enableClass(isResizing, 'aside-resizing');
+ };
+
+ // End a drag with the given event
+ self._endDrag = function(e) {
+ if (isAsideExposed) return;
+
+ if (isDragging) {
+ self.snapToRest(e);
+ }
+ startX = null;
+ lastX = null;
+ offsetX = null;
+ };
+
+ // Handle a drag event
+ self._handleDrag = function(e) {
+ if (isAsideExposed) return;
+
+ // If we don't have start coords, grab and store them
+ if (!startX) {
+ startX = e.gesture.touches[0].pageX;
+ lastX = startX;
+ } else {
+ // Grab the current tap coords
+ lastX = e.gesture.touches[0].pageX;
+ }
+
+ // Calculate difference from the tap points
+ if (!isDragging && Math.abs(lastX - startX) > self.dragThresholdX) {
+ // if the difference is greater than threshold, start dragging using the current
+ // point as the starting point
+ startX = lastX;
+
+ isDragging = true;
+ // Initialize dragging
+ self.content.disableAnimation();
+ offsetX = self.getOpenAmount();
+ }
+
+ if (isDragging) {
+ self.openAmount(offsetX + (lastX - startX));
+ }
+ };
+
+ self.canDragContent = function(canDrag) {
+ if (arguments.length) {
+ $scope.dragContent = !!canDrag;
+ }
+ return $scope.dragContent;
+ };
+
+ self.edgeThreshold = 25;
+ self.edgeThresholdEnabled = false;
+ self.edgeDragThreshold = function(value) {
+ if (arguments.length) {
+ if (angular.isNumber(value) && value > 0) {
+ self.edgeThreshold = value;
+ self.edgeThresholdEnabled = true;
+ } else {
+ self.edgeThresholdEnabled = !!value;
+ }
+ }
+ return self.edgeThresholdEnabled;
+ };
+
+ self.isDraggableTarget = function(e) {
+ //Only restrict edge when sidemenu is closed and restriction is enabled
+ var shouldOnlyAllowEdgeDrag = self.edgeThresholdEnabled && !self.isOpen();
+ var startX = e.gesture.startEvent && e.gesture.startEvent.center &&
+ e.gesture.startEvent.center.pageX;
+
+ var dragIsWithinBounds = !shouldOnlyAllowEdgeDrag ||
+ startX <= self.edgeThreshold ||
+ startX >= self.content.element.offsetWidth - self.edgeThreshold;
+
+ var backView = $ionicHistory.backView();
+ var menuEnabled = enableMenuWithBackViews ? true : !backView;
+ if (!menuEnabled) {
+ var currentView = $ionicHistory.currentView() || {};
+ return backView.historyId !== currentView.historyId;
+ }
+
+ return ($scope.dragContent || self.isOpen()) &&
+ dragIsWithinBounds &&
+ !e.gesture.srcEvent.defaultPrevented &&
+ menuEnabled &&
+ !e.target.tagName.match(/input|textarea|select|object|embed/i) &&
+ !e.target.isContentEditable &&
+ !(e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-scroll') == 'true');
+ };
+
+ $scope.sideMenuContentTranslateX = 0;
+
+ var deregisterBackButtonAction = angular.noop;
+ var closeSideMenu = angular.bind(self, self.close);
+
+ $scope.$watch(function() {
+ return self.getOpenAmount() !== 0;
+ }, function(isOpen) {
+ deregisterBackButtonAction();
+ if (isOpen) {
+ deregisterBackButtonAction = $ionicPlatform.registerBackButtonAction(
+ closeSideMenu,
+ PLATFORM_BACK_BUTTON_PRIORITY_SIDE_MENU
+ );
+ }
+ });
+
+ var deregisterInstance = $ionicSideMenuDelegate._registerInstance(
+ self, $attrs.delegateHandle, function() {
+ return $ionicHistory.isActiveScope($scope);
+ }
+ );
+
+ $scope.$on('$destroy', function() {
+ deregisterInstance();
+ deregisterBackButtonAction();
+ self.$scope = null;
+ if (self.content) {
+ self.content.element = null;
+ self.content = null;
+ }
+ });
+
+ self.initialize({
+ left: {
+ width: 275
+ },
+ right: {
+ width: 275
+ }
+ });
+
+}]);
+
+IonicModule
+.controller('$ionicTab', [
+ '$scope',
+ '$ionicHistory',
+ '$attrs',
+ '$location',
+ '$state',
+function($scope, $ionicHistory, $attrs, $location, $state) {
+ this.$scope = $scope;
+
+ //All of these exposed for testing
+ this.hrefMatchesState = function() {
+ return $attrs.href && $location.path().indexOf(
+ $attrs.href.replace(/^#/, '').replace(/\/$/, '')
+ ) === 0;
+ };
+ this.srefMatchesState = function() {
+ return $attrs.uiSref && $state.includes($attrs.uiSref.split('(')[0]);
+ };
+ this.navNameMatchesState = function() {
+ return this.navViewName && $ionicHistory.isCurrentStateNavView(this.navViewName);
+ };
+
+ this.tabMatchesState = function() {
+ return this.hrefMatchesState() || this.srefMatchesState() || this.navNameMatchesState();
+ };
+}]);
+
+IonicModule
+.controller('$ionicTabs', [
+ '$scope',
+ '$element',
+ '$ionicHistory',
+function($scope, $element, $ionicHistory) {
+ var self = this;
+ var selectedTab = null;
+ var selectedTabIndex;
+ self.tabs = [];
+
+ self.selectedIndex = function() {
+ return self.tabs.indexOf(selectedTab);
+ };
+ self.selectedTab = function() {
+ return selectedTab;
+ };
+
+ self.add = function(tab) {
+ $ionicHistory.registerHistory(tab);
+ self.tabs.push(tab);
+ };
+
+ self.remove = function(tab) {
+ var tabIndex = self.tabs.indexOf(tab);
+ if (tabIndex === -1) {
+ return;
+ }
+ //Use a field like '$tabSelected' so developers won't accidentally set it in controllers etc
+ if (tab.$tabSelected) {
+ self.deselect(tab);
+ //Try to select a new tab if we're removing a tab
+ if (self.tabs.length === 1) {
+ //do nothing if there are no other tabs to select
+ } else {
+ //Select previous tab if it's the last tab, else select next tab
+ var newTabIndex = tabIndex === self.tabs.length - 1 ? tabIndex - 1 : tabIndex + 1;
+ self.select(self.tabs[newTabIndex]);
+ }
+ }
+ self.tabs.splice(tabIndex, 1);
+ };
+
+ self.deselect = function(tab) {
+ if (tab.$tabSelected) {
+ selectedTab = selectedTabIndex = null;
+ tab.$tabSelected = false;
+ (tab.onDeselect || angular.noop)();
+ tab.$broadcast && tab.$broadcast('$ionicHistory.deselect');
+ }
+ };
+
+ self.select = function(tab, shouldEmitEvent) {
+ var tabIndex;
+ if (angular.isNumber(tab)) {
+ tabIndex = tab;
+ if (tabIndex >= self.tabs.length) return;
+ tab = self.tabs[tabIndex];
+ } else {
+ tabIndex = self.tabs.indexOf(tab);
+ }
+
+ if (arguments.length === 1) {
+ shouldEmitEvent = !!(tab.navViewName || tab.uiSref);
+ }
+
+ if (selectedTab && selectedTab.$historyId == tab.$historyId) {
+ if (shouldEmitEvent) {
+ $ionicHistory.goToHistoryRoot(tab.$historyId);
+ }
+
+ } else if (selectedTabIndex !== tabIndex) {
+ forEach(self.tabs, function(tab) {
+ self.deselect(tab);
+ });
+
+ selectedTab = tab;
+ selectedTabIndex = tabIndex;
+
+ if (self.$scope && self.$scope.$parent) {
+ self.$scope.$parent.$activeHistoryId = tab.$historyId;
+ }
+
+ //Use a funny name like $tabSelected so the developer doesn't overwrite the var in a child scope
+ tab.$tabSelected = true;
+ (tab.onSelect || angular.noop)();
+
+ if (shouldEmitEvent) {
+ $scope.$emit('$ionicHistory.change', {
+ type: 'tab',
+ tabIndex: tabIndex,
+ historyId: tab.$historyId,
+ navViewName: tab.navViewName,
+ hasNavView: !!tab.navViewName,
+ title: tab.title,
+ url: tab.href,
+ uiSref: tab.uiSref
+ });
+ }
+ }
+ };
+
+ self.hasActiveScope = function() {
+ for (var x = 0; x < self.tabs.length; x++) {
+ if ($ionicHistory.isActiveScope(self.tabs[x])) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+}]);
+
+IonicModule
+.controller('$ionicView', [
+ '$scope',
+ '$element',
+ '$attrs',
+ '$compile',
+ '$rootScope',
+ '$ionicViewSwitcher',
+function($scope, $element, $attrs, $compile, $rootScope, $ionicViewSwitcher) {
+ var self = this;
+ var navElementHtml = {};
+ var navViewCtrl;
+ var navBarDelegateHandle;
+ var hasViewHeaderBar;
+ var deregisters = [];
+ var viewTitle;
+
+ var deregIonNavBarInit = $scope.$on('ionNavBar.init', function(ev, delegateHandle) {
+ // this view has its own ion-nav-bar, remember the navBarDelegateHandle for this view
+ ev.stopPropagation();
+ navBarDelegateHandle = delegateHandle;
+ });
+
+
+ self.init = function() {
+ deregIonNavBarInit();
+
+ var modalCtrl = $element.inheritedData('$ionModalController');
+ navViewCtrl = $element.inheritedData('$ionNavViewController');
+
+ // don't bother if inside a modal or there's no parent navView
+ if (!navViewCtrl || modalCtrl) return;
+
+ // add listeners for when this view changes
+ $scope.$on('$ionicView.beforeEnter', self.beforeEnter);
+ $scope.$on('$ionicView.afterEnter', afterEnter);
+ $scope.$on('$ionicView.beforeLeave', deregisterFns);
+ };
+
+ self.beforeEnter = function(ev, transData) {
+ // this event was emitted, starting at intial ion-view, then bubbles up
+ // only the first ion-view should do something with it, parent ion-views should ignore
+ if (transData && !transData.viewNotified) {
+ transData.viewNotified = true;
+
+ if (!$rootScope.$$phase) $scope.$digest();
+ viewTitle = isDefined($attrs.viewTitle) ? $attrs.viewTitle : $attrs.title;
+
+ var navBarItems = {};
+ for (var n in navElementHtml) {
+ navBarItems[n] = generateNavBarItem(navElementHtml[n]);
+ }
+
+ navViewCtrl.beforeEnter({
+ title: viewTitle,
+ direction: transData.direction,
+ transition: transData.transition,
+ navBarTransition: transData.navBarTransition,
+ transitionId: transData.transitionId,
+ shouldAnimate: transData.shouldAnimate,
+ enableBack: transData.enableBack,
+ showBack: !attrTrue('hideBackButton'),
+ navBarItems: navBarItems,
+ navBarDelegate: navBarDelegateHandle || null,
+ showNavBar: !attrTrue('hideNavBar'),
+ hasHeaderBar: !!hasViewHeaderBar
+ });
+
+ // make sure any existing observers are cleaned up
+ deregisterFns();
+ }
+ };
+
+
+ function afterEnter() {
+ // only listen for title updates after it has entered
+ // but also deregister the observe before it leaves
+ var viewTitleAttr = isDefined($attrs.viewTitle) && 'viewTitle' || isDefined($attrs.title) && 'title';
+ if (viewTitleAttr) {
+ titleUpdate($attrs[viewTitleAttr]);
+ deregisters.push($attrs.$observe(viewTitleAttr, titleUpdate));
+ }
+
+ if (isDefined($attrs.hideBackButton)) {
+ deregisters.push($scope.$watch($attrs.hideBackButton, function(val) {
+ navViewCtrl.showBackButton(!val);
+ }));
+ }
+
+ if (isDefined($attrs.hideNavBar)) {
+ deregisters.push($scope.$watch($attrs.hideNavBar, function(val) {
+ navViewCtrl.showBar(!val);
+ }));
+ }
+ }
+
+
+ function titleUpdate(newTitle) {
+ if (isDefined(newTitle) && newTitle !== viewTitle) {
+ viewTitle = newTitle;
+ navViewCtrl.title(viewTitle);
+ }
+ }
+
+
+ function deregisterFns() {
+ // remove all existing $attrs.$observe's
+ for (var x = 0; x < deregisters.length; x++) {
+ deregisters[x]();
+ }
+ deregisters = [];
+ }
+
+
+ function generateNavBarItem(html) {
+ if (html) {
+ // every time a view enters we need to recreate its view buttons if they exist
+ return $compile(html)($scope.$new());
+ }
+ }
+
+
+ function attrTrue(key) {
+ return !!$scope.$eval($attrs[key]);
+ }
+
+
+ self.navElement = function(type, html) {
+ navElementHtml[type] = html;
+ };
+
+}]);
+
+/*
+ * We don't document the ionActionSheet directive, we instead document
+ * the $ionicActionSheet service
+ */
+IonicModule
+.directive('ionActionSheet', ['$document', function($document) {
+ return {
+ restrict: 'E',
+ scope: true,
+ replace: true,
+ link: function($scope, $element){
+ var keyUp = function(e) {
+ if(e.which == 27) {
+ $scope.cancel();
+ $scope.$apply();
+ }
+ };
+
+ var backdropClick = function(e) {
+ if(e.target == $element[0]) {
+ $scope.cancel();
+ $scope.$apply();
+ }
+ };
+ $scope.$on('$destroy', function() {
+ $element.remove();
+ $document.unbind('keyup', keyUp);
+ });
+
+ $document.bind('keyup', keyUp);
+ $element.bind('click', backdropClick);
+ },
+ template: '
' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ '
'
+ };
+}]);
+
+
+/**
+ * @ngdoc directive
+ * @name ionCheckbox
+ * @module ionic
+ * @restrict E
+ * @codepen hqcju
+ * @description
+ * The checkbox is no different than the HTML checkbox input, except it's styled differently.
+ *
+ * The checkbox behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]).
+ *
+ * @usage
+ * ```html
+ * Checkbox Label
+ * ```
+ */
+
+IonicModule
+.directive('ionCheckbox', ['$ionicConfig', function($ionicConfig) {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: '?ngModel',
+ transclude: true,
+ template:
+ '',
+ compile: function(element, attr) {
+ var input = element.find('input');
+ forEach({
+ 'name': attr.name,
+ 'ng-value': attr.ngValue,
+ 'ng-model': attr.ngModel,
+ 'ng-checked': attr.ngChecked,
+ 'ng-disabled': attr.ngDisabled,
+ 'ng-true-value': attr.ngTrueValue,
+ 'ng-false-value': attr.ngFalseValue,
+ 'ng-change': attr.ngChange
+ }, function(value, name) {
+ if (isDefined(value)) {
+ input.attr(name, value);
+ }
+ });
+ var checkboxWrapper = element[0].querySelector('.checkbox');
+ checkboxWrapper.classList.add('checkbox-' + $ionicConfig.form.checkbox());
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @module ionic
+ * @name collectionRepeat
+ * @restrict A
+ * @codepen mFygh
+ * @description
+ * `collection-repeat` is a directive that allows you to render lists with
+ * thousands of items in them, and experience little to no performance penalty.
+ *
+ * Demo:
+ *
+ * The directive renders onto the screen only the items that should be currently visible.
+ * So if you have 1,000 items in your list but only ten fit on your screen,
+ * collection-repeat will only render into the DOM the ten that are in the current
+ * scroll position.
+ *
+ * Here are a few things to keep in mind while using collection-repeat:
+ *
+ * 1. The data supplied to collection-repeat must be an array.
+ * 2. You must explicitly tell the directive what size your items will be in the DOM, using directive attributes.
+ * Pixel amounts or percentages are allowed (see below).
+ * 3. The elements rendered will be absolutely positioned: be sure to let your CSS work with
+ * this (see below).
+ * 4. Each collection-repeat list will take up all of its parent scrollView's space.
+ * If you wish to have multiple lists on one page, put each list within its own
+ * {@link ionic.directive:ionScroll ionScroll} container.
+ * 5. You should not use the ng-show and ng-hide directives on your ion-content/ion-scroll elements that
+ * have a collection-repeat inside. ng-show and ng-hide apply the `display: none` css rule to the content's
+ * style, causing the scrollView to read the width and height of the content as 0. Resultingly,
+ * collection-repeat will render elements that have just been un-hidden incorrectly.
+ *
+ *
+ * @usage
+ *
+ * #### Basic Usage (single rows of items)
+ *
+ * Notice two things here: we use ng-style to set the height of the item to match
+ * what the repeater thinks our item height is. Additionally, we add a css rule
+ * to make our item stretch to fit the full screen (since it will be absolutely
+ * positioned).
+ *
+ * ```html
+ *
+ *
+ *
+ * ```
+ * Percentage of total visible list dimensions. This example shows a 3 by 3 matrix that fits on the screen (3 rows and 3 colums). Note that dimensions are used in the creation of the element and therefore a measurement of the item cannnot be used as an input dimension.
+ * ```css
+ * .my-image-item img {
+ * height: 33%;
+ * width: 33%;
+ * }
+ * ```
+ *
+ * @param {expression} collection-repeat The expression indicating how to enumerate a collection. These
+ * formats are currently supported:
+ *
+ * * `variable in expression` – where variable is the user defined loop variable and `expression`
+ * is a scope expression giving the collection to enumerate.
+ *
+ * For example: `album in artist.albums`.
+ *
+ * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
+ * which can be used to associate the objects in the collection with the DOM elements. If no tracking function
+ * is specified the collection-repeat associates elements by identity in the collection. It is an error to have
+ * more than one tracking function to resolve to the same key. (This would mean that two distinct objects are
+ * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression,
+ * before specifying a tracking expression.
+ *
+ * For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements
+ * will be associated by item identity in the array.
+ *
+ * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
+ * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
+ * with the corresponding item in the array by identity. Moving the same object in array would move the DOM
+ * element in the same way in the DOM.
+ *
+ * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this
+ * case the object identity does not matter. Two objects are considered equivalent as long as their `id`
+ * property is same.
+ *
+ * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
+ * to items in conjunction with a tracking expression.
+ *
+ * @param {expression} collection-item-width The width of the repeated element. Can be a number (in pixels) or a percentage.
+ * @param {expression} collection-item-height The height of the repeated element. Can be a number (in pixels), or a percentage.
+ *
+ */
+var COLLECTION_REPEAT_SCROLLVIEW_XY_ERROR = "Cannot create a collection-repeat within a scrollView that is scrollable on both x and y axis. Choose either x direction or y direction.";
+var COLLECTION_REPEAT_ATTR_HEIGHT_ERROR = "collection-repeat expected attribute collection-item-height to be a an expression that returns a number (in pixels) or percentage.";
+var COLLECTION_REPEAT_ATTR_WIDTH_ERROR = "collection-repeat expected attribute collection-item-width to be a an expression that returns a number (in pixels) or percentage.";
+var COLLECTION_REPEAT_ATTR_REPEAT_ERROR = "collection-repeat expected expression in form of '_item_ in _collection_[ track by _id_]' but got '%'";
+
+IonicModule
+.directive('collectionRepeat', [
+ '$collectionRepeatManager',
+ '$collectionDataSource',
+ '$parse',
+function($collectionRepeatManager, $collectionDataSource, $parse) {
+ return {
+ priority: 1000,
+ transclude: 'element',
+ terminal: true,
+ $$tlb: true,
+ require: ['^$ionicScroll', '^?ionNavView'],
+ controller: [function(){}],
+ link: function($scope, $element, $attr, ctrls, $transclude) {
+ var scrollCtrl = ctrls[0];
+ var navViewCtrl = ctrls[1];
+ var wrap = jqLite('
');
+ $element.parent()[0].insertBefore(wrap[0], $element[0]);
+ wrap.append($element);
+
+ var scrollView = scrollCtrl.scrollView;
+ if (scrollView.options.scrollingX && scrollView.options.scrollingY) {
+ throw new Error(COLLECTION_REPEAT_SCROLLVIEW_XY_ERROR);
+ }
+
+ var isVertical = !!scrollView.options.scrollingY;
+ if (isVertical && !$attr.collectionItemHeight) {
+ throw new Error(COLLECTION_REPEAT_ATTR_HEIGHT_ERROR);
+ } else if (!isVertical && !$attr.collectionItemWidth) {
+ throw new Error(COLLECTION_REPEAT_ATTR_WIDTH_ERROR);
+ }
+
+ var heightParsed = $parse($attr.collectionItemHeight || '"100%"');
+ var widthParsed = $parse($attr.collectionItemWidth || '"100%"');
+
+ var heightGetter = function(scope, locals) {
+ var result = heightParsed(scope, locals);
+ if (isString(result) && result.indexOf('%') > -1) {
+ return Math.floor(parseInt(result) / 100 * scrollView.__clientHeight);
+ }
+ return parseInt(result);
+ };
+ var widthGetter = function(scope, locals) {
+ var result = widthParsed(scope, locals);
+ if (isString(result) && result.indexOf('%') > -1) {
+ return Math.floor(parseInt(result) / 100 * scrollView.__clientWidth);
+ }
+ return parseInt(result);
+ };
+
+ var match = $attr.collectionRepeat.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
+ if (!match) {
+ throw new Error(COLLECTION_REPEAT_ATTR_REPEAT_ERROR
+ .replace('%', $attr.collectionRepeat));
+ }
+ var keyExpr = match[1];
+ var listExpr = match[2];
+ var trackByExpr = match[3];
+
+ var dataSource = new $collectionDataSource({
+ scope: $scope,
+ transcludeFn: $transclude,
+ transcludeParent: $element.parent(),
+ keyExpr: keyExpr,
+ listExpr: listExpr,
+ trackByExpr: trackByExpr,
+ heightGetter: heightGetter,
+ widthGetter: widthGetter
+ });
+ var collectionRepeatManager = new $collectionRepeatManager({
+ dataSource: dataSource,
+ element: scrollCtrl.$element,
+ scrollView: scrollCtrl.scrollView,
+ });
+
+ var listExprParsed = $parse(listExpr);
+ $scope.$watchCollection(listExprParsed, function(value) {
+ if (value && !angular.isArray(value)) {
+ throw new Error("collection-repeat expects an array to repeat over, but instead got '" + typeof value + "'.");
+ }
+ rerender(value);
+ });
+
+ // Find every sibling before and after the repeated items, and pass them
+ // to the dataSource
+ var scrollViewContent = scrollCtrl.scrollView.__content;
+ function rerender(value) {
+ var beforeSiblings = [];
+ var afterSiblings = [];
+ var before = true;
+
+ forEach(scrollViewContent.children, function(node, i) {
+ if ( ionic.DomUtil.elementIsDescendant($element[0], node, scrollViewContent) ) {
+ before = false;
+ } else {
+ if (node.hasAttribute('collection-repeat-ignore')) return;
+ var width = node.offsetWidth;
+ var height = node.offsetHeight;
+ if (width && height) {
+ var element = jqLite(node);
+ (before ? beforeSiblings : afterSiblings).push({
+ width: node.offsetWidth,
+ height: node.offsetHeight,
+ element: element,
+ scope: element.isolateScope() || element.scope(),
+ isOutside: true
+ });
+ }
+ }
+ });
+
+ scrollView.resize();
+ dataSource.setData(value, beforeSiblings, afterSiblings);
+ collectionRepeatManager.resize();
+ }
+
+ var requiresRerender;
+ function rerenderOnResize() {
+ rerender(listExprParsed($scope));
+ requiresRerender = (!scrollViewContent.clientWidth && !scrollViewContent.clientHeight);
+ }
+
+ function viewEnter() {
+ if (requiresRerender) {
+ rerenderOnResize();
+ }
+ }
+
+ scrollCtrl.$element.on('scroll.resize', rerenderOnResize);
+ ionic.on('resize', rerenderOnResize, window);
+ var deregisterViewListener;
+ if (navViewCtrl) {
+ deregisterViewListener = navViewCtrl.scope.$on('$ionicView.afterEnter', viewEnter);
+ }
+
+ $scope.$on('$destroy', function() {
+ collectionRepeatManager.destroy();
+ dataSource.destroy();
+ ionic.off('resize', rerenderOnResize, window);
+ (deregisterViewListener || angular.noop)();
+ });
+ }
+ };
+}])
+.directive({
+ ngSrc: collectionRepeatSrcDirective('ngSrc', 'src'),
+ ngSrcset: collectionRepeatSrcDirective('ngSrcset', 'srcset'),
+ ngHref: collectionRepeatSrcDirective('ngHref', 'href')
+});
+
+// Fix for #1674
+// Problem: if an ngSrc or ngHref expression evaluates to a falsy value, it will
+// not erase the previous truthy value of the href.
+// In collectionRepeat, we re-use elements from before. So if the ngHref expression
+// evaluates to truthy for item 1 and then falsy for item 2, if an element changes
+// from representing item 1 to representing item 2, item 2 will still have
+// item 1's href value.
+// Solution: erase the href or src attribute if ngHref/ngSrc are falsy.
+function collectionRepeatSrcDirective(ngAttrName, attrName) {
+ return [function() {
+ return {
+ priority: '99', // it needs to run after the attributes are interpolated
+ link: function(scope, element, attr) {
+ attr.$observe(ngAttrName, function(value) {
+ if (!value) {
+ element[0].removeAttribute(attrName);
+ }
+ });
+ }
+ };
+ }];
+}
+
+/**
+ * @ngdoc directive
+ * @name ionContent
+ * @module ionic
+ * @delegate ionic.service:$ionicScrollDelegate
+ * @restrict E
+ *
+ * @description
+ * The ionContent directive provides an easy to use content area that can be configured
+ * to use Ionic's custom Scroll View, or the built in overflow scrolling of the browser.
+ *
+ * While we recommend using the custom Scroll features in Ionic in most cases, sometimes
+ * (for performance reasons) only the browser's native overflow scrolling will suffice,
+ * and so we've made it easy to toggle between the Ionic scroll implementation and
+ * overflow scrolling.
+ *
+ * You can implement pull-to-refresh with the {@link ionic.directive:ionRefresher}
+ * directive, and infinite scrolling with the {@link ionic.directive:ionInfiniteScroll}
+ * directive.
+ *
+ * Be aware that this directive gets its own child scope. If you do not understand why this
+ * is important, you can read [https://docs.angularjs.org/guide/scope](https://docs.angularjs.org/guide/scope).
+ *
+ * @param {string=} delegate-handle The handle used to identify this scrollView
+ * with {@link ionic.service:$ionicScrollDelegate}.
+ * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'.
+ * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true.
+ * @param {boolean=} padding Whether to add padding to the content.
+ * of the content. Defaults to true on iOS, false on Android.
+ * @param {boolean=} scroll Whether to allow scrolling of content. Defaults to true.
+ * @param {boolean=} overflow-scroll Whether to use overflow-scrolling instead of
+ * Ionic scroll.
+ * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true.
+ * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true.
+ * @param {string=} start-x Initial horizontal scroll position. Default 0.
+ * @param {string=} start-y Initial vertical scroll position. Default 0.
+ * @param {expression=} on-scroll Expression to evaluate when the content is scrolled.
+ * @param {expression=} on-scroll-complete Expression to evaluate when a scroll action completes.
+ * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges
+ * of the content. Defaults to true on iOS, false on Android.
+ * @param {number=} scroll-event-interval Number of milliseconds between each firing of the 'on-scroll' expression. Default 10.
+ */
+IonicModule
+.directive('ionContent', [
+ '$timeout',
+ '$controller',
+ '$ionicBind',
+function($timeout, $controller, $ionicBind) {
+ return {
+ restrict: 'E',
+ require: '^?ionNavView',
+ scope: true,
+ priority: 800,
+ compile: function(element, attr) {
+ var innerElement;
+
+ element.addClass('scroll-content ionic-scroll');
+
+ if (attr.scroll != 'false') {
+ //We cannot use normal transclude here because it breaks element.data()
+ //inheritance on compile
+ innerElement = jqLite('');
+ innerElement.append(element.contents());
+ element.append(innerElement);
+ } else {
+ element.addClass('scroll-content-false');
+ }
+
+ return { pre: prelink };
+ function prelink($scope, $element, $attr, navViewCtrl) {
+ var parentScope = $scope.$parent;
+ $scope.$watch(function() {
+ return (parentScope.$hasHeader ? ' has-header' : '') +
+ (parentScope.$hasSubheader ? ' has-subheader' : '') +
+ (parentScope.$hasFooter ? ' has-footer' : '') +
+ (parentScope.$hasSubfooter ? ' has-subfooter' : '') +
+ (parentScope.$hasTabs ? ' has-tabs' : '') +
+ (parentScope.$hasTabsTop ? ' has-tabs-top' : '');
+ }, function(className, oldClassName) {
+ $element.removeClass(oldClassName);
+ $element.addClass(className);
+ });
+
+ //Only this ionContent should use these variables from parent scopes
+ $scope.$hasHeader = $scope.$hasSubheader =
+ $scope.$hasFooter = $scope.$hasSubfooter =
+ $scope.$hasTabs = $scope.$hasTabsTop =
+ false;
+ $ionicBind($scope, $attr, {
+ $onScroll: '&onScroll',
+ $onScrollComplete: '&onScrollComplete',
+ hasBouncing: '@',
+ padding: '@',
+ direction: '@',
+ scrollbarX: '@',
+ scrollbarY: '@',
+ startX: '@',
+ startY: '@',
+ scrollEventInterval: '@'
+ });
+ $scope.direction = $scope.direction || 'y';
+
+ if (angular.isDefined($attr.padding)) {
+ $scope.$watch($attr.padding, function(newVal) {
+ (innerElement || $element).toggleClass('padding', !!newVal);
+ });
+ }
+
+ if ($attr.scroll === "false") {
+ //do nothing
+ } else if(attr.overflowScroll === "true") {
+ $element.addClass('overflow-scroll');
+ } else {
+ var scrollViewOptions = {
+ el: $element[0],
+ delegateHandle: attr.delegateHandle,
+ locking: (attr.locking || 'true') === 'true',
+ bouncing: $scope.$eval($scope.hasBouncing),
+ startX: $scope.$eval($scope.startX) || 0,
+ startY: $scope.$eval($scope.startY) || 0,
+ scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
+ scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
+ scrollingX: $scope.direction.indexOf('x') >= 0,
+ scrollingY: $scope.direction.indexOf('y') >= 0,
+ scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 10,
+ scrollingComplete: function() {
+ $scope.$onScrollComplete({
+ scrollTop: this.__scrollTop,
+ scrollLeft: this.__scrollLeft
+ });
+ }
+ };
+ $controller('$ionicScroll', {
+ $scope: $scope,
+ scrollViewOptions: scrollViewOptions
+ });
+
+ $scope.$on('$destroy', function() {
+ scrollViewOptions.scrollingComplete = angular.noop;
+ delete scrollViewOptions.el;
+ innerElement = null;
+ $element = null;
+ attr.$$element = null;
+ });
+ }
+
+ }
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name exposeAsideWhen
+ * @module ionic
+ * @restrict A
+ * @parent ionic.directive:ionSideMenus
+ *
+ * @description
+ * It is common for a tablet application to hide a menu when in portrait mode, but to show the
+ * same menu on the left side when the tablet is in landscape mode. The `exposeAsideWhen` attribute
+ * directive can be used to accomplish a similar interface.
+ *
+ * By default, side menus are hidden underneath its side menu content, and can be opened by either
+ * swiping the content left or right, or toggling a button to show the side menu. However, by adding the
+ * `exposeAsideWhen` attribute directive to an {@link ionic.directive:ionSideMenu} element directive,
+ * a side menu can be given instructions on "when" the menu should be exposed (always viewable). For
+ * example, the `expose-aside-when="large"` attribute will keep the side menu hidden when the viewport's
+ * width is less than `768px`, but when the viewport's width is `768px` or greater, the menu will then
+ * always be shown and can no longer be opened or closed like it could when it was hidden for smaller
+ * viewports.
+ *
+ * Using `large` as the attribute's value is a shortcut value to `(min-width:768px)` since it is
+ * the most common use-case. However, for added flexibility, any valid media query could be added
+ * as the value, such as `(min-width:600px)` or even multiple queries such as
+ * `(min-width:750px) and (max-width:1200px)`.
+
+ * @usage
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ * For a complete side menu example, see the
+ * {@link ionic.directive:ionSideMenus} documentation.
+ */
+IonicModule
+.directive('exposeAsideWhen', ['$window', function($window) {
+ return {
+ restrict: 'A',
+ require: '^ionSideMenus',
+ link: function($scope, $element, $attr, sideMenuCtrl) {
+
+ function checkAsideExpose() {
+ var mq = $attr.exposeAsideWhen == 'large' ? '(min-width:768px)' : $attr.exposeAsideWhen;
+ sideMenuCtrl.exposeAside( $window.matchMedia(mq).matches );
+ sideMenuCtrl.activeAsideResizing(false);
+ }
+
+ function onResize() {
+ sideMenuCtrl.activeAsideResizing(true);
+ debouncedCheck();
+ }
+
+ var debouncedCheck = ionic.debounce(function() {
+ $scope.$apply(function(){
+ checkAsideExpose();
+ });
+ }, 300, false);
+
+ checkAsideExpose();
+
+ ionic.on('resize', onResize, $window);
+
+ $scope.$on('$destroy', function(){
+ ionic.off('resize', onResize, $window);
+ });
+
+ }
+ };
+}]);
+
+
+var GESTURE_DIRECTIVES = 'onHold onTap onTouch onRelease onDrag onDragUp onDragRight onDragDown onDragLeft onSwipe onSwipeUp onSwipeRight onSwipeDown onSwipeLeft'.split(' ');
+
+GESTURE_DIRECTIVES.forEach(function(name) {
+ IonicModule.directive(name, gestureDirective(name));
+});
+
+
+/**
+ * @ngdoc directive
+ * @name onHold
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Touch stays at the same location for 500ms. Similar to long touch events available for AngularJS and jQuery.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onTap
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Quick touch at a location. If the duration of the touch goes
+ * longer than 250ms it is no longer a tap gesture.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onTouch
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called immediately when the user first begins a touch. This
+ * gesture does not wait for a touchend/mouseup.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onRelease
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the user ends a touch.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDrag
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Move with one touch 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.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDragUp
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the element is dragged up.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDragRight
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the element is dragged to the right.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDragDown
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the element is dragged down.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDragLeft
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the element is dragged to the left.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipe
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity in any direction.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipeUp
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity moving up.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipeRight
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity moving to the right.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipeDown
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity moving down.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipeLeft
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity moving to the left.
+ *
+ * @usage
+ * ```html
+ *
+ * ```
+ */
+
+
+function gestureDirective(directiveName) {
+ return ['$ionicGesture', '$parse', function($ionicGesture, $parse) {
+ var eventType = directiveName.substr(2).toLowerCase();
+
+ return function(scope, element, attr) {
+ var fn = $parse( attr[directiveName] );
+
+ var listener = function(ev) {
+ scope.$apply(function() {
+ fn(scope, {
+ $event: ev
+ });
+ });
+ };
+
+ var gesture = $ionicGesture.on(eventType, listener, element);
+
+ scope.$on('$destroy', function() {
+ $ionicGesture.off(gesture, eventType, listener);
+ });
+ };
+ }];
+}
+
+
+IonicModule
+.directive('ionHeaderBar', tapScrollToTopDirective())
+
+/**
+ * @ngdoc directive
+ * @name ionHeaderBar
+ * @module ionic
+ * @restrict E
+ *
+ * @description
+ * Adds a fixed header bar above some content.
+ *
+ * Can also be a subheader (lower down) if the 'bar-subheader' class is applied.
+ * See [the header CSS docs](/docs/components/#subheader).
+ *
+ * @param {string=} align-title How to align the title. By default the title
+ * will be aligned the same as how the platform aligns its titles (iOS centers
+ * titles, Android aligns them left).
+ * Available: 'left', 'right', or 'center'. Defaults to the same as the platform.
+ * @param {boolean=} no-tap-scroll By default, the header bar will scroll the
+ * content to the top when tapped. Set no-tap-scroll to true to disable this
+ * behavior.
+ * Available: true or false. Defaults to false.
+ *
+ * @usage
+ * ```html
+ *
+ *
+ *
+ *
+ *
Title!
+ *
+ *
+ *
+ *
+ *
+ * Some content!
+ *
+ * ```
+ */
+.directive('ionHeaderBar', headerFooterBarDirective(true))
+
+/**
+ * @ngdoc directive
+ * @name ionFooterBar
+ * @module ionic
+ * @restrict E
+ *
+ * @description
+ * Adds a fixed footer bar below some content.
+ *
+ * Can also be a subfooter (higher up) if the 'bar-subfooter' class is applied.
+ * See [the footer CSS docs](/docs/components/#footer).
+ *
+ * Note: If you use ionFooterBar in combination with ng-if, the surrounding content
+ * will not align correctly. This will be fixed soon.
+ *
+ * @param {string=} align-title Where to align the title.
+ * Available: 'left', 'right', or 'center'. Defaults to 'center'.
+ *
+ * @usage
+ * ```html
+ *
+ * Some content!
+ *
+ *
+ *
+ *
+ *
+ *
Title!
+ *
+ *
+ *
+ *
+ * ```
+ */
+.directive('ionFooterBar', headerFooterBarDirective(false));
+
+function tapScrollToTopDirective() {
+ return ['$ionicScrollDelegate', function($ionicScrollDelegate) {
+ return {
+ restrict: 'E',
+ link: function($scope, $element, $attr) {
+ if ($attr.noTapScroll == 'true') {
+ return;
+ }
+ ionic.on('tap', onTap, $element[0]);
+ $scope.$on('$destroy', function() {
+ ionic.off('tap', onTap, $element[0]);
+ });
+
+ function onTap(e) {
+ var depth = 3;
+ var current = e.target;
+ //Don't scroll to top in certain cases
+ while (depth-- && current) {
+ if (current.classList.contains('button') ||
+ current.tagName.match(/input|textarea|select/i) ||
+ current.isContentEditable) {
+ return;
+ }
+ current = current.parentNode;
+ }
+ var touch = e.gesture && e.gesture.touches[0] || e.detail.touches[0];
+ var bounds = $element[0].getBoundingClientRect();
+ if (ionic.DomUtil.rectContains(
+ touch.pageX, touch.pageY,
+ bounds.left, bounds.top - 20,
+ bounds.left + bounds.width, bounds.top + bounds.height
+ )) {
+ $ionicScrollDelegate.scrollTop(true);
+ }
+ }
+ }
+ };
+ }];
+}
+
+function headerFooterBarDirective(isHeader) {
+ return ['$document', '$timeout',function($document, $timeout) {
+ return {
+ restrict: 'E',
+ controller: '$ionicHeaderBar',
+ compile: function(tElement, $attr) {
+ tElement.addClass(isHeader ? 'bar bar-header' : 'bar bar-footer');
+ // top style tabs? if so, remove bottom border for seamless display
+ $timeout(function() {
+ if (isHeader && $document[0].getElementsByClassName('tabs-top').length) tElement.addClass('has-tabs-top');
+ });
+
+ return { pre: prelink };
+ function prelink($scope, $element, $attr, ctrl) {
+ if (isHeader) {
+ $scope.$watch(function() { return $element[0].className; }, function(value) {
+ var isShown = value.indexOf('ng-hide') === -1;
+ var isSubheader = value.indexOf('bar-subheader') !== -1;
+ $scope.$hasHeader = isShown && !isSubheader;
+ $scope.$hasSubheader = isShown && isSubheader;
+ });
+ $scope.$on('$destroy', function() {
+ delete $scope.$hasHeader;
+ delete $scope.$hasSubheader;
+ });
+ ctrl.align();
+ $scope.$on('$ionicHeader.align', function() {
+ ionic.requestAnimationFrame(ctrl.align);
+ });
+
+ } else {
+ $scope.$watch(function() { return $element[0].className; }, function(value) {
+ var isShown = value.indexOf('ng-hide') === -1;
+ var isSubfooter = value.indexOf('bar-subfooter') !== -1;
+ $scope.$hasFooter = isShown && !isSubfooter;
+ $scope.$hasSubfooter = isShown && isSubfooter;
+ });
+ $scope.$on('$destroy', function() {
+ delete $scope.$hasFooter;
+ delete $scope.$hasSubfooter;
+ });
+ $scope.$watch('$hasTabs', function(val) {
+ $element.toggleClass('has-tabs', !!val);
+ });
+ }
+ }
+ }
+ };
+ }];
+}
+
+/**
+ * @ngdoc directive
+ * @name ionInfiniteScroll
+ * @module ionic
+ * @parent ionic.directive:ionContent, ionic.directive:ionScroll
+ * @restrict E
+ *
+ * @description
+ * The ionInfiniteScroll directive allows you to call a function whenever
+ * the user gets to the bottom of the page or near the bottom of the page.
+ *
+ * The expression you pass in for `on-infinite` is called when the user scrolls
+ * greater than `distance` away from the bottom of the content. Once `on-infinite`
+ * is done loading new data, it should broadcast the `scroll.infiniteScrollComplete`
+ * event from your controller (see below example).
+ *
+ * @param {expression} on-infinite What to call when the scroller reaches the
+ * bottom.
+ * @param {string=} distance The distance from the bottom that the scroll must
+ * reach to trigger the on-infinite expression. Default: 1%.
+ * @param {string=} icon The icon to show while loading. Default: 'ion-loading-d'.
+ *
+ * @usage
+ * ```html
+ *
+ *
+ * ....
+ * ....
+ *
+ *
+ *
+ *
+ *
+ * ```
+ * ```js
+ * function MyController($scope, $http) {
+ * $scope.items = [];
+ * $scope.loadMore = function() {
+ * $http.get('/more-items').success(function(items) {
+ * useItems(items);
+ * $scope.$broadcast('scroll.infiniteScrollComplete');
+ * });
+ * };
+ *
+ * $scope.$on('$stateChangeSuccess', function() {
+ * $scope.loadMore();
+ * });
+ * }
+ * ```
+ *
+ * An easy to way to stop infinite scroll once there is no more data to load
+ * is to use angular's `ng-if` directive:
+ *
+ * ```html
+ *
+ *
+ * ```
+ */
+IonicModule
+.directive('ionInfiniteScroll', ['$timeout', function($timeout) {
+ function calculateMaxValue(distance, maximum, isPercent) {
+ return isPercent ?
+ maximum * (1 - parseFloat(distance,10) / 100) :
+ maximum - parseFloat(distance, 10);
+ }
+ return {
+ restrict: 'E',
+ require: ['^$ionicScroll', 'ionInfiniteScroll'],
+ template: '',
+ scope: {
+ load: '&onInfinite'
+ },
+ controller: ['$scope', '$attrs', function($scope, $attrs) {
+ this.isLoading = false;
+ this.scrollView = null; //given by link function
+ this.getMaxScroll = function() {
+ var distance = ($attrs.distance || '2.5%').trim();
+ var isPercent = distance.indexOf('%') !== -1;
+ var maxValues = this.scrollView.getScrollMax();
+ return {
+ left: this.scrollView.options.scrollingX ?
+ calculateMaxValue(distance, maxValues.left, isPercent) :
+ -1,
+ top: this.scrollView.options.scrollingY ?
+ calculateMaxValue(distance, maxValues.top, isPercent) :
+ -1
+ };
+ };
+ }],
+ link: function($scope, $element, $attrs, ctrls) {
+ var scrollCtrl = ctrls[0];
+ var infiniteScrollCtrl = ctrls[1];
+ var scrollView = infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
+
+ $scope.icon = function() {
+ return angular.isDefined($attrs.icon) ? $attrs.icon : 'ion-loading-d';
+ };
+
+ var onInfinite = function() {
+ $element[0].classList.add('active');
+ infiniteScrollCtrl.isLoading = true;
+ $scope.load();
+ };
+
+ var finishInfiniteScroll = function() {
+ $element[0].classList.remove('active');
+ $timeout(function() {
+ scrollView.resize();
+ checkBounds();
+ }, 0, false);
+ infiniteScrollCtrl.isLoading = false;
+ };
+
+ $scope.$on('scroll.infiniteScrollComplete', function() {
+ finishInfiniteScroll();
+ });
+
+ $scope.$on('$destroy', function() {
+ if(scrollCtrl && scrollCtrl.$element)scrollCtrl.$element.off('scroll', checkBounds);
+ });
+
+ var checkBounds = ionic.animationFrameThrottle(checkInfiniteBounds);
+
+ //Check bounds on start, after scrollView is fully rendered
+ $timeout(checkBounds, 0, false);
+ scrollCtrl.$element.on('scroll', checkBounds);
+
+ function checkInfiniteBounds() {
+ if (infiniteScrollCtrl.isLoading) return;
+
+ var scrollValues = scrollView.getValues();
+ var maxScroll = infiniteScrollCtrl.getMaxScroll();
+
+ if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) ||
+ (maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) {
+ onInfinite();
+ }
+ }
+ }
+ };
+}]);
+
+var ITEM_TPL_CONTENT_ANCHOR =
+ '';
+var ITEM_TPL_CONTENT =
+ '';
+/**
+* @ngdoc directive
+* @name ionItem
+* @parent ionic.directive:ionList
+* @module ionic
+* @restrict E
+* Creates a list-item that can easily be swiped,
+* deleted, reordered, edited, and more.
+*
+* See {@link ionic.directive:ionList} for a complete example & explanation.
+*
+* Can be assigned any item class name. See the
+* [list CSS documentation](/docs/components/#list).
+*
+* @usage
+*
+* ```html
+*
+* Hello!
+*
+* Link to detail page
+*
+*
+* ```
+*/
+IonicModule
+.directive('ionItem', function() {
+ return {
+ restrict: 'E',
+ controller: ['$scope', '$element', function($scope, $element) {
+ this.$scope = $scope;
+ this.$element = $element;
+ }],
+ scope: true,
+ compile: function($element, $attrs) {
+ var isAnchor = angular.isDefined($attrs.href) ||
+ angular.isDefined($attrs.ngHref) ||
+ angular.isDefined($attrs.uiSref);
+ var isComplexItem = isAnchor ||
+ //Lame way of testing, but we have to know at compile what to do with the element
+ /ion-(delete|option|reorder)-button/i.test($element.html());
+
+ if (isComplexItem) {
+ var innerElement = jqLite(isAnchor ? ITEM_TPL_CONTENT_ANCHOR : ITEM_TPL_CONTENT);
+ innerElement.append($element.contents());
+
+ $element.append(innerElement);
+ $element.addClass('item item-complex');
+ } else {
+ $element.addClass('item');
+ }
+
+ return function link($scope, $element, $attrs) {
+ $scope.$href = function() {
+ return $attrs.href || $attrs.ngHref;
+ };
+ $scope.$target = function() {
+ return $attrs.target || '_self';
+ };
+ };
+ }
+ };
+});
+
+var ITEM_TPL_DELETE_BUTTON =
+ '
' +
+ '
';
+/**
+* @ngdoc directive
+* @name ionDeleteButton
+* @parent ionic.directive:ionItem
+* @module ionic
+* @restrict E
+* Creates a delete button inside a list item, that is visible when the
+* {@link ionic.directive:ionList ionList parent's} `show-delete` evaluates to true or
+* `$ionicListDelegate.showDelete(true)` is called.
+*
+* Takes any ionicon as a class.
+*
+* See {@link ionic.directive:ionList} for a complete example & explanation.
+*
+* @usage
+*
+* ```html
+*
+*
+*
+* Hello, list item!
+*
+*
+*
+* Show Delete?
+*
+* ```
+*/
+IonicModule
+.directive('ionDeleteButton', function() {
+ return {
+ restrict: 'E',
+ require: ['^ionItem', '^?ionList'],
+ //Run before anything else, so we can move it before other directives process
+ //its location (eg ngIf relies on the location of the directive in the dom)
+ priority: Number.MAX_VALUE,
+ compile: function($element, $attr) {
+ //Add the classes we need during the compile phase, so that they stay
+ //even if something else like ngIf removes the element and re-addss it
+ $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true);
+ return function($scope, $element, $attr, ctrls) {
+ var itemCtrl = ctrls[0];
+ var listCtrl = ctrls[1];
+ var container = jqLite(ITEM_TPL_DELETE_BUTTON);
+ container.append($element);
+ itemCtrl.$element.append(container).addClass('item-left-editable');
+
+ if (listCtrl && listCtrl.showDelete()) {
+ container.addClass('visible active');
+ }
+ };
+ }
+ };
+});
+
+
+IonicModule
+.directive('itemFloatingLabel', function() {
+ return {
+ restrict: 'C',
+ link: function(scope, element) {
+ var el = element[0];
+ var input = el.querySelector('input, textarea');
+ var inputLabel = el.querySelector('.input-label');
+
+ if ( !input || !inputLabel ) return;
+
+ var onInput = function() {
+ if ( input.value ) {
+ inputLabel.classList.add('has-input');
+ } else {
+ inputLabel.classList.remove('has-input');
+ }
+ };
+
+ input.addEventListener('input', onInput);
+
+ var ngModelCtrl = angular.element(input).controller('ngModel');
+ if ( ngModelCtrl ) {
+ ngModelCtrl.$render = function() {
+ input.value = ngModelCtrl.$viewValue || '';
+ onInput();
+ };
+ }
+
+ scope.$on('$destroy', function() {
+ input.removeEventListener('input', onInput);
+ });
+ }
+ };
+});
+
+var ITEM_TPL_OPTION_BUTTONS =
+ '
' +
+ '
';
+/**
+* @ngdoc directive
+* @name ionOptionButton
+* @parent ionic.directive:ionItem
+* @module ionic
+* @restrict E
+* Creates an option button inside a list item, that is visible when the item is swiped
+* to the left by the user. Swiped open option buttons can be hidden with
+* {@link ionic.service:$ionicListDelegate#closeOptionButtons $ionicListDelegate#closeOptionButtons}.
+*
+* Can be assigned any button class.
+*
+* See {@link ionic.directive:ionList} for a complete example & explanation.
+*
+* @usage
+*
+* ```html
+*
+*
+* I love kittens!
+* Share
+* Edit
+*
+*
+* ```
+*/
+IonicModule
+.directive('ionOptionButton', ['$compile', function($compile) {
+ function stopPropagation(e) {
+ e.stopPropagation();
+ }
+ return {
+ restrict: 'E',
+ require: '^ionItem',
+ priority: Number.MAX_VALUE,
+ compile: function($element, $attr) {
+ $attr.$set('class', ($attr['class'] || '') + ' button', true);
+ return function($scope, $element, $attr, itemCtrl) {
+ if (!itemCtrl.optionsContainer) {
+ itemCtrl.optionsContainer = jqLite(ITEM_TPL_OPTION_BUTTONS);
+ itemCtrl.$element.append(itemCtrl.optionsContainer);
+ }
+ itemCtrl.optionsContainer.append($element);
+
+ itemCtrl.$element.addClass('item-right-editable');
+
+ //Don't bubble click up to main .item
+ $element.on('click', stopPropagation);
+ };
+ }
+ };
+}]);
+
+var ITEM_TPL_REORDER_BUTTON =
+ '
' +
+ '
';
+
+/**
+* @ngdoc directive
+* @name ionReorderButton
+* @parent ionic.directive:ionItem
+* @module ionic
+* @restrict E
+* Creates a reorder button inside a list item, that is visible when the
+* {@link ionic.directive:ionList ionList parent's} `show-reorder` evaluates to true or
+* `$ionicListDelegate.showReorder(true)` is called.
+*
+* Can be dragged to reorder items in the list. Takes any ionicon class.
+*
+* Note: Reordering works best when used with `ng-repeat`. Be sure that all `ion-item` children of an `ion-list` are part of the same `ng-repeat` expression.
+*
+* When an item reorder is complete, the expression given in the `on-reorder` attribute is called. The `on-reorder` expression is given two locals that can be used: `$fromIndex` and `$toIndex`. See below for an example.
+*
+* Look at {@link ionic.directive:ionList} for more examples.
+*
+* @usage
+*
+* ```html
+*
+*
+* Item {{item}}
+*
+*
+*
+*
+* ```
+* ```js
+* function MyCtrl($scope) {
+* $scope.items = [1, 2, 3, 4];
+* $scope.moveItem = function(item, fromIndex, toIndex) {
+* //Move the item in the array
+* $scope.items.splice(fromIndex, 1);
+* $scope.items.splice(toIndex, 0, item);
+* };
+* }
+* ```
+*
+* @param {expression=} on-reorder Expression to call when an item is reordered.
+* Parameters given: $fromIndex, $toIndex.
+*/
+IonicModule
+.directive('ionReorderButton', ['$parse', function($parse) {
+ return {
+ restrict: 'E',
+ require: ['^ionItem', '^?ionList'],
+ priority: Number.MAX_VALUE,
+ compile: function($element, $attr) {
+ $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true);
+ $element[0].setAttribute('data-prevent-scroll', true);
+ return function($scope, $element, $attr, ctrls) {
+ var itemCtrl = ctrls[0];
+ var listCtrl = ctrls[1];
+ var onReorderFn = $parse($attr.onReorder);
+
+ $scope.$onReorder = function(oldIndex, newIndex) {
+ onReorderFn($scope, {
+ $fromIndex: oldIndex,
+ $toIndex: newIndex
+ });
+ };
+
+ // prevent clicks from bubbling up to the item
+ if(!$attr.ngClick && !$attr.onClick && !$attr.onclick){
+ $element[0].onclick = function(e){e.stopPropagation(); return false;};
+ }
+
+ var container = jqLite(ITEM_TPL_REORDER_BUTTON);
+ container.append($element);
+ itemCtrl.$element.append(container).addClass('item-right-editable');
+
+ if (listCtrl && listCtrl.showReorder()) {
+ container.addClass('visible active');
+ }
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name keyboardAttach
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * keyboard-attach is an attribute directive which will cause an element to float above
+ * the keyboard when the keyboard shows. Currently only supports the
+ * [ion-footer-bar]({{ page.versionHref }}/api/directive/ionFooterBar/) directive.
+ *
+ * ### Notes
+ * - This directive requires the
+ * [Ionic Keyboard Plugin](https://github.com/driftyco/ionic-plugins-keyboard).
+ * - On Android not in fullscreen mode, i.e. you have
+ * `` or no preference in your `config.xml` file,
+ * this directive is unnecessary since it is the default behavior.
+ * - On iOS, if there is an input in your footer, you will need to set
+ * `cordova.plugins.Keyboard.disableScroll(true)`.
+ *
+ * @usage
+ *
+ * ```html
+ *
+ *
Title!
+ *
+ * ```
+ */
+
+IonicModule
+.directive('keyboardAttach', function() {
+ return function(scope, element, attrs) {
+ ionic.on('native.keyboardshow', onShow, window);
+ ionic.on('native.keyboardhide', onHide, window);
+
+ //deprecated
+ ionic.on('native.showkeyboard', onShow, window);
+ ionic.on('native.hidekeyboard', onHide, window);
+
+
+ var scrollCtrl;
+
+ function onShow(e) {
+ if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
+ return;
+ }
+
+ //for testing
+ var keyboardHeight = e.keyboardHeight || e.detail.keyboardHeight;
+ element.css('bottom', keyboardHeight + "px");
+ scrollCtrl = element.controller('$ionicScroll');
+ if ( scrollCtrl ) {
+ scrollCtrl.scrollView.__container.style.bottom = keyboardHeight + keyboardAttachGetClientHeight(element[0]) + "px";
+ }
+ }
+
+ function onHide() {
+ if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
+ return;
+ }
+
+ element.css('bottom', '');
+ if ( scrollCtrl ) {
+ scrollCtrl.scrollView.__container.style.bottom = '';
+ }
+ }
+
+ scope.$on('$destroy', function() {
+ ionic.off('native.keyboardshow', onShow, window);
+ ionic.off('native.keyboardhide', onHide, window);
+
+ //deprecated
+ ionic.off('native.showkeyboard', onShow, window);
+ ionic.off('native.hidekeyboard', onHide, window);
+ });
+ };
+});
+
+function keyboardAttachGetClientHeight(element) {
+ return element.clientHeight;
+}
+
+/**
+* @ngdoc directive
+* @name ionList
+* @module ionic
+* @delegate ionic.service:$ionicListDelegate
+* @codepen JsHjf
+* @restrict E
+* @description
+* The List is a widely used interface element in almost any mobile app, and can include
+* content ranging from basic text all the way to buttons, toggles, icons, and thumbnails.
+*
+* Both the list, which contains items, and the list items themselves can be any HTML
+* element. The containing element requires the `list` class and each list item requires
+* the `item` class.
+*
+* However, using the ionList and ionItem directives make it easy to support various
+* interaction modes such as swipe to edit, drag to reorder, and removing items.
+*
+* Related: {@link ionic.directive:ionItem}, {@link ionic.directive:ionOptionButton}
+* {@link ionic.directive:ionReorderButton}, {@link ionic.directive:ionDeleteButton}, [`list CSS documentation`](/docs/components/#list).
+*
+* @usage
+*
+* Basic Usage:
+*
+* ```html
+*
+*
+* {% raw %}Hello, {{item}}!{% endraw %}
+*
+*
+* ```
+*
+* Advanced Usage: Thumbnails, Delete buttons, Reordering, Swiping
+*
+* ```html
+*
+*
+*
+* {% raw %}
+*
{{item.title}}
+*
{{item.description}}
{% endraw %}
+*
+* Share
+*
+*
+* Edit
+*
+*
+*
+*
+*
+*
+*
+*
+* ```
+*
+* @param {string=} delegate-handle The handle used to identify this list with
+* {@link ionic.service:$ionicListDelegate}.
+* @param type {string=} The type of list to use (list-inset or card)
+* @param show-delete {boolean=} Whether the delete buttons for the items in the list are
+* currently shown or hidden.
+* @param show-reorder {boolean=} Whether the reorder buttons for the items in the list are
+* currently shown or hidden.
+* @param can-swipe {boolean=} Whether the items in the list are allowed to be swiped to reveal
+* option buttons. Default: true.
+*/
+IonicModule
+.directive('ionList', [
+ '$timeout',
+function($timeout) {
+ return {
+ restrict: 'E',
+ require: ['ionList', '^?$ionicScroll'],
+ controller: '$ionicList',
+ compile: function($element, $attr) {
+ var listEl = jqLite('
')
+ .append( $element.contents() )
+ .addClass($attr.type);
+ $element.append(listEl);
+
+ return function($scope, $element, $attrs, ctrls) {
+ var listCtrl = ctrls[0];
+ var scrollCtrl = ctrls[1];
+
+ //Wait for child elements to render...
+ $timeout(init);
+
+ function init() {
+ var listView = listCtrl.listView = new ionic.views.ListView({
+ el: $element[0],
+ listEl: $element.children()[0],
+ scrollEl: scrollCtrl && scrollCtrl.element,
+ scrollView: scrollCtrl && scrollCtrl.scrollView,
+ onReorder: function(el, oldIndex, newIndex) {
+ var itemScope = jqLite(el).scope();
+ if (itemScope && itemScope.$onReorder) {
+ //Make sure onReorder is called in apply cycle,
+ //but also make sure it has no conflicts by doing
+ //$evalAsync
+ $timeout(function() {
+ itemScope.$onReorder(oldIndex, newIndex);
+ });
+ }
+ },
+ canSwipe: function() {
+ return listCtrl.canSwipeItems();
+ }
+ });
+
+ $scope.$on('$destroy', function() {
+ if(listView) {
+ listView.deregister && listView.deregister();
+ listView = null;
+ }
+ });
+
+ if (isDefined($attr.canSwipe)) {
+ $scope.$watch('!!(' + $attr.canSwipe + ')', function(value) {
+ listCtrl.canSwipeItems(value);
+ });
+ }
+ if (isDefined($attr.showDelete)) {
+ $scope.$watch('!!(' + $attr.showDelete + ')', function(value) {
+ listCtrl.showDelete(value);
+ });
+ }
+ if (isDefined($attr.showReorder)) {
+ $scope.$watch('!!(' + $attr.showReorder + ')', function(value) {
+ listCtrl.showReorder(value);
+ });
+ }
+
+ $scope.$watch(function() {
+ return listCtrl.showDelete();
+ }, function(isShown, wasShown) {
+ //Only use isShown=false if it was already shown
+ if (!isShown && !wasShown) { return; }
+
+ if (isShown) listCtrl.closeOptionButtons();
+ listCtrl.canSwipeItems(!isShown);
+
+ $element.children().toggleClass('list-left-editing', isShown);
+ $element.toggleClass('disable-pointer-events', isShown);
+
+ var deleteButton = jqLite($element[0].getElementsByClassName('item-delete'));
+ setButtonShown(deleteButton, listCtrl.showDelete);
+ });
+
+ $scope.$watch(function() {
+ return listCtrl.showReorder();
+ }, function(isShown, wasShown) {
+ //Only use isShown=false if it was already shown
+ if (!isShown && !wasShown) { return; }
+
+ if (isShown) listCtrl.closeOptionButtons();
+ listCtrl.canSwipeItems(!isShown);
+
+ $element.children().toggleClass('list-right-editing', isShown);
+ $element.toggleClass('disable-pointer-events', isShown);
+
+ var reorderButton = jqLite($element[0].getElementsByClassName('item-reorder'));
+ setButtonShown(reorderButton, listCtrl.showReorder);
+ });
+
+ function setButtonShown(el, shown) {
+ shown() && el.addClass('visible') || el.removeClass('active');
+ ionic.requestAnimationFrame(function() {
+ shown() && el.addClass('active') || el.removeClass('visible');
+ });
+ }
+ }
+
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name menuClose
+ * @module ionic
+ * @restrict AC
+ *
+ * @description
+ * `menu-close` is an attribute directive that closes a currently opened side menu.
+ * Note that by default, navigation transitions will not animate between views when
+ * the menu is open. Additionally, this directive will reset the entering view's
+ * history stack, making the new page the root of the history stack. This is done
+ * to replicate the user experience seen in most side menu implementations, which is
+ * to not show the back button at the root of the stack and show only the
+ * menu button. We recommend that you also use the `enable-menu-with-back-views="false"`
+ * {@link ionic.directive:ionSideMenus} attribute when using the menuClose directive.
+ *
+ * @usage
+ * Below is an example of a link within a side menu. Tapping this link would
+ * automatically close the currently opened menu.
+ *
+ * ```html
+ * Home
+ * ```
+ */
+IonicModule
+.directive('menuClose', ['$ionicHistory', function($ionicHistory) {
+ return {
+ restrict: 'AC',
+ link: function($scope, $element) {
+ $element.bind('click', function() {
+ var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
+ if (sideMenuCtrl) {
+ $ionicHistory.nextViewOptions({
+ historyRoot: true,
+ disableAnimate: true,
+ expire: 300
+ });
+ sideMenuCtrl.close();
+ }
+ });
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name menuToggle
+ * @module ionic
+ * @restrict AC
+ *
+ * @description
+ * Toggle a side menu on the given side.
+ *
+ * @usage
+ * Below is an example of a link within a nav bar. Tapping this button
+ * would open the given side menu, and tapping it again would close it.
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ */
+IonicModule
+.directive('menuToggle', function() {
+ return {
+ restrict: 'AC',
+ link: function($scope, $element, $attr) {
+ $scope.$on('$ionicView.beforeEnter', function(ev, viewData) {
+ if (viewData.enableBack) {
+ var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
+ if (!sideMenuCtrl.enableMenuWithBackViews()) {
+ $element.addClass('hide');
+ }
+ } else {
+ $element.removeClass('hide');
+ }
+ });
+
+ $element.bind('click', function() {
+ var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
+ sideMenuCtrl && sideMenuCtrl.toggle($attr.menuToggle);
+ });
+ }
+ };
+});
+
+/*
+ * We don't document the ionModal directive, we instead document
+ * the $ionicModal service
+ */
+IonicModule
+.directive('ionModal', [function() {
+ return {
+ restrict: 'E',
+ transclude: true,
+ replace: true,
+ controller: [function(){}],
+ template: '
' +
+ '' +
+ '
'
+ };
+}]);
+
+IonicModule
+.directive('ionModalView', function() {
+ return {
+ restrict: 'E',
+ compile: function(element, attr) {
+ element.addClass('modal');
+ }
+ };
+});
+
+/**
+ * @ngdoc directive
+ * @name ionNavBackButton
+ * @module ionic
+ * @restrict E
+ * @parent ionNavBar
+ * @description
+ * Creates a back button inside an {@link ionic.directive:ionNavBar}.
+ *
+ * The back button will appear when the user is able to go back in the current navigation stack. By
+ * default, the markup of the back button is automatically built using platform-appropriate defaults
+ * (iOS back button icon on iOS and Android icon on Android).
+ *
+ * Additionally, the button is automatically set to `$ionicGoBack()` on click/tap. By default, the
+ * app will navigate back one view when the back button is clicked. More advanced behavior is also
+ * possible, as outlined below.
+ *
+ * @usage
+ *
+ * Recommended markup for default settings:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * With custom inner markup, and automatically adds a default click action:
+ *
+ * ```html
+ *
+ *
+ * Back
+ *
+ *
+ * ```
+ *
+ * With custom inner markup and custom click action, using {@link ionic.service:$ionicNavBarDelegate}:
+ *
+ * ```html
+ *
+ *
+ * Back
+ *
+ *
+ * ```
+ * ```js
+ * function MyCtrl($scope, $ionicNavBarDelegate) {
+ * $scope.myGoBack = function() {
+ * $ionicNavBarDelegate.back();
+ * };
+ * }
+ * ```
+ */
+IonicModule
+.directive('ionNavBackButton', ['$ionicConfig', '$document', function($ionicConfig, $document) {
+ return {
+ restrict: 'E',
+ require: '^ionNavBar',
+ compile: function(tElement, tAttrs) {
+
+ // clone the back button, but as a
+ var buttonEle = $document[0].createElement('button');
+ for (var n in tAttrs.$attr) {
+ buttonEle.setAttribute(tAttrs.$attr[n], tAttrs[n]);
+ }
+
+ if (!tAttrs.ngClick) {
+ buttonEle.setAttribute('ng-click', '$ionicGoBack($event)');
+ }
+
+ buttonEle.className = 'button back-button hide buttons ' + (tElement.attr('class') || '');
+ buttonEle.innerHTML = tElement.html() || '';
+
+ var childNode;
+ var hasIcon = hasIconClass(tElement[0]);
+ var hasInnerText;
+ var hasButtonText;
+ var hasPreviousTitle;
+
+ for (var x = 0; x < tElement[0].childNodes.length; x++) {
+ childNode = tElement[0].childNodes[x];
+ if (childNode.nodeType === 1) {
+ if (hasIconClass(childNode)) {
+ hasIcon = true;
+ } else if (childNode.classList.contains('default-title')) {
+ hasButtonText = true;
+ } else if (childNode.classList.contains('previous-title')) {
+ hasPreviousTitle = true;
+ }
+ } else if (!hasInnerText && childNode.nodeType === 3) {
+ hasInnerText = !!childNode.nodeValue.trim();
+ }
+ }
+
+ function hasIconClass(ele) {
+ return /ion-|icon/.test(ele.className);
+ }
+
+ var defaultIcon = $ionicConfig.backButton.icon();
+ if (!hasIcon && defaultIcon && defaultIcon !== 'none') {
+ buttonEle.innerHTML = ' ' + buttonEle.innerHTML;
+ buttonEle.className += ' button-clear';
+ }
+
+ if (!hasInnerText) {
+ var buttonTextEle = $document[0].createElement('span');
+ buttonTextEle.className = 'back-text';
+
+ if (!hasButtonText && $ionicConfig.backButton.text()) {
+ buttonTextEle.innerHTML += '' + $ionicConfig.backButton.text() + '';
+ }
+ if (!hasPreviousTitle && $ionicConfig.backButton.previousTitleText()) {
+ buttonTextEle.innerHTML += '';
+ }
+ buttonEle.appendChild(buttonTextEle);
+
+ }
+
+ tElement.attr('class', 'hide');
+ tElement.empty();
+
+ return {
+ pre: function($scope, $element, $attr, navBarCtrl) {
+ // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
+ navBarCtrl.navElement('backButton', buttonEle.outerHTML);
+ buttonEle = null;
+ }
+ };
+ }
+ };
+}]);
+
+
+/**
+ * @ngdoc directive
+ * @name ionNavBar
+ * @module ionic
+ * @delegate ionic.service:$ionicNavBarDelegate
+ * @restrict E
+ *
+ * @description
+ * If we have an {@link ionic.directive:ionNavView} directive, we can also create an
+ * ``, which will create a topbar that updates as the application state changes.
+ *
+ * We can add a back button by putting an {@link ionic.directive:ionNavBackButton} inside.
+ *
+ * We can add buttons depending on the currently visible view using
+ * {@link ionic.directive:ionNavButtons}.
+ *
+ * Note that the ion-nav-bar element will only work correctly if your content has an
+ * ionView around it.
+ *
+ * @usage
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Hello!
+ *
+ *
+ *
+ * ```
+ *
+ * @param {string=} delegate-handle The handle used to identify this navBar
+ * with {@link ionic.service:$ionicNavBarDelegate}.
+ * @param align-title {string=} Where to align the title of the navbar.
+ * Available: 'left', 'right', 'center'. Defaults to 'center'.
+ * @param {boolean=} no-tap-scroll By default, the navbar will scroll the content
+ * to the top when tapped. Set no-tap-scroll to true to disable this behavior.
+ *
+ *
+ *
+ * ### Alternative Usage
+ *
+ * Alternatively, you may put ion-nav-bar inside of each individual view's ion-view element.
+ * This will allow you to have the whole navbar, not just its contents, transition every view change.
+ *
+ * This is similar to using a header bar inside your ion-view, except it will have all the power of a navbar.
+ *
+ * If you do this, simply put nav buttons inside the navbar itself; do not use ``.
+ *
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ */
+IonicModule
+.directive('ionNavBar', function() {
+ return {
+ restrict: 'E',
+ controller: '$ionicNavBar',
+ scope: true,
+ link: function($scope, $element, $attr, ctrl) {
+ ctrl.init();
+ }
+ };
+});
+
+
+/**
+ * @ngdoc directive
+ * @name ionNavButtons
+ * @module ionic
+ * @restrict E
+ * @parent ionNavView
+ *
+ * @description
+ * Use nav buttons to set the buttons on your {@link ionic.directive:ionNavBar}
+ * from within an {@link ionic.directive:ionView}. This gives each
+ * view template the ability to specify which buttons should show in the nav bar,
+ * overriding any default buttons already placed in the nav bar.
+ *
+ * Any buttons you declare will be positioned on the navbar's corresponding side. Primary
+ * buttons generally map to the left side of the header, and secondary buttons are
+ * generally on the right side. However, their exact locations are platform-specific.
+ * For example, in iOS, the primary buttons are on the far left of the header, and
+ * secondary buttons are on the far right, with the header title centered between them.
+ * For Android, however, both groups of buttons are on the far right of the header,
+ * with the header title aligned left.
+ *
+ * We recommend always using `primary` and `secondary`, so the buttons correctly map
+ * to the side familiar to users of each platform. However, in cases where buttons should
+ * always be on an exact side, both `left` and `right` sides are still available. For
+ * example, a toggle button for a left side menu should be on the left side; in this case,
+ * we'd recommend using `side="left"`, so it's always on the left, no matter the platform.
+ *
+ * Note that `ion-nav-buttons` must be immediate descendants of the `ion-view` or
+ * `ion-nav-bar` element (basically, don't wrap it in another div).
+ *
+ * @usage
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Some super content here!
+ *
+ *
+ *
+ * ```
+ *
+ * @param {string} side The side to place the buttons in the
+ * {@link ionic.directive:ionNavBar}. Available sides: `primary`, `secondary`, `left`, and `right`.
+ */
+IonicModule
+.directive('ionNavButtons', ['$document', function($document) {
+ return {
+ require: '^ionNavBar',
+ restrict: 'E',
+ compile: function(tElement, tAttrs) {
+ var side = 'left';
+
+ if (/^primary|secondary|right$/i.test(tAttrs.side || '')) {
+ side = tAttrs.side.toLowerCase();
+ }
+
+ var spanEle = $document[0].createElement('span');
+ spanEle.className = side + '-buttons';
+ spanEle.innerHTML = tElement.html();
+
+ var navElementType = side + 'Buttons';
+
+ tElement.attr('class', 'hide');
+ tElement.empty();
+
+ return {
+ pre: function($scope, $element, $attrs, navBarCtrl) {
+ // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
+
+ var parentViewCtrl = $element.parent().data('$ionViewController');
+ if (parentViewCtrl) {
+ // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
+ parentViewCtrl.navElement(navElementType, spanEle.outerHTML);
+
+ } else {
+ // these are buttons for all views that do not have their own ion-nav-buttons
+ navBarCtrl.navElement(navElementType, spanEle.outerHTML);
+ }
+
+ spanEle = null;
+ }
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name navDirection
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * The direction which the nav view transition should animate. Available options
+ * are: `forward`, `back`, `enter`, `exit`, `swap`.
+ *
+ * @usage
+ *
+ * ```html
+ * Home
+ * ```
+ */
+IonicModule
+.directive('navDirection', ['$ionicViewSwitcher', function($ionicViewSwitcher) {
+ return {
+ restrict: 'A',
+ priority: 1000,
+ link: function($scope, $element, $attr) {
+ $element.bind('click', function() {
+ $ionicViewSwitcher.nextDirection($attr.navDirection);
+ });
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionNavTitle
+ * @module ionic
+ * @restrict E
+ * @parent ionNavView
+ *
+ * @description
+ *
+ * The nav title directive replaces an {@link ionic.directive:ionNavBar} title text with
+ * custom HTML from within an {@link ionic.directive:ionView} template. This gives each
+ * view the ability to specify its own custom title element, such as an image or any HTML,
+ * rather than being text-only. Alternatively, text-only titles can be updated using the
+ * `view-title` {@link ionic.directive:ionView} attribute.
+ *
+ * Note that `ion-nav-title` must be an immediate descendant of the `ion-view` or
+ * `ion-nav-bar` element (basically don't wrap it in another div).
+ *
+ * @usage
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Some super content here!
+ *
+ *
+ *
+ * ```
+ *
+ */
+IonicModule
+.directive('ionNavTitle', ['$document', function($document) {
+ return {
+ require: '^ionNavBar',
+ restrict: 'E',
+ compile: function(tElement, tAttrs) {
+ var navElementType = 'title';
+ var spanEle = $document[0].createElement('span');
+ for (var n in tAttrs.$attr) {
+ spanEle.setAttribute(tAttrs.$attr[n], tAttrs[n]);
+ }
+ spanEle.classList.add('nav-bar-title');
+ spanEle.innerHTML = tElement.html();
+
+ tElement.attr('class', 'hide');
+ tElement.empty();
+
+ return {
+ pre: function($scope, $element, $attrs, navBarCtrl) {
+ // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
+
+ var parentViewCtrl = $element.parent().data('$ionViewController');
+ if (parentViewCtrl) {
+ // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
+ parentViewCtrl.navElement(navElementType, spanEle.outerHTML);
+
+ } else {
+ // these are buttons for all views that do not have their own ion-nav-buttons
+ navBarCtrl.navElement(navElementType, spanEle.outerHTML);
+ }
+
+ spanEle = null;
+ }
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name navTransition
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * The transition type which the nav view transition should use when it animates.
+ * Current, options are `ios`, `android`, and `none`. More options coming soon.
+ *
+ * @usage
+ *
+ * ```html
+ * Home
+ * ```
+ */
+IonicModule
+.directive('navTransition', ['$ionicViewSwitcher', function($ionicViewSwitcher) {
+ return {
+ restrict: 'A',
+ priority: 1000,
+ link: function($scope, $element, $attr) {
+ $element.bind('click', function() {
+ $ionicViewSwitcher.nextTransition($attr.navTransition);
+ });
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionNavView
+ * @module ionic
+ * @restrict E
+ * @codepen odqCz
+ *
+ * @description
+ * As a user navigates throughout your app, Ionic is able to keep track of their
+ * navigation history. By knowing their history, transitions between views
+ * correctly enter and exit using the platform's transition style. An additional
+ * benefit to Ionic's navigation system is its ability to manage multiple
+ * histories. For example, each tab can have it's own navigation history stack.
+ *
+ * Ionic uses the AngularUI Router module so app interfaces can be organized
+ * into various "states". Like Angular's core $route service, URLs can be used
+ * to control the views. However, the AngularUI Router provides a more powerful
+ * state manager in that states are bound to named, nested, and parallel views,
+ * allowing more than one template to be rendered on the same page.
+ * Additionally, each state is not required to be bound to a URL, and data can
+ * be pushed to each state which allows much flexibility.
+ *
+ * The ionNavView directive is used to render templates in your application. Each template
+ * is part of a state. States are usually mapped to a url, and are defined programatically
+ * using angular-ui-router (see [their docs](https://github.com/angular-ui/ui-router/wiki),
+ * and remember to replace ui-view with ion-nav-view in examples).
+ *
+ * @usage
+ * In this example, we will create a navigation view that contains our different states for the app.
+ *
+ * To do this, in our markup we use ionNavView top level directive. To display a header bar we use
+ * the {@link ionic.directive:ionNavBar} directive that updates as we navigate through the
+ * navigation stack.
+ *
+ * Next, we need to setup our states that will be rendered.
+ *
+ * ```js
+ * var app = angular.module('myApp', ['ionic']);
+ * app.config(function($stateProvider) {
+ * $stateProvider
+ * .state('index', {
+ * url: '/',
+ * templateUrl: 'home.html'
+ * })
+ * .state('music', {
+ * url: '/music',
+ * templateUrl: 'music.html'
+ * });
+ * });
+ * ```
+ * Then on app start, $stateProvider will look at the url, see it matches the index state,
+ * and then try to load home.html into the ``.
+ *
+ * Pages are loaded by the URLs given. One simple way to create templates in Angular is to put
+ * them directly into your HTML file and use the `
+ * ```
+ *
+ * This is good to do because the template will be cached for very fast loading, instead of
+ * having to fetch them from the network.
+ *
+ ## Caching
+ *
+ * By default, views are cached to improve performance. When a view is navigated away from, its
+ * element is left in the DOM, and its scope is disconnected from the `$watch` cycle. When
+ * navigating to a view that is already cached, its scope is then reconnected, and the existing
+ * element that was left in the DOM becomes the active view. This also allows for the scroll
+ * position of previous views to be maintained.
+ *
+ * Caching can be disabled and enabled in multiple ways. By default, Ionic will cache a maximum of
+ * 10 views, and not only can this be configured, but apps can also explicitly state which views
+ * should and should not be cached.
+ *
+ * Note that because we are caching these views, *we aren’t destroying scopes*. Instead, scopes
+ * are being disconnected from the watch cycle. Because scopes are not being destroyed and
+ * recreated, controllers are not loading again on a subsequent viewing. If the app/controller
+ * needs to know when a view has entered or has left, then view events emitted from the
+ * {@link ionic.directive:ionView} scope, such as `$ionicView.enter`, may be useful
+ *
+ * #### Disable cache globally
+ *
+ * The {@link ionic.provider:$ionicConfigProvider} can be used to set the maximum allowable views
+ * which can be cached, but this can also be use to disable all caching by setting it to 0.
+ *
+ * ```js
+ * $ionicConfigProvider.views.maxCache(0);
+ * ```
+ *
+ * #### Disable cache within state provider
+ *
+ * ```js
+ * $stateProvider.state('myState', {
+ * cache: false,
+ * url : '/myUrl',
+ * templateUrl : 'my-template.html'
+ * })
+ * ```
+ *
+ * #### Disable cache with an attribute
+ *
+ * ```html
+ *
+ * ...
+ *
+ * ```
+ *
+ *
+ * ## AngularUI Router
+ *
+ * Please visit [AngularUI Router's docs](https://github.com/angular-ui/ui-router/wiki) for
+ * more info. Below is a great video by the AngularUI Router team that may help to explain
+ * how it all works:
+ *
+ *
+ *
+ * @param {string=} name A view name. The name should be unique amongst the other views in the
+ * same state. You can have views of the same name that live in different states. For more
+ * information, see ui-router's
+ * [ui-view documentation](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.directive:ui-view).
+ */
+IonicModule
+.directive('ionNavView', [
+ '$state',
+ '$ionicConfig',
+function($state, $ionicConfig) {
+ // IONIC's fork of Angular UI Router, v0.2.10
+ // the navView handles registering views in the history and how to transition between them
+ return {
+ restrict: 'E',
+ terminal: true,
+ priority: 2000,
+ transclude: true,
+ controller: '$ionicNavView',
+ compile: function(tElement, tAttrs, transclude) {
+
+ // a nav view element is a container for numerous views
+ tElement.addClass('view-container');
+ ionic.DomUtil.cachedAttr(tElement, 'nav-view-transition', $ionicConfig.views.transition());
+
+ return function($scope, $element, $attr, navViewCtrl) {
+ var latestLocals;
+
+ // Put in the compiled initial view
+ transclude($scope, function(clone) {
+ $element.append(clone);
+ });
+
+ var viewData = navViewCtrl.init();
+
+ // listen for $stateChangeSuccess
+ $scope.$on('$stateChangeSuccess', function() {
+ updateView(false);
+ });
+ $scope.$on('$viewContentLoading', function() {
+ updateView(false);
+ });
+
+ // initial load, ready go
+ updateView(true);
+
+
+ function updateView(firstTime) {
+ // get the current local according to the $state
+ var viewLocals = $state.$current && $state.$current.locals[viewData.name];
+
+ // do not update THIS nav-view if its is not the container for the given state
+ // if the viewLocals are the same as THIS latestLocals, then nothing to do
+ if (!viewLocals || (!firstTime && viewLocals === latestLocals)) return;
+
+ // update the latestLocals
+ latestLocals = viewLocals;
+ viewData.state = viewLocals.$$state;
+
+ // register, update and transition to the new view
+ navViewCtrl.register(viewLocals);
+ }
+
+ };
+ }
+ };
+}]);
+
+IonicModule
+
+.config(['$provide', function($provide) {
+ $provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
+ // drop the default ngClick directive
+ $delegate.shift();
+ return $delegate;
+ }]);
+}])
+
+/**
+ * @private
+ */
+.factory('$ionicNgClick', ['$parse', function($parse) {
+ return function(scope, element, clickExpr) {
+ var clickHandler = angular.isFunction(clickExpr) ?
+ clickExpr :
+ $parse(clickExpr);
+
+ element.on('click', function(event) {
+ scope.$apply(function() {
+ clickHandler(scope, {$event: (event)});
+ });
+ });
+
+ // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
+ // something else nearby.
+ element.onclick = function(event) { };
+ };
+}])
+
+.directive('ngClick', ['$ionicNgClick', function($ionicNgClick) {
+ return function(scope, element, attr) {
+ $ionicNgClick(scope, element, attr.ngClick);
+ };
+}])
+
+.directive('ionStopEvent', function() {
+ return {
+ restrict: 'A',
+ link: function(scope, element, attr) {
+ element.bind(attr.ionStopEvent, eventStopPropagation);
+ }
+ };
+});
+function eventStopPropagation(e) {
+ e.stopPropagation();
+}
+
+
+/**
+ * @ngdoc directive
+ * @name ionPane
+ * @module ionic
+ * @restrict E
+ *
+ * @description A simple container that fits content, with no side effects. Adds the 'pane' class to the element.
+ */
+IonicModule
+.directive('ionPane', function() {
+ return {
+ restrict: 'E',
+ link: function(scope, element, attr) {
+ element.addClass('pane');
+ }
+ };
+});
+
+/*
+ * We don't document the ionPopover directive, we instead document
+ * the $ionicPopover service
+ */
+IonicModule
+.directive('ionPopover', [function() {
+ return {
+ restrict: 'E',
+ transclude: true,
+ replace: true,
+ controller: [function(){}],
+ template: '
' +
+ '' +
+ '
'
+ };
+}]);
+
+IonicModule
+.directive('ionPopoverView', function() {
+ return {
+ restrict: 'E',
+ compile: function(element) {
+ element.append( angular.element('') );
+ element.addClass('popover');
+ }
+ };
+});
+
+/**
+ * @ngdoc directive
+ * @name ionRadio
+ * @module ionic
+ * @restrict E
+ * @codepen saoBG
+ * @description
+ * The radio directive is no different than the HTML radio input, except it's styled differently.
+ *
+ * Radio behaves like any [AngularJS radio](http://docs.angularjs.org/api/ng/input/input[radio]).
+ *
+ * @usage
+ * ```html
+ * Choose A
+ * Choose B
+ * Choose C
+ * ```
+ *
+ * @param {string=} name The name of the radio input.
+ * @param {expression=} value The value of the radio input.
+ * @param {boolean=} disabled The state of the radio input.
+ * @param {string=} icon The icon to use when the radio input is selected.
+ * @param {expression=} ng-value Angular equivalent of the value attribute.
+ * @param {expression=} ng-model The angular model for the radio input.
+ * @param {boolean=} ng-disabled Angular equivalent of the disabled attribute.
+ * @param {expression=} ng-change Triggers given expression when radio input's model changes
+ */
+IonicModule
+.directive('ionRadio', function() {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: '?ngModel',
+ transclude: true,
+ template:
+ '',
+
+ compile: function(element, attr) {
+ if(attr.icon) element.children().eq(2).removeClass('ion-checkmark').addClass(attr.icon);
+ var input = element.find('input');
+ forEach({
+ 'name': attr.name,
+ 'value': attr.value,
+ 'disabled': attr.disabled,
+ 'ng-value': attr.ngValue,
+ 'ng-model': attr.ngModel,
+ 'ng-disabled': attr.ngDisabled,
+ 'ng-change': attr.ngChange
+ }, function(value, name) {
+ if (isDefined(value)) {
+ input.attr(name, value);
+ }
+ });
+
+ return function(scope, element, attr) {
+ scope.getValue = function() {
+ return scope.ngValue || attr.value;
+ };
+ };
+ }
+ };
+});
+
+
+/**
+ * @ngdoc directive
+ * @name ionRefresher
+ * @module ionic
+ * @restrict E
+ * @parent ionic.directive:ionContent, ionic.directive:ionScroll
+ * @description
+ * Allows you to add pull-to-refresh to a scrollView.
+ *
+ * Place it as the first child of your {@link ionic.directive:ionContent} or
+ * {@link ionic.directive:ionScroll} element.
+ *
+ * When refreshing is complete, $broadcast the 'scroll.refreshComplete' event
+ * from your controller.
+ *
+ * @usage
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ * ```js
+ * angular.module('testApp', ['ionic'])
+ * .controller('MyController', function($scope, $http) {
+ * $scope.items = [1,2,3];
+ * $scope.doRefresh = function() {
+ * $http.get('/new-items')
+ * .success(function(newItems) {
+ * $scope.items = newItems;
+ * })
+ * .finally(function() {
+ * // Stop the ion-refresher from spinning
+ * $scope.$broadcast('scroll.refreshComplete');
+ * });
+ * };
+ * });
+ * ```
+ *
+ * @param {expression=} on-refresh Called when the user pulls down enough and lets go
+ * of the refresher.
+ * @param {expression=} on-pulling Called when the user starts to pull down
+ * on the refresher.
+ * @param {string=} pulling-icon The icon to display while the user is pulling down.
+ * Default: 'ion-arrow-down-c'.
+ * @param {string=} pulling-text The text to display while the user is pulling down.
+ * @param {string=} refreshing-icon The icon to display after user lets go of the
+ * refresher.
+ * @param {string=} refreshing-text The text to display after the user lets go of
+ * the refresher.
+ * @param {boolean=} disable-pulling-rotation Disables the rotation animation of the pulling
+ * icon when it reaches its activated threshold. To be used with a custom `pulling-icon`.
+ *
+ */
+IonicModule
+.directive('ionRefresher', ['$ionicBind', function($ionicBind) {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: '^$ionicScroll',
+ template:
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '' +
+ '
' +
+ '' +
+ '
' +
+ '
',
+ compile: function($element, $attrs) {
+ if (angular.isUndefined($attrs.pullingIcon)) {
+ $attrs.$set('pullingIcon', 'ion-ios7-arrow-down');
+ }
+ if (angular.isUndefined($attrs.refreshingIcon)) {
+ $attrs.$set('refreshingIcon', 'ion-loading-d');
+ }
+ return function($scope, $element, $attrs, scrollCtrl) {
+ $ionicBind($scope, $attrs, {
+ pullingIcon: '@',
+ pullingText: '@',
+ refreshingIcon: '@',
+ refreshingText: '@',
+ disablePullingRotation: '@',
+ $onRefresh: '&onRefresh',
+ $onPulling: '&onPulling'
+ });
+
+ scrollCtrl._setRefresher($scope, $element[0]);
+ $scope.$on('scroll.refreshComplete', function() {
+ $scope.$evalAsync(function() {
+ scrollCtrl.scrollView.finishPullToRefresh();
+ });
+ });
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionScroll
+ * @module ionic
+ * @delegate ionic.service:$ionicScrollDelegate
+ * @codepen mwFuh
+ * @restrict E
+ *
+ * @description
+ * Creates a scrollable container for all content inside.
+ *
+ * @usage
+ *
+ * Basic usage:
+ *
+ * ```html
+ *
+ *
+ *
+ * ```
+ *
+ * Note that it's important to set the height of the scroll box as well as the height of the inner
+ * content to enable scrolling. This makes it possible to have full control over scrollable areas.
+ *
+ * If you'd just like to have a center content scrolling area, use {@link ionic.directive:ionContent} instead.
+ *
+ * @param {string=} delegate-handle The handle used to identify this scrollView
+ * with {@link ionic.service:$ionicScrollDelegate}.
+ * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'.
+ * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true.
+ * @param {boolean=} paging Whether to scroll with paging.
+ * @param {expression=} on-refresh Called on pull-to-refresh, triggered by an {@link ionic.directive:ionRefresher}.
+ * @param {expression=} on-scroll Called whenever the user scrolls.
+ * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true.
+ * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true.
+ * @param {boolean=} zooming Whether to support pinch-to-zoom
+ * @param {integer=} min-zoom The smallest zoom amount allowed (default is 0.5)
+ * @param {integer=} max-zoom The largest zoom amount allowed (default is 3)
+ * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges
+ * of the content. Defaults to true on iOS, false on Android.
+ */
+IonicModule
+.directive('ionScroll', [
+ '$timeout',
+ '$controller',
+ '$ionicBind',
+function($timeout, $controller, $ionicBind) {
+ return {
+ restrict: 'E',
+ scope: true,
+ controller: function() {},
+ compile: function(element, attr) {
+ element.addClass('scroll-view ionic-scroll');
+
+ //We cannot transclude here because it breaks element.data() inheritance on compile
+ var innerElement = jqLite('');
+ innerElement.append(element.contents());
+ element.append(innerElement);
+
+ return { pre: prelink };
+ function prelink($scope, $element, $attr) {
+ var scrollView, scrollCtrl;
+
+ $ionicBind($scope, $attr, {
+ direction: '@',
+ paging: '@',
+ $onScroll: '&onScroll',
+ scroll: '@',
+ scrollbarX: '@',
+ scrollbarY: '@',
+ zooming: '@',
+ minZoom: '@',
+ maxZoom: '@'
+ });
+ $scope.direction = $scope.direction || 'y';
+
+ if (angular.isDefined($attr.padding)) {
+ $scope.$watch($attr.padding, function(newVal) {
+ innerElement.toggleClass('padding', !!newVal);
+ });
+ }
+ if($scope.$eval($scope.paging) === true) {
+ innerElement.addClass('scroll-paging');
+ }
+
+ if(!$scope.direction) { $scope.direction = 'y'; }
+ var isPaging = $scope.$eval($scope.paging) === true;
+
+ var scrollViewOptions= {
+ el: $element[0],
+ delegateHandle: $attr.delegateHandle,
+ locking: ($attr.locking || 'true') === 'true',
+ bouncing: $scope.$eval($attr.hasBouncing),
+ paging: isPaging,
+ scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
+ scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
+ scrollingX: $scope.direction.indexOf('x') >= 0,
+ scrollingY: $scope.direction.indexOf('y') >= 0,
+ zooming: $scope.$eval($scope.zooming) === true,
+ maxZoom: $scope.$eval($scope.maxZoom) || 3,
+ minZoom: $scope.$eval($scope.minZoom) || 0.5,
+ preventDefault: true
+ };
+ if (isPaging) {
+ scrollViewOptions.speedMultiplier = 0.8;
+ scrollViewOptions.bouncing = false;
+ }
+
+ scrollCtrl = $controller('$ionicScroll', {
+ $scope: $scope,
+ scrollViewOptions: scrollViewOptions
+ });
+ scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView;
+ }
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionSideMenu
+ * @module ionic
+ * @restrict E
+ * @parent ionic.directive:ionSideMenus
+ *
+ * @description
+ * A container for a side menu, sibling to an {@link ionic.directive:ionSideMenuContent} directive.
+ *
+ * @usage
+ * ```html
+ *
+ *
+ * ```
+ * For a complete side menu example, see the
+ * {@link ionic.directive:ionSideMenus} documentation.
+ *
+ * @param {string} side Which side the side menu is currently on. Allowed values: 'left' or 'right'.
+ * @param {boolean=} is-enabled Whether this side menu is enabled.
+ * @param {number=} width How many pixels wide the side menu should be. Defaults to 275.
+ */
+IonicModule
+.directive('ionSideMenu', function() {
+ return {
+ restrict: 'E',
+ require: '^ionSideMenus',
+ scope: true,
+ compile: function(element, attr) {
+ angular.isUndefined(attr.isEnabled) && attr.$set('isEnabled', 'true');
+ angular.isUndefined(attr.width) && attr.$set('width', '275');
+
+ element.addClass('menu menu-' + attr.side);
+
+ return function($scope, $element, $attr, sideMenuCtrl) {
+ $scope.side = $attr.side || 'left';
+
+ var sideMenu = sideMenuCtrl[$scope.side] = new ionic.views.SideMenu({
+ width: attr.width,
+ el: $element[0],
+ isEnabled: true
+ });
+
+ $scope.$watch($attr.width, function(val) {
+ var numberVal = +val;
+ if (numberVal && numberVal == val) {
+ sideMenu.setWidth(+val);
+ }
+ });
+ $scope.$watch($attr.isEnabled, function(val) {
+ sideMenu.setIsEnabled(!!val);
+ });
+ };
+ }
+ };
+});
+
+
+/**
+ * @ngdoc directive
+ * @name ionSideMenuContent
+ * @module ionic
+ * @restrict E
+ * @parent ionic.directive:ionSideMenus
+ *
+ * @description
+ * A container for the main visible content, sibling to one or more
+ * {@link ionic.directive:ionSideMenu} directives.
+ *
+ * @usage
+ * ```html
+ *
+ *
+ * ```
+ * For a complete side menu example, see the
+ * {@link ionic.directive:ionSideMenus} documentation.
+ *
+ * @param {boolean=} drag-content Whether the content can be dragged. Default true.
+ * @param {boolean|number=} edge-drag-threshold Whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Default false. Accepts three types of values:
+ * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu.
+ * - If true is given, the default number of pixels (25) is used as the maximum allowed distance.
+ * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed.
+ *
+ */
+IonicModule
+.directive('ionSideMenuContent', [
+ '$timeout',
+ '$ionicGesture',
+ '$window',
+function($timeout, $ionicGesture, $window) {
+
+ return {
+ restrict: 'EA', //DEPRECATED 'A'
+ require: '^ionSideMenus',
+ scope: true,
+ compile: function(element, attr) {
+ element.addClass('menu-content pane');
+
+ return { pre: prelink };
+ function prelink($scope, $element, $attr, sideMenuCtrl) {
+ var startCoord = null;
+ var primaryScrollAxis = null;
+
+ if (isDefined(attr.dragContent)) {
+ $scope.$watch(attr.dragContent, function(value) {
+ sideMenuCtrl.canDragContent(value);
+ });
+ } else {
+ sideMenuCtrl.canDragContent(true);
+ }
+
+ if (isDefined(attr.edgeDragThreshold)) {
+ $scope.$watch(attr.edgeDragThreshold, function(value) {
+ sideMenuCtrl.edgeDragThreshold(value);
+ });
+ }
+
+ // Listen for taps on the content to close the menu
+ function onContentTap(gestureEvt) {
+ if (sideMenuCtrl.getOpenAmount() !== 0) {
+ sideMenuCtrl.close();
+ gestureEvt.gesture.srcEvent.preventDefault();
+ startCoord = null;
+ primaryScrollAxis = null;
+ } else if (!startCoord) {
+ startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
+ }
+ }
+
+ function onDragX(e) {
+ if (!sideMenuCtrl.isDraggableTarget(e)) return;
+
+ if (getPrimaryScrollAxis(e) == 'x') {
+ sideMenuCtrl._handleDrag(e);
+ e.gesture.srcEvent.preventDefault();
+ }
+ }
+
+ function onDragY(e) {
+ if (getPrimaryScrollAxis(e) == 'x') {
+ e.gesture.srcEvent.preventDefault();
+ }
+ }
+
+ function onDragRelease(e) {
+ sideMenuCtrl._endDrag(e);
+ startCoord = null;
+ primaryScrollAxis = null;
+ }
+
+ function getPrimaryScrollAxis(gestureEvt) {
+ // gets whether the user is primarily scrolling on the X or Y
+ // If a majority of the drag has been on the Y since the start of
+ // the drag, but the X has moved a little bit, it's still a Y drag
+
+ if (primaryScrollAxis) {
+ // we already figured out which way they're scrolling
+ return primaryScrollAxis;
+ }
+
+ if (gestureEvt && gestureEvt.gesture) {
+
+ if (!startCoord) {
+ // get the starting point
+ startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
+
+ } else {
+ // we already have a starting point, figure out which direction they're going
+ var endCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
+
+ var xDistance = Math.abs(endCoord.x - startCoord.x);
+ var yDistance = Math.abs(endCoord.y - startCoord.y);
+
+ var scrollAxis = (xDistance < yDistance ? 'y' : 'x');
+
+ if (Math.max(xDistance, yDistance) > 30) {
+ // ok, we pretty much know which way they're going
+ // let's lock it in
+ primaryScrollAxis = scrollAxis;
+ }
+
+ return scrollAxis;
+ }
+ }
+ return 'y';
+ }
+
+ var content = {
+ element: element[0],
+ onDrag: function(e) {},
+ endDrag: function(e) {},
+ getTranslateX: function() {
+ return $scope.sideMenuContentTranslateX || 0;
+ },
+ setTranslateX: ionic.animationFrameThrottle(function(amount) {
+ var xTransform = content.offsetX + amount;
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + xTransform + 'px,0,0)';
+ $timeout(function() {
+ $scope.sideMenuContentTranslateX = amount;
+ });
+ }),
+ setMarginLeft: ionic.animationFrameThrottle(function(amount) {
+ if (amount) {
+ amount = parseInt(amount, 10);
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amount + 'px,0,0)';
+ $element[0].style.width = ($window.innerWidth - amount) + 'px';
+ content.offsetX = amount;
+ } else {
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
+ $element[0].style.width = '';
+ content.offsetX = 0;
+ }
+ }),
+ setMarginRight: ionic.animationFrameThrottle(function(amount) {
+ if (amount) {
+ amount = parseInt(amount, 10);
+ $element[0].style.width = ($window.innerWidth - amount) + 'px';
+ content.offsetX = amount;
+ } else {
+ $element[0].style.width = '';
+ content.offsetX = 0;
+ }
+ // reset incase left gets grabby
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
+ }),
+ enableAnimation: function() {
+ $scope.animationEnabled = true;
+ $element[0].classList.add('menu-animated');
+ },
+ disableAnimation: function() {
+ $scope.animationEnabled = false;
+ $element[0].classList.remove('menu-animated');
+ },
+ offsetX: 0
+ };
+
+ sideMenuCtrl.setContent(content);
+
+ // add gesture handlers
+ var gestureOpts = { stop_browser_behavior: false };
+ var contentTapGesture = $ionicGesture.on('tap', onContentTap, $element, gestureOpts);
+ var dragRightGesture = $ionicGesture.on('dragright', onDragX, $element, gestureOpts);
+ var dragLeftGesture = $ionicGesture.on('dragleft', onDragX, $element, gestureOpts);
+ var dragUpGesture = $ionicGesture.on('dragup', onDragY, $element, gestureOpts);
+ var dragDownGesture = $ionicGesture.on('dragdown', onDragY, $element, gestureOpts);
+ var releaseGesture = $ionicGesture.on('release', onDragRelease, $element, gestureOpts);
+
+ // Cleanup
+ $scope.$on('$destroy', function() {
+ if (content) {
+ content.element = null;
+ content = null;
+ }
+ $ionicGesture.off(dragLeftGesture, 'dragleft', onDragX);
+ $ionicGesture.off(dragRightGesture, 'dragright', onDragX);
+ $ionicGesture.off(dragUpGesture, 'dragup', onDragY);
+ $ionicGesture.off(dragDownGesture, 'dragdown', onDragY);
+ $ionicGesture.off(releaseGesture, 'release', onDragRelease);
+ $ionicGesture.off(contentTapGesture, 'tap', onContentTap);
+ });
+ }
+ }
+ };
+}]);
+
+IonicModule
+
+/**
+ * @ngdoc directive
+ * @name ionSideMenus
+ * @module ionic
+ * @delegate ionic.service:$ionicSideMenuDelegate
+ * @restrict E
+ *
+ * @description
+ * A container element for side menu(s) and the main content. Allows the left and/or right side menu
+ * to be toggled by dragging the main content area side to side.
+ *
+ * To automatically close an opened menu, you can add the {@link ionic.directive:menuClose} attribute
+ * directive. The `menu-close` attribute is usually added to links and buttons within
+ * `ion-side-menu-content`, so that when the element is clicked, the opened side menu will
+ * automatically close.
+ *
+ * By default, side menus are hidden underneath their side menu content and can be opened by swiping
+ * the content left or right or by toggling a button to show the side menu. Additionally, by adding the
+ * {@link ionic.directive:exposeAsideWhen} attribute directive to an
+ * {@link ionic.directive:ionSideMenu} element directive, a side menu can be given instructions about
+ * "when" the menu should be exposed (always viewable).
+ *
+ * ![Side Menu](http://ionicframework.com.s3.amazonaws.com/docs/controllers/sidemenu.gif)
+ *
+ * For more information on side menus, check out:
+ *
+ * - {@link ionic.directive:ionSideMenuContent}
+ * - {@link ionic.directive:ionSideMenu}
+ * - {@link ionic.directive:menuToggle}
+ * - {@link ionic.directive:menuClose}
+ * - {@link ionic.directive:exposeAsideWhen}
+ *
+ * @usage
+ * To use side menus, add an `` parent element,
+ * an `` for the center content,
+ * and one or more `` directives.
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ * ```js
+ * function ContentController($scope, $ionicSideMenuDelegate) {
+ * $scope.toggleLeft = function() {
+ * $ionicSideMenuDelegate.toggleLeft();
+ * };
+ * }
+ * ```
+ *
+ * @param {bool=} enable-menu-with-back-views Determines whether the side menu is enabled when the
+ * back button is showing. When set to `false`, any {@link ionic.directive:menuToggle} will be hidden,
+ * and the user cannot swipe to open the menu. When going back to the root page of the side menu (the
+ * page without a back button visible), then any menuToggle buttons will show again, and menus will be
+ * enabled again.
+ * @param {string=} delegate-handle The handle used to identify this side menu
+ * with {@link ionic.service:$ionicSideMenuDelegate}.
+ *
+ */
+.directive('ionSideMenus', ['$ionicBody', function($ionicBody) {
+ return {
+ restrict: 'ECA',
+ controller: '$ionicSideMenus',
+ compile: function(element, attr) {
+ attr.$set('class', (attr['class'] || '') + ' view');
+
+ return { pre: prelink };
+ function prelink($scope, $element, $attrs, ctrl) {
+
+ ctrl.enableMenuWithBackViews($scope.$eval($attrs.enableMenuWithBackViews));
+
+ $scope.$on('$ionicExposeAside', function(evt, isAsideExposed) {
+ if (!$scope.$exposeAside) $scope.$exposeAside = {};
+ $scope.$exposeAside.active = isAsideExposed;
+ $ionicBody.enableClass(isAsideExposed, 'aside-open');
+ });
+
+ $scope.$on('$ionicView.beforeEnter', function(ev, d){
+ if (d.historyId) {
+ $scope.$activeHistoryId = d.historyId;
+ }
+ });
+
+ $scope.$on('$destroy', function() {
+ $ionicBody.removeClass('menu-open', 'aside-open');
+ });
+
+ }
+ }
+ };
+}]);
+
+
+/**
+ * @ngdoc directive
+ * @name ionSlideBox
+ * @module ionic
+ * @delegate ionic.service:$ionicSlideBoxDelegate
+ * @restrict E
+ * @description
+ * The Slide Box is a multi-page container where each page can be swiped or dragged between:
+ *
+ * ![SlideBox](http://ionicframework.com.s3.amazonaws.com/docs/controllers/slideBox.gif)
+ *
+ * @usage
+ * ```html
+ *
+ *
+ *
BLUE
+ *
+ *
+ *
YELLOW
+ *
+ *
+ *
PINK
+ *
+ *
+ * ```
+ *
+ * @param {string=} delegate-handle The handle used to identify this slideBox
+ * with {@link ionic.service:$ionicSlideBoxDelegate}.
+ * @param {boolean=} does-continue Whether the slide box should loop.
+ * @param {boolean=} auto-play Whether the slide box should automatically slide. Default true if does-continue is true.
+ * @param {number=} slide-interval How many milliseconds to wait to change slides (if does-continue is true). Defaults to 4000.
+ * @param {boolean=} show-pager Whether a pager should be shown for this slide box.
+ * @param {expression=} pager-click Expression to call when a pager is clicked (if show-pager is true). Is passed the 'index' variable.
+ * @param {expression=} on-slide-changed Expression called whenever the slide is changed. Is passed an '$index' variable.
+ * @param {expression=} active-slide Model to bind the current slide to.
+ */
+IonicModule
+.directive('ionSlideBox', [
+ '$timeout',
+ '$compile',
+ '$ionicSlideBoxDelegate',
+ '$ionicHistory',
+function($timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory) {
+ return {
+ restrict: 'E',
+ replace: true,
+ transclude: true,
+ scope: {
+ autoPlay: '=',
+ doesContinue: '@',
+ slideInterval: '@',
+ showPager: '@',
+ pagerClick: '&',
+ disableScroll: '@',
+ onSlideChanged: '&',
+ activeSlide: '=?'
+ },
+ controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
+ var _this = this;
+
+ var continuous = $scope.$eval($scope.doesContinue) === true;
+ var shouldAutoPlay = isDefined($attrs.autoPlay) ? !!$scope.autoPlay : false;
+ var slideInterval = shouldAutoPlay ? $scope.$eval($scope.slideInterval) || 4000 : 0;
+
+ var slider = new ionic.views.Slider({
+ el: $element[0],
+ auto: slideInterval,
+ continuous: continuous,
+ startSlide: $scope.activeSlide,
+ slidesChanged: function() {
+ $scope.currentSlide = slider.currentIndex();
+
+ // Try to trigger a digest
+ $timeout(function() {});
+ },
+ callback: function(slideIndex) {
+ $scope.currentSlide = slideIndex;
+ $scope.onSlideChanged({ index: $scope.currentSlide, $index: $scope.currentSlide});
+ $scope.$parent.$broadcast('slideBox.slideChanged', slideIndex);
+ $scope.activeSlide = slideIndex;
+ // Try to trigger a digest
+ $timeout(function() {});
+ }
+ });
+
+ slider.enableSlide($scope.$eval($attrs.disableScroll) !== true);
+
+ $scope.$watch('activeSlide', function(nv) {
+ if(angular.isDefined(nv)){
+ slider.slide(nv);
+ }
+ });
+
+ $scope.$on('slideBox.nextSlide', function() {
+ slider.next();
+ });
+
+ $scope.$on('slideBox.prevSlide', function() {
+ slider.prev();
+ });
+
+ $scope.$on('slideBox.setSlide', function(e, index) {
+ slider.slide(index);
+ });
+
+ //Exposed for testing
+ this.__slider = slider;
+
+ var deregisterInstance = $ionicSlideBoxDelegate._registerInstance(
+ slider, $attrs.delegateHandle, function() {
+ return $ionicHistory.isActiveScope($scope);
+ }
+ );
+ $scope.$on('$destroy', deregisterInstance);
+
+ this.slidesCount = function() {
+ return slider.slidesCount();
+ };
+
+ this.onPagerClick = function(index) {
+ void 0;
+ $scope.pagerClick({index: index});
+ };
+
+ $timeout(function() {
+ slider.load();
+ });
+ }],
+ template: '
',
+ link: function($scope, $element, $attr, slideBox) {
+ var selectPage = function(index) {
+ var children = $element[0].children;
+ var length = children.length;
+ for(var i = 0; i < length; i++) {
+ if(i == index) {
+ children[i].classList.add('active');
+ } else {
+ children[i].classList.remove('active');
+ }
+ }
+ };
+
+ $scope.pagerClick = function(index) {
+ slideBox.onPagerClick(index);
+ };
+
+ $scope.numSlides = function() {
+ return new Array(slideBox.slidesCount());
+ };
+
+ $scope.$watch('currentSlide', function(v) {
+ selectPage(v);
+ });
+ }
+ };
+
+});
+
+/**
+ * @ngdoc directive
+ * @name ionTab
+ * @module ionic
+ * @restrict E
+ * @parent ionic.directive:ionTabs
+ *
+ * @description
+ * Contains a tab's content. The content only exists while the given tab is selected.
+ *
+ * Each ionTab has its own view history.
+ *
+ * @usage
+ * ```html
+ *
+ *
+ * ```
+ * For a complete, working tab bar example, see the {@link ionic.directive:ionTabs} documentation.
+ *
+ * @param {string} title The title of the tab.
+ * @param {string=} href The link that this tab will navigate to when tapped.
+ * @param {string=} icon The icon of the tab. If given, this will become the default for icon-on and icon-off.
+ * @param {string=} icon-on The icon of the tab while it is selected.
+ * @param {string=} icon-off The icon of the tab while it is not selected.
+ * @param {expression=} badge The badge to put on this tab (usually a number).
+ * @param {expression=} badge-style The style of badge to put on this tab (eg: badge-positive).
+ * @param {expression=} on-select Called when this tab is selected.
+ * @param {expression=} on-deselect Called when this tab is deselected.
+ * @param {expression=} ng-click By default, the tab will be selected on click. If ngClick is set, it will not. You can explicitly switch tabs using {@link ionic.service:$ionicTabsDelegate#select $ionicTabsDelegate.select()}.
+ */
+IonicModule
+.directive('ionTab', [
+ '$compile',
+ '$ionicConfig',
+ '$ionicBind',
+ '$ionicViewSwitcher',
+function($compile, $ionicConfig, $ionicBind, $ionicViewSwitcher) {
+
+ //Returns ' key="value"' if value exists
+ function attrStr(k, v) {
+ return angular.isDefined(v) ? ' ' + k + '="' + v + '"' : '';
+ }
+ return {
+ restrict: 'E',
+ require: ['^ionTabs', 'ionTab'],
+ controller: '$ionicTab',
+ scope: true,
+ compile: function(element, attr) {
+
+ //We create the tabNavTemplate in the compile phase so that the
+ //attributes we pass down won't be interpolated yet - we want
+ //to pass down the 'raw' versions of the attributes
+ var tabNavTemplate = '';
+
+ //Remove the contents of the element so we can compile them later, if tab is selected
+ var tabContentEle = document.createElement('div');
+ for (var x = 0; x < element[0].children.length; x++) {
+ tabContentEle.appendChild(element[0].children[x].cloneNode(true));
+ }
+ var childElementCount = tabContentEle.childElementCount;
+ element.empty();
+
+ var navViewName, isNavView;
+ if (childElementCount) {
+ if (tabContentEle.children[0].tagName === 'ION-NAV-VIEW') {
+ // get the name if it's a nav-view
+ navViewName = tabContentEle.children[0].getAttribute('name');
+ tabContentEle.children[0].classList.add('view-container');
+ isNavView = true;
+ }
+ if (childElementCount === 1) {
+ // make the 1 child element the primary tab content container
+ tabContentEle = tabContentEle.children[0];
+ }
+ if (!isNavView) tabContentEle.classList.add('pane');
+ tabContentEle.classList.add('tab-content');
+ }
+
+ return function link($scope, $element, $attr, ctrls) {
+ var childScope;
+ var childElement;
+ var tabsCtrl = ctrls[0];
+ var tabCtrl = ctrls[1];
+ var isTabContentAttached = false;
+
+ $ionicBind($scope, $attr, {
+ onSelect: '&',
+ onDeselect: '&',
+ title: '@',
+ uiSref: '@',
+ href: '@'
+ });
+
+ tabsCtrl.add($scope);
+ $scope.$on('$destroy', function() {
+ if (!$scope.$tabsDestroy) {
+ // if the containing ionTabs directive is being destroyed
+ // then don't bother going through the controllers remove
+ // method, since remove will reset the active tab as each tab
+ // is being destroyed, causing unnecessary view loads and transitions
+ tabsCtrl.remove($scope);
+ }
+ tabNavElement.isolateScope().$destroy();
+ tabNavElement.remove();
+ tabNavElement = tabContentEle = childElement = null;
+ });
+
+ //Remove title attribute so browser-tooltip does not apear
+ $element[0].removeAttribute('title');
+
+ if (navViewName) {
+ tabCtrl.navViewName = $scope.navViewName = navViewName;
+ }
+ $scope.$on('$stateChangeSuccess', selectIfMatchesState);
+ selectIfMatchesState();
+ function selectIfMatchesState() {
+ if (tabCtrl.tabMatchesState()) {
+ tabsCtrl.select($scope, false);
+ }
+ }
+
+ var tabNavElement = jqLite(tabNavTemplate);
+ tabNavElement.data('$ionTabsController', tabsCtrl);
+ tabNavElement.data('$ionTabController', tabCtrl);
+ tabsCtrl.$tabsElement.append($compile(tabNavElement)($scope));
+
+
+ function tabSelected(isSelected) {
+ if (isSelected && childElementCount) {
+ // this tab is being selected
+
+ // check if the tab is already in the DOM
+ // only do this if the tab has child elements
+ if (!isTabContentAttached) {
+ // tab should be selected and is NOT in the DOM
+ // create a new scope and append it
+ childScope = $scope.$new();
+ childElement = jqLite(tabContentEle);
+ $ionicViewSwitcher.viewEleIsActive(childElement, true);
+ tabsCtrl.$element.append(childElement);
+ $compile(childElement)(childScope);
+ isTabContentAttached = true;
+ }
+
+ // remove the hide class so the tabs content shows up
+ $ionicViewSwitcher.viewEleIsActive(childElement, true);
+
+ } else if (isTabContentAttached && childElement) {
+ // this tab should NOT be selected, and it is already in the DOM
+
+ if ($ionicConfig.views.maxCache() > 0) {
+ // keep the tabs in the DOM, only css hide it
+ $ionicViewSwitcher.viewEleIsActive(childElement, false);
+
+ } else {
+ // do not keep tabs in the DOM
+ destroyTab();
+ }
+
+ }
+ }
+
+ function destroyTab() {
+ childScope && childScope.$destroy();
+ isTabContentAttached && childElement && childElement.remove();
+ isTabContentAttached = childScope = childElement = null;
+ }
+
+ $scope.$watch('$tabSelected', tabSelected);
+
+ $scope.$on('$ionicView.afterEnter', function() {
+ $ionicViewSwitcher.viewEleIsActive(childElement, $scope.$tabSelected);
+ });
+
+ $scope.$on('$ionicView.clearCache', function() {
+ if (!$scope.$tabSelected) {
+ destroyTab();
+ }
+ });
+
+ };
+ }
+ };
+}]);
+
+IonicModule
+.directive('ionTabNav', [function() {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: ['^ionTabs', '^ionTab'],
+ template:
+ '' +
+ '{{badge}}' +
+ '' +
+ '' +
+ '' +
+ '',
+ scope: {
+ title: '@',
+ icon: '@',
+ iconOn: '@',
+ iconOff: '@',
+ badge: '=',
+ hidden: '@',
+ badgeStyle: '@',
+ 'class': '@'
+ },
+ compile: function(element, attr, transclude) {
+ return function link($scope, $element, $attrs, ctrls) {
+ var tabsCtrl = ctrls[0],
+ tabCtrl = ctrls[1];
+
+ //Remove title attribute so browser-tooltip does not apear
+ $element[0].removeAttribute('title');
+
+ $scope.selectTab = function(e) {
+ e.preventDefault();
+ tabsCtrl.select(tabCtrl.$scope, true);
+ };
+ if (!$attrs.ngClick) {
+ $element.on('click', function(event) {
+ $scope.$apply(function() {
+ $scope.selectTab(event);
+ });
+ });
+ }
+
+ $scope.isHidden = function() {
+ if ($attrs.hidden === 'true' || $attrs.hidden === true) return true;
+ return false;
+ };
+
+ $scope.getIconOn = function() {
+ return $scope.iconOn || $scope.icon;
+ };
+ $scope.getIconOff = function() {
+ return $scope.iconOff || $scope.icon;
+ };
+
+ $scope.isTabActive = function() {
+ return tabsCtrl.selectedTab() === tabCtrl.$scope;
+ };
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionTabs
+ * @module ionic
+ * @delegate ionic.service:$ionicTabsDelegate
+ * @restrict E
+ * @codepen KbrzJ
+ *
+ * @description
+ * Powers a multi-tabbed interface with a Tab Bar and a set of "pages" that can be tabbed
+ * through.
+ *
+ * Assign any [tabs class](/docs/components#tabs) or
+ * [animation class](/docs/components#animation) to the element to define
+ * its look and feel.
+ *
+ * See the {@link ionic.directive:ionTab} directive's documentation for more details on
+ * individual tabs.
+ *
+ * Note: do not place ion-tabs inside of an ion-content element; it has been known to cause a
+ * certain CSS bug.
+ *
+ * @usage
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * @param {string=} delegate-handle The handle used to identify these tabs
+ * with {@link ionic.service:$ionicTabsDelegate}.
+ */
+
+IonicModule
+.directive('ionTabs', [
+ '$ionicTabsDelegate',
+ '$ionicConfig',
+ '$ionicHistory',
+function($ionicTabsDelegate, $ionicConfig, $ionicHistory) {
+ return {
+ restrict: 'E',
+ scope: true,
+ controller: '$ionicTabs',
+ compile: function(tElement) {
+ //We cannot use regular transclude here because it breaks element.data()
+ //inheritance on compile
+ var innerElement = jqLite('
');
+ innerElement.append(tElement.contents());
+
+ tElement.append(innerElement)
+ .addClass('tabs-' + $ionicConfig.tabs.position() + ' tabs-' + $ionicConfig.tabs.style());
+
+ return { pre: prelink, post: postLink };
+ function prelink($scope, $element, $attr, tabsCtrl) {
+ var deregisterInstance = $ionicTabsDelegate._registerInstance(
+ tabsCtrl, $attr.delegateHandle, tabsCtrl.hasActiveScope
+ );
+
+ tabsCtrl.$scope = $scope;
+ tabsCtrl.$element = $element;
+ tabsCtrl.$tabsElement = jqLite($element[0].querySelector('.tabs'));
+
+ $scope.$watch(function() { return $element[0].className; }, function(value) {
+ var isTabsTop = value.indexOf('tabs-top') !== -1;
+ var isHidden = value.indexOf('tabs-item-hide') !== -1;
+ $scope.$hasTabs = !isTabsTop && !isHidden;
+ $scope.$hasTabsTop = isTabsTop && !isHidden;
+ });
+
+ $scope.$on('$destroy', function() {
+ // variable to inform child tabs that they're all being blown away
+ // used so that while destorying an individual tab, each one
+ // doesn't select the next tab as the active one, which causes unnecessary
+ // loading of tab views when each will eventually all go away anyway
+ $scope.$tabsDestroy = true;
+ deregisterInstance();
+ tabsCtrl.$tabsElement = tabsCtrl.$element = tabsCtrl.$scope = innerElement = null;
+ delete $scope.$hasTabs;
+ delete $scope.$hasTabsTop;
+ });
+ }
+
+ function postLink($scope, $element, $attr, tabsCtrl) {
+ if (!tabsCtrl.selectedTab()) {
+ // all the tabs have been added
+ // but one hasn't been selected yet
+ tabsCtrl.select(0);
+ }
+ }
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionToggle
+ * @module ionic
+ * @codepen tfAzj
+ * @restrict E
+ *
+ * @description
+ * A toggle is an animated switch which binds a given model to a boolean.
+ *
+ * Allows dragging of the switch's nub.
+ *
+ * The toggle behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]) otherwise.
+ *
+ * @param toggle-class {string=} Sets the CSS class on the inner `label.toggle` element created by the directive.
+ *
+ * @usage
+ * Below is an example of a toggle directive which is wired up to the `airplaneMode` model
+ * and has the `toggle-calm` CSS class assigned to the inner element.
+ *
+ * ```html
+ * Airplane Mode
+ * ```
+ */
+IonicModule
+.directive('ionToggle', [
+ '$ionicGesture',
+ '$timeout',
+function($ionicGesture, $timeout) {
+
+ return {
+ restrict: 'E',
+ replace: true,
+ require: '?ngModel',
+ transclude: true,
+ template:
+ '
' +
+ '' +
+ '' +
+ '
',
+
+ compile: function(element, attr) {
+ var input = element.find('input');
+ forEach({
+ 'name': attr.name,
+ 'ng-value': attr.ngValue,
+ 'ng-model': attr.ngModel,
+ 'ng-checked': attr.ngChecked,
+ 'ng-disabled': attr.ngDisabled,
+ 'ng-true-value': attr.ngTrueValue,
+ 'ng-false-value': attr.ngFalseValue,
+ 'ng-change': attr.ngChange
+ }, function(value, name) {
+ if (isDefined(value)) {
+ input.attr(name, value);
+ }
+ });
+
+ if(attr.toggleClass) {
+ element[0].getElementsByTagName('label')[0].classList.add(attr.toggleClass);
+ }
+
+ return function($scope, $element, $attr) {
+ var el, checkbox, track, handle;
+
+ el = $element[0].getElementsByTagName('label')[0];
+ checkbox = el.children[0];
+ track = el.children[1];
+ handle = track.children[0];
+
+ var ngModelController = jqLite(checkbox).controller('ngModel');
+
+ $scope.toggle = new ionic.views.Toggle({
+ el: el,
+ track: track,
+ checkbox: checkbox,
+ handle: handle,
+ onChange: function() {
+ if(checkbox.checked) {
+ ngModelController.$setViewValue(true);
+ } else {
+ ngModelController.$setViewValue(false);
+ }
+ $scope.$apply();
+ }
+ });
+
+ $scope.$on('$destroy', function() {
+ $scope.toggle.destroy();
+ });
+ };
+ }
+
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionView
+ * @module ionic
+ * @restrict E
+ * @parent ionNavView
+ *
+ * @description
+ * A container for view content and any navigational and header bar information. When a view
+ * enters and exists its parent {@link ionic.directive:ionNavView}, the view also emits view
+ * information, such as its title, whether the back button should show or not, whether the
+ * corresponding {@link ionic.directive:ionNavBar} should show or not, which transition the view
+ * should use to animate, and which direction to animate.
+ *
+ * *Views are cached to improve performance.* When a view is navigated away from, its element is
+ * left in the DOM, and its scope is disconnected from the `$watch` cycle. When navigating to a
+ * view that is already cached, its scope is reconnected, and the existing element, which was
+ * left in the DOM, becomes active again. This can be disabled, or the maximum number of cached
+ * views changed in {@link ionic.directive:ionicConfig}, in the view's `$state` configuration, or
+ * as an attribute on the view itself (see below).
+ *
+ * @usage
+ * Below is an example where our page will load with a {@link ionic.directive:ionNavBar} containing
+ * "My Page" as the title.
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ * Hello!
+ *
+ *
+ *
+ * ```
+ *
+ * ## View LifeCycle and Events
+ *
+ * Views can be cached, which means *controllers normally only load once*, which may
+ * affect your controller logic. To know when a view has entered or left, events
+ * have been added that are emitted from the view's scope. These events also
+ * contain data about the view, such as the title and whether the back button should
+ * show. Also contained is transition data, such as the transition type and
+ * direction that will be or was used.
+ *
+ *
+ *
+ *
$ionicView.loaded
+ *
The view has loaded. This event only happens once per
+ * view being created and added to the DOM. If a view leaves but is cached,
+ * then this event will not fire again on a subsequent viewing. The loaded event
+ * is good place to put your setup code for the view; however, it is not the
+ * recommended event to listen to when a view becomes active.
+ *
+ *
+ *
$ionicView.enter
+ *
The view has fully entered and is now the active view.
+ * This event will fire, whether it was the first load or a cached view.
+ *
+ *
+ *
$ionicView.leave
+ *
The view has finished leaving and is no longer the
+ * active view. This event will fire, whether it is cached or destroyed.
+ *
+ *
+ *
$ionicView.beforeEnter
+ *
The view is about to enter and become the active view.
+ *
+ *
+ *
$ionicView.beforeLeave
+ *
The view is about to leave and no longer be the active view.
+ *
+ *
+ *
$ionicView.afterEnter
+ *
The view has fully entered and is now the active view.
+ *
+ *
+ *
$ionicView.afterLeave
+ *
The view has finished leaving and is no longer the active view.
+ *
+ *
+ *
$ionicView.unloaded
+ *
The view's controller has been destroyed and its element has been
+ * removed from the DOM.
+ *
+ *
+ *
+ * ## Caching
+ *
+ * Caching can be disabled and enabled in multiple ways. By default, Ionic will
+ * cache a maximum of 10 views. You can optionally choose to disable caching at
+ * either an individual view basis, or by global configuration. Please see the
+ * _Caching_ section in {@link ionic.directive:ionNavView} for more info.
+ *
+ * @param {string=} view-title A text-only title to display on the parent {@link ionic.directive:ionNavBar}.
+ * For an HTML title, such as an image, see {@link ionic.directive:ionNavTitle} instead.
+ * @param {boolean=} cache-view If this view should be allowed to be cached or not.
+ * Please see the _Caching_ section in {@link ionic.directive:ionNavView} for
+ * more info. Default `true`
+ * @param {boolean=} hide-back-button Whether to hide the back button on the parent
+ * {@link ionic.directive:ionNavBar} by default.
+ * @param {boolean=} hide-nav-bar Whether to hide the parent
+ * {@link ionic.directive:ionNavBar} by default.
+ */
+IonicModule
+.directive('ionView', function() {
+ return {
+ restrict: 'EA',
+ priority: 1000,
+ controller: '$ionicView',
+ compile: function(tElement) {
+ tElement.addClass('pane');
+ tElement[0].removeAttribute('title');
+ return function link($scope, $element, $attrs, viewCtrl) {
+ viewCtrl.init();
+ };
+ }
+ };
+});
+
+})();
\ No newline at end of file
diff --git a/lib/js/ionic.js b/lib/js/ionic.js
new file mode 100755
index 00000000..f75795f1
--- /dev/null
+++ b/lib/js/ionic.js
@@ -0,0 +1,7811 @@
+/*!
+ * Copyright 2014 Drifty Co.
+ * http://drifty.com/
+ *
+ * Ionic, v1.0.0-beta.14
+ * 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-beta.14';
+
+(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;
+ });
+ }
+ };
+ },
+
+ /**
+ * @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
+ *
+ * Framework events handles various mobile browser events, and
+ * detects special events like tap/swipe/etc. and emits them
+ * as custom events that can be used in an app.
+ *
+ * Portions lovingly adapted from github.com/maker/ratchet and github.com/alexgibson/tap.js - thanks guys!
+ */
+
+(function(ionic) {
+
+ // Custom event polyfill
+ ionic.CustomEvent = (function() {
+ if( typeof window.CustomEvent === 'function' ) return CustomEvent;
+
+ var customEvent = function(event, params) {
+ var evt;
+ params = params || {
+ bubbles: false,
+ cancelable: false,
+ detail: undefined
+ };
+ try {
+ evt = document.createEvent("CustomEvent");
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
+ } catch (error) {
+ // fallback for browsers that don't support createEvent('CustomEvent')
+ evt = document.createEvent("Event");
+ for (var param in params) {
+ evt[param] = params[param];
+ }
+ evt.initEvent(event, params.bubbles, params.cancelable);
+ }
+ return evt;
+ };
+ customEvent.prototype = window.Event.prototype;
+ return customEvent;
+ })();
+
+
+ /**
+ * @ngdoc utility
+ * @name ionic.EventController
+ * @module ionic
+ */
+ ionic.EventController = {
+ VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'],
+
+ /**
+ * @ngdoc method
+ * @name ionic.EventController#trigger
+ * @alias ionic.trigger
+ * @param {string} eventType The event to trigger.
+ * @param {object} data The data for the event. Hint: pass in
+ * `{target: targetElement}`
+ * @param {boolean=} bubbles Whether the event should bubble up the DOM.
+ * @param {boolean=} cancelable Whether the event should be cancelable.
+ */
+ // Trigger a new event
+ trigger: function(eventType, data, bubbles, cancelable) {
+ var event = new ionic.CustomEvent(eventType, {
+ detail: data,
+ bubbles: !!bubbles,
+ cancelable: !!cancelable
+ });
+
+ // Make sure to trigger the event on the given target, or dispatch it from
+ // the window if we don't have an event target
+ data && data.target && data.target.dispatchEvent && data.target.dispatchEvent(event) || window.dispatchEvent(event);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionic.EventController#on
+ * @alias ionic.on
+ * @description Listen to an event on an element.
+ * @param {string} type The event to listen for.
+ * @param {function} callback The listener to be called.
+ * @param {DOMElement} element The element to listen for the event on.
+ */
+ on: function(type, callback, element) {
+ var e = element || window;
+
+ // Bind a gesture if it's a virtual event
+ for(var i = 0, j = this.VIRTUALIZED_EVENTS.length; i < j; i++) {
+ if(type == this.VIRTUALIZED_EVENTS[i]) {
+ var gesture = new ionic.Gesture(element);
+ gesture.on(type, callback);
+ return gesture;
+ }
+ }
+
+ // Otherwise bind a normal event
+ e.addEventListener(type, callback);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionic.EventController#off
+ * @alias ionic.off
+ * @description Remove an event listener.
+ * @param {string} type
+ * @param {function} callback
+ * @param {DOMElement} element
+ */
+ off: function(type, callback, element) {
+ element.removeEventListener(type, callback);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionic.EventController#onGesture
+ * @alias ionic.onGesture
+ * @description Add an event listener for a gesture on an element.
+ *
+ * Available eventTypes (from [hammer.js](http://eightmedia.github.io/hammer.js/)):
+ *
+ * `hold`, `tap`, `doubletap`, `drag`, `dragstart`, `dragend`, `dragup`, `dragdown`,
+ * `dragleft`, `dragright`, `swipe`, `swipeup`, `swipedown`, `swipeleft`, `swiperight`,
+ * `transform`, `transformstart`, `transformend`, `rotate`, `pinch`, `pinchin`, `pinchout`,
+ * `touch`, `release`
+ *
+ * @param {string} eventType The gesture event to listen for.
+ * @param {function(e)} callback The function to call when the gesture
+ * happens.
+ * @param {DOMElement} element The angular element to listen for the event on.
+ */
+ 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 on an element.
+ * @param {string} eventType The gesture event.
+ * @param {function(e)} callback The listener that was added earlier.
+ * @param {DOMElement} element The element the listener was added on.
+ */
+ offGesture: function(gesture, type, callback) {
+ gesture.off(type, callback);
+ },
+
+ handlePopState: function(event) {}
+ };
+
+
+ // 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);
+
+/**
+ * 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;
+ }
+
+ // 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 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= 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;
+ }
+
+ // 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 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.7
+ },
+ 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
+ },
+ triggered: false,
+ handler: function dragGesture(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;
+ }
+
+ // 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';
+
+ /**
+ * @ngdoc utility
+ * @name ionic.Platform
+ * @module ionic
+ */
+ 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 (this.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() {
+ ionic.Platform._checkPlatforms();
+
+ ionic.requestAnimationFrame(function() {
+ // only add to the body class if we got platform info
+ for (var i = 0; i < ionic.Platform.platforms.length; i++) {
+ document.body.classList.add('platform-' + ionic.Platform.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 = this.grade;
+ this.grade = grade;
+ ionic.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(platforms) {
+ this.platforms = [];
+ var grade = 'a';
+
+ if (this.isWebView()) {
+ this.platforms.push('webview');
+ this.platforms.push('cordova');
+ } else {
+ this.platforms.push('browser');
+ }
+ if (this.isIPad()) this.platforms.push('ipad');
+
+ var platform = this.platform();
+ if (platform) {
+ this.platforms.push(platform);
+
+ var version = this.version();
+ if (version) {
+ var v = version.toString();
+ if (v.indexOf('.') > 0) {
+ v = v.replace('.', '_');
+ } else {
+ v += '_0';
+ }
+ this.platforms.push(platform + v.split('_')[0]);
+ this.platforms.push(platform + v);
+
+ if (this.isAndroid() && version < 4.4) {
+ grade = (version < 4 ? 'c' : 'b');
+ } else if (this.isWindowsPhone()) {
+ grade = 'b';
+ }
+ }
+ }
+
+ this.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);
+ },
+ /**
+ * @ngdoc method
+ * @name ionic.Platform#isIPad
+ * @returns {boolean} Whether we are running on iPad.
+ */
+ isIPad: function() {
+ if (/iPad/i.test(ionic.Platform.navigator.platform)) {
+ return true;
+ }
+ return /iPad/i.test(this.ua);
+ },
+ /**
+ * @ngdoc method
+ * @name ionic.Platform#isIOS
+ * @returns {boolean} Whether we are running on iOS.
+ */
+ isIOS: function() {
+ return this.is(IOS);
+ },
+ /**
+ * @ngdoc method
+ * @name ionic.Platform#isAndroid
+ * @returns {boolean} Whether we are running on Android.
+ */
+ isAndroid: function() {
+ return this.is(ANDROID);
+ },
+ /**
+ * @ngdoc method
+ * @name ionic.Platform#isWindowsPhone
+ * @returns {boolean} Whether we are running on Windows Phone.
+ */
+ isWindowsPhone: function() {
+ return this.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) this.setPlatform(this.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 (this.ua.indexOf('Android') > 0) {
+ platformName = ANDROID;
+ } else if (this.ua.indexOf('iPhone') > -1 || this.ua.indexOf('iPad') > -1 || this.ua.indexOf('iPod') > -1) {
+ platformName = IOS;
+ } else if (this.ua.indexOf('Windows Phone') > -1) {
+ platformName = WINDOWS_PHONE;
+ } else {
+ platformName = ionic.Platform.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) this.setVersion(this.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 = this.platform();
+ var versionMatch = {
+ 'android': /Android (\d+).(\d+)?/,
+ 'ios': /OS (\d+)_(\d+)?/,
+ 'windowsphone': /Windows Phone (\d+).(\d+)?/
+ };
+ if (versionMatch[pName]) {
+ v = this.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 (this.platforms) {
+ for (var x = 0; x < this.platforms.length; x++) {
+ if (this.platforms[x] === type) return true;
+ }
+ }
+ // exact match
+ var pName = this.platform();
+ if (pName) {
+ return pName === type.toLowerCase();
+ }
+
+ // A quick hack for to check userAgent
+ return this.ua.toLowerCase().indexOf(type) >= 0;
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionic.Platform#exitApp
+ * @description Exit the app.
+ */
+ exitApp: function() {
+ this.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).
+ * @param {boolean} shouldShow Whether or not to show the status bar.
+ */
+ showStatusBar: function(val) {
+ // Only useful when run within cordova
+ this._showStatusBar = val;
+ this.ready(function() {
+ // run this only when or if the platform (cordova) is ready
+ ionic.requestAnimationFrame(function() {
+ if (ionic.Platform._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.
+ * @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
+ this.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
+ ionic.requestAnimationFrame(function() {
+ // fixing pane height before we adjust this
+ panes = document.getElementsByClassName('pane');
+ for (var i = 0; i < panes.length; i++) {
+ panes[i].style.height = panes[i].offsetHeight + "px";
+ }
+ if (ionic.Platform.isFullScreen) {
+ document.body.classList.add('fullscreen');
+ } else {
+ document.body.classList.remove('fullscreen');
+ }
+ });
+ // showStatusBar: default is false if no param provided
+ ionic.Platform.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 (ionic.Platform.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);
+ }
+
+ window.addEventListener("load", onWindowLoad, false);
+
+ function onPlatformReady() {
+ // the device is all set to go, init our own stuff then fire off our event
+ ionic.Platform.isReady = true;
+ ionic.Platform.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 });
+
+ ionic.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
+ *
+ *
+ *
+ * ```
+ *
+ * ### 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)$/i).test(ele.type)));
+ },
+
+ isDateInput: function(ele) {
+ return !!ele &&
+ (ele.tagName == 'INPUT' && (/^(date|time|datetime-local|month|week)$/i).test(ele.type));
+ },
+
+ 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, scrollIntance) {
+ 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.parentElement.querySelector('.cloned-text-input');
+ if (!clonedInput) {
+ clonedInput = document.createElement(focusInput.tagName);
+ clonedInput.placeholder = focusInput.placeholder;
+ clonedInput.type = focusInput.type;
+ clonedInput.value = focusInput.value;
+ clonedInput.style = focusInput.style;
+ clonedInput.className = focusInput.className;
+ 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.style.top = focusInput.offsetTop;
+ focusInput.classList.add('previous-input-focus');
+ }
+ }
+ });
+ },
+
+ hasCheckedClone: false,
+
+ removeClonedInputs: function(container, scrollIntance) {
+ 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 = '';
+ 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) {
+ if (e.target.type == 'submit' && e.detail === 0) {
+ // do not prevent click if it came from an "Enter" or "Go" keypress submit
+ return;
+ }
+
+ // 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) {
+ if (e.isIonicTap || tapIgnoreEvent(e)) return;
+
+ 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) {
+ 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) {
+ 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) {
+ 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(e) {
+ tapEventListener(tapTouchMoveListener, false);
+ ionic.activator.end();
+ tapPointerMoved = false;
+}
+
+function tapEnableTouchEvents() {
+ tapEnabledTouchEvents = true;
+ clearTimeout(tapMouseResetTimer);
+ tapMouseResetTimer = setTimeout(function() {
+ tapEnabledTouchEvents = false;
+ }, 2000);
+}
+
+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) {
+ // Because a text input doesn't preventDefault (so the caret still works) there's a chance
+ // that it's 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() {
+ 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 self = this;
+
+ // 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.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.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 uid = ['0','0','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, old_index, new_index) {
+ if (new_index >= arr.length) {
+ var k = new_index - arr.length;
+ while ((k--) + 1) {
+ arr.push(undefined);
+ }
+ }
+ arr.splice(new_index, 0, arr.splice(old_index, 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;
+ },
+
+ /**
+ * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric
+ * characters such as '012ABC'. The reason why we are not using simply a number counter is that
+ * the number string gets longer over time, and it can also overflow, where as the nextId
+ * will grow much slower, it is a string, and it will never overflow.
+ *
+ * @returns an unique alpha-numeric string
+ */
+ nextUid: function() {
+ var index = uid.length;
+ var digit;
+
+ while (index) {
+ index--;
+ digit = uid[index].charCodeAt(0);
+ if (digit == 57 /*'9'*/) {
+ uid[index] = 'A';
+ return uid.join('');
+ }
+ if (digit == 90 /*'Z'*/) {
+ uid[index] = '0';
+ } else {
+ uid[index] = String.fromCharCode(digit + 1);
+ return uid.join('');
+ }
+ }
+ uid.unshift('0');
+ return uid.join('');
+ },
+
+ 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');
+ // 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');
+ // 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
+ *
+ *
+ *
+ * ```
+ * ----------
+ *
+ * ### Plugin Usage
+ * Information on using the plugin can be found at
+ * [https://github.com/driftyco/ionic-plugins-keyboard](https://github.com/driftyco/ionic-plugins-keyboard).
+ *
+ * ----------
+ *
+ * ### Android Notes
+ * - If your app is running in fullscreen, i.e. you have
+ * `` in your `config.xml` file
+ * you will need to set `ionic.Platform.isFullScreen = true` manually.
+ *
+ * - You can configure the behavior of the web view when the keyboard shows by setting
+ * [android:windowSoftInputMode](http://developer.android.com/reference/android/R.attr.html#windowSoftInputMode)
+ * to either `adjustPan`, `adjustResize` or `adjustNothing` in your app's
+ * activity in `AndroidManifest.xml`. `adjustResize` is the recommended setting
+ * for Ionic, but if for some reason you do use `adjustPan` you will need to
+ * set `ionic.Platform.isFullScreen = true`.
+ *
+ * ```xml
+ *
+ *
+ * ```
+ *
+ * ### iOS Notes
+ * - If the content of your app (including the header) is being pushed up and
+ * out of view on input focus, try setting `cordova.plugins.Keyboard.disableScroll(true)`.
+ * This does **not** disable scrolling in the Ionic scroll view, rather it
+ * disables the native overflow scrolling that happens automatically as a
+ * result of focusing on inputs below the keyboard.
+ *
+ */
+
+var keyboardViewportHeight = getViewportHeight();
+var keyboardIsOpen;
+var keyboardActiveElement;
+var keyboardFocusOutTimer;
+var keyboardFocusInTimer;
+var keyboardPollHeightTimer;
+var keyboardLastShow = 0;
+
+var KEYBOARD_OPEN_CSS = 'keyboard-open';
+var SCROLL_CONTAINER_CSS = 'scroll';
+
+ionic.keyboard = {
+ isOpen: false,
+ height: null,
+ landscape: false,
+
+ hide: function() {
+ clearTimeout(keyboardFocusInTimer);
+ clearTimeout(keyboardFocusOutTimer);
+ clearTimeout(keyboardPollHeightTimer);
+
+ ionic.keyboard.isOpen = 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( keyboardHasPlugin() ) {
+ cordova.plugins.Keyboard.close();
+ }
+ },
+
+ show: function() {
+ if( keyboardHasPlugin() ) {
+ cordova.plugins.Keyboard.show();
+ }
+ }
+};
+
+function keyboardInit() {
+ if( keyboardHasPlugin() ) {
+ window.addEventListener('native.keyboardshow', keyboardNativeShow);
+ window.addEventListener('native.keyboardhide', keyboardFocusOut);
+
+ //deprecated
+ window.addEventListener('native.showkeyboard', keyboardNativeShow);
+ window.addEventListener('native.hidekeyboard', keyboardFocusOut);
+
+ } else {
+ document.body.addEventListener('focusout', keyboardFocusOut);
+ }
+
+ document.body.addEventListener('ionic.focusin', keyboardBrowserFocusIn);
+ document.body.addEventListener('focusin', keyboardBrowserFocusIn);
+
+ document.body.addEventListener('orientationchange', keyboardOrientationChange);
+
+ if (window.navigator.msPointerEnabled) {
+ document.removeEventListener("MSPointerDown", keyboardInit);
+ } else {
+ document.removeEventListener('touchstart', keyboardInit);
+ }
+}
+
+function keyboardNativeShow(e) {
+ clearTimeout(keyboardFocusOutTimer);
+ ionic.keyboard.height = e.keyboardHeight;
+}
+
+function keyboardBrowserFocusIn(e) {
+ if( !e.target || e.target.readOnly || !ionic.tap.isTextInput(e.target) || ionic.tap.isDateInput(e.target) || !keyboardIsWithinScroll(e.target) ) return;
+
+ document.addEventListener('keydown', keyboardOnKeyDown, false);
+
+ document.body.scrollTop = 0;
+ document.body.querySelector('.scroll-content').scrollTop = 0;
+
+ keyboardActiveElement = e.target;
+
+ keyboardSetShow(e);
+}
+
+function keyboardSetShow(e) {
+ clearTimeout(keyboardFocusInTimer);
+ clearTimeout(keyboardFocusOutTimer);
+
+ keyboardFocusInTimer = setTimeout(function(){
+ if ( keyboardLastShow + 350 > Date.now() ) return;
+ void 0;
+ keyboardLastShow = Date.now();
+ var keyboardHeight;
+ var elementBounds = keyboardActiveElement.getBoundingClientRect();
+ var count = 0;
+
+ keyboardPollHeightTimer = setInterval(function(){
+
+ keyboardHeight = keyboardGetHeight();
+ if (count > 10){
+ clearInterval(keyboardPollHeightTimer);
+ //waited long enough, just guess
+ keyboardHeight = 275;
+ }
+ if (keyboardHeight){
+ clearInterval(keyboardPollHeightTimer);
+ keyboardShow(e.target, elementBounds.top, elementBounds.bottom, keyboardViewportHeight, keyboardHeight);
+ }
+ count++;
+
+ }, 100);
+ }, 32);
+}
+
+function keyboardShow(element, elementTop, elementBottom, viewportHeight, keyboardHeight) {
+ var details = {
+ target: element,
+ elementTop: Math.round(elementTop),
+ elementBottom: Math.round(elementBottom),
+ keyboardHeight: keyboardHeight,
+ viewportHeight: viewportHeight
+ };
+
+ details.hasPlugin = keyboardHasPlugin();
+
+ details.contentHeight = viewportHeight - keyboardHeight;
+
+ void 0;
+
+ // figure out if the element is under the keyboard
+ details.isElementUnderKeyboard = (details.elementBottom > details.contentHeight);
+
+ ionic.keyboard.isOpen = true;
+
+ // send event so the scroll view adjusts
+ keyboardActiveElement = element;
+ ionic.trigger('scrollChildIntoView', details, true);
+
+ ionic.requestAnimationFrame(function(){
+ document.body.classList.add(KEYBOARD_OPEN_CSS);
+ });
+
+ // 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);
+ }
+
+ return details;
+}
+
+function keyboardFocusOut(e) {
+ clearTimeout(keyboardFocusOutTimer);
+
+ keyboardFocusOutTimer = setTimeout(ionic.keyboard.hide, 350);
+}
+
+function keyboardUpdateViewportHeight() {
+ if( getViewportHeight() > keyboardViewportHeight ) {
+ keyboardViewportHeight = getViewportHeight();
+ }
+}
+
+function keyboardOnKeyDown(e) {
+ if( ionic.scroll.isScrolling ) {
+ keyboardPreventDefault(e);
+ }
+}
+
+function keyboardPreventDefault(e) {
+ if( e.target.tagName !== 'TEXTAREA' ) {
+ e.preventDefault();
+ }
+}
+
+function keyboardOrientationChange() {
+ var updatedViewportHeight = getViewportHeight();
+
+ //too slow, have to wait for updated height
+ if (updatedViewportHeight === keyboardViewportHeight){
+ var count = 0;
+ var pollViewportHeight = setInterval(function(){
+ //give up
+ if (count > 10){
+ clearInterval(pollViewportHeight);
+ }
+
+ updatedViewportHeight = getViewportHeight();
+
+ if (updatedViewportHeight !== keyboardViewportHeight){
+ if (updatedViewportHeight < keyboardViewportHeight){
+ ionic.keyboard.landscape = true;
+ } else {
+ ionic.keyboard.landscape = false;
+ }
+ keyboardViewportHeight = updatedViewportHeight;
+ clearInterval(pollViewportHeight);
+ }
+ count++;
+
+ }, 50);
+ } else {
+ keyboardViewportHeight = updatedViewportHeight;
+ }
+}
+
+function keyboardGetHeight() {
+ // check if we are already have a keyboard height from the plugin
+ 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, wait for the screen to resize
+ if ( getViewportHeight() < keyboardViewportHeight ){
+ return keyboardViewportHeight - getViewportHeight();
+ } else {
+ return 0;
+ }
+ }
+
+ // fallback for when its the webview without the plugin
+ // or for just the standard web browser
+ if( ionic.Platform.isIOS() ) {
+ if ( ionic.keyboard.landscape ){
+ return 206;
+ }
+
+ if (!ionic.Platform.isWebView()){
+ return 216;
+ }
+
+ return 260;
+ }
+
+ // safe guess
+ return 275;
+}
+
+function getViewportHeight() {
+ return window.innerHeight || screen.height;
+}
+
+function keyboardIsWithinScroll(ele) {
+ while(ele) {
+ if(ele.classList.contains(SCROLL_CONTAINER_CSS)) {
+ return true;
+ }
+ ele = ele.parentElement;
+ }
+ return false;
+}
+
+function keyboardHasPlugin() {
+ return !!(window.cordova && cordova.plugins && cordova.plugins.Keyboard);
+}
+
+ionic.Platform.ready(function() {
+ keyboardUpdateViewportHeight();
+
+ // Android sometimes reports bad innerHeight on window.load
+ // try it again in a lil bit to play it safe
+ setTimeout(keyboardUpdateViewportHeight, 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, root) {
+ 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)
+ */
+
+var Scroller;
+
+(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,
+
+ /** 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,
+
+ 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.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;
+
+ self.scrollChildIntoView = function(e) {
+
+ //distance from bottom of scrollview to top of viewport
+ var scrollBottomOffsetToTop;
+
+ if ( !self.isScrolledIntoView ) {
+ // 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
+ if ((ionic.Platform.isIOS() || ionic.Platform.isFullScreen)){
+
+ // if there are things below the scroll view account for them and
+ // subtract them from the keyboard height when resizing
+ scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
+ var scrollBottomOffsetToBottom = e.detail.viewportHeight - scrollBottomOffsetToTop;
+ var keyboardOffset = Math.max(0, e.detail.keyboardHeight - scrollBottomOffsetToBottom);
+ container.style.height = (container.clientHeight - keyboardOffset) + "px";
+ container.style.overflow = "visible";
+ //update scroll view
+ self.resize();
+ }
+ self.isScrolledIntoView = true;
+ }
+
+ //If the element is positioned under the keyboard...
+ if ( e.detail.isElementUnderKeyboard ) {
+ var delay;
+ // Wait on android for web view to resize
+ if ( ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen ) {
+ // android y u resize so slow
+ if ( ionic.Platform.version() < 4.4) {
+ delay = 500;
+ } else {
+ // probably overkill for chrome
+ delay = 350;
+ }
+ } else {
+ delay = 80;
+ }
+
+ //Put element in middle of visible screen
+ //Wait for android to update view height and resize() to reset scroll position
+ ionic.scroll.isScrolling = true;
+ setTimeout(function(){
+ //middle of the scrollview, where we want to scroll to
+ var scrollMidpointOffset = container.clientHeight * 0.5;
+
+ scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
+ //distance from top of focused element to the bottom of the scroll view
+ var elementTopOffsetToScrollBottom = e.detail.elementTop - scrollBottomOffsetToTop;
+
+ var scrollTop = elementTopOffsetToScrollBottom + scrollMidpointOffset;
+
+ if (scrollTop > 0){
+ ionic.tap.cloneFocusedInput(container, self);
+ self.scrollBy(0, scrollTop, true);
+ self.onScroll();
+ }
+ }, delay);
+ }
+
+ //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(e) {
+ //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;
+ }
+ };
+
+ //Broadcasted when keyboard is shown on some platforms.
+ //See js/utils/keyboard.js
+ container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);
+ container.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.__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.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);
+ }
+ };
+
+ 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);
+
+ } 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);
+
+ } 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 (!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.timeStamp);
+
+ mousedown = false;
+ };
+
+ self.mouseWheel = ionic.animationFrameThrottle(function(e) {
+ var scrollParent = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'ionic-scroll');
+ if (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);
+ }
+ });
+
+ 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);
+ 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.__callback = self.scrollChildIntoView = self.resetScrollView = angular.noop;
+
+ self.mouseMove = self.mouseDown = self.mouseUp = self.mouseWheel =
+ self.touchStart = self.touchMove = self.touchEnd = self.touchCancel = angular.noop;
+
+ self.resize = self.scrollTo = self.zoomTo =
+ self.__scrollingComplete = angular.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, width, heightScale,
+ 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() {
+ 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()
+ );
+ },
+ /*
+ ---------------------------------------------------------------------------
+ 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) {
+ 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
+ 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.
+ */
+ activatePullToRefresh: function(height, activateCallback, deactivateCallback, startCallback, showCallback, hideCallback, tailCallback) {
+ var self = this;
+
+ self.__refreshHeight = height;
+ self.__refreshActivate = function(){ionic.requestAnimationFrame(activateCallback);};
+ self.__refreshDeactivate = function(){ionic.requestAnimationFrame(deactivateCallback);};
+ self.__refreshStart = function(){ionic.requestAnimationFrame(startCallback);};
+ self.__refreshShow = function(){ionic.requestAnimationFrame(showCallback);};
+ self.__refreshHide = function(){ionic.requestAnimationFrame(hideCallback);};
+ self.__refreshTail = function(){ionic.requestAnimationFrame(tailCallback);};
+ self.__refreshTailTime = 100;
+ self.__minSpinTime = 600;
+ },
+
+
+ /**
+ * Starts pull-to-refresh manually.
+ */
+ triggerPullToRefresh: function() {
+ // Use publish instead of scrollTo to allow scrolling to out of boundary position
+ // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
+ this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);
+
+ var d = new Date();
+ this.refreshStartTime = d.getTime();
+
+ if (this.__refreshStart) {
+ this.__refreshStart();
+ }
+ },
+
+
+ /**
+ * Signalizes that pull-to-refresh is finished.
+ */
+ finishPullToRefresh: function() {
+ var self = this;
+ // delay to make sure the spinner has a chance to spin for a split second before it's dismissed
+ var d = new Date();
+ var delay = 0;
+ if (self.refreshStartTime + self.__minSpinTime > d.getTime()){
+ delay = self.refreshStartTime + self.__minSpinTime - d.getTime();
+ }
+ setTimeout(function(){
+ if (self.__refreshTail){
+ self.__refreshTail();
+ }
+ setTimeout(function(){
+ self.__refreshActive = false;
+ if (self.__refreshDeactivate) {
+ self.__refreshDeactivate();
+ }
+ if (self.__refreshHide){
+ self.__refreshHide();
+ }
+
+ self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
+ },self.__refreshTailTime);
+ },delay);
+ },
+
+
+ /**
+ * Returns the scroll position and zooming values
+ *
+ * @return {Map} `left` and `top` scroll position and `zoom` level
+ */
+ getValues: function() {
+ return {
+ left: this.__scrollLeft,
+ top: this.__scrollTop,
+ zoom: this.__zoomLevel
+ };
+ },
+
+
+ /**
+ * Returns the maximum scroll values
+ *
+ * @return {Map} `left` and `top` maximum scroll values
+ */
+ getScrollMax: function() {
+ return {
+ left: this.__maxScrollLeft,
+ top: this.__maxScrollTop
+ };
+ },
+
+
+ /**
+ * Zooms to the given level. Supports optional animation. Zooms
+ * the center when no coordinates are given.
+ *
+ * @param level {Number} Level to zoom to
+ * @param animate {Boolean} Whether to use animation
+ * @param originLeft {Number} Zoom in at given left coordinate
+ * @param originTop {Number} Zoom in at given top coordinate
+ */
+ zoomTo: function(level, animate, originLeft, originTop) {
+ var self = this;
+
+ if (!self.options.zooming) {
+ throw new Error("Zooming is not enabled!");
+ }
+
+ // Stop deceleration
+ if (self.__isDecelerating) {
+ zyngaCore.effect.Animate.stop(self.__isDecelerating);
+ self.__isDecelerating = false;
+ }
+
+ var oldLevel = self.__zoomLevel;
+
+ // Normalize input origin to center of viewport if not defined
+ if (originLeft == null) {
+ originLeft = self.__clientWidth / 2;
+ }
+
+ if (originTop == null) {
+ originTop = self.__clientHeight / 2;
+ }
+
+ // Limit level according to configuration
+ level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
+
+ // Recompute maximum values while temporary tweaking maximum scroll ranges
+ self.__computeScrollMax(level);
+
+ // Recompute left and top coordinates based on new zoom level
+ var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft;
+ var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop;
+
+ // Limit x-axis
+ if (left > self.__maxScrollLeft) {
+ left = self.__maxScrollLeft;
+ } else if (left < 0) {
+ left = 0;
+ }
+
+ // Limit y-axis
+ if (top > self.__maxScrollTop) {
+ top = self.__maxScrollTop;
+ } else if (top < 0) {
+ top = 0;
+ }
+
+ // Push values out
+ self.__publish(left, top, level, animate);
+
+ },
+
+
+ /**
+ * Zooms the content by the given factor.
+ *
+ * @param factor {Number} Zoom by given factor
+ * @param animate {Boolean} Whether to use animation
+ * @param originLeft {Number} Zoom in at given left coordinate
+ * @param originTop {Number} Zoom in at given top coordinate
+ */
+ zoomBy: function(factor, animate, originLeft, originTop) {
+ this.zoomTo(this.__zoomLevel * factor, animate, originLeft, originTop);
+ },
+
+
+ /**
+ * Scrolls to the given position. Respect limitations and snapping automatically.
+ *
+ * @param left {Number} Horizontal scroll position, keeps current if value is null
+ * @param top {Number} Vertical scroll position, keeps current if value is null
+ * @param animate {Boolean} Whether the scrolling should happen using an animation
+ * @param zoom {Number} Zoom level to go to
+ */
+ scrollTo: function(left, top, animate, zoom, wasResize) {
+ var self = this;
+
+ // Stop deceleration
+ if (self.__isDecelerating) {
+ zyngaCore.effect.Animate.stop(self.__isDecelerating);
+ self.__isDecelerating = false;
+ }
+
+ // Correct coordinates based on new zoom level
+ if (zoom != null && zoom !== self.__zoomLevel) {
+
+ if (!self.options.zooming) {
+ throw new Error("Zooming is not enabled!");
+ }
+
+ left *= zoom;
+ top *= zoom;
+
+ // Recompute maximum values while temporary tweaking maximum scroll ranges
+ self.__computeScrollMax(zoom);
+
+ } else {
+
+ // Keep zoom when not defined
+ zoom = self.__zoomLevel;
+
+ }
+
+ if (!self.options.scrollingX) {
+
+ left = self.__scrollLeft;
+
+ } else {
+
+ if (self.options.paging) {
+ left = Math.round(left / self.__clientWidth) * self.__clientWidth;
+ } else if (self.options.snapping) {
+ left = Math.round(left / self.__snapWidth) * self.__snapWidth;
+ }
+
+ }
+
+ if (!self.options.scrollingY) {
+
+ top = self.__scrollTop;
+
+ } else {
+
+ if (self.options.paging) {
+ top = Math.round(top / self.__clientHeight) * self.__clientHeight;
+ } else if (self.options.snapping) {
+ top = Math.round(top / self.__snapHeight) * self.__snapHeight;
+ }
+
+ }
+
+ // Limit for allowed ranges
+ left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
+ top = Math.max(Math.min(self.__maxScrollTop, top), 0);
+
+ // Don't animate when no change detected, still call publish to make sure
+ // that rendered position is really in-sync with internal data
+ if (left === self.__scrollLeft && top === self.__scrollTop) {
+ animate = false;
+ }
+
+ // Publish new values
+ self.__publish(left, top, zoom, animate, wasResize);
+
+ },
+
+
+ /**
+ * Scroll by the given offset
+ *
+ * @param left {Number} Scroll x-axis by given offset
+ * @param top {Number} Scroll y-axis by given offset
+ * @param animate {Boolean} Whether to animate the given change
+ */
+ scrollBy: function(left, top, animate) {
+ var self = this;
+
+ var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
+ var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
+
+ self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
+ },
+
+
+
+ /*
+ ---------------------------------------------------------------------------
+ EVENT CALLBACKS
+ ---------------------------------------------------------------------------
+ */
+
+ /**
+ * Mouse wheel handler for zooming support
+ */
+ doMouseZoom: function(wheelDelta, timeStamp, pageX, pageY) {
+ var change = wheelDelta > 0 ? 0.97 : 1.03;
+ return this.zoomTo(this.__zoomLevel * change, false, pageX - this.__clientLeft, pageY - this.__clientTop);
+ },
+
+ /**
+ * Touch start handler for scrolling support
+ */
+ doTouchStart: function(touches, timeStamp) {
+ var self = this;
+
+ 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) {
+
+ // 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(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 ? 4 : 1;
+
+ // 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();
+ }
+ }
+
+ // 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();
+
+ // if ((self.options.scrollingX && !self.__maxScrollLeft) || (self.options.scrollingY && !self.__maxScrollTop)) {
+ // //self.__sizerTimeout = setTimeout(sizer, 1000);
+ // }
+ };
+
+ sizer();
+ self.__sizerTimeout = setTimeout(sizer, 1000);
+ },
+
+ /*
+ ---------------------------------------------------------------------------
+ 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(timeStamp) {
+ 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(renderedFramesPerSecond, animationId, wasFinished) {
+ 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) {
+'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(e) {
+ },
+ drag: function(e) {
+ },
+ end: function(e) {
+ },
+ isSameItem: function(item) {
+ return false;
+ }
+ };
+
+ var SlideDrag = function(opts) {
+ this.dragThresholdX = opts.dragThresholdX || 10;
+ this.el = opts.el;
+ 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(e) {
+ var lastDrag = this._lastDrag;
+
+ if (!lastDrag || !lastDrag.content) return;
+
+ lastDrag.content.style[ionic.CSS.TRANSITION] = '';
+ lastDrag.content.style[ionic.CSS.TRANSFORM] = '';
+ ionic.requestAnimationFrame(function() {
+ setTimeout(function() {
+ lastDrag.buttons && lastDrag.buttons.classList.add('invisible');
+ }, 250);
+ });
+ };
+
+ 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.style[ionic.CSS.TRANSFORM] = 'translate3d(' + newX + 'px, 0, 0)';
+ this._currentDrag.content.style[ionic.CSS.TRANSITION] = 'none';
+ }
+ });
+
+ SlideDrag.prototype.end = function(e, doneCallback) {
+ var _this = this;
+
+ // There is no drag, just end immediately
+ if (!this._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 = -this._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 > -(this._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) {
+ _this._currentDrag.content.style[ionic.CSS.TRANSFORM] = '';
+ var buttons = _this._currentDrag.buttons;
+ setTimeout(function() {
+ buttons && buttons.classList.add('invisible');
+ }, 250);
+ } else {
+ _this._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + restingPoint + 'px, 0, 0)';
+ }
+ _this._currentDrag.content.style[ionic.CSS.TRANSITION] = '';
+
+
+ // Kill the current drag
+ if (!_this._lastDrag) {
+ _this._lastDrag = {};
+ }
+ angular.extend(_this._lastDrag, _this._currentDrag);
+ if (_this._currentDrag) {
+ _this._currentDrag.buttons = null;
+ _this._currentDrag.content = null;
+ }
+ _this._currentDrag = null;
+
+ // We are done, notify caller
+ doneCallback && doneCallback();
+ });
+ };
+
+ var ReorderDrag = function(opts) {
+ this.dragThresholdY = opts.dragThresholdY || 0;
+ this.onReorder = opts.onReorder;
+ this.listEl = opts.listEl;
+ this.el = opts.el;
+ this.scrollEl = opts.scrollEl;
+ this.scrollView = opts.scrollView;
+ // Get the True Top of the list el http://www.quirksmode.org/js/findpos.html
+ this.listElTrueTop = 0;
+ if (this.listEl.offsetParent) {
+ var obj = this.listEl;
+ do {
+ this.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 = null;
+ this.el = null;
+ this.scrollEl = null;
+ this.scrollView = null;
+ };
+
+ ReorderDrag.prototype.start = function(e) {
+ var content;
+
+ 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 placeholder = this._currentDrag.placeholder;
+ var siblings = Array.prototype.slice.call(this._currentDrag.placeholder.parentNode.children)
+ .filter(function(el) {
+ return el.nodeName === self.el.nodeName && el !== self.el;
+ });
+
+ var dragOffsetTop = this._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 this._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 _this = this;
+
+ opts = ionic.extend({
+ onReorder: function(el, oldIndex, newIndex) {},
+ virtualRemoveThreshold: -200,
+ virtualAddThreshold: 200,
+ canSwipe: function() {
+ return true;
+ }
+ }, opts);
+
+ ionic.extend(this, opts);
+
+ if (!this.itemHeight && this.listEl) {
+ this.itemHeight = this.listEl.children[0] && parseInt(this.listEl.children[0].style.height, 10);
+ }
+
+ //ionic.views.ListView.__super__.initialize.call(this, opts);
+
+ this.onRefresh = opts.onRefresh || function() {};
+ this.onRefreshOpening = opts.onRefreshOpening || function() {};
+ this.onRefreshHolding = opts.onRefreshHolding || function() {};
+
+ window.ionic.onGesture('release', function(e) {
+ _this._handleEndDrag(e);
+ }, this.el);
+
+ window.ionic.onGesture('drag', function(e) {
+ _this._handleDrag(e);
+ }, this.el);
+ // Start the drag states
+ this._initDrag();
+ },
+
+ /**
+ * Be sure to cleanup references.
+ */
+ deregister: function() {
+ this.el = null;
+ this.listEl = null;
+ this.scrollEl = null;
+ this.scrollView = null;
+ },
+
+ /**
+ * 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) {
+ if (this.isVirtual) {
+ var itemHeight = this.itemHeight;
+
+ // TODO: This would be inaccurate if we are windowed
+ var totalItems = this.listEl.children.length;
+
+ // Grab the total height of the list
+ var scrollHeight = e.target.scrollHeight;
+
+ // Get the viewport height
+ var viewportHeight = this.el.parentNode.offsetHeight;
+
+ // scrollTop is the current scroll position
+ var scrollTop = e.scrollTop;
+
+ // 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 + this.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 + this.virtualAddThreshold);
+
+ // Compute how many items per viewport size can show
+ var itemsPerViewport = Math.floor((lowWater - highWater) / itemHeight);
+
+ // 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
+ this._virtualItemsToRemove = Array.prototype.slice.call(this.listEl.children, 0, first);
+
+ // Grab the nodes we will be showing
+ var nodes = Array.prototype.slice.call(this.listEl.children, first, first + itemsPerViewport);
+
+ this.renderViewport && this.renderViewport(highWater, lowWater, first, last);
+ }
+ },
+
+ didStopScrolling: function(e) {
+ if (this.isVirtual) {
+ for (var i = 0; i < this._virtualItemsToRemove.length; i++) {
+ var el = this._virtualItemsToRemove[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() {
+ if (this._lastDragOp) {
+ this._lastDragOp.clean && this._lastDragOp.clean();
+ this._lastDragOp.deregister && this._lastDragOp.deregister();
+ this._lastDragOp = null;
+ }
+ },
+
+ _initDrag: function() {
+ //ionic.views.ListView.__super__._initDrag.call(this);
+
+ // 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 _this = this;
+
+ var didStart = false;
+
+ this._isDragging = false;
+
+ var lastDragOp = this._lastDragOp;
+ var item;
+
+ // If we have an open SlideDrag and we're scrolling the list. Clear it.
+ if (this._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 = this._getItem(e.target);
+
+ if (item) {
+ this._dragOp = new ReorderDrag({
+ listEl: this.el,
+ el: item,
+ scrollEl: this.scrollEl,
+ scrollView: this.scrollView,
+ onReorder: function(el, start, end) {
+ _this.onReorder && _this.onReorder(el, start, end);
+ }
+ });
+ this._dragOp.start(e);
+ e.preventDefault();
+ }
+ }
+
+ // Or check if this is a swipe to the side drag
+ else if (!this._didDragUpOrDown && (e.gesture.direction == 'left' || e.gesture.direction == 'right') && Math.abs(e.gesture.deltaX) > 5) {
+
+ // Make sure this is an item with buttons
+ item = this._getItem(e.target);
+ if (item && item.querySelector('.item-options')) {
+ this._dragOp = new SlideDrag({ el: this.el, canSwipe: this.canSwipe });
+ this._dragOp.start(e);
+ e.preventDefault();
+ }
+ }
+
+ // If we had a last drag operation and this is a new one on a different item, clean that last one
+ if (lastDragOp && this._dragOp && !this._dragOp.isSameItem(lastDragOp) && e.defaultPrevented) {
+ lastDragOp.clean && lastDragOp.clean();
+ }
+ },
+
+
+ _handleEndDrag: function(e) {
+ var _this = this;
+
+ this._didDragUpOrDown = false;
+
+ if (!this._dragOp) {
+ //ionic.views.ListView.__super__._handleEndDrag.call(this, e);
+ return;
+ }
+
+ this._dragOp.end(e, function() {
+ _this._initDrag();
+ });
+ },
+
+ /**
+ * Process the drag event to move the item to the left or right.
+ */
+ _handleDrag: function(e) {
+ var _this = this, content, buttons;
+
+ if (Math.abs(e.gesture.deltaY) > 5) {
+ this._didDragUpOrDown = true;
+ }
+
+ // If we get a drag event, make sure we aren't in another drag, then check if we should
+ // start one
+ if (!this.isDragging && !this._dragOp) {
+ this._startDrag(e);
+ }
+
+ // No drag still, pass it up
+ if (!this._dragOp) {
+ //ionic.views.ListView.__super__._handleDrag.call(this, e);
+ return;
+ }
+
+ e.gesture.srcEvent.preventDefault();
+ this._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(e) {},
+ onEndDrag: function(e) {}
+ }, 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() {
+
+ // 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() {
+
+ if (options.continuous) slide(index-1);
+ else if (index) slide(index-1);
+
+ }
+
+ function next() {
+
+ if (options.continuous) slide(index+1);
+ else if (index < slides.length - 1) slide(index+1);
+
+ }
+
+ 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 natural_direction = 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 !== natural_direction) 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);
+ }
+
+ }
+
+ },
+ end: function(event) {
+
+ // 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);
+ }
+
+ },
+ 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
+ var pos = slides.length;
+ while(pos--) {
+
+ var slide = slides[pos];
+ slide.style.width = '';
+ slide.style.left = '';
+
+ if (browser.transitions) translate(pos, 0, 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(e) {
+ 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(amount) {
+ if (!self._dragInfo) { return; }
+
+ var slidePageLeft = self.track.offsetLeft + (self.handle.offsetWidth / 2);
+ var slidePageRight = self.track.offsetLeft + self.track.offsetWidth - (self.handle.offsetWidth / 2);
+ var dx = e.gesture.deltaX;
+
+ 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(e) {
+ this._dragInfo = null;
+ },
+
+ hold: function(e) {
+ 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
diff --git a/init.js b/override-fastclick.js
similarity index 100%
rename from init.js
rename to override-fastclick.js
diff --git a/package.js b/package.js
index 31d55f70..18988ab9 100644
--- a/package.js
+++ b/package.js
@@ -1,25 +1,32 @@
Package.describe({
summary: "Ionic framework packaged for Meteor.",
- version: "0.2.1",
+ version: "0.3.0",
git: "https://github.com/Urigo/meteor-ionic.git"
});
Package.on_use(function (api, where) {
api.versionsFrom('METEOR@0.9.0.1');
- api.use('urigo:angular@0.6.1', 'client', {weak: true});
- api.use('mquandalle:bower@0.1.11', 'client');
+ api.use('urigo:angular@0.6.5', 'client', {weak: true});
+ api.use('angularui:angular-ui-router@0.2.13', 'client');
api.use([
- 'fastclick@1.0.1'
+ 'fastclick@1.0.2'
], 'client', {weak : true});
- // Add bower
- api.addFiles('smart.json', 'client');
-
// Fix icons to absolute path
- api.addFiles('ionic-override.css', 'client');
+ api.addFiles([
+ 'dependencies/angular-animate.js',
+ 'dependencies/angular-sanitize.js',
+ 'lib/css/ionic.css',
+ 'lib/fonts/ionicons.eot',
+ 'lib/fonts/ionicons.svg',
+ 'lib/fonts/ionicons.ttf',
+ 'lib/fonts/ionicons.woff',
+ 'lib/js/ionic.js',
+ 'lib/js/ionic-angular.js'
+ ], 'client');
- // ionic files
- api.addFiles('init.js', 'client');
+ // Stop Meteor's Fastclick for Ionic one
+ api.addFiles('override-fastclick.js', 'client');
});
diff --git a/smart.json b/smart.json
deleted file mode 100644
index 4a2a02e5..00000000
--- a/smart.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "name": "ionic",
- "description": "Ionic framework packaged for Meteor",
- "homepage": "https://github.com/urigo/meteor-ionic",
- "author": "Uri Goldshtein & Marc Rovira ",
- "git": "https://github.com/urigo/meteor-ionic.git",
- "version": "0.2.1",
- "packages": {
- "bower": {}
- },
- "bower": {
- "angular-sanitize": "1.3.6",
- "angular-animate": "1.3.6",
- "ionic": "1.0.0-beta.14"
- }
-}
\ No newline at end of file