text: /*!
   * Copyright 2015 Drifty Co.
   * http://drifty.com/
   *
   * Ionic, v1.3.5
   * 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 () {
    /* eslint no-unused-vars:0 */
    var IonicModule = angular.module('ionic', ['ngAnimate', 'ngSanitize', 'ui.router', 'ngIOS9UIWebViewPatch']),
      extend = angular.extend,
      forEach = angular.forEach,
      isDefined = angular.isDefined,
      isNumber = angular.isNumber,
      isString = angular.isString,
      jqLite = angular.element,
      noop = angular.noop;
  
    /**
   * @ngdoc service
   * @name $ionicActionSheet
   * @module ionic
   * @description
   * The Action Sheet is a slide-up pane that lets the user choose from a set of options.
   * Dangerous options are highlighted in red and made obvious.
   *
   * There are easy ways to cancel out of the action sheet, such as tapping the backdrop or even
   * hitting escape on the keyboard for desktop testing.
   *
   * ![Action Sheet](http://ionicframework.com.s3.amazonaws.com/docs/controllers/actionSheet.gif)
   *
   * @usage
   * To trigger an Action Sheet in your code, use the $ionicActionSheet service in your angular controllers:
   *
   * ```js
   * angular.module('mySuperApp', ['ionic'])
   * .controller(function($scope, $ionicActionSheet, $timeout) {
   *
   *  // Triggered on a button click, or some other target
   *  $scope.show = function() {
   *
   *    // Show the action sheet
   *    var hideSheet = $ionicActionSheet.show({
   *      buttons: [
   *        { text: '<b>Share</b> This' },
   *        { text: 'Move' }
   *      ],
   *      destructiveText: 'Delete',
   *      titleText: 'Modify your album',
   *      cancelText: 'Cancel',
   *      cancel: function() {
            // add cancel code..
          },
   *      buttonClicked: function(index) {
   *        return true;
   *      }
   *    });
   *
   *    // For example's sake, hide the sheet after two seconds
   *    $timeout(function() {
   *      hideSheet();
   *    }, 2000);
   *
   *  };
   * });
   * ```
   *
   */
    IonicModule.factory('$ionicActionSheet', [
      '$rootScope',
      '$compile',
      '$animate',
      '$timeout',
      '$ionicTemplateLoader',
      '$ionicPlatform',
      '$ionicBody',
      'IONIC_BACK_PRIORITY',
      function ($rootScope, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform, $ionicBody, IONIC_BACK_PRIORITY) {
        return {
          show: actionSheet,
        };
  
        /**
         * @ngdoc method
         * @name $ionicActionSheet#show
         * @description
         * Load and return a new action sheet.
         *
         * A new isolated scope will be created for the
         * action sheet and the new element will be appended into the body.
         *
         * @param {object} options The options for this ActionSheet. Properties:
         *
         *  - `[Object]` `buttons` Which buttons to show.  Each button is an object with a `text` field.
         *  - `{string}` `titleText` The title to show on the action sheet.
         *  - `{string=}` `cancelText` the text for a 'cancel' button on the action sheet.
         *  - `{string=}` `destructiveText` The text for a 'danger' on the action sheet.
         *  - `{function=}` `cancel` Called if the cancel button is pressed, the backdrop is tapped or
         *     the hardware back button is pressed.
         *  - `{function=}` `buttonClicked` Called when one of the non-destructive buttons is clicked,
         *     with the index of the button that was clicked and the button object. Return true to close
         *     the action sheet, or false to keep it opened.
         *  - `{function=}` `destructiveButtonClicked` Called when the destructive button is clicked.
         *     Return true to close the action sheet, or false to keep it opened.
         *  -  `{boolean=}` `cancelOnStateChange` Whether to cancel the actionSheet when navigating
         *     to a new state.  Default true.
         *  - `{string}` `cssClass` The custom CSS class name.
         *
         * @returns {function} `hideSheet` A function which, when called, hides & cancels the action sheet.
         */
        function actionSheet(opts) {
          var scope = $rootScope.$new(true);
  
          extend(
            scope,
            {
              cancel: noop,
              destructiveButtonClicked: noop,
              buttonClicked: noop,
              $deregisterBackButton: noop,
              buttons: [],
              cancelOnStateChange: true,
            },
            opts || {}
          );
  
          function textForIcon(text) {
            if (text && /icon/.test(text)) {
              scope.$actionSheetHasIcon = true;
            }
          }
  
          for (var x = 0; x < scope.buttons.length; x++) {
            textForIcon(scope.buttons[x].text);
          }
          textForIcon(scope.cancelText);
          textForIcon(scope.destructiveText);
  
          // Compile the template
          var element = (scope.element = $compile('<ion-action-sheet ng-class="cssClass" buttons="buttons"></ion-action-sheet>')(
            scope
          ));
  
          // Grab the sheet element for animation
          var sheetEl = jqLite(element[0].querySelector('.action-sheet-wrapper'));
  
          var stateChangeListenDone = scope.cancelOnStateChange
            ? $rootScope.$on('$stateChangeSuccess', function () {
                scope.cancel();
              })
            : noop;
  
          // removes the actionSheet from the screen
          scope.removeSheet = function (done) {
            if (scope.removed) return;
  
            scope.removed = true;
            sheetEl.removeClass('action-sheet-up');
            $timeout(function () {
              // wait to remove this due to a 300ms delay native
              // click which would trigging whatever was underneath this
              $ionicBody.removeClass('action-sheet-open');
            }, 400);
            scope.$deregisterBackButton();
            stateChangeListenDone();
  
            $animate.removeClass(element, 'active').then(function () {
              scope.$destroy();
              element.remove();
              // scope.cancel.$scope is defined near the bottom
              scope.cancel.$scope = sheetEl = null;
              (done || noop)(opts.buttons);
            });
          };
  
          scope.showSheet = function (done) {
            if (scope.removed) return;
  
            $ionicBody.append(element).addClass('action-sheet-open');
  
            $animate.addClass(element, 'active').then(function () {
              if (scope.removed) return;
              (done || noop)();
            });
            $timeout(
              function () {
                if (scope.removed) return;
                sheetEl.addClass('action-sheet-up');
              },
              20,
              false
            );
          };
  
          // registerBackButtonAction returns a callback to deregister the action
          scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(function () {
            $timeout(scope.cancel);
          }, IONIC_BACK_PRIORITY.actionSheet);
  
          // called when the user presses the cancel button
          scope.cancel = function () {
            // after the animation is out, call the cancel callback
            scope.removeSheet(opts.cancel);
          };
  
          scope.buttonClicked = function (index) {
            // Check if the button click event returned true, which means
            // we can close the action sheet
            if (opts.buttonClicked(index, opts.buttons[index]) === true) {
              scope.removeSheet();
            }
          };
  
          scope.destructiveButtonClicked = function () {
            // Check if the destructive button click event returned true, which means
            // we can close the action sheet
            if (opts.destructiveButtonClicked() === true) {
              scope.removeSheet();
            }
          };
  
          scope.showSheet();
  
          // Expose the scope on $ionicActionSheet's return value for the sake
          // of testing it.
          scope.cancel.$scope = scope;
  
          return scope.cancel;
        }
      },
    ]);
  
    jqLite.prototype.addClass = function (cssClasses) {
      var x, y, cssClass, el, splitClasses, existingClasses;
      if (cssClasses && cssClasses != 'ng-scope' && cssClasses != 'ng-isolate-scope') {
        for (x = 0; x < this.length; x++) {
          el = this[x];
          if (el.setAttribute) {
            if (cssClasses.indexOf(' ') < 0 && el.classList.add) {
              el.classList.add(cssClasses);
            } else {
              existingClasses = (' ' + (el.getAttribute('class') || '') + ' ').replace(/[\n\t]/g, ' ');
              splitClasses = cssClasses.split(' ');
  
              for (y = 0; y < splitClasses.length; y++) {
                cssClass = splitClasses[y].trim();
                if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) {
                  existingClasses += cssClass + ' ';
                }
              }
              el.setAttribute('class', existingClasses.trim());
            }
          }
        }
      }
      return this;
    };
  
    jqLite.prototype.removeClass = function (cssClasses) {
      var x, y, splitClasses, cssClass, el;
      if (cssClasses) {
        for (x = 0; x < this.length; x++) {
          el = this[x];
          if (el.getAttribute) {
            if (cssClasses.indexOf(' ') < 0 && el.classList.remove) {
              el.classList.remove(cssClasses);
            } else {
              splitClasses = cssClasses.split(' ');
  
              for (y = 0; y < splitClasses.length; y++) {
                cssClass = splitClasses[y];
                el.setAttribute(
                  'class',
                  (' ' + (el.getAttribute('class') || '') + ' ')
                    .replace(/[\n\t]/g, ' ')
                    .replace(' ' + cssClass.trim() + ' ', ' ')
                    .trim()
                );
              }
            }
          }
        }
      }
      return this;
    };
  
    /**
     * @ngdoc service
     * @name $ionicBackdrop
     * @module ionic
     * @description
     * Shows and hides a backdrop over the UI.  Appears behind popups, loading,
     * and other overlays.
     *
     * Often, multiple UI components require a backdrop, but only one backdrop is
     * ever needed in the DOM at a time.
     *
     * Therefore, each component that requires the backdrop to be shown calls
     * `$ionicBackdrop.retain()` when it wants the backdrop, then `$ionicBackdrop.release()`
     * when it is done with the backdrop.
     *
     * For each time `retain` is called, the backdrop will be shown until `release` is called.
     *
     * For example, if `retain` is called three times, the backdrop will be shown until `release`
     * is called three times.
     *
     * **Notes:**
     * - The backdrop service will broadcast 'backdrop.shown' and 'backdrop.hidden' events from the root scope,
     * this is useful for alerting native components not in html.
     *
     * @usage
     *
     * ```js
     * function MyController($scope, $ionicBackdrop, $timeout, $rootScope) {
     *   //Show a backdrop for one second
     *   $scope.action = function() {
     *     $ionicBackdrop.retain();
     *     $timeout(function() {
     *       $ionicBackdrop.release();
     *     }, 1000);
     *   };
     *
     *   // Execute action on backdrop disappearing
     *   $scope.$on('backdrop.hidden', function() {
     *     // Execute action
     *   });
     *
     *   // Execute action on backdrop appearing
     *   $scope.$on('backdrop.shown', function() {
     *     // Execute action
     *   });
     *
     * }
     * ```
     */
    IonicModule.factory('$ionicBackdrop', [
      '$document',
      '$timeout',
      '$$rAF',
      '$rootScope',
      function ($document, $timeout, $$rAF, $rootScope) {
        var el = jqLite('<div class="backdrop">');
        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() {
          backdropHolds++;
          if (backdropHolds === 1) {
            el.addClass('visible');
            $rootScope.$broadcast('backdrop.shown');
            $$rAF(function () {
              // If we're still at >0 backdropHolds after async...
              if (backdropHolds >= 1) el.addClass('active');
            });
          }
        }
        function release() {
          if (backdropHolds === 1) {
            el.removeClass('active');
            $rootScope.$broadcast('backdrop.hidden');
            $timeout(
              function () {
                // If we're still at 0 backdropHolds after async...
                if (backdropHolds === 0) el.removeClass('visible');
              },
              400,
              false
            );
          }
          backdropHolds = Math.max(0, backdropHolds - 1);
        }
  
        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#addClass
           * @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 preventClick(ev) {
          ev.preventDefault();
          ev.stopPropagation();
        }
  
        function addClickBlock() {
          if (pendingShow) {
            if (cbEle) {
              cbEle.classList.remove(CSS_HIDE);
            } else {
              cbEle = $document[0].createElement('div');
              cbEle.className = 'click-block';
              $ionicBody.append(cbEle);
              cbEle.addEventListener('touchstart', preventClick);
              cbEle.addEventListener('mousedown', preventClick);
            }
            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, false);
            addClickBlock();
          },
          hide: function () {
            pendingShow = false;
            $timeout.cancel(fallbackTimer);
            removeClickBlock();
          },
        };
      },
    ]);
  
    /**
     * @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, deregisterStateChangeListener, 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 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) {
                // the new view is being removed from it's old position in the history and being placed at the top,
                // so we need to update any views that reference it as a backview, otherwise there will be infinitely loops
                var viewIds = Object.keys(viewHistory.views);
                viewIds.forEach(function (viewId) {
                  var view = viewHistory.views[viewId];
                  if (view.backViewId === switchToView.viewId && view.historyId !== switchToView.historyId) {
                    view.backViewId = null;
                  }
                });
  
                hist.stack[hist.cursor].backViewId = currentView.viewId;
              }
            } else {
              // create an element from the viewLocals template
              ele = $ionicViewSwitcher.createViewEle(viewLocals);
              if (this.isAbstractEle(ele, viewLocals)) {
                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
                      var stackItem = tmp.stack[x];
                      stackItem && stackItem.destroy && stackItem.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) {
                  // DB: this is a new view in a different tab
                  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,
                canSwipeBack: canSwipeBack(ele, viewLocals),
              });
  
              // add the new view to this history's stack
              hist.stack.push(viewHistory.views[viewId]);
            }
  
            deregisterStateChangeListener && deregisterStateChangeListener();
            $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;
                  if (x > 0) {
                    hist.stack[x - 1].forwardViewId = null;
                  }
                  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;
                }
              }
            }
  
            hist.cursor = viewHistory.currentView.index;
  
            return {
              viewId: viewId,
              action: action,
              direction: direction,
              historyId: historyId,
              enableBack: this.enabledBack(viewHistory.currentView),
              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 (view) {
            var backView = (view && getViewById(view.backViewId)) || viewHistory.backView;
            return backView && 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
           * @param {number=} backCount Optional negative integer setting how many views to go
           * back. By default it'll go back one view by using the value `-1`. To go back two
           * views you would use `-2`. If the number goes farther back than the number of views
           * in the current history's stack then it'll go to the first view in the current history's
           * stack. If the number is zero or greater then it'll do nothing. It also does not
           * cross history stacks, meaning it can only go as far back as the current history.
           * @description Navigates the app to the back view, if a back view exists.
           */
          goBack: function (backCount) {
            if (isDefined(backCount) && backCount !== -1) {
              if (backCount > -1) return;
  
              var currentHistory = viewHistory.histories[this.currentHistoryId()];
              var newCursor = currentHistory.cursor + backCount + 1;
              if (newCursor < 1) {
                newCursor = 1;
              }
  
              currentHistory.cursor = newCursor;
              setNavViews(currentHistory.stack[newCursor].viewId);
  
              var cursor = newCursor - 1;
              var clearStateIds = [];
              var fwdView = getViewById(currentHistory.stack[cursor].forwardViewId);
              while (fwdView) {
                clearStateIds.push(fwdView.stateId || fwdView.viewId);
                cursor++;
                if (cursor >= currentHistory.stack.length) break;
                fwdView = getViewById(currentHistory.stack[cursor].forwardViewId);
              }
  
              var self = this;
              if (clearStateIds.length) {
                $timeout(function () {
                  self.clearCache(clearStateIds);
                }, 300);
              }
            }
  
            viewHistory.backView && viewHistory.backView.go();
          },
  
          /**
           * @ngdoc method
           * @name $ionicHistory#removeBackView
           * @description Remove the previous view from the history completely, including the
           * cached element and scope (if they exist).
           */
          removeBackView: function () {
            var self = this;
            var currentHistory = viewHistory.histories[this.currentHistoryId()];
            var currentCursor = currentHistory.cursor;
  
            var currentView = currentHistory.stack[currentCursor];
            var backView = currentHistory.stack[currentCursor - 1];
            var replacementView = currentHistory.stack[currentCursor - 2];
  
            // fail if we dont have enough views in the history
            if (!backView || !replacementView) {
              return;
            }
  
            // remove the old backView and the cached element/scope
            currentHistory.stack.splice(currentCursor - 1, 1);
            self.clearCache([backView.viewId]);
            // make the replacementView and currentView point to each other (bypass the old backView)
            currentView.backViewId = replacementView.viewId;
            currentView.index = currentView.index - 1;
            replacementView.forwardViewId = currentView.viewId;
            // update the cursor and set new backView
            viewHistory.backView = replacementView;
            currentHistory.currentCursor += -1;
          },
  
          enabledBack: function (view) {
            var backView = getBackView(view);
            return !!(backView && backView.historyId === view.historyId);
          },
  
          /**
           * @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
           * @return promise
           * @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 (stateIds) {
            return $timeout(function () {
              $ionicNavViewDelegate._instances.forEach(function (instance) {
                instance.clearCache(stateIds);
              });
            });
          },
  
          /**
           * @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) {
            deregisterStateChangeListener && deregisterStateChangeListener();
            if (arguments.length) {
              $timeout.cancel(nextViewExpireTimer);
              if (opts === null) {
                nextViewOptions = opts;
              } else {
                nextViewOptions = nextViewOptions || {};
                extend(nextViewOptions, opts);
                if (nextViewOptions.expire) {
                  deregisterStateChangeListener = $rootScope.$on('$stateChangeSuccess', function () {
                    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);
        }
  
        function canSwipeBack(ele, viewLocals) {
          if (viewLocals && viewLocals.$$state && viewLocals.$$state.self.canSwipeBack === false) {
            return false;
          }
          if (ele && ele.attr('can-swipe-back') === 'false') {
            return false;
          }
          var eleChild = ele.find('ion-view');
          if (eleChild && eleChild.attr('can-swipe-back') === 'false') {
            return false;
          }
          return true;
        }
      },
    ]).run([
      '$rootScope',
      '$state',
      '$location',
      '$document',
      '$ionicPlatform',
      '$ionicHistory',
      'IONIC_BACK_PRIORITY',
      function ($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory, IONIC_BACK_PRIORITY) {
        // 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 null;
  
          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 (backCount) {
          $ionicHistory.goBack(backCount);
        };
  
        // 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, IONIC_BACK_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 its 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 perform 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#views.swipeBackEnabled
     * @description  By default on iOS devices, swipe to go back functionality is enabled by default.
     * This method can be used to disable it globally, or on a per-view basis.
     * Note: This functionality is only supported on iOS.
     * @param {boolean} value
     * @returns {boolean}
     */
  
    /**
     * @ngdoc method
     * @name $ionicConfigProvider#scrolling.jsScrolling
     * @description  Whether to use JS or Native scrolling. Defaults to native scrolling. Setting this to
     * `true` has the same effect as setting each `ion-content` to have `overflow-scroll='false'`.
     * @param {boolean} value Defaults to `false` as of Ionic 1.2
     * @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#form.checkbox
     * @description Checkbox style. Android defaults to `square` and iOS defaults to `circle`.
     * @param {string} value
     * @returns {string}
     */
  
    /**
     * @ngdoc method
     * @name $ionicConfigProvider#form.toggle
     * @description Toggle item style. Android defaults to `small` and iOS defaults to `large`.
     * @param {string} value
     * @returns {string}
     */
  
    /**
     * @ngdoc method
     * @name $ionicConfigProvider#spinner.icon
     * @description Default spinner icon to use.
     * @param {string} value Can be: `android`, `ios`, `ios-small`, `bubbles`, `circles`, `crescent`,
     * `dots`, `lines`, `ripple`, or `spiral`.
     * @returns {string}
     */
  
    /**
     * @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,
          swipeBackEnabled: PLATFORM,
          swipeBackHitWidth: PLATFORM,
        },
        navBar: {
          alignTitle: PLATFORM,
          positionPrimaryButtons: PLATFORM,
          positionSecondaryButtons: PLATFORM,
          transition: PLATFORM,
        },
        backButton: {
          icon: PLATFORM,
          text: PLATFORM,
          previousTitleText: PLATFORM,
        },
        form: {
          checkbox: PLATFORM,
          toggle: PLATFORM,
        },
        scrolling: {
          jsScrolling: PLATFORM,
        },
        spinner: {
          icon: PLATFORM,
        },
        tabs: {
          style: PLATFORM,
          position: PLATFORM,
        },
        templates: {
          maxPrefetch: PLATFORM,
        },
        platform: {},
      };
      createConfig(configProperties, provider, '');
  
      // Default
      // -------------------------
      setPlatformConfig('default', {
        views: {
          maxCache: 10,
          forwardCache: false,
          transition: 'ios',
          swipeBackEnabled: true,
          swipeBackHitWidth: 45,
        },
  
        navBar: {
          alignTitle: 'center',
          positionPrimaryButtons: 'left',
          positionSecondaryButtons: 'right',
          transition: 'view',
        },
  
        backButton: {
          icon: 'ion-ios-arrow-back',
          text: 'Back',
          previousTitleText: true,
        },
  
        form: {
          checkbox: 'circle',
          toggle: 'large',
        },
  
        scrolling: {
          jsScrolling: true,
        },
  
        spinner: {
          icon: 'ios',
        },
  
        tabs: {
          style: 'standard',
          position: 'bottom',
        },
  
        templates: {
          maxPrefetch: 30,
        },
      });
  
      // iOS (it is the default already)
      // -------------------------
      setPlatformConfig('ios', {});
  
      // Android
      // -------------------------
      setPlatformConfig('android', {
        views: {
          transition: 'android',
          swipeBackEnabled: false,
        },
  
        navBar: {
          alignTitle: 'left',
          positionPrimaryButtons: 'right',
          positionSecondaryButtons: 'right',
        },
  
        backButton: {
          icon: 'ion-android-arrow-back',
          text: false,
          previousTitleText: false,
        },
  
        form: {
          checkbox: 'square',
          toggle: 'small',
        },
  
        spinner: {
          icon: 'android',
        },
  
        tabs: {
          style: 'striped',
          position: 'top',
        },
  
        scrolling: {
          jsScrolling: false,
        },
      });
  
      // Windows Phone
      // -------------------------
      setPlatformConfig('windowsphone', {
        //scrolling: {
        //  jsScrolling: false
        //}
        spinner: {
          icon: 'android',
        },
      });
  
      provider.transitions = {
        views: {},
        navBar: {},
      };
  
      // iOS Transitions
      // -----------------------
      provider.transitions.views.ios = function (enteringEle, leavingEle, direction, shouldAnimate) {
        function setStyles(ele, opacity, x, boxShadowOpacity) {
          var css = {};
          css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0;
          css.opacity = opacity;
          if (boxShadowOpacity > -1) {
            css.boxShadow = '0 0 10px rgba(0,0,0,' + (d.shouldAnimate ? boxShadowOpacity * 0.45 : 0.3) + ')';
          }
          css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
          ionic.DomUtil.cachedStyles(ele, css);
        }
  
        var d = {
          run: function (step) {
            if (direction == 'forward') {
              setStyles(enteringEle, 1, (1 - step) * 99, 1 - step); // starting at 98% prevents a flicker
              setStyles(leavingEle, 1 - 0.1 * step, step * -33, -1);
            } else if (direction == 'back') {
              setStyles(enteringEle, 1 - 0.1 * (1 - step), (1 - step) * -33, -1);
              setStyles(leavingEle, 1, step * 100, 1 - step);
            } else {
              // swap, enter, exit
              setStyles(enteringEle, 1, 0, -1);
              setStyles(leavingEle, 0, 0, -1);
            }
          },
          shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back'),
        };
  
        return d;
      };
  
      provider.transitions.navBar.ios = function (enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {
        function setStyles(ctrl, opacity, titleX, backTextX) {
          var css = {};
          css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : '0ms';
          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 || !ctrlB) 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 || !ctrlB) return;
          var titleX = (-(ctrlA.titleTextX() - ctrlB.backButtonTextLeft()) - ctrlA.titleLeftRight()) * step;
          setStyles(ctrlA, 1 - step, titleX, 0);
        }
  
        var d = {
          run: function (step) {
            var enteringHeaderCtrl = enteringHeaderBar.controller();
            var leavingHeaderCtrl = leavingHeaderBar && leavingHeaderBar.controller();
            if (d.direction == 'back') {
              leave(enteringHeaderCtrl, leavingHeaderCtrl, 1 - step);
              enter(leavingHeaderCtrl, enteringHeaderCtrl, 1 - step);
            } else {
              enter(enteringHeaderCtrl, leavingHeaderCtrl, step);
              leave(leavingHeaderCtrl, enteringHeaderCtrl, step);
            }
          },
          direction: direction,
          shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back'),
        };
  
        return d;
      };
  
      // Android Transitions
      // -----------------------
  
      provider.transitions.views.android = function (enteringEle, leavingEle, direction, shouldAnimate) {
        shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back');
  
        function setStyles(ele, x, opacity) {
          var css = {};
          css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0;
          css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
          css.opacity = opacity;
          ionic.DomUtil.cachedStyles(ele, css);
        }
  
        var d = {
          run: function (step) {
            if (direction == 'forward') {
              setStyles(enteringEle, (1 - step) * 99, 1); // starting at 98% prevents a flicker
              setStyles(leavingEle, step * -100, 1);
            } else if (direction == 'back') {
              setStyles(enteringEle, (1 - step) * -100, 1);
              setStyles(leavingEle, step * 100, 1);
            } else {
              // swap, enter, exit
              setStyles(enteringEle, 0, 1);
              setStyles(leavingEle, 0, 0);
            }
          },
          shouldAnimate: shouldAnimate,
        };
  
        return d;
      };
  
      provider.transitions.navBar.android = function (enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {
        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: shouldAnimate && (direction == 'forward' || direction == 'back'),
        };
      };
  
      // No Transition
      // -----------------------
  
      provider.transitions.views.none = function (enteringEle, leavingEle) {
        return {
          run: function (step) {
            provider.transitions.views.android(enteringEle, leavingEle, false, false).run(step);
          },
          shouldAnimate: false,
        };
      };
  
      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);
          },
          shouldAnimate: false,
        };
      };
  
      // 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;
      };
    })
      // Fix for URLs in Cordova apps on Windows Phone
      // http://blogs.msdn.com/b/msdn_answers/archive/2015/02/10/
      // running-cordova-apps-on-windows-and-windows-phone-8-1-using-ionic-angularjs-and-other-frameworks.aspx
      .config([
        '$compileProvider',
        function ($compileProvider) {
          $compileProvider.aHrefSanitizationWhitelist(
            /^\s*(https?|sms|tel|geo|ftp|mailto|file|ghttps?|ms-appx-web|ms-appx|x-wmapp0):/
          );
          $compileProvider.imgSrcSanitizationWhitelist(
            /^\s*(https?|ftp|file|content|blob|ms-appx|ms-appx-web|x-wmapp0):|data:image\//
          );
        },
      ]);
  
    var LOADING_TPL = '<div class="loading-container">' + '<div class="loading">' + '</div>' + '</div>';
  
    /**
     * @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...',
     *       duration: 3000
     *     }).then(function(){
     *        console.log("The loading indicator is now displayed");
     *     });
     *   };
     *   $scope.hide = function(){
     *     $ionicLoading.hide().then(function(){
     *        console.log("The loading indicator is now hidden");
     *     });
     *   };
     * });
     * ```
     */
    /**
     * @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() {
     *     //options default to values in $ionicLoadingConfig
     *     $ionicLoading.show().then(function(){
     *        console.log("The loading indicator is now displayed");
     *     });
     *   };
     * });
     * ```
     */
    IonicModule.constant('$ionicLoadingConfig', {
      template: '<ion-spinner></ion-spinner>',
    }).factory('$ionicLoading', [
      '$ionicLoadingConfig',
      '$ionicBody',
      '$ionicTemplateLoader',
      '$ionicBackdrop',
      '$timeout',
      '$q',
      '$log',
      '$compile',
      '$ionicPlatform',
      '$rootScope',
      'IONIC_BACK_PRIORITY',
      function (
        $ionicLoadingConfig,
        $ionicBody,
        $ionicTemplateLoader,
        $ionicBackdrop,
        $timeout,
        $q,
        $log,
        $compile,
        $ionicPlatform,
        $rootScope,
        IONIC_BACK_PRIORITY
      ) {
        var loaderInstance;
        //default values
        var deregisterBackAction = noop;
        var deregisterStateListener1 = noop;
        var deregisterStateListener2 = 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.
           * @returns {promise} A promise which is resolved when the loading indicator is presented.
           * @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.
           * @returns {promise} A promise which is resolved when the loading indicator is hidden.
           */
          hide: hideLoader,
          /**
           * @private for testing
           */
          _getLoader: getLoader,
        };
  
        function getLoader() {
          if (!loaderInstance) {
            loaderInstance = $ionicTemplateLoader
              .compile({
                template: LOADING_TPL,
                appendTo: $ionicBody.get(),
              })
              .then(function (self) {
                self.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 (!self.isShown) {
                    //options.showBackdrop: deprecated
                    self.hasBackdrop = !options.noBackdrop && options.showBackdrop !== false;
                    if (self.hasBackdrop) {
                      $ionicBackdrop.retain();
                      $ionicBackdrop.getElement().addClass('backdrop-loading');
                    }
                  }
  
                  if (options.duration) {
                    $timeout.cancel(self.durationTimeout);
                    self.durationTimeout = $timeout(angular.bind(self, self.hide), +options.duration);
                  }
  
                  deregisterBackAction();
                  //Disable hardware back button while loading
                  deregisterBackAction = $ionicPlatform.registerBackButtonAction(noop, IONIC_BACK_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');
                        }
                      });
                    }
                  });
  
                  self.isShown = true;
                };
                self.hide = function () {
                  deregisterBackAction();
                  if (self.isShown) {
                    if (self.hasBackdrop) {
                      $ionicBackdrop.release();
                      $ionicBackdrop.getElement().removeClass('backdrop-loading');
                    }
                    self.element.removeClass('active');
                    $ionicBody.removeClass('loading-active');
                    self.element.removeClass('visible');
                    ionic.requestAnimationFrame(function () {
                      !self.isShown && self.element.removeClass('visible');
                    });
                  }
                  $timeout.cancel(self.durationTimeout);
                  self.isShown = false;
                  var loading = self.element.children();
                  loading.html('');
                };
  
                return self;
              });
          }
          return loaderInstance;
        }
  
        function showLoader(options) {
          options = extend({}, $ionicLoadingConfig || {}, options || {});
          // use a default delay of 100 to avoid some issues reported on github
          // https://github.com/ionic-team/ionic/issues/3717
          var delay = options.delay || options.showDelay || 0;
  
          deregisterStateListener1();
          deregisterStateListener2();
          if (options.hideOnStateChange) {
            deregisterStateListener1 = $rootScope.$on('$stateChangeSuccess', hideLoader);
            deregisterStateListener2 = $rootScope.$on('$stateChangeError', hideLoader);
          }
  
          //If loading.show() was called previously, cancel it and show with our new options
          $timeout.cancel(loadingShowDelay);
          loadingShowDelay = $timeout(noop, delay);
          return loadingShowDelay.then(getLoader).then(function (loader) {
            return loader.show(options);
          });
        }
  
        function hideLoader() {
          deregisterStateListener1();
          deregisterStateListener2();
          $timeout.cancel(loadingShowDelay);
          return getLoader().then(function (loader) {
            return loader.hide();
          });
        }
      },
    ]);
  
    /**
     * @ngdoc service
     * @name $ionicModal
     * @module ionic
     * @codepen gblny
     * @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 `<ion-modal-view>` 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
     * <script id="my-modal.html" type="text/ng-template">
     *   <ion-modal-view>
     *     <ion-header-bar>
     *       <h1 class="title">My Modal title</h1>
     *     </ion-header-bar>
     *     <ion-content>
     *       Hello!
     *     </ion-content>
     *   </ion-modal-view>
     * </script>
     * ```
     * ```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',
      '$ionicClickBlock',
      '$window',
      'IONIC_BACK_PRIORITY',
      function (
        $rootScope,
        $ionicBody,
        $compile,
        $timeout,
        $ionicPlatform,
        $ionicTemplateLoader,
        $$q,
        $log,
        $ionicClickBlock,
        $window,
        IONIC_BACK_PRIORITY
      ) {
        /**
         * @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. Will only show the keyboard on iOS, to force the keyboard to show
           *    on Android, please use the [Ionic keyboard plugin](https://github.com/ionic-team/ionic-plugin-keyboard#keyboardshow).
           *    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 $$q.when();
            }
  
            // on iOS, clicks will sometimes bleed through/ghost click on underlying
            // elements
            $ionicClickBlock.show(600);
            stack.add(self);
  
            var modalEl = jqLite(self.modalEl);
  
            self.el.classList.remove('hide');
            $timeout(
              function () {
                if (!self._isShown) return;
                $ionicBody.addClass(self.viewType + '-open');
              },
              400,
              false
            );
  
            if (!self.el.parentElement) {
              modalEl.addClass(self.animation);
              $ionicBody.append(self.el);
            }
  
            // if modal was closed while the keyboard was up, reset scroll view on
            // next show since we can only resize it once it's visible
            var scrollCtrl = modalEl.data('$$ionicScrollController');
            scrollCtrl && scrollCtrl.resize();
  
            if (target && self.positionView) {
              self.positionView(target, modalEl);
              // set up a listener for in case the window size changes
  
              self._onWindowResize = function () {
                if (self._isShown) self.positionView(target, modalEl);
              };
              ionic.on('resize', self._onWindowResize, 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) : noop,
              IONIC_BACK_PRIORITY.modal
            );
  
            ionic.views.Modal.prototype.show.call(self);
  
            $timeout(function () {
              if (!self._isShown) return;
              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');
              self.scope.$broadcast('$ionicFooter.align');
              self.scope.$broadcast('$ionic.modalPresented');
            }, 20);
  
            return $timeout(function () {
              if (!self._isShown) return;
              self.$el.on('touchmove', function (e) {
                //Don't allow scrolling while open by dragging on backdrop
                var isInScroll = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'scroll');
                if (!isInScroll) {
                  e.preventDefault();
                }
              });
              //After animating in, allow hide on backdrop click
              self.$el.on('click', function (e) {
                if (self.backdropClickToClose && e.target === self.el && stack.isHighest(self)) {
                  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);
  
            // on iOS, clicks will sometimes bleed through/ghost click on underlying
            // elements
            $ionicClickBlock.show(600);
            stack.remove(self);
  
            self.el.classList.remove('active');
            modalEl.addClass('ng-leave');
  
            $timeout(
              function () {
                if (self._isShown) return;
                modalEl.addClass('ng-leave-active').removeClass('ng-enter ng-enter-active active');
  
                self.scope.$broadcast('$ionic.modalRemoved');
              },
              20,
              false
            );
  
            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', self._onWindowResize, window);
            }
  
            return $timeout(function () {
              var otherSibling = false;
              for (var i = 0; i < modalStack.length; ++i) {
                if (modalStack[i].viewType === self.viewType) {
                  // there are other modal (or popover, depending on viewType)
                  otherSibling = true;
                  break;
                }
              }
              if (!otherSibling) {
                $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,
              deferred,
              promise;
            self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self);
  
            // Only hide modal, when it is actually shown!
            // The hide function shows a click-block-div for a split second, because on iOS,
            // clicks will sometimes bleed through/ghost click on underlying elements.
            // However, this will make the app unresponsive for short amount of time.
            // We don't want that, if the modal window is already hidden.
            if (self._isShown) {
              promise = self.hide();
            } else {
              deferred = $$q.defer();
              deferred.resolve();
              promise = deferred.promise;
            }
  
            return promise.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('<ion-' + options.viewType + '>' + templateString + '</ion-' + options.viewType + '>')(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;
        };
  
        var modalStack = [];
        var stack = {
          add: function (modal) {
            modalStack.push(modal);
          },
          remove: function (modal) {
            var index = modalStack.indexOf(modal);
            if (index > -1 && index < modalStack.length) {
              modalStack.splice(index, 1);
            }
          },
          isHighest: function (modal) {
            var index = modalStack.indexOf(modal);
            return index > -1 && index === modalStack.length - 1;
          },
        };
  
        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;
            });
          },
  
          stack: stack,
        };
      },
    ]);
  
    /**
     * @ngdoc service
     * @name $ionicNavBarDelegate
     * @module ionic
     * @description
     * Delegate for controlling the {@link ionic.directive:ionNavBar} directive.
     *
     * @usage
     *
     * ```html
     * <body ng-controller="MyCtrl">
     *   <ion-nav-bar>
     *     <button ng-click="setNavTitle('banana')">
     *       Set title to banana!
     *     </button>
     *   </ion-nav-bar>
     * </body>
     * ```
     * ```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']));
  
    /**
     * @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.constant('IONIC_BACK_PRIORITY', {
      view: 100,
      sideMenu: 150,
      modal: 200,
      actionSheet: 300,
      popup: 400,
      loading: 500,
    }).provider('$ionicPlatform', function () {
      return {
        $get: [
          '$q',
          '$ionicScrollDelegate',
          function ($q, $ionicScrollDelegate) {
            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.
               *
               * The priorities for the existing back button hooks are as follows:
               *   Return to previous view = 100
               *   Close side menu = 150
               *   Dismiss modal = 200
               *   Close action sheet = 300
               *   Dismiss popup = 400
               *   Dismiss loading overlay = 500
               *
               * Your back button action will override each of the above actions
               * whose priority is less than the priority you provide. For example,
               * an action assigned a priority of 101 will override the 'return to
               * previous view' action, but not any of the other actions.
               *
               * @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/latest/cordova/events/events.html).
               * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/latest/cordova/events/events.html).
               * @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 () {
                  window.addEventListener('statusTap', function () {
                    $ionicScrollDelegate.scrollTop(true);
                  });
  
                  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 `<ion-popover-view>` element.
     *
     * @usage
     * ```html
     * <p>
     *   <button ng-click="openPopover($event)">Open Popover</button>
     * </p>
     *
     * <script id="my-popover.html" type="text/ng-template">
     *   <ion-popover-view>
     *     <ion-header-bar>
     *       <h1 class="title">My Popover Title</h1>
     *     </ion-header-bar>
     *     <ion-content>
     *       Hello!
     *     </ion-content>
     *   </ion-popover-view>
     * </script>
     * ```
     * ```js
     * angular.module('testApp', ['ionic'])
     * .controller('MyController', function($scope, $ionicPopover) {
     *
     *   // .fromTemplate() method
     *   var template = '<ion-popover-view><ion-header-bar> <h1 class="title">My Popover Title</h1> </ion-header-bar> <ion-content> Hello! </ion-content></ion-popover-view>';
     *
     *   $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 hidden 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 = jqLite(target.target || target);
          var buttonOffset = $ionicPosition.offset(targetEle);
          var popoverWidth = popoverEle.prop('offsetWidth');
          var popoverHeight = popoverEle.prop('offsetHeight');
          // Use innerWidth and innerHeight, because clientWidth and clientHeight
          // doesn't work consistently for body on all platforms
          var bodyWidth = $window.innerWidth;
          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 there's room above
          if (buttonOffset.top + buttonOffset.height + popoverHeight > bodyHeight && buttonOffset.top - popoverHeight > 0) {
            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 =
      '<div class="popup-container" ng-class="cssClass">' +
      '<div class="popup">' +
      '<div class="popup-head">' +
      '<h3 class="popup-title" ng-bind-html="title"></h3>' +
      '<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
      '</div>' +
      '<div class="popup-body">' +
      '</div>' +
      '<div class="popup-buttons" ng-show="buttons.length">' +
      '<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
      '</div>' +
      '</div>' +
      '</div>';
  
    /**
     * @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: '<input type="password" ng-model="data.wifi">',
     *     title: 'Enter Wi-Fi Password',
     *     subTitle: 'Please use normal things',
     *     scope: $scope,
     *     buttons: [
     *       { text: 'Cancel' },
     *       {
     *         text: '<b>Save</b>',
     *         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',
      '$ionicModal',
      'IONIC_BACK_PRIORITY',
      function (
        $ionicTemplateLoader,
        $ionicBackdrop,
        $q,
        $timeout,
        $rootScope,
        $ionicBody,
        $compile,
        $ionicPlatform,
        $ionicModal,
        IONIC_BACK_PRIORITY
      ) {
        //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
           *   defaultText: // String (default: ''). The initial value placed into the input.
           *   maxLength: // Integer (default: null). Specify a maxlength attribute for the input.
           *   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 self = {};
          self.scope = (options.scope || $rootScope).$new();
          self.element = jqLite(POPUP_TPL);
          self.responseDeferred = $q.defer();
  
          $ionicBody.get().appendChild(self.element[0]);
          $compile(self.element)(self.scope);
  
          extend(self.scope, {
            title: options.title,
            buttons: options.buttons,
            subTitle: options.subTitle,
            cssClass: options.cssClass,
            $buttonTapped: function (button, event) {
              var result = (button.onTap || noop).apply(self, [event]);
              event = event.originalEvent || event; //jquery events
  
              if (!event.defaultPrevented) {
                self.responseDeferred.resolve(result);
              }
            },
          });
  
          $q.when(
            options.templateUrl ? $ionicTemplateLoader.load(options.templateUrl) : options.template || options.content || ''
          ).then(function (template) {
            var popupBody = jqLite(self.element[0].querySelector('.popup-body'));
            if (template) {
              popupBody.html(template);
              $compile(popupBody.contents())(self.scope);
            } else {
              popupBody.remove();
            }
          });
  
          self.show = function () {
            if (self.isShown || self.removed) return;
  
            $ionicModal.stack.add(self);
            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 || noop;
            if (!self.isShown) return callback();
  
            $ionicModal.stack.remove(self);
            self.isShown = false;
            self.element.removeClass('active');
            self.element.addClass('popup-hidden');
            $timeout(callback, 250, false);
          };
  
          self.remove = function () {
            if (self.removed) return;
  
            self.hide(function () {
              self.element.remove();
              self.scope.$destroy();
            });
  
            self.removed = true;
          };
  
          return self;
        }
  
        function onHardwareBackButton() {
          var last = popupStack[popupStack.length - 1];
          last && last.responseDeferred.resolve();
        }
  
        function showPopup(options) {
          var popup = $ionicPopup._createPopup(options);
          var showDelay = 0;
  
          if (popupStack.length > 0) {
            showDelay = config.stackPushDelay;
            $timeout(popupStack[popupStack.length - 1].hide, showDelay, false);
          } else {
            //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,
              IONIC_BACK_PRIORITY.popup
            );
          }
  
          // Expose a 'close' method on the returned promise
          popup.responseDeferred.promise.close = function popupClose(result) {
            if (!popup.removed) popup.responseDeferred.resolve(result);
          };
          //DEPRECATED: notify the promise with an object with a close method
          popup.responseDeferred.notify({ close: popup.responseDeferred.close });
  
          doShow();
  
          return popup.responseDeferred.promise;
  
          function doShow() {
            popupStack.push(popup);
            $timeout(popup.show, showDelay, false);
  
            popup.responseDeferred.promise.then(function (result) {
              var index = popupStack.indexOf(popup);
              if (index !== -1) {
                popupStack.splice(index, 1);
              }
  
              popup.remove();
  
              if (popupStack.length > 0) {
                popupStack[popupStack.length - 1].show();
              } else {
                $ionicBackdrop.release();
                //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
                    if (!popupStack.length) {
                      $ionicBody.removeClass('popup-open');
                    }
                  },
                  400,
                  false
                );
                ($ionicPopup._backButtonActionDone || noop)();
              }
  
              return result;
            });
          }
        }
  
        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 () {
                      return true;
                    },
                  },
                ],
              },
              opts || {}
            )
          );
        }
  
        function showConfirm(opts) {
          return showPopup(
            extend(
              {
                buttons: [
                  {
                    text: opts.cancelText || 'Cancel',
                    type: opts.cancelType || 'button-default',
                    onTap: function () {
                      return false;
                    },
                  },
                  {
                    text: opts.okText || 'OK',
                    type: opts.okType || 'button-positive',
                    onTap: function () {
                      return true;
                    },
                  },
                ],
              },
              opts || {}
            )
          );
        }
  
        function showPrompt(opts) {
          var scope = $rootScope.$new(true);
          scope.data = {};
          scope.data.fieldtype = opts.inputType ? opts.inputType : 'text';
          scope.data.response = opts.defaultText ? opts.defaultText : '';
          scope.data.placeholder = opts.inputPlaceholder ? opts.inputPlaceholder : '';
          scope.data.maxlength = opts.maxLength ? parseInt(opts.maxLength) : '';
          var text = '';
          if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) {
            text = '<span>' + opts.template + '</span>';
            delete opts.template;
          }
          return showPopup(
            extend(
              {
                template:
                  text +
                  '<input ng-model="data.response" ' +
                  'type="{{ data.fieldtype }}"' +
                  'maxlength="{{ data.maxlength }}"' +
                  'placeholder="{{ data.placeholder }}"' +
                  '>',
                scope: scope,
                buttons: [
                  {
                    text: opts.cancelText || 'Cancel',
                    type: opts.cancelType || 'button-default',
                    onTap: function () {},
                  },
                  {
                    text: opts.okText || 'OK',
                    type: opts.okType || 'button-positive',
                    onTap: function () {
                      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(jqLite(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
     * <body ng-controller="MainCtrl">
     *   <ion-content>
     *     <button ng-click="scrollTop()">Scroll to Top!</button>
     *   </ion-content>
     * </body>
     * ```
     * ```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
     * <body ng-controller="MainCtrl">
     *   <ion-content delegate-handle="mainScroll">
     *     <button ng-click="scrollMainToTop()">
     *       Scroll content to top!
     *     </button>
     *     <ion-scroll delegate-handle="small" style="height: 100px;">
     *       <button ng-click="scrollSmallToTop()">
     *         Scroll small area to top!
     *       </button>
     *     </ion-scroll>
     *   </ion-content>
     * </body>
     * ```
     * ```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).
         *  - `{number}` `zoom` The current zoom level.
         */
        '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#freezeScroll
         * @description Does not allow this scroll view to scroll either x or y.
         * @param {boolean=} shouldFreeze Should this scroll view be prevented from scrolling or not.
         * @returns {boolean} If the scroll view is being prevented from scrolling or not.
         */
        'freezeScroll',
        /**
         * @ngdoc method
         * @name $ionicScrollDelegate#freezeAllScrolls
         * @description Does not allow any of the app's scroll views to scroll either x or y.
         * @param {boolean=} shouldFreeze Should all app scrolls be prevented from scrolling or not.
         */
        'freezeAllScrolls',
        /**
         * @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
     * <body ng-controller="MainCtrl">
     *   <ion-side-menus>
     *     <ion-side-menu-content>
     *       Content!
     *       <button ng-click="toggleLeftSideMenu()">
     *         Toggle Left Side Menu
     *       </button>
     *     </ion-side-menu-content>
     *     <ion-side-menu side="left">
     *       Left Menu!
     *     <ion-side-menu>
     *   </ion-side-menus>
     * </body>
     * ```
     * ```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
     * <ion-view>
     *   <ion-slide-box>
     *     <ion-slide>
     *       <div class="box blue">
     *         <button ng-click="nextSlide()">Next slide!</button>
     *       </div>
     *     </ion-slide>
     *     <ion-slide>
     *       <div class="box red">
     *         Slide 2!
     *       </div>
     *     </ion-slide>
     *   </ion-slide-box>
     * </ion-view>
     * ```
     * ```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 the change should 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
         * @param {number=} speed The number of milliseconds the change should take.
         * @description Go to the previous slide. Wraps around if at the beginning.
         */
        'previous',
        /**
         * @ngdoc method
         * @name $ionicSlideBoxDelegate#next
         * @param {number=} speed The number of milliseconds the change should take.
         * @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
     * <body ng-controller="MyCtrl">
     *   <ion-tabs>
     *
     *     <ion-tab title="Tab 1">
     *       Hello tab 1!
     *       <button ng-click="selectTabWithIndex(1)">Select tab 2!</button>
     *     </ion-tab>
     *     <ion-tab title="Tab 2">Hello tab 2!</ion-tab>
     *
     *   </ion-tabs>
     * </body>
     * ```
     * ```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#showBar
         * @description
         * Set/get whether the {@link ionic.directive:ionTabs} is shown
         * @param {boolean} show Whether to show the bar.
         * @returns {boolean} Whether the bar is shown.
         */
        'showBar',
        /**
         * @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() {
            var template;
            $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('<div>').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 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 defaultTimeout = 1100;
  
        var ionicViewSwitcher = {
          create: function (navViewCtrl, viewLocals, enteringView, leavingView, renderStart, renderEnd) {
            // 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 x, l, viewEle;
                var viewElements = navViewCtrl.getViewElements();
                var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView);
                var navViewActiveEleId = navViewCtrl.activeEleId();
  
                for (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 (isDefined(navViewActiveEleId) && 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);
                }
  
                if (renderEnd) {
                  navViewCtrl.activeEleId(enteringEleIdentifier);
                }
  
                registerData.ele = null;
              },
  
              render: function (registerData, callback) {
                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, allowAnimate) {
                var deferred;
                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;
                enteringData.renderStart = renderStart;
                enteringData.renderEnd = renderEnd;
  
                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));
  
                // 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 && allowAnimate && renderEnd
                );
  
                if (viewTransition.shouldAnimate) {
                  // attach transitionend events (and fallback timer)
                  enteringEle.on(TRANSITIONEND_EVENT, completeOnTransitionEnd);
                  enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, defaultTimeout));
                  $ionicClickBlock.show(defaultTimeout);
                }
  
                if (renderStart) {
                  // notify the views "before" the transition starts
                  switcher.emit('before', enteringData, leavingData);
  
                  // stage entering element, opacity 0, no transition duration
                  navViewAttr(enteringEle, VIEW_STATUS_STAGED);
  
                  // render the elements in the correct location for their starting point
                  viewTransition.run(0);
                }
  
                if (renderEnd) {
                  // create a promise so we can keep track of when all transitions finish
                  // only required if this transition should complete
                  deferred = $q.defer();
                  transitionPromises.push(deferred.promise);
                }
  
                if (renderStart && renderEnd) {
                  // CSS "auto" transitioned, not manually transitioned
                  // wait a frame so the styles apply before auto transitioning
                  $timeout(function () {
                    ionic.requestAnimationFrame(onReflow);
                  });
                } else if (!renderEnd) {
                  // just the start of a manual transition
                  // but it will not render the end of the transition
                  navViewAttr(enteringEle, 'entering');
                  navViewAttr(leavingEle, 'leaving');
  
                  // return the transition run method so each step can be ran manually
                  return {
                    run: viewTransition.run,
                    cancel: function (shouldAnimate) {
                      if (shouldAnimate) {
                        enteringEle.on(TRANSITIONEND_EVENT, cancelOnTransitionEnd);
                        enteringEle.data(DATA_FALLBACK_TIMER, $timeout(cancelTransition, defaultTimeout));
                        $ionicClickBlock.show(defaultTimeout);
                      } else {
                        cancelTransition();
                      }
                      viewTransition.shouldAnimate = shouldAnimate;
                      viewTransition.run(0);
                      viewTransition = null;
                    },
                  };
                } else if (renderEnd) {
                  // just the end of a manual transition
                  // happens after the manual transition has completed
                  // and a full history change has happened
                  onReflow();
                }
  
                function onReflow() {
                  // remove that we're staging the entering element so it can auto transition
                  navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE);
                  navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED);
  
                  // start the auto transition and let the CSS take over
                  viewTransition.run(1);
  
                  // trigger auto transitions on the associated nav bars
                  $ionicNavBarDelegate._instances.forEach(function (instance) {
                    instance.triggerTransitionStart(transitionId);
                  });
  
                  if (!viewTransition.shouldAnimate) {
                    // no animated auto transition
                    transitionComplete();
                  }
                }
  
                // Make sure that transitionend events bubbling up from children won't fire
                // transitionComplete. Will only go forward if ev.target == the element listening.
                function completeOnTransitionEnd(ev) {
                  if (ev.target !== this) return;
                  transitionComplete();
                }
                function transitionComplete() {
                  if (transitionComplete.x) return;
                  transitionComplete.x = true;
  
                  enteringEle.off(TRANSITIONEND_EVENT, completeOnTransitionEnd);
                  $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
                  leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER));
  
                  // resolve that this one transition (there could be many w/ nested views)
                  deferred && deferred.resolve(navViewCtrl);
  
                  // 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);
  
                    // emit that the views have finished transitioning
                    // each parent nav-view will update which views are active and cached
                    switcher.emit('after', enteringData, leavingData);
                    switcher.cleanup(enteringData);
                  }
  
                  // tell the nav bars that the transition has ended
                  $ionicNavBarDelegate._instances.forEach(function (instance) {
                    instance.triggerTransitionEnd();
                  });
  
                  // remove any references that could cause memory issues
                  nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null;
                }
  
                // Make sure that transitionend events bubbling up from children won't fire
                // transitionComplete. Will only go forward if ev.target == the element listening.
                function cancelOnTransitionEnd(ev) {
                  if (ev.target !== this) return;
                  cancelTransition();
                }
                function cancelTransition() {
                  navViewAttr(enteringEle, VIEW_STATUS_CACHED);
                  navViewAttr(leavingEle, VIEW_STATUS_ACTIVE);
                  enteringEle.off(TRANSITIONEND_EVENT, cancelOnTransitionEnd);
                  $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
                  ionicViewSwitcher.transitionEnd([navViewCtrl]);
                }
              },
  
              emit: function (step, enteringData, leavingData) {
                var enteringScope = getScopeForElement(enteringEle, enteringData);
                var leavingScope = getScopeForElement(leavingEle, leavingData);
  
                var prefixesAreEqual;
  
                if (!enteringData.viewId || enteringData.abstractView) {
                  // it's an abstract view, so treat it accordingly
  
                  // we only get access to the leaving scope once in the transition,
                  // so dispatch all events right away if it exists
                  if (leavingScope) {
                    leavingScope.$emit('$ionicView.beforeLeave', leavingData);
                    leavingScope.$emit('$ionicView.leave', leavingData);
                    leavingScope.$emit('$ionicView.afterLeave', leavingData);
                    leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData);
                    leavingScope.$broadcast('$ionicParentView.leave', leavingData);
                    leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData);
                  }
                } else {
                  // it's a regular view, so do the normal process
                  if (step == 'after') {
                    if (enteringScope) {
                      enteringScope.$emit('$ionicView.enter', enteringData);
                      enteringScope.$broadcast('$ionicParentView.enter', enteringData);
                    }
  
                    if (leavingScope) {
                      leavingScope.$emit('$ionicView.leave', leavingData);
                      leavingScope.$broadcast('$ionicParentView.leave', leavingData);
                    } else if (
                      enteringScope &&
                      leavingData &&
                      leavingData.viewId &&
                      enteringData.stateName !== leavingData.stateName
                    ) {
                      // we only want to dispatch this when we are doing a single-tier
                      // state change such as changing a tab, so compare the state
                      // for the same state-prefix but different suffix
                      prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
                      if (prefixesAreEqual) {
                        enteringScope.$emit('$ionicNavView.leave', leavingData);
                      }
                    }
                  }
  
                  if (enteringScope) {
                    enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData);
                    enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData);
                  }
  
                  if (leavingScope) {
                    leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData);
                    leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData);
                  } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) {
                    // we only want to dispatch this when we are doing a single-tier
                    // state change such as changing a tab, so compare the state
                    // for the same state-prefix but different suffix
                    prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
                    if (prefixesAreEqual) {
                      enteringScope.$emit('$ionicNavView.' + step + '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');
                if (viewLocals.$$state && viewLocals.$$state.self && viewLocals.$$state.self['abstract']) {
                  angular.element(containerEle.children[0]).attr('abstract', 'true');
                } else {
                  if (viewLocals.$$state && viewLocals.$$state.self) {
                    angular.element(containerEle.children[0]).attr('state', viewLocals.$$state.self.name);
                  }
                }
                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();
          }
        }
  
        function compareStatePrefixes(enteringStateName, exitingStateName) {
          var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.');
          var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.');
  
          // if either of the prefixes are empty, just return false
          if (enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0) {
            return false;
          }
  
          var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex);
          var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex);
  
          return enteringPrefix === exitingPrefix;
        }
  
        function getScopeForElement(element, stateData) {
          if (!element) {
            return null;
          }
          // check if it's abstract
          var attributeValue = angular.element(element).attr('abstract');
          var stateValue = angular.element(element).attr('state');
  
          if (attributeValue !== 'true') {
            // it's not an abstract view, so make sure the element
            // matches the state.  Due to abstract view weirdness,
            // sometimes it doesn't. If it doesn't, don't dispatch events
            // so leave the scope undefined
            if (stateValue === stateData.stateName) {
              return angular.element(element).scope();
            }
            return null;
          } else {
            // it is an abstract element, so look for element with the "state" attributeValue
            // set to the name of the stateData state
            var elements = aggregateNavViewChildren(element);
            for (var i = 0; i < elements.length; i++) {
              var state = angular.element(elements[i]).attr('state');
              if (state === stateData.stateName) {
                stateData.abstractView = true;
                return angular.element(elements[i]).scope();
              }
            }
            // we didn't find a match, so return null
            return null;
          }
        }
  
        function aggregateNavViewChildren(element) {
          var aggregate = [];
          var navViews = angular.element(element).find('ion-nav-view');
          for (var i = 0; i < navViews.length; i++) {
            var children = angular.element(navViews[i]).children();
            var childrenAggregated = [];
            for (var j = 0; j < children.length; j++) {
              childrenAggregated = childrenAggregated.concat(children[j]);
            }
            aggregate = aggregate.concat(childrenAggregated);
          }
          return aggregate;
        }
      },
    ]);
  
    /**
     * ==================  angular-ios9-uiwebview.patch.js v1.1.1 ==================
     *
     * This patch works around iOS9 UIWebView regression that causes infinite digest
     * errors in Angular.
     *
     * The patch can be applied to Angular 1.2.0 – 1.4.5. Newer versions of Angular
     * have the workaround baked in.
     *
     * To apply this patch load/bundle this file with your application and add a
     * dependency on the "ngIOS9UIWebViewPatch" module to your main app module.
     *
     * For example:
     *
     * ```
     * angular.module('myApp', ['ngRoute'])`
     * ```
     *
     * becomes
     *
     * ```
     * angular.module('myApp', ['ngRoute', 'ngIOS9UIWebViewPatch'])
     * ```
     *
     *
     * More info:
     * - https://openradar.appspot.com/22186109
     * - https://github.com/angular/angular.js/issues/12241
     * - https://github.com/ionic-team/ionic/issues/4082
     *
     *
     * @license AngularJS
     * (c) 2010-2015 Google, Inc. http://angularjs.org
     * License: MIT
     */
  
    angular.module('ngIOS9UIWebViewPatch', ['ng']).config([
      '$provide',
      function ($provide) {
        'use strict';
  
        $provide.decorator('$browser', [
          '$delegate',
          '$window',
          function ($delegate, $window) {
            if (isIOS9UIWebView($window.navigator.userAgent)) {
              return applyIOS9Shim($delegate);
            }
  
            return $delegate;
  
            function isIOS9UIWebView(userAgent) {
              return /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent);
            }
  
            function applyIOS9Shim(browser) {
              var pendingLocationUrl = null;
              var originalUrlFn = browser.url;
  
              browser.url = function () {
                if (arguments.length) {
                  pendingLocationUrl = arguments[0];
                  return originalUrlFn.apply(browser, arguments);
                }
  
                return pendingLocationUrl || originalUrlFn.apply(browser, arguments);
              };
  
              window.addEventListener('popstate', clearPendingLocationUrl, false);
              window.addEventListener('hashchange', clearPendingLocationUrl, false);
  
              function clearPendingLocationUrl() {
                pendingLocationUrl = null;
              }
  
              return browser;
            }
          },
        ]);
      },
    ]);
  
    /**
     * @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 (isDefined(value) && value.length > 0) {
              $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 () {
          var ele;
          if ((isBackShown && isNavBackShown && isBackEnabled) !== isBackElementShown) {
            isBackElementShown = isBackShown && isNavBackShown && isBackEnabled;
            ele = getEle(BACK_BUTTON);
            ele && ele.classList[isBackElementShown ? 'remove' : 'add'](HIDE);
          }
  
          if (isBackEnabled) {
            ele = ele || getEle(BACK_BUTTON);
            if (ele) {
              if (self.backButtonIcon !== $ionicConfig.backButton.icon()) {
                ele = getEle(BACK_BUTTON + ' .icon');
                if (ele) {
                  self.backButtonIcon = $ionicConfig.backButton.icon();
                  ele.className = 'icon ' + self.backButtonIcon;
                }
              }
  
              if (self.backButtonText !== $ionicConfig.backButton.text()) {
                ele = getEle(BACK_BUTTON + ' .back-text');
                if (ele) {
                  ele.textContent = self.backButtonText = $ionicConfig.backButton.text();
                }
              }
            }
          }
        };
  
        self.titleTextWidth = function () {
          var element = getEle(TITLE);
          if (element) {
            // If the element has a nav-bar-title, use that instead
            // to calculate the width of the title
            var children = angular.element(element).children();
            for (var i = 0; i < children.length; i++) {
              if (angular.element(children[i]).hasClass('nav-bar-title')) {
                element = children[i];
                break;
              }
            }
          }
          var bounds = ionic.DomUtil.getTextBounds(element);
          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 (viewData) {
          if ($ionicConfig.backButton.previousTitleText()) {
            var previousTitleEle = getEle(PREVIOUS_TITLE);
            if (previousTitleEle) {
              previousTitleEle.classList.remove(HIDE);
  
              var view = viewData && $ionicHistory.getViewById(viewData.viewId);
              var newPreviousTitleText = $ionicHistory.backTitle(view);
  
              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;
        });
      },
    ]);
  
    IonicModule.controller('$ionInfiniteScroll', [
      '$scope',
      '$attrs',
      '$element',
      '$timeout',
      function ($scope, $attrs, $element, $timeout) {
        var self = this;
        self.isLoading = false;
  
        $scope.icon = function () {
          return isDefined($attrs.icon) ? $attrs.icon : 'ion-load-d';
        };
  
        $scope.spinner = function () {
          return isDefined($attrs.spinner) ? $attrs.spinner : '';
        };
  
        $scope.$on('scroll.infiniteScrollComplete', function () {
          finishInfiniteScroll();
        });
  
        $scope.$on('$destroy', function () {
          if (self.scrollCtrl && self.scrollCtrl.$element) self.scrollCtrl.$element.off('scroll', self.checkBounds);
          if (self.scrollEl && self.scrollEl.removeEventListener) {
            self.scrollEl.removeEventListener('scroll', self.checkBounds);
          }
        });
  
        // debounce checking infinite scroll events
        self.checkBounds = ionic.Utils.throttle(checkInfiniteBounds, 300);
  
        function onInfinite() {
          ionic.requestAnimationFrame(function () {
            $element[0].classList.add('active');
          });
          self.isLoading = true;
          $scope.$parent && $scope.$parent.$apply($attrs.onInfinite || '');
        }
  
        function finishInfiniteScroll() {
          ionic.requestAnimationFrame(function () {
            $element[0].classList.remove('active');
          });
          $timeout(
            function () {
              if (self.jsScrolling) self.scrollView.resize();
              // only check bounds again immediately if the page isn't cached (scroll el has height)
              if (
                (self.jsScrolling && self.scrollView.__container && self.scrollView.__container.offsetHeight > 0) ||
                !self.jsScrolling
              ) {
                self.checkBounds();
              }
            },
            30,
            false
          );
          self.isLoading = false;
        }
  
        // check if we've scrolled far enough to trigger an infinite scroll
        function checkInfiniteBounds() {
          if (self.isLoading) return;
          var maxScroll = {};
  
          if (self.jsScrolling) {
            maxScroll = self.getJSMaxScroll();
            var scrollValues = self.scrollView.getValues();
            if (
              (maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) ||
              (maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)
            ) {
              onInfinite();
            }
          } else {
            maxScroll = self.getNativeMaxScroll();
            if (
              (maxScroll.left !== -1 && self.scrollEl.scrollLeft >= maxScroll.left - self.scrollEl.clientWidth) ||
              (maxScroll.top !== -1 && self.scrollEl.scrollTop >= maxScroll.top - self.scrollEl.clientHeight)
            ) {
              onInfinite();
            }
          }
        }
  
        // determine the threshold at which we should fire an infinite scroll
        // note: this gets processed every scroll event, can it be cached?
        self.getJSMaxScroll = function () {
          var maxValues = self.scrollView.getScrollMax();
          return {
            left: self.scrollView.options.scrollingX ? calculateMaxValue(maxValues.left) : -1,
            top: self.scrollView.options.scrollingY ? calculateMaxValue(maxValues.top) : -1,
          };
        };
  
        self.getNativeMaxScroll = function () {
          var maxValues = {
            left: self.scrollEl.scrollWidth,
            top: self.scrollEl.scrollHeight,
          };
          var computedStyle = window.getComputedStyle(self.scrollEl) || {};
          return {
            left:
              maxValues.left &&
              (computedStyle.overflowX === 'scroll' ||
                computedStyle.overflowX === 'auto' ||
                self.scrollEl.style['overflow-x'] === 'scroll')
                ? calculateMaxValue(maxValues.left)
                : -1,
            top:
              maxValues.top &&
              (computedStyle.overflowY === 'scroll' ||
                computedStyle.overflowY === 'auto' ||
                self.scrollEl.style['overflow-y'] === 'scroll')
                ? calculateMaxValue(maxValues.top)
                : -1,
          };
        };
  
        // determine pixel refresh distance based on % or value
        function calculateMaxValue(maximum) {
          var distance = ($attrs.distance || '2.5%').trim();
          var isPercent = distance.indexOf('%') !== -1;
          return isPercent ? maximum * (1 - parseFloat(distance) / 100) : maximum - parseFloat(distance);
        }
  
        //for testing
        self.__finishInfiniteScroll = finishInfiniteScroll;
      },
    ]);
  
    /**
   * @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 %}
   * <ion-content ng-controller="MyCtrl">
   *   <button class="button" ng-click="showDeleteButtons()"></button>
   *   <ion-list>
   *     <ion-item ng-repeat="i in items">
   *       Hello, {{i}}!
   *       <ion-delete-button class="ion-minus-circled"></ion-delete-button>
   *     </ion-item>
   *   </ion-list>
   * </ion-content>
   * {% 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) {
          var containerEle = jqLite('<div class="nav-bar-block">');
          ionic.DomUtil.cachedAttr(containerEle, 'nav-bar', isActive ? 'active' : 'cached');
  
          var alignTitle = $attrs.alignTitle || $ionicConfig.navBar.alignTitle();
          var headerBarEle = jqLite('<ion-header-bar>').addClass($attrs['class']).attr('align-title', alignTitle);
          if (isDefined($attrs.noTapScroll)) headerBarEle.attr('no-tap-scroll', $attrs.noTapScroll);
          var titleEle = jqLite('<div class="title title-' + alignTitle + '">');
          var navEle = {};
          var lastViewItemEle = {};
          var leftButtonsEle, rightButtonsEle;
  
          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');
          headerBarCtrl.backButtonIcon = $ionicConfig.backButton.icon();
          headerBarCtrl.backButtonText = $ionicConfig.backButton.text();
  
          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('<div class="buttons buttons-right">');
                headerBarEle.append(rightButtonsEle);
              }
              if (itemType == SECONDARY_BUTTONS) {
                rightButtonsEle.append(ele);
              } else {
                rightButtonsEle.prepend(ele);
              }
            } else {
              // left side
              if (!leftButtonsEle) {
                leftButtonsEle = jqLite('<div class="buttons buttons-left">');
                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;
          navSwipeAttr('');
        };
  
        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 && viewData.renderEnd) {
            navBarAttr(enteringHeaderBar, 'stage');
          } else {
            navBarAttr(enteringHeaderBar, 'entering');
            navBarAttr(leavingHeaderBar, 'leaving');
          }
  
          enteringHeaderBarCtrl.resetBackButton(viewData);
  
          navBarTransition.run(0);
  
          self.activeTransition = {
            run: function (step) {
              navBarTransition.shouldAnimate = false;
              navBarTransition.direction = 'back';
              navBarTransition.run(step);
            },
            cancel: function (shouldAnimate, speed, cancelData) {
              navSwipeAttr(speed);
              navBarAttr(leavingHeaderBar, 'active');
              navBarAttr(enteringHeaderBar, 'cached');
              navBarTransition.shouldAnimate = shouldAnimate;
              navBarTransition.run(0);
              self.activeTransition = navBarTransition = null;
  
              var runApply;
              if (cancelData.showBar !== self.showBar()) {
                self.showBar(cancelData.showBar);
              }
              if (cancelData.showBackButton !== self.showBackButton()) {
                self.showBackButton(cancelData.showBackButton);
              }
              if (runApply) {
                $scope.$apply();
              }
            },
            complete: function (shouldAnimate, speed) {
              navSwipeAttr(speed);
              navBarTransition.shouldAnimate = shouldAnimate;
              navBarTransition.run(1);
              queuedTransitionEnd = transitionEnd;
            },
          };
  
          $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) {
                transitionEnd();
              }
            };
  
            queuedTransitionStart = null;
          };
  
          function transitionEnd() {
            for (var x = 0; x < headerBars.length; x++) {
              headerBars[x].isActive = false;
            }
            enteringHeaderBar.isActive = true;
  
            navBarAttr(enteringHeaderBar, 'active');
            navBarAttr(leavingHeaderBar, 'cached');
  
            self.activeTransition = navBarTransition = queuedTransitionEnd = 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);
            self.align();
          } 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) {
          if (arguments.length) {
            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();
          if (headerBar) {
            if (arguments.length) {
              return headerBar.controller().showBack(shouldShow);
            }
            return headerBar.controller().showBack();
          }
        };
  
        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);
        };
  
        self.hasTabsTop = function (isTabsTop) {
          $element[isTabsTop ? 'addClass' : 'removeClass']('nav-bar-tabs-top');
        };
  
        self.hasBarSubheader = function (isBarSubheader) {
          $element[isBarSubheader ? 'addClass' : 'removeClass']('nav-bar-has-subheader');
        };
  
        // 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.call(console, '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);
        }
  
        function navSwipeAttr(val) {
          ionic.DomUtil.cachedAttr($element, 'nav-swipe', 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',
      '$ionicConfig',
      '$ionicScrollDelegate',
      '$ionicSideMenuDelegate',
      function (
        $scope,
        $element,
        $attrs,
        $compile,
        $controller,
        $ionicNavBarDelegate,
        $ionicNavViewDelegate,
        $ionicHistory,
        $ionicViewSwitcher,
        $ionicConfig,
        $ionicScrollDelegate,
        $ionicSideMenuDelegate
      ) {
        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;
        var disableRenderStartViewId, disableAnimation;
  
        self.scope = $scope;
        self.element = $element;
  
        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', function () {
            deregisterInstance();
  
            // ensure no scrolls have been left frozen
            if (self.isSwipeFreeze) {
              $ionicScrollDelegate.freezeAllScrolls(false);
            }
          });
  
          $scope.$on('$ionicHistory.deselect', self.cacheCleanup);
          $scope.$on('$ionicTabs.top', onTabsTop);
          $scope.$on('$ionicSubheader', onBarSubheader);
  
          $scope.$on('$ionicTabs.beforeLeave', onTabsLeave);
          $scope.$on('$ionicTabs.afterLeave', onTabsLeave);
          $scope.$on('$ionicTabs.leave', onTabsLeave);
  
          ionic.Platform.ready(function () {
            if (ionic.Platform.isWebView() && ionic.Platform.isIOS()) {
              self.initSwipeBack();
            }
          });
  
          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
          var enteringView = $ionicHistory.getViewById(registerData.viewId) || {};
  
          var renderStart = disableRenderStartViewId !== registerData.viewId;
          self.render(registerData, viewLocals, enteringView, leavingView, renderStart, true);
        };
  
        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, enteringView, leavingView, renderStart, renderEnd) {
          // 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, renderStart, renderEnd);
  
          // 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
  
            // kick off the transition of views
            switcher.transition(self.direction(), registerData.enableBack, !disableAnimation);
  
            // reset private vars for next time
            disableRenderStartViewId = disableAnimation = null;
          });
        };
  
        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);
            navSwipeAttr('');
          }
        };
  
        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);
  
                // disconnect the leaving scope
                ionic.Utils.disconnectScope(viewElement.scope());
              }
            }
          }
  
          navSwipeAttr('');
  
          // ensure no scrolls have been left frozen
          if (self.isSwipeFreeze) {
            $ionicScrollDelegate.freezeAllScrolls(false);
          }
        };
  
        function onTabsLeave(ev, data) {
          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_ACTIVE) {
              viewScope = viewElement.scope();
              viewScope && viewScope.$emit(ev.name.replace('Tabs', 'View'), data);
              viewScope && viewScope.$broadcast(ev.name.replace('Tabs', 'ParentView'), data);
              break;
            }
          }
        }
  
        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 (stateIds) {
          var viewElements = $element.children();
          var viewElement, viewScope, x, l, y, eleIdentifier;
  
          for (x = 0, l = viewElements.length; x < l; x++) {
            viewElement = viewElements.eq(x);
  
            if (stateIds) {
              eleIdentifier = viewElement.data(DATA_ELE_IDENTIFIER);
  
              for (y = 0; y < stateIds.length; y++) {
                if (eleIdentifier === stateIds[y]) {
                  $ionicViewSwitcher.destroyViewEle(viewElement);
                }
              }
              continue;
            }
  
            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);
            if (viewLocals.$$controllerAs) {
              viewScope[viewLocals.$$controllerAs] = controller;
            }
            $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();
          if (associatedNavBarCtrl) {
            if (arguments.length) {
              return associatedNavBarCtrl.showActiveBackButton(shouldShow);
            }
            return associatedNavBarCtrl.showActiveBackButton();
          }
          return true;
        };
  
        self.showBar = function (val) {
          var associatedNavBarCtrl = getAssociatedNavBarCtrl();
          if (associatedNavBarCtrl) {
            if (arguments.length) {
              return associatedNavBarCtrl.showBar(val);
            }
            return associatedNavBarCtrl.showBar();
          }
          return true;
        };
  
        self.isPrimary = function (val) {
          if (arguments.length) {
            isPrimary = val;
          }
          return isPrimary;
        };
  
        self.direction = function (val) {
          if (arguments.length) {
            direction = val;
          }
          return direction;
        };
  
        self.initSwipeBack = function () {
          var swipeBackHitWidth = $ionicConfig.views.swipeBackHitWidth();
          var viewTransition, associatedNavBarCtrl, backView;
          var deregDragStart, deregDrag, deregRelease;
          var windowWidth, startDragX, dragPoints;
          var cancelData = {};
  
          function onDragStart(ev) {
            if (!isPrimary || !$ionicConfig.views.swipeBackEnabled() || $ionicSideMenuDelegate.isOpenRight()) return;
  
            startDragX = getDragX(ev);
            if (startDragX > swipeBackHitWidth) return;
  
            backView = $ionicHistory.backView();
  
            var currentView = $ionicHistory.currentView();
  
            if (!backView || backView.historyId !== currentView.historyId || currentView.canSwipeBack === false) return;
  
            if (!windowWidth) windowWidth = window.innerWidth;
  
            self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(true);
  
            var registerData = {
              direction: 'back',
            };
  
            dragPoints = [];
  
            cancelData = {
              showBar: self.showBar(),
              showBackButton: self.showBackButton(),
            };
  
            var switcher = $ionicViewSwitcher.create(self, registerData, backView, currentView, true, false);
            switcher.loadViewElements(registerData);
            switcher.render(registerData);
  
            viewTransition = switcher.transition('back', $ionicHistory.enabledBack(backView), true);
  
            associatedNavBarCtrl = getAssociatedNavBarCtrl();
  
            deregDrag = ionic.onGesture('drag', onDrag, $element[0]);
            deregRelease = ionic.onGesture('release', onRelease, $element[0]);
          }
  
          function onDrag(ev) {
            if (isPrimary && viewTransition) {
              var dragX = getDragX(ev);
  
              dragPoints.push({
                t: Date.now(),
                x: dragX,
              });
  
              if (dragX >= windowWidth - 15) {
                onRelease(ev);
              } else {
                var step = Math.min(Math.max(getSwipeCompletion(dragX), 0), 1);
                viewTransition.run(step);
                associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.run(step);
              }
            }
          }
  
          function onRelease(ev) {
            if (isPrimary && viewTransition && dragPoints && dragPoints.length > 1) {
              var now = Date.now();
              var releaseX = getDragX(ev);
              var startDrag = dragPoints[dragPoints.length - 1];
  
              for (var x = dragPoints.length - 2; x >= 0; x--) {
                if (now - startDrag.t > 200) {
                  break;
                }
                startDrag = dragPoints[x];
              }
  
              var isSwipingRight = releaseX >= dragPoints[dragPoints.length - 2].x;
              var releaseSwipeCompletion = getSwipeCompletion(releaseX);
              var velocity = Math.abs(startDrag.x - releaseX) / (now - startDrag.t);
  
              // private variables because ui-router has no way to pass custom data using $state.go
              disableRenderStartViewId = backView.viewId;
              disableAnimation = releaseSwipeCompletion < 0.03 || releaseSwipeCompletion > 0.97;
  
              if (isSwipingRight && (releaseSwipeCompletion > 0.5 || velocity > 0.1)) {
                // complete view transition on release
                var speed = velocity > 0.5 || velocity < 0.05 || releaseX > windowWidth - 45 ? 'fast' : 'slow';
                navSwipeAttr(disableAnimation ? '' : speed);
                backView.go();
                associatedNavBarCtrl &&
                  associatedNavBarCtrl.activeTransition &&
                  associatedNavBarCtrl.activeTransition.complete(!disableAnimation, speed);
              } else {
                // cancel view transition on release
                navSwipeAttr(disableAnimation ? '' : 'fast');
                disableRenderStartViewId = null;
                viewTransition.cancel(!disableAnimation);
                associatedNavBarCtrl &&
                  associatedNavBarCtrl.activeTransition &&
                  associatedNavBarCtrl.activeTransition.cancel(!disableAnimation, 'fast', cancelData);
                disableAnimation = null;
              }
            }
  
            ionic.offGesture(deregDrag, 'drag', onDrag);
            ionic.offGesture(deregRelease, 'release', onRelease);
  
            windowWidth = viewTransition = dragPoints = null;
  
            self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(false);
          }
  
          function getDragX(ev) {
            return ionic.tap.pointerCoord(ev.gesture.srcEvent).x;
          }
  
          function getSwipeCompletion(dragX) {
            return (dragX - startDragX) / windowWidth;
          }
  
          deregDragStart = ionic.onGesture('dragstart', onDragStart, $element[0]);
  
          $scope.$on('$destroy', function () {
            ionic.offGesture(deregDragStart, 'dragstart', onDragStart);
            ionic.offGesture(deregDrag, 'drag', onDrag);
            ionic.offGesture(deregRelease, 'release', onRelease);
            self.element = viewTransition = associatedNavBarCtrl = null;
          });
        };
  
        function navSwipeAttr(val) {
          ionic.DomUtil.cachedAttr($element, 'nav-swipe', val);
        }
  
        function onTabsTop(ev, isTabsTop) {
          var associatedNavBarCtrl = getAssociatedNavBarCtrl();
          associatedNavBarCtrl && associatedNavBarCtrl.hasTabsTop(isTabsTop);
        }
  
        function onBarSubheader(ev, isBarSubheader) {
          var associatedNavBarCtrl = getAssociatedNavBarCtrl();
          associatedNavBarCtrl && associatedNavBarCtrl.hasBarSubheader(isBarSubheader);
        }
  
        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');
        }
      },
    ]);
  
    IonicModule.controller('$ionicRefresher', [
      '$scope',
      '$attrs',
      '$element',
      '$ionicBind',
      '$timeout',
      function ($scope, $attrs, $element, $ionicBind, $timeout) {
        var self = this,
          isDragging = false,
          isOverscrolling = false,
          dragOffset = 0,
          lastOverscroll = 0,
          ptrThreshold = 60,
          activated = false,
          scrollTime = 500,
          startY = null,
          deltaY = null,
          canOverscroll = true,
          scrollParent,
          scrollChild;
  
        if (!isDefined($attrs.pullingIcon)) {
          $attrs.$set('pullingIcon', 'ion-android-arrow-down');
        }
  
        $scope.showSpinner = !isDefined($attrs.refreshingIcon) && $attrs.spinner != 'none';
  
        $scope.showIcon = isDefined($attrs.refreshingIcon);
  
        $ionicBind($scope, $attrs, {
          pullingIcon: '@',
          pullingText: '@',
          refreshingIcon: '@',
          refreshingText: '@',
          spinner: '@',
          disablePullingRotation: '@',
          $onRefresh: '&onRefresh',
          $onPulling: '&onPulling',
        });
  
        function handleMousedown(e) {
          e.touches = e.touches || [
            {
              screenX: e.screenX,
              screenY: e.screenY,
            },
          ];
          // Mouse needs this
          startY = Math.floor(e.touches[0].screenY);
        }
  
        function handleTouchstart(e) {
          e.touches = e.touches || [
            {
              screenX: e.screenX,
              screenY: e.screenY,
            },
          ];
  
          startY = e.touches[0].screenY;
        }
  
        function handleTouchend() {
          // reset Y
          startY = null;
          // if this wasn't an overscroll, get out immediately
          if (!canOverscroll && !isDragging) {
            return;
          }
          // the user has overscrolled but went back to native scrolling
          if (!isDragging) {
            dragOffset = 0;
            isOverscrolling = false;
            setScrollLock(false);
          } else {
            isDragging = false;
            dragOffset = 0;
  
            // the user has scroll far enough to trigger a refresh
            if (lastOverscroll > ptrThreshold) {
              start();
              scrollTo(ptrThreshold, scrollTime);
  
              // the user has overscrolled but not far enough to trigger a refresh
            } else {
              scrollTo(0, scrollTime, deactivate);
              isOverscrolling = false;
            }
          }
        }
  
        function handleTouchmove(e) {
          e.touches = e.touches || [
            {
              screenX: e.screenX,
              screenY: e.screenY,
            },
          ];
  
          // Force mouse events to have had a down event first
          if (!startY && e.type == 'mousemove') {
            return;
          }
  
          // if multitouch or regular scroll event, get out immediately
          if (!canOverscroll || e.touches.length > 1) {
            return;
          }
          //if this is a new drag, keep track of where we start
          if (startY === null) {
            startY = e.touches[0].screenY;
          }
  
          deltaY = e.touches[0].screenY - startY;
  
          // how far have we dragged so far?
          // kitkat fix for touchcancel events http://updates.html5rocks.com/2014/05/A-More-Compatible-Smoother-Touch
          // Only do this if we're not on crosswalk
          if (
            ionic.Platform.isAndroid() &&
            ionic.Platform.version() === 4.4 &&
            !ionic.Platform.isCrosswalk() &&
            scrollParent.scrollTop === 0 &&
            deltaY > 0
          ) {
            isDragging = true;
            e.preventDefault();
          }
  
          // if we've dragged up and back down in to native scroll territory
          if (deltaY - dragOffset <= 0 || scrollParent.scrollTop !== 0) {
            if (isOverscrolling) {
              isOverscrolling = false;
              setScrollLock(false);
            }
  
            if (isDragging) {
              nativescroll(scrollParent, deltaY - dragOffset * -1);
            }
  
            // if we're not at overscroll 0 yet, 0 out
            if (lastOverscroll !== 0) {
              overscroll(0);
            }
            return;
          } else if (deltaY > 0 && scrollParent.scrollTop === 0 && !isOverscrolling) {
            // starting overscroll, but drag started below scrollTop 0, so we need to offset the position
            dragOffset = deltaY;
          }
  
          // prevent native scroll events while overscrolling
          e.preventDefault();
  
          // if not overscrolling yet, initiate overscrolling
          if (!isOverscrolling) {
            isOverscrolling = true;
            setScrollLock(true);
          }
  
          isDragging = true;
          // overscroll according to the user's drag so far
          overscroll((deltaY - dragOffset) / 3);
  
          // update the icon accordingly
          if (!activated && lastOverscroll > ptrThreshold) {
            activated = true;
            ionic.requestAnimationFrame(activate);
          } else if (activated && lastOverscroll < ptrThreshold) {
            activated = false;
            ionic.requestAnimationFrame(deactivate);
          }
        }
  
        function handleScroll(e) {
          // canOverscrol is used to greatly simplify the drag handler during normal scrolling
          canOverscroll = e.target.scrollTop === 0 || isDragging;
        }
  
        function overscroll(val) {
          scrollChild.style[ionic.CSS.TRANSFORM] = 'translate3d(0px, ' + val + 'px, 0px)';
          lastOverscroll = val;
        }
  
        function nativescroll(target, newScrollTop) {
          // creates a scroll event that bubbles, can be cancelled, and with its view
          // and detail property initialized to window and 1, respectively
          target.scrollTop = newScrollTop;
          var e = document.createEvent('UIEvents');
          e.initUIEvent('scroll', true, true, window, 1);
          target.dispatchEvent(e);
        }
  
        function setScrollLock(enabled) {
          // set the scrollbar to be position:fixed in preparation to overscroll
          // or remove it so the app can be natively scrolled
          if (enabled) {
            ionic.requestAnimationFrame(function () {
              scrollChild.classList.add('overscroll');
              show();
            });
          } else {
            ionic.requestAnimationFrame(function () {
              scrollChild.classList.remove('overscroll');
              hide();
              deactivate();
            });
          }
        }
  
        $scope.$on('scroll.refreshComplete', function () {
          // prevent the complete from firing before the scroll has started
          $timeout(function () {
            ionic.requestAnimationFrame(tail);
  
            // scroll back to home during tail animation
            scrollTo(0, scrollTime, deactivate);
  
            // return to native scrolling after tail animation has time to finish
            $timeout(function () {
              if (isOverscrolling) {
                isOverscrolling = false;
                setScrollLock(false);
              }
            }, scrollTime);
          }, scrollTime);
        });
  
        function scrollTo(Y, duration, callback) {
          // scroll animation loop w/ easing
          // credit https://gist.github.com/dezinezync/5487119
          var start = Date.now(),
            from = lastOverscroll;
  
          if (from === Y) {
            callback();
            return; /* Prevent scrolling to the Y point if already there */
          }
  
          // decelerating to zero velocity
          function easeOutCubic(t) {
            return --t * t * t + 1;
          }
  
          // scroll loop
          function scroll() {
            var currentTime = Date.now(),
              time = Math.min(1, (currentTime - start) / duration),
              // where .5 would be 50% of time on a linear scale easedT gives a
              // fraction based on the easing method
              easedT = easeOutCubic(time);
  
            overscroll(Math.floor(easedT * (Y - from) + from));
  
            if (time < 1) {
              ionic.requestAnimationFrame(scroll);
            } else {
              if (Y < 5 && Y > -5) {
                isOverscrolling = false;
                setScrollLock(false);
              }
  
              callback && callback();
            }
          }
  
          // start scroll loop
          ionic.requestAnimationFrame(scroll);
        }
  
        var touchStartEvent, touchMoveEvent, touchEndEvent;
        if (window.navigator.pointerEnabled) {
          touchStartEvent = 'pointerdown';
          touchMoveEvent = 'pointermove';
          touchEndEvent = 'pointerup';
        } else if (window.navigator.msPointerEnabled) {
          touchStartEvent = 'MSPointerDown';
          touchMoveEvent = 'MSPointerMove';
          touchEndEvent = 'MSPointerUp';
        } else {
          touchStartEvent = 'touchstart';
          touchMoveEvent = 'touchmove';
          touchEndEvent = 'touchend';
        }
  
        self.init = function () {
          scrollParent = $element.parent().parent()[0];
          scrollChild = $element.parent()[0];
  
          if (
            !scrollParent ||
            !scrollParent.classList.contains('ionic-scroll') ||
            !scrollChild ||
            !scrollChild.classList.contains('scroll')
          ) {
            throw new Error('Refresher must be immediate child of ion-content or ion-scroll');
          }
  
          ionic.on(touchStartEvent, handleTouchstart, scrollChild);
          ionic.on(touchMoveEvent, handleTouchmove, scrollChild);
          ionic.on(touchEndEvent, handleTouchend, scrollChild);
          ionic.on('mousedown', handleMousedown, scrollChild);
          ionic.on('mousemove', handleTouchmove, scrollChild);
          ionic.on('mouseup', handleTouchend, scrollChild);
          ionic.on('scroll', handleScroll, scrollParent);
  
          // cleanup when done
          $scope.$on('$destroy', destroy);
        };
  
        function destroy() {
          if (scrollChild) {
            ionic.off(touchStartEvent, handleTouchstart, scrollChild);
            ionic.off(touchMoveEvent, handleTouchmove, scrollChild);
            ionic.off(touchEndEvent, handleTouchend, scrollChild);
            ionic.off('mousedown', handleMousedown, scrollChild);
            ionic.off('mousemove', handleTouchmove, scrollChild);
            ionic.off('mouseup', handleTouchend, scrollChild);
          }
          if (scrollParent) {
            ionic.off('scroll', handleScroll, scrollParent);
          }
          scrollParent = null;
          scrollChild = null;
        }
  
        // DOM manipulation and broadcast methods shared by JS and Native Scrolling
        // getter used by JS Scrolling
        self.getRefresherDomMethods = function () {
          return {
            activate: activate,
            deactivate: deactivate,
            start: start,
            show: show,
            hide: hide,
            tail: tail,
          };
        };
  
        function activate() {
          $element[0].classList.add('active');
          $scope.$onPulling();
        }
  
        function deactivate() {
          // give tail 150ms to finish
          $timeout(function () {
            // deactivateCallback
            $element.removeClass('active refreshing refreshing-tail');
            if (activated) activated = false;
          }, 150);
        }
  
        function start() {
          // startCallback
          $element[0].classList.add('refreshing');
          var q = $scope.$onRefresh();
  
          if (q && q.then) {
            q['finally'](function () {
              $scope.$broadcast('scroll.refreshComplete');
            });
          }
        }
  
        function show() {
          // showCallback
          $element[0].classList.remove('invisible');
        }
  
        function hide() {
          // showCallback
          $element[0].classList.add('invisible');
        }
  
        function tail() {
          // tailCallback
          $element[0].classList.add('refreshing-tail');
        }
  
        // for testing
        self.__handleTouchmove = handleTouchmove;
        self.__getScrollChild = function () {
          return scrollChild;
        };
        self.__getScrollParent = function () {
          return scrollParent;
        };
      },
    ]);
  
    /**
     * @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
        self.isNative = function () {
          return !!scrollViewOptions.nativeScrolling;
        };
  
        var element = (self.element = scrollViewOptions.el);
        var $element = (self.$element = jqLite(element));
        var scrollView;
        if (self.isNative()) {
          scrollView = self.scrollView = new ionic.views.ScrollNative(scrollViewOptions);
        } else {
          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 (!isDefined(scrollViewOptions.bouncing)) {
          ionic.Platform.ready(function () {
            if (scrollView && 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);
        angular.element($window).on('resize', resize);
  
        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();
          scrollView && scrollView.__cleanup && scrollView.__cleanup();
          angular.element($window).off('resize', resize);
          if ($element) {
            $element.off('scroll', scrollFunc);
          }
          if (self._scrollViewOptions) {
            self._scrollViewOptions.el = null;
          }
          if (scrollViewOptions) {
            scrollViewOptions.el = null;
          }
  
          scrollView = self.scrollView = scrollViewOptions = self._scrollViewOptions = element = self.$element = $element = null;
        });
  
        $timeout(function () {
          scrollView && scrollView.run && scrollView.run();
        });
  
        self.getScrollView = function () {
          return scrollView;
        };
  
        self.getScrollPosition = function () {
          return scrollView.getValues();
        };
  
        self.resize = function () {
          return $timeout(resize, 0, false).then(function () {
            $element && $element.triggerHandler('scroll-resize');
          });
        };
  
        self.scrollTop = function (shouldAnimate) {
          self.resize().then(function () {
            if (!scrollView) {
              return;
            }
            scrollView.scrollTo(0, 0, !!shouldAnimate);
          });
        };
  
        self.scrollBottom = function (shouldAnimate) {
          self.resize().then(function () {
            if (!scrollView) {
              return;
            }
            var max = scrollView.getScrollMax();
            scrollView.scrollTo(max.left, max.top, !!shouldAnimate);
          });
        };
  
        self.scrollTo = function (left, top, shouldAnimate) {
          self.resize().then(function () {
            if (!scrollView) {
              return;
            }
            scrollView.scrollTo(left, top, !!shouldAnimate);
          });
        };
  
        self.zoomTo = function (zoom, shouldAnimate, originLeft, originTop) {
          self.resize().then(function () {
            if (!scrollView) {
              return;
            }
            scrollView.zoomTo(zoom, !!shouldAnimate, originLeft, originTop);
          });
        };
  
        self.zoomBy = function (zoom, shouldAnimate, originLeft, originTop) {
          self.resize().then(function () {
            if (!scrollView) {
              return;
            }
            scrollView.zoomBy(zoom, !!shouldAnimate, originLeft, originTop);
          });
        };
  
        self.scrollBy = function (left, top, shouldAnimate) {
          self.resize().then(function () {
            if (!scrollView) {
              return;
            }
            scrollView.scrollBy(left, top, !!shouldAnimate);
          });
        };
  
        self.anchorScroll = function (shouldAnimate) {
          self.resize().then(function () {
            if (!scrollView) {
              return;
            }
            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;
            do {
              if (curElm !== null) scrollLeft += curElm.offsetLeft;
              if (curElm !== null) scrollTop += curElm.offsetTop;
              curElm = curElm.offsetParent;
            } while (curElm.attributes != self.element.attributes && curElm.offsetParent);
            scrollView.scrollTo(scrollLeft, scrollTop, !!shouldAnimate);
          });
        };
  
        self.freezeScroll = scrollView.freeze;
        self.freezeScrollShut = scrollView.freezeShut;
  
        self.freezeAllScrolls = function (shouldFreeze) {
          for (var i = 0; i < $ionicScrollDelegate._instances.length; i++) {
            $ionicScrollDelegate._instances[i].freezeScroll(shouldFreeze);
          }
        };
  
        /**
         * @private
         */
        self._setRefresher = function (refresherScope, refresherElement, refresherMethods) {
          self.refresher = refresherElement;
          var refresherHeight = self.refresher.clientHeight || 60;
          scrollView.activatePullToRefresh(refresherHeight, refresherMethods);
        };
      },
    ]);
  
    IonicModule.controller('$ionicSideMenus', [
      '$scope',
      '$attrs',
      '$ionicSideMenuDelegate',
      '$ionicPlatform',
      '$ionicBody',
      '$ionicHistory',
      '$ionicScrollDelegate',
      'IONIC_BACK_PRIORITY',
      '$rootScope',
      function (
        $scope,
        $attrs,
        $ionicSideMenuDelegate,
        $ionicPlatform,
        $ionicBody,
        $ionicHistory,
        $ionicScrollDelegate,
        IONIC_BACK_PRIORITY,
        $rootScope
      ) {
        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);
            $rootScope.$emit('$ionicSideMenuClose', 'left');
          } else {
            self.openPercentage(100);
            $rootScope.$emit('$ionicSideMenuOpen', 'left');
          }
        };
  
        /**
         * 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);
            $rootScope.$emit('$ionicSideMenuClose', 'right');
          } else {
            self.openPercentage(-100);
            $rootScope.$emit('$ionicSideMenuOpen', 'right');
          }
        };
  
        self.toggle = function (side) {
          if (side == 'right') {
            self.toggleRight();
          } else {
            self.toggleLeft();
          }
        };
  
        /**
         * Close all menus.
         */
        self.close = function () {
          self.openPercentage(0);
          $rootScope.$emit('$ionicSideMenuClose', 'left');
          $rootScope.$emit('$ionicSideMenuClose', 'right');
        };
  
        /**
         * @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) {
            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');
  
          self.content.setCanScroll(percentage == 0);
        };
  
        /*
    function freezeAllScrolls(shouldFreeze) {
      if (shouldFreeze && !self.isScrollFreeze) {
        $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);
  
      } else if (!shouldFreeze && self.isScrollFreeze) {
        $ionicScrollDelegate.freezeAllScrolls(false);
      }
      self.isScrollFreeze = shouldFreeze;
    }
    */
  
        /**
         * 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);
  
          leftShowing = amount > 0;
          rightShowing = amount < 0;
  
          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 {
            // 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 && self.right && self.right.isEnabled) {
            self.content.setMarginLeftAndRight(isAsideExposed ? self.left.width : 0, isAsideExposed ? self.right.width : 0);
          } else 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 || !$scope.dragContent) 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.content.setCanScroll(false);
          }
        };
  
        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 (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 dragIsWithinBounds && 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 = noop;
        var closeSideMenu = angular.bind(self, self.close);
  
        $scope.$watch(
          function () {
            return self.getOpenAmount() !== 0;
          },
          function (isOpen) {
            deregisterBackButtonAction();
            if (isOpen) {
              deregisterBackButtonAction = $ionicPlatform.registerBackButtonAction(closeSideMenu, IONIC_BACK_PRIORITY.sideMenu);
            }
          }
        );
  
        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.setCanScroll(true);
            self.content.element = null;
            self.content = null;
          }
        });
  
        self.initialize({
          left: {
            width: 275,
          },
          right: {
            width: 275,
          },
        });
      },
    ]);
    (function (ionic) {
      var TRANSLATE32 = 'translate(32,32)';
      var STROKE_OPACITY = 'stroke-opacity';
      var ROUND = 'round';
      var INDEFINITE = 'indefinite';
      var DURATION = '750ms';
      var NONE = 'none';
      var SHORTCUTS = {
        a: 'animate',
        an: 'attributeName',
        at: 'animateTransform',
        c: 'circle',
        da: 'stroke-dasharray',
        os: 'stroke-dashoffset',
        f: 'fill',
        lc: 'stroke-linecap',
        rc: 'repeatCount',
        sw: 'stroke-width',
        t: 'transform',
        v: 'values',
      };
  
      var SPIN_ANIMATION = {
        v: '0,32,32;360,32,32',
        an: 'transform',
        type: 'rotate',
        rc: INDEFINITE,
        dur: DURATION,
      };
  
      function createSvgElement(tagName, data, parent, spinnerName) {
        var ele = document.createElement(SHORTCUTS[tagName] || tagName);
        var k, x, y;
  
        for (k in data) {
          if (angular.isArray(data[k])) {
            for (x = 0; x < data[k].length; x++) {
              if (data[k][x].fn) {
                for (y = 0; y < data[k][x].t; y++) {
                  createSvgElement(k, data[k][x].fn(y, spinnerName), ele, spinnerName);
                }
              } else {
                createSvgElement(k, data[k][x], ele, spinnerName);
              }
            }
          } else {
            setSvgAttribute(ele, k, data[k]);
          }
        }
  
        parent.appendChild(ele);
      }
  
      function setSvgAttribute(ele, k, v) {
        ele.setAttribute(SHORTCUTS[k] || k, v);
      }
  
      function animationValues(strValues, i) {
        var values = strValues.split(';');
        var back = values.slice(i);
        var front = values.slice(0, values.length - back.length);
        values = back.concat(front).reverse();
        return values.join(';') + ';' + values[0];
      }
  
      var IOS_SPINNER = {
        sw: 4,
        lc: ROUND,
        line: [
          {
            fn: function (i, spinnerName) {
              return {
                y1: spinnerName == 'ios' ? 17 : 12,
                y2: spinnerName == 'ios' ? 29 : 20,
                t: TRANSLATE32 + ' rotate(' + (30 * i + (i < 6 ? 180 : -180)) + ')',
                a: [
                  {
                    fn: function () {
                      return {
                        an: STROKE_OPACITY,
                        dur: DURATION,
                        v: animationValues('0;.1;.15;.25;.35;.45;.55;.65;.7;.85;1', i),
                        rc: INDEFINITE,
                      };
                    },
                    t: 1,
                  },
                ],
              };
            },
            t: 12,
          },
        ],
      };
  
      var spinners = {
        android: {
          c: [
            {
              sw: 6,
              da: 128,
              os: 82,
              r: 26,
              cx: 32,
              cy: 32,
              f: NONE,
            },
          ],
        },
  
        ios: IOS_SPINNER,
  
        'ios-small': IOS_SPINNER,
  
        bubbles: {
          sw: 0,
          c: [
            {
              fn: function (i) {
                return {
                  cx: 24 * Math.cos((2 * Math.PI * i) / 8),
                  cy: 24 * Math.sin((2 * Math.PI * i) / 8),
                  t: TRANSLATE32,
                  a: [
                    {
                      fn: function () {
                        return {
                          an: 'r',
                          dur: DURATION,
                          v: animationValues('1;2;3;4;5;6;7;8', i),
                          rc: INDEFINITE,
                        };
                      },
                      t: 1,
                    },
                  ],
                };
              },
              t: 8,
            },
          ],
        },
  
        circles: {
          c: [
            {
              fn: function (i) {
                return {
                  r: 5,
                  cx: 24 * Math.cos((2 * Math.PI * i) / 8),
                  cy: 24 * Math.sin((2 * Math.PI * i) / 8),
                  t: TRANSLATE32,
                  sw: 0,
                  a: [
                    {
                      fn: function () {
                        return {
                          an: 'fill-opacity',
                          dur: DURATION,
                          v: animationValues('.3;.3;.3;.4;.7;.85;.9;1', i),
                          rc: INDEFINITE,
                        };
                      },
                      t: 1,
                    },
                  ],
                };
              },
              t: 8,
            },
          ],
        },
  
        crescent: {
          c: [
            {
              sw: 4,
              da: 128,
              os: 82,
              r: 26,
              cx: 32,
              cy: 32,
              f: NONE,
              at: [SPIN_ANIMATION],
            },
          ],
        },
  
        dots: {
          c: [
            {
              fn: function (i) {
                return {
                  cx: 16 + 16 * i,
                  cy: 32,
                  sw: 0,
                  a: [
                    {
                      fn: function () {
                        return {
                          an: 'fill-opacity',
                          dur: DURATION,
                          v: animationValues('.5;.6;.8;1;.8;.6;.5', i),
                          rc: INDEFINITE,
                        };
                      },
                      t: 1,
                    },
                    {
                      fn: function () {
                        return {
                          an: 'r',
                          dur: DURATION,
                          v: animationValues('4;5;6;5;4;3;3', i),
                          rc: INDEFINITE,
                        };
                      },
                      t: 1,
                    },
                  ],
                };
              },
              t: 3,
            },
          ],
        },
  
        lines: {
          sw: 7,
          lc: ROUND,
          line: [
            {
              fn: function (i) {
                return {
                  x1: 10 + i * 14,
                  x2: 10 + i * 14,
                  a: [
                    {
                      fn: function () {
                        return {
                          an: 'y1',
                          dur: DURATION,
                          v: animationValues('16;18;28;18;16', i),
                          rc: INDEFINITE,
                        };
                      },
                      t: 1,
                    },
                    {
                      fn: function () {
                        return {
                          an: 'y2',
                          dur: DURATION,
                          v: animationValues('48;44;36;46;48', i),
                          rc: INDEFINITE,
                        };
                      },
                      t: 1,
                    },
                    {
                      fn: function () {
                        return {
                          an: STROKE_OPACITY,
                          dur: DURATION,
                          v: animationValues('1;.8;.5;.4;1', i),
                          rc: INDEFINITE,
                        };
                      },
                      t: 1,
                    },
                  ],
                };
              },
              t: 4,
            },
          ],
        },
  
        ripple: {
          f: NONE,
          'fill-rule': 'evenodd',
          sw: 3,
          circle: [
            {
              fn: function (i) {
                return {
                  cx: 32,
                  cy: 32,
                  a: [
                    {
                      fn: function () {
                        return {
                          an: 'r',
                          begin: i * -1 + 's',
                          dur: '2s',
                          v: '0;24',
                          keyTimes: '0;1',
                          keySplines: '0.1,0.2,0.3,1',
                          calcMode: 'spline',
                          rc: INDEFINITE,
                        };
                      },
                      t: 1,
                    },
                    {
                      fn: function () {
                        return {
                          an: STROKE_OPACITY,
                          begin: i * -1 + 's',
                          dur: '2s',
                          v: '.2;1;.2;0',
                          rc: INDEFINITE,
                        };
                      },
                      t: 1,
                    },
                  ],
                };
              },
              t: 2,
            },
          ],
        },
  
        spiral: {
          defs: [
            {
              linearGradient: [
                {
                  id: 'sGD',
                  gradientUnits: 'userSpaceOnUse',
                  x1: 55,
                  y1: 46,
                  x2: 2,
                  y2: 46,
                  stop: [
                    {
                      offset: 0.1,
                      class: 'stop1',
                    },
                    {
                      offset: 1,
                      class: 'stop2',
                    },
                  ],
                },
              ],
            },
          ],
          g: [
            {
              sw: 4,
              lc: ROUND,
              f: NONE,
              path: [
                {
                  stroke: 'url(#sGD)',
                  d: 'M4,32 c0,15,12,28,28,28c8,0,16-4,21-9',
                },
                {
                  d: 'M60,32 C60,16,47.464,4,32,4S4,16,4,32',
                },
              ],
              at: [SPIN_ANIMATION],
            },
          ],
        },
      };
  
      var animations = {
        android: function (ele) {
          // Note that this is called as a function, not a constructor.
          var self = {};
  
          this.stop = false;
  
          var rIndex = 0;
          var rotateCircle = 0;
          var startTime;
          var svgEle = ele.querySelector('g');
          var circleEle = ele.querySelector('circle');
  
          function run() {
            if (self.stop) return;
  
            var v = easeInOutCubic(Date.now() - startTime, 650);
            var scaleX = 1;
            var translateX = 0;
            var dasharray = 188 - 58 * v;
            var dashoffset = 182 - 182 * v;
  
            if (rIndex % 2) {
              scaleX = -1;
              translateX = -64;
              dasharray = 128 - -58 * v;
              dashoffset = 182 * v;
            }
  
            var rotateLine = [0, -101, -90, -11, -180, 79, -270, -191][rIndex];
  
            setSvgAttribute(circleEle, 'da', Math.max(Math.min(dasharray, 188), 128));
            setSvgAttribute(circleEle, 'os', Math.max(Math.min(dashoffset, 182), 0));
            setSvgAttribute(
              circleEle,
              't',
              'scale(' + scaleX + ',1) translate(' + translateX + ',0) rotate(' + rotateLine + ',32,32)'
            );
  
            rotateCircle += 4.1;
            if (rotateCircle > 359) rotateCircle = 0;
            setSvgAttribute(svgEle, 't', 'rotate(' + rotateCircle + ',32,32)');
  
            if (v >= 1) {
              rIndex++;
              if (rIndex > 7) rIndex = 0;
              startTime = Date.now();
            }
  
            ionic.requestAnimationFrame(run);
          }
  
          return function () {
            startTime = Date.now();
            run();
            return self;
          };
        },
      };
  
      function easeInOutCubic(t, c) {
        t /= c / 2;
        if (t < 1) return (1 / 2) * t * t * t;
        t -= 2;
        return (1 / 2) * (t * t * t + 2);
      }
  
      IonicModule.controller('$ionicSpinner', [
        '$element',
        '$attrs',
        '$ionicConfig',
        function ($element, $attrs, $ionicConfig) {
          var spinnerName, anim;
  
          this.init = function () {
            spinnerName = $attrs.icon || $ionicConfig.spinner.icon();
  
            var container = document.createElement('div');
            createSvgElement(
              'svg',
              {
                viewBox: '0 0 64 64',
                g: [spinners[spinnerName]],
              },
              container,
              spinnerName
            );
  
            // Specifically for animations to work,
            // Android 4.3 and below requires the element to be
            // added as an html string, rather than dynmically
            // building up the svg element and appending it.
            $element.html(container.innerHTML);
  
            this.start();
  
            return spinnerName;
          };
  
          this.start = function () {
            animations[spinnerName] && (anim = animations[spinnerName]($element[0])());
          };
  
          this.stop = function () {
            animations[spinnerName] && (anim.stop = true);
          };
        },
      ]);
    })(ionic);
  
    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 previousSelectedTab = null;
        var selectedTabIndex;
        var isVisible = true;
        self.tabs = [];
  
        self.selectedIndex = function () {
          return self.tabs.indexOf(selectedTab);
        };
        self.selectedTab = function () {
          return selectedTab;
        };
        self.previousSelectedTab = function () {
          return previousSelectedTab;
        };
  
        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) {
            previousSelectedTab = selectedTab;
            selectedTab = selectedTabIndex = null;
            tab.$tabSelected = false;
            (tab.onDeselect || noop)();
            tab.$broadcast && tab.$broadcast('$ionicHistory.deselect');
          }
        };
  
        self.select = function (tab, shouldEmitEvent) {
          var tabIndex;
          if (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 || 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,
              });
            }
  
            $scope.$broadcast('tabSelected', { selectedTab: tab, selectedTabIndex: tabIndex });
          }
        };
  
        self.hasActiveScope = function () {
          for (var x = 0; x < self.tabs.length; x++) {
            if ($ionicHistory.isActiveScope(self.tabs[x])) {
              return true;
            }
          }
          return false;
        };
  
        self.showBar = function (show) {
          if (arguments.length) {
            if (show) {
              $element.removeClass('tabs-item-hide');
            } else {
              $element.addClass('tabs-item-hide');
            }
            isVisible = !!show;
          }
          return isVisible;
        };
      },
    ]);
  
    IonicModule.controller('$ionicView', [
      '$scope',
      '$element',
      '$attrs',
      '$compile',
      '$rootScope',
      function ($scope, $element, $attrs, $compile, $rootScope) {
        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(
              extend(transData, {
                title: viewTitle,
                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:
            '<div class="action-sheet-backdrop">' +
            '<div class="action-sheet-wrapper">' +
            '<div class="action-sheet" ng-class="{\'action-sheet-has-icons\': $actionSheetHasIcon}">' +
            '<div class="action-sheet-group action-sheet-options">' +
            '<div class="action-sheet-title" ng-if="titleText" ng-bind-html="titleText"></div>' +
            '<button class="button action-sheet-option" ng-click="buttonClicked($index)" ng-class="b.className" ng-repeat="b in buttons" ng-bind-html="b.text"></button>' +
            '<button class="button destructive action-sheet-destructive" ng-if="destructiveText" ng-click="destructiveButtonClicked()" ng-bind-html="destructiveText"></button>' +
            '</div>' +
            '<div class="action-sheet-group action-sheet-cancel" ng-if="cancelText">' +
            '<button class="button" ng-click="cancel()" ng-bind-html="cancelText"></button>' +
            '</div>' +
            '</div>' +
            '</div>' +
            '</div>',
        };
      },
    ]);
  
    /**
     * @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
     * <ion-checkbox ng-model="isChecked">Checkbox Label</ion-checkbox>
     * ```
     */
  
    IonicModule.directive('ionCheckbox', [
      '$ionicConfig',
      function ($ionicConfig) {
        return {
          restrict: 'E',
          replace: true,
          require: '?ngModel',
          transclude: true,
          template:
            '<label class="item item-checkbox">' +
            '<div class="checkbox checkbox-input-hidden disable-pointer-events">' +
            '<input type="checkbox">' +
            '<i class="checkbox-icon"></i>' +
            '</div>' +
            '<div class="item-content disable-pointer-events" ng-transclude></div>' +
            '</label>',
          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,
                'ng-required': attr.ngRequired,
                required: attr.required,
              },
              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
     * @restrict A
     * @name collectionRepeat
     * @module ionic
     * @codepen 7ec1ec58f2489ab8f359fa1a0fe89c15
     * @description
     * `collection-repeat` allows an app to show huge lists of items much more performantly than
     * `ng-repeat`.
     *
     * It renders into the DOM only as many items as are currently visible.
     *
     * This means that on a phone screen that can fit eight items, only the eight items matching
     * the current scroll position will be rendered.
     *
     * **The Basics**:
     *
     * - The data given to collection-repeat must be an array.
     * - If the `item-height` and `item-width` attributes are not supplied, it will be assumed that
     *   every item in the list has the same dimensions as the first item.
     * - Don't use angular one-time binding (`::`) with collection-repeat. The scope of each item is
     *   assigned new data and re-digested as you scroll. Bindings need to update, and one-time bindings
     *   won't.
     *
     * **Performance Tips**:
     *
     * - The iOS webview has a performance bottleneck when switching out `<img src>` attributes.
     *   To increase performance of images on iOS, cache your images in advance and,
     *   if possible, lower the number of unique images. We're working on [a solution](https://github.com/ionic-team/ionic/issues/3194).
     *
     * @usage
     * #### Basic Item List ([codepen](http://codepen.io/ionic/pen/0c2c35a34a8b18ad4d793fef0b081693))
     * ```html
     * <ion-content>
     *   <ion-item collection-repeat="item in items">
     *     {% raw %}{{item}}{% endraw %}
     *   </ion-item>
     * </ion-content>
     * ```
     *
     * #### Grid of Images ([codepen](http://codepen.io/ionic/pen/5515d4efd9d66f780e96787387f41664))
     * ```html
     * <ion-content>
     *   <img collection-repeat="photo in photos"
     *     item-width="33%"
     *     item-height="200px"
     *     ng-src="{% raw %}{{photo.url}}{% endraw %}">
     * </ion-content>
     * ```
     *
     * #### Horizontal Scroller, Dynamic Item Width ([codepen](http://codepen.io/ionic/pen/67cc56b349124a349acb57a0740e030e))
     * ```html
     * <ion-content>
     *   <h2>Available Kittens:</h2>
     *   <ion-scroll direction="x" class="available-scroller">
     *     <div class="photo" collection-repeat="photo in main.photos"
     *        item-height="250" item-width="photo.width + 30">
     *        <img ng-src="{% raw %}{{photo.src}}{% endraw %}">
     *     </div>
     *   </ion-scroll>
     * </ion-content>
     * ```
     *
     * @param {expression} collection-repeat The expression indicating how to enumerate a collection,
     *   of the format  `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` or `album in artist.albums | orderBy:'name'`.
     * @param {expression=} item-width The width of the repeated element. The expression must return
     *   a number (pixels) or a percentage. Defaults to the width of the first item in the list.
     *   (previously named collection-item-width)
     * @param {expression=} item-height The height of the repeated element. The expression must return
     *   a number (pixels) or a percentage. Defaults to the height of the first item in the list.
     *   (previously named collection-item-height)
     * @param {number=} item-render-buffer The number of items to load before and after the visible
     *   items in the list. Default 3. Tip: set this higher if you have lots of images to preload, but
     *   don't set it too high or you'll see performance loss.
     * @param {boolean=} force-refresh-images Force images to refresh as you scroll. This fixes a problem
     *   where, when an element is interchanged as scrolling, its image will still have the old src
     *   while the new src loads. Setting this to true comes with a small performance loss.
     */
  
    IonicModule.directive('collectionRepeat', CollectionRepeatDirective).factory('$ionicCollectionManager', RepeatManagerFactory);
  
    var ONE_PX_TRANSPARENT_IMG_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
    var WIDTH_HEIGHT_REGEX = /height:.*?px;\s*width:.*?px/;
    var DEFAULT_RENDER_BUFFER = 3;
  
    CollectionRepeatDirective.$inject = ['$ionicCollectionManager', '$parse', '$window', '$$rAF', '$rootScope', '$timeout'];
    function CollectionRepeatDirective($ionicCollectionManager, $parse, $window, $$rAF, $rootScope, $timeout) {
      return {
        restrict: 'A',
        priority: 1000,
        transclude: 'element',
        $$tlb: true,
        require: '^^$ionicScroll',
        link: postLink,
      };
  
      function postLink(scope, element, attr, scrollCtrl, transclude) {
        var scrollView = scrollCtrl.scrollView;
        var node = element[0];
        var containerNode = angular.element('<div class="collection-repeat-container">')[0];
        node.parentNode.replaceChild(containerNode, node);
  
        if (scrollView.options.scrollingX && scrollView.options.scrollingY) {
          throw new Error('collection-repeat expected a parent x or y scrollView, not ' + 'an xy scrollView.');
        }
  
        var repeatExpr = attr.collectionRepeat;
        var match = repeatExpr.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
        if (!match) {
          throw new Error(
            "collection-repeat expected expression in form of '_item_ in " +
              "_collection_[ track by _id_]' but got '" +
              attr.collectionRepeat +
              "'."
          );
        }
        var keyExpr = match[1];
        var listExpr = match[2];
        var listGetter = $parse(listExpr);
        var heightData = {};
        var widthData = {};
        var computedStyleDimensions = {};
        var data = [];
        var repeatManager;
  
        // attr.collectionBufferSize is deprecated
        var renderBufferExpr = attr.itemRenderBuffer || attr.collectionBufferSize;
        var renderBuffer = angular.isDefined(renderBufferExpr) ? parseInt(renderBufferExpr) : DEFAULT_RENDER_BUFFER;
  
        // attr.collectionItemHeight is deprecated
        var heightExpr = attr.itemHeight || attr.collectionItemHeight;
        // attr.collectionItemWidth is deprecated
        var widthExpr = attr.itemWidth || attr.collectionItemWidth;
  
        var afterItemsContainer = initAfterItemsContainer();
  
        var changeValidator = makeChangeValidator();
        initDimensions();
  
        // Dimensions are refreshed on resize or data change.
        scrollCtrl.$element.on('scroll-resize', refreshDimensions);
  
        angular.element($window).on('resize', onResize);
        var unlistenToExposeAside = $rootScope.$on(
          '$ionicExposeAside',
          ionic.animationFrameThrottle(function () {
            scrollCtrl.scrollView.resize();
            onResize();
          })
        );
        $timeout(refreshDimensions, 0, false);
  
        function onResize() {
          if (changeValidator.resizeRequiresRefresh(scrollView.__clientWidth, scrollView.__clientHeight)) {
            refreshDimensions();
          }
        }
  
        scope.$watchCollection(listGetter, function (newValue) {
          data = newValue || (newValue = []);
          if (!angular.isArray(newValue)) {
            throw new Error("collection-repeat expected an array for '" + listExpr + "', " + 'but got a ' + typeof value);
          }
          // Wait for this digest to end before refreshing everything.
          scope.$$postDigest(function () {
            getRepeatManager().setData(data);
            if (changeValidator.dataChangeRequiresRefresh(data)) refreshDimensions();
          });
        });
  
        scope.$on('$destroy', function () {
          angular.element($window).off('resize', onResize);
          unlistenToExposeAside();
          scrollCtrl.$element && scrollCtrl.$element.off('scroll-resize', refreshDimensions);
  
          computedStyleNode && computedStyleNode.parentNode && computedStyleNode.parentNode.removeChild(computedStyleNode);
          computedStyleScope && computedStyleScope.$destroy();
          computedStyleScope = computedStyleNode = null;
  
          repeatManager && repeatManager.destroy();
          repeatManager = null;
        });
  
        function makeChangeValidator() {
          var self;
          return (self = {
            dataLength: 0,
            width: 0,
            height: 0,
            // A resize triggers a refresh only if we have data, the scrollView has size,
            // and the size has changed.
            resizeRequiresRefresh: function (newWidth, newHeight) {
              var requiresRefresh =
                self.dataLength && newWidth && newHeight && (newWidth !== self.width || newHeight !== self.height);
  
              self.width = newWidth;
              self.height = newHeight;
  
              return !!requiresRefresh;
            },
            // A change in data only triggers a refresh if the data has length, or if the data's
            // length is less than before.
            dataChangeRequiresRefresh: function (newData) {
              var requiresRefresh = newData.length > 0 || newData.length < self.dataLength;
  
              self.dataLength = newData.length;
  
              return !!requiresRefresh;
            },
          });
        }
  
        function getRepeatManager() {
          return (
            repeatManager ||
            (repeatManager = new $ionicCollectionManager({
              afterItemsNode: afterItemsContainer[0],
              containerNode: containerNode,
              heightData: heightData,
              widthData: widthData,
              forceRefreshImages: !!(isDefined(attr.forceRefreshImages) && attr.forceRefreshImages !== 'false'),
              keyExpression: keyExpr,
              renderBuffer: renderBuffer,
              scope: scope,
              scrollView: scrollCtrl.scrollView,
              transclude: transclude,
            }))
          );
        }
  
        function initAfterItemsContainer() {
          var container = angular.element(scrollView.__content.querySelector('.collection-repeat-after-container'));
          // Put everything in the view after the repeater into a container.
          if (!container.length) {
            var elementIsAfterRepeater = false;
            var afterNodes = [].filter.call(scrollView.__content.childNodes, function (node) {
              if (ionic.DomUtil.contains(node, containerNode)) {
                elementIsAfterRepeater = true;
                return false;
              }
              return elementIsAfterRepeater;
            });
            container = angular.element('<span class="collection-repeat-after-container">');
            if (scrollView.options.scrollingX) {
              container.addClass('horizontal');
            }
            container.append(afterNodes);
            scrollView.__content.appendChild(container[0]);
          }
          return container;
        }
  
        function initDimensions() {
          //Height and width have four 'modes':
          //1) Computed Mode
          //  - Nothing is supplied, so we getComputedStyle() on one element in the list and use
          //    that width and height value for the width and height of every item. This is re-computed
          //    every resize.
          //2) Constant Mode, Static Integer
          //  - The user provides a constant number for width or height, in pixels. We parse it,
          //    store it on the `value` field, and it never changes
          //3) Constant Mode, Percent
          //  - The user provides a percent string for width or height. The getter for percent is
          //    stored on the `getValue()` field, and is re-evaluated once every resize. The result
          //    is stored on the `value` field.
          //4) Dynamic Mode
          //  - The user provides a dynamic expression for the width or height.  This is re-evaluated
          //    for every item, stored on the `.getValue()` field.
          if (heightExpr) {
            parseDimensionAttr(heightExpr, heightData);
          } else {
            heightData.computed = true;
          }
          if (widthExpr) {
            parseDimensionAttr(widthExpr, widthData);
          } else {
            widthData.computed = true;
          }
        }
  
        function refreshDimensions() {
          var hasData = data.length > 0;
  
          if (hasData && (heightData.computed || widthData.computed)) {
            computeStyleDimensions();
          }
  
          if (hasData && heightData.computed) {
            heightData.value = computedStyleDimensions.height;
            if (!heightData.value) {
              throw new Error(
                'collection-repeat tried to compute the height of repeated elements "' +
                  repeatExpr +
                  '", but was unable to. Please provide the "item-height" attribute. ' +
                  'http://ionicframework.com/docs/api/directive/collectionRepeat/'
              );
            }
          } else if (!heightData.dynamic && heightData.getValue) {
            // If it's a constant with a getter (eg percent), we just refresh .value after resize
            heightData.value = heightData.getValue();
          }
  
          if (hasData && widthData.computed) {
            widthData.value = computedStyleDimensions.width;
            if (!widthData.value) {
              throw new Error(
                'collection-repeat tried to compute the width of repeated elements "' +
                  repeatExpr +
                  '", but was unable to. Please provide the "item-width" attribute. ' +
                  'http://ionicframework.com/docs/api/directive/collectionRepeat/'
              );
            }
          } else if (!widthData.dynamic && widthData.getValue) {
            // If it's a constant with a getter (eg percent), we just refresh .value after resize
            widthData.value = widthData.getValue();
          }
          // Dynamic dimensions aren't updated on resize. Since they're already dynamic anyway,
          // .getValue() will be used.
  
          getRepeatManager().refreshLayout();
        }
  
        function parseDimensionAttr(attrValue, dimensionData) {
          if (!attrValue) return;
  
          var parsedValue;
          // Try to just parse the plain attr value
          try {
            parsedValue = $parse(attrValue);
          } catch (e) {
            // If the parse fails and the value has `px` or `%` in it, surround the attr in
            // quotes, to attempt to let the user provide a simple `attr="100%"` or `attr="100px"`
            if (attrValue.trim().match(/\d+(px|%)$/)) {
              attrValue = '"' + attrValue + '"';
            }
            parsedValue = $parse(attrValue);
          }
  
          var constantAttrValue = attrValue.replace(/(\'|\"|px|%)/g, '').trim();
          var isConstant = constantAttrValue.length && !/([a-zA-Z]|\$|:|\?)/.test(constantAttrValue);
          dimensionData.attrValue = attrValue;
  
          // If it's a constant, it's either a percent or just a constant pixel number.
          if (isConstant) {
            // For percents, store the percent getter on .getValue()
            if (attrValue.indexOf('%') > -1) {
              var decimalValue = parseFloat(parsedValue()) / 100;
              dimensionData.getValue =
                dimensionData === heightData
                  ? function () {
                      return Math.floor(decimalValue * scrollView.__clientHeight);
                    }
                  : function () {
                      return Math.floor(decimalValue * scrollView.__clientWidth);
                    };
            } else {
              // For static constants, just store the static constant.
              dimensionData.value = parseInt(parsedValue());
            }
          } else {
            dimensionData.dynamic = true;
            dimensionData.getValue =
              dimensionData === heightData
                ? function heightGetter(scope, locals) {
                    var result = parsedValue(scope, locals);
                    if (result.charAt && result.charAt(result.length - 1) === '%') {
                      return Math.floor((parseFloat(result) / 100) * scrollView.__clientHeight);
                    }
                    return parseInt(result);
                  }
                : function widthGetter(scope, locals) {
                    var result = parsedValue(scope, locals);
                    if (result.charAt && result.charAt(result.length - 1) === '%') {
                      return Math.floor((parseFloat(result) / 100) * scrollView.__clientWidth);
                    }
                    return parseInt(result);
                  };
          }
        }
  
        var computedStyleNode;
        var computedStyleScope;
        function computeStyleDimensions() {
          if (!computedStyleNode) {
            transclude((computedStyleScope = scope.$new()), function (clone) {
              clone[0].removeAttribute('collection-repeat'); // remove absolute position styling
              computedStyleNode = clone[0];
            });
          }
  
          computedStyleScope[keyExpr] = (listGetter(scope) || [])[0];
          if (!$rootScope.$$phase) computedStyleScope.$digest();
          containerNode.appendChild(computedStyleNode);
  
          var style = $window.getComputedStyle(computedStyleNode);
          computedStyleDimensions.width = parseInt(style.width);
          computedStyleDimensions.height = parseInt(style.height);
  
          containerNode.removeChild(computedStyleNode);
        }
      }
    }
  
    RepeatManagerFactory.$inject = ['$rootScope', '$window', '$$rAF'];
    function RepeatManagerFactory($rootScope, $window, $$rAF) {
      var EMPTY_DIMENSION = { primaryPos: 0, secondaryPos: 0, primarySize: 0, secondarySize: 0, rowPrimarySize: 0 };
  
      return function RepeatController(options) {
        var afterItemsNode = options.afterItemsNode;
        var containerNode = options.containerNode;
        var forceRefreshImages = options.forceRefreshImages;
        var heightData = options.heightData;
        var widthData = options.widthData;
        var keyExpression = options.keyExpression;
        var renderBuffer = options.renderBuffer;
        var scope = options.scope;
        var scrollView = options.scrollView;
        var transclude = options.transclude;
  
        var data = [];
  
        var getterLocals = {};
        var heightFn =
          heightData.getValue ||
          function () {
            return heightData.value;
          };
        var heightGetter = function (index, value) {
          getterLocals[keyExpression] = value;
          getterLocals.$index = index;
          return heightFn(scope, getterLocals);
        };
  
        var widthFn =
          widthData.getValue ||
          function () {
            return widthData.value;
          };
        var widthGetter = function (index, value) {
          getterLocals[keyExpression] = value;
          getterLocals.$index = index;
          return widthFn(scope, getterLocals);
        };
  
        var isVertical = !!scrollView.options.scrollingY;
  
        // We say it's a grid view if we're either dynamic or not 100% width
        var isGridView = isVertical
          ? widthData.dynamic || widthData.value !== scrollView.__clientWidth
          : heightData.dynamic || heightData.value !== scrollView.__clientHeight;
  
        var isStaticView = !heightData.dynamic && !widthData.dynamic;
  
        var PRIMARY = 'PRIMARY';
        var SECONDARY = 'SECONDARY';
        var TRANSLATE_TEMPLATE_STR = isVertical ? 'translate3d(SECONDARYpx,PRIMARYpx,0)' : 'translate3d(PRIMARYpx,SECONDARYpx,0)';
        var WIDTH_HEIGHT_TEMPLATE_STR = isVertical
          ? 'height: PRIMARYpx; width: SECONDARYpx;'
          : 'height: SECONDARYpx; width: PRIMARYpx;';
  
        var estimatedHeight;
        var estimatedWidth;
  
        var repeaterBeforeSize = 0;
        var repeaterAfterSize = 0;
  
        var renderStartIndex = -1;
        var renderEndIndex = -1;
        var renderAfterBoundary = -1;
        var renderBeforeBoundary = -1;
  
        var itemsPool = [];
        var itemsLeaving = [];
        var itemsEntering = [];
        var itemsShownMap = {};
        var nextItemId = 0;
  
        var scrollViewSetDimensions = isVertical
          ? function () {
              scrollView.setDimensions(null, null, null, view.getContentSize(), true);
            }
          : function () {
              scrollView.setDimensions(null, null, view.getContentSize(), null, true);
            };
  
        // view is a mix of list/grid methods + static/dynamic methods.
        // See bottom for implementations. Available methods:
        //
        // getEstimatedPrimaryPos(i), getEstimatedSecondaryPos(i), getEstimatedIndex(scrollTop),
        // calculateDimensions(toIndex), getDimensions(index),
        // updateRenderRange(scrollTop, scrollValueEnd), onRefreshLayout(), onRefreshData()
        var view = isVertical ? new VerticalViewType() : new HorizontalViewType();
        (isGridView ? GridViewType : ListViewType).call(view);
        (isStaticView ? StaticViewType : DynamicViewType).call(view);
  
        var contentSizeStr = isVertical ? 'getContentHeight' : 'getContentWidth';
        var originalGetContentSize = scrollView.options[contentSizeStr];
        scrollView.options[contentSizeStr] = angular.bind(view, view.getContentSize);
  
        scrollView.__$callback = scrollView.__callback;
        scrollView.__callback = function (transformLeft, transformTop, zoom, wasResize) {
          var scrollValue = view.getScrollValue();
          if (
            renderStartIndex === -1 ||
            scrollValue + view.scrollPrimarySize > renderAfterBoundary ||
            scrollValue < renderBeforeBoundary
          ) {
            render();
          }
          scrollView.__$callback(transformLeft, transformTop, zoom, wasResize);
        };
  
        var isLayoutReady = false;
        var isDataReady = false;
        this.refreshLayout = function () {
          if (data.length) {
            estimatedHeight = heightGetter(0, data[0]);
            estimatedWidth = widthGetter(0, data[0]);
          } else {
            // If we don't have any data in our array, just guess.
            estimatedHeight = 100;
            estimatedWidth = 100;
          }
  
          // Get the size of every element AFTER the repeater. We have to get the margin before and
          // after the first/last element to fix a browser bug with getComputedStyle() not counting
          // the first/last child's margins into height.
          var style = getComputedStyle(afterItemsNode) || {};
          var firstStyle = (afterItemsNode.firstElementChild && getComputedStyle(afterItemsNode.firstElementChild)) || {};
          var lastStyle = (afterItemsNode.lastElementChild && getComputedStyle(afterItemsNode.lastElementChild)) || {};
          repeaterAfterSize =
            (parseInt(style[isVertical ? 'height' : 'width']) || 0) +
            ((firstStyle && parseInt(firstStyle[isVertical ? 'marginTop' : 'marginLeft'])) || 0) +
            ((lastStyle && parseInt(lastStyle[isVertical ? 'marginBottom' : 'marginRight'])) || 0);
  
          // Get the offsetTop of the repeater.
          repeaterBeforeSize = 0;
          var current = containerNode;
          do {
            repeaterBeforeSize += current[isVertical ? 'offsetTop' : 'offsetLeft'];
          } while (ionic.DomUtil.contains(scrollView.__content, (current = current.offsetParent)));
  
          var containerPrevNode = containerNode.previousElementSibling;
          var beforeStyle = containerPrevNode ? $window.getComputedStyle(containerPrevNode) : {};
          var beforeMargin = parseInt(beforeStyle[isVertical ? 'marginBottom' : 'marginRight'] || 0);
  
          // Because we position the collection container with position: relative, it doesn't take
          // into account where to position itself relative to the previous element's marginBottom.
          // To compensate, we translate the container up by the previous element's margin.
          containerNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR.replace(PRIMARY, -beforeMargin).replace(SECONDARY, 0);
          repeaterBeforeSize -= beforeMargin;
  
          if (!scrollView.__clientHeight || !scrollView.__clientWidth) {
            scrollView.__clientWidth = scrollView.__container.clientWidth;
            scrollView.__clientHeight = scrollView.__container.clientHeight;
          }
  
          (view.onRefreshLayout || angular.noop)();
          view.refreshDirection();
          scrollViewSetDimensions();
  
          // Create the pool of items for reuse, setting the size to (estimatedItemsOnScreen) * 2,
          // plus the size of the renderBuffer.
          if (!isLayoutReady) {
            var poolSize = Math.max(20, renderBuffer * 3);
            for (var i = 0; i < poolSize; i++) {
              itemsPool.push(new RepeatItem());
            }
          }
  
          isLayoutReady = true;
          if (isLayoutReady && isDataReady) {
            // If the resize or latest data change caused the scrollValue to
            // now be out of bounds, resize the scrollView.
            if (scrollView.__scrollLeft > scrollView.__maxScrollLeft || scrollView.__scrollTop > scrollView.__maxScrollTop) {
              scrollView.resize();
            }
            forceRerender(true);
          }
        };
  
        this.setData = function (newData) {
          data = newData;
          (view.onRefreshData || angular.noop)();
          isDataReady = true;
        };
  
        this.destroy = function () {
          render.destroyed = true;
  
          itemsPool.forEach(function (item) {
            item.scope.$destroy();
            item.scope = item.element = item.node = item.images = null;
          });
          itemsPool.length = itemsEntering.length = itemsLeaving.length = 0;
          itemsShownMap = {};
  
          //Restore the scrollView's normal behavior and resize it to normal size.
          scrollView.options[contentSizeStr] = originalGetContentSize;
          scrollView.__callback = scrollView.__$callback;
          scrollView.resize();
          (view.onDestroy || angular.noop)();
        };
  
        function forceRerender() {
          return render(true);
        }
        function render(forceRerender) {
          if (render.destroyed) return;
          var i;
          var ii;
          var item;
          var dim;
          var scope;
          var scrollValue = view.getScrollValue();
          var scrollValueEnd = scrollValue + view.scrollPrimarySize;
  
          view.updateRenderRange(scrollValue, scrollValueEnd);
  
          renderStartIndex = Math.max(0, renderStartIndex - renderBuffer);
          renderEndIndex = Math.min(data.length - 1, renderEndIndex + renderBuffer);
  
          for (i in itemsShownMap) {
            if (i < renderStartIndex || i > renderEndIndex) {
              item = itemsShownMap[i];
              delete itemsShownMap[i];
              itemsLeaving.push(item);
              item.isShown = false;
            }
          }
  
          // Render indicies that aren't shown yet
          //
          // NOTE(ajoslin): this may sound crazy, but calling any other functions during this render
          // loop will often push the render time over the edge from less than one frame to over
          // one frame, causing visible jank.
          // DON'T call any other functions inside this loop unless it's vital.
          for (i = renderStartIndex; i <= renderEndIndex; i++) {
            // We only go forward with render if the index is in data, the item isn't already shown,
            // or forceRerender is on.
            if (i >= data.length || (itemsShownMap[i] && !forceRerender)) continue;
  
            item =
              itemsShownMap[i] ||
              (itemsShownMap[i] = itemsLeaving.length ? itemsLeaving.pop() : itemsPool.length ? itemsPool.shift() : new RepeatItem());
            itemsEntering.push(item);
            item.isShown = true;
  
            scope = item.scope;
            scope.$index = i;
            scope[keyExpression] = data[i];
            scope.$first = i === 0;
            scope.$last = i === data.length - 1;
            scope.$middle = !(scope.$first || scope.$last);
            scope.$odd = !(scope.$even = (i & 1) === 0);
  
            if (scope.$$disconnected) ionic.Utils.reconnectScope(item.scope);
  
            dim = view.getDimensions(i);
            if (item.secondaryPos !== dim.secondaryPos || item.primaryPos !== dim.primaryPos) {
              item.node.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR.replace(
                PRIMARY,
                (item.primaryPos = dim.primaryPos)
              ).replace(SECONDARY, (item.secondaryPos = dim.secondaryPos));
            }
            if (item.secondarySize !== dim.secondarySize || item.primarySize !== dim.primarySize) {
              item.node.style.cssText = item.node.style.cssText.replace(
                WIDTH_HEIGHT_REGEX,
                WIDTH_HEIGHT_TEMPLATE_STR
                  //TODO fix item.primarySize + 1 hack
                  .replace(PRIMARY, (item.primarySize = dim.primarySize) + 1)
                  .replace(SECONDARY, (item.secondarySize = dim.secondarySize))
              );
            }
          }
  
          // If we reach the end of the list, render the afterItemsNode - this contains all the
          // elements the developer placed after the collection-repeat
          if (renderEndIndex === data.length - 1) {
            dim = view.getDimensions(data.length - 1) || EMPTY_DIMENSION;
            afterItemsNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR.replace(
              PRIMARY,
              dim.primaryPos + dim.primarySize
            ).replace(SECONDARY, 0);
          }
  
          while (itemsLeaving.length) {
            item = itemsLeaving.pop();
            item.scope.$broadcast('$collectionRepeatLeave');
            ionic.Utils.disconnectScope(item.scope);
            itemsPool.push(item);
            item.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
            item.primaryPos = item.secondaryPos = null;
          }
  
          if (forceRefreshImages) {
            for (i = 0, ii = itemsEntering.length; i < ii && (item = itemsEntering[i]); i++) {
              if (!item.images) continue;
              for (var j = 0, jj = item.images.length, img; j < jj && (img = item.images[j]); j++) {
                var src = img.src;
                img.src = ONE_PX_TRANSPARENT_IMG_SRC;
                img.src = src;
              }
            }
          }
          if (forceRerender) {
            var rootScopePhase = $rootScope.$$phase;
            while (itemsEntering.length) {
              item = itemsEntering.pop();
              if (!rootScopePhase) item.scope.$digest();
            }
          } else {
            digestEnteringItems();
          }
        }
  
        function digestEnteringItems() {
          var item;
          if (digestEnteringItems.running) return;
          digestEnteringItems.running = true;
  
          $$rAF(function process() {
            var rootScopePhase = $rootScope.$$phase;
            while (itemsEntering.length) {
              item = itemsEntering.pop();
              if (item.isShown) {
                if (!rootScopePhase) item.scope.$digest();
              }
            }
            digestEnteringItems.running = false;
          });
        }
  
        function RepeatItem() {
          var self = this;
          this.scope = scope.$new();
          this.id = 'item' + nextItemId++;
          transclude(this.scope, function (clone) {
            self.element = clone;
            self.element.data('$$collectionRepeatItem', self);
            // TODO destroy
            self.node = clone[0];
            // Batch style setting to lower repaints
            self.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
            self.node.style.cssText += ' height: 0px; width: 0px;';
            ionic.Utils.disconnectScope(self.scope);
            containerNode.appendChild(self.node);
            self.images = clone[0].getElementsByTagName('img');
          });
        }
  
        function VerticalViewType() {
          this.getItemPrimarySize = heightGetter;
          this.getItemSecondarySize = widthGetter;
  
          this.getScrollValue = function () {
            return Math.max(
              0,
              Math.min(
                scrollView.__scrollTop - repeaterBeforeSize,
                scrollView.__maxScrollTop - repeaterBeforeSize - repeaterAfterSize
              )
            );
          };
  
          this.refreshDirection = function () {
            this.scrollPrimarySize = scrollView.__clientHeight;
            this.scrollSecondarySize = scrollView.__clientWidth;
  
            this.estimatedPrimarySize = estimatedHeight;
            this.estimatedSecondarySize = estimatedWidth;
            this.estimatedItemsAcross = (isGridView && Math.floor(scrollView.__clientWidth / estimatedWidth)) || 1;
          };
        }
        function HorizontalViewType() {
          this.getItemPrimarySize = widthGetter;
          this.getItemSecondarySize = heightGetter;
  
          this.getScrollValue = function () {
            return Math.max(
              0,
              Math.min(
                scrollView.__scrollLeft - repeaterBeforeSize,
                scrollView.__maxScrollLeft - repeaterBeforeSize - repeaterAfterSize
              )
            );
          };
  
          this.refreshDirection = function () {
            this.scrollPrimarySize = scrollView.__clientWidth;
            this.scrollSecondarySize = scrollView.__clientHeight;
  
            this.estimatedPrimarySize = estimatedWidth;
            this.estimatedSecondarySize = estimatedHeight;
            this.estimatedItemsAcross = (isGridView && Math.floor(scrollView.__clientHeight / estimatedHeight)) || 1;
          };
        }
  
        function GridViewType() {
          this.getEstimatedSecondaryPos = function (index) {
            return (index % this.estimatedItemsAcross) * this.estimatedSecondarySize;
          };
          this.getEstimatedPrimaryPos = function (index) {
            return Math.floor(index / this.estimatedItemsAcross) * this.estimatedPrimarySize;
          };
          this.getEstimatedIndex = function (scrollValue) {
            return Math.floor(scrollValue / this.estimatedPrimarySize) * this.estimatedItemsAcross;
          };
        }
  
        function ListViewType() {
          this.getEstimatedSecondaryPos = function () {
            return 0;
          };
          this.getEstimatedPrimaryPos = function (index) {
            return index * this.estimatedPrimarySize;
          };
          this.getEstimatedIndex = function (scrollValue) {
            return Math.floor(scrollValue / this.estimatedPrimarySize);
          };
        }
  
        function StaticViewType() {
          this.getContentSize = function () {
            return this.getEstimatedPrimaryPos(data.length - 1) + this.estimatedPrimarySize + repeaterBeforeSize + repeaterAfterSize;
          };
          // static view always returns the same object for getDimensions, to avoid memory allocation
          // while scrolling. This could be dangerous if this was a public function, but it's not.
          // Only we use it.
          var dim = {};
          this.getDimensions = function (index) {
            dim.primaryPos = this.getEstimatedPrimaryPos(index);
            dim.secondaryPos = this.getEstimatedSecondaryPos(index);
            dim.primarySize = this.estimatedPrimarySize;
            dim.secondarySize = this.estimatedSecondarySize;
            return dim;
          };
          this.updateRenderRange = function (scrollValue, scrollValueEnd) {
            renderStartIndex = Math.max(0, this.getEstimatedIndex(scrollValue));
  
            // Make sure the renderEndIndex takes into account all the items on the row
            renderEndIndex = Math.min(data.length - 1, this.getEstimatedIndex(scrollValueEnd) + this.estimatedItemsAcross - 1);
  
            renderBeforeBoundary = Math.max(0, this.getEstimatedPrimaryPos(renderStartIndex));
            renderAfterBoundary = this.getEstimatedPrimaryPos(renderEndIndex) + this.estimatedPrimarySize;
          };
        }
  
        function DynamicViewType() {
          var self = this;
          var debouncedScrollViewSetDimensions = ionic.debounce(scrollViewSetDimensions, 25, true);
          var calculateDimensions = isGridView ? calculateDimensionsGrid : calculateDimensionsList;
          var dimensionsIndex;
          var dimensions = [];
  
          // Get the dimensions at index. {width, height, left, top}.
          // We start with no dimensions calculated, then any time dimensions are asked for at an
          // index we calculate dimensions up to there.
          function calculateDimensionsList(toIndex) {
            var i, prevDimension, dim;
            for (i = Math.max(0, dimensionsIndex); i <= toIndex && (dim = dimensions[i]); i++) {
              prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
              dim.primarySize = self.getItemPrimarySize(i, data[i]);
              dim.secondarySize = self.scrollSecondarySize;
              dim.primaryPos = prevDimension.primaryPos + prevDimension.primarySize;
              dim.secondaryPos = 0;
            }
          }
          function calculateDimensionsGrid(toIndex) {
            var i, prevDimension, dim;
            for (i = Math.max(dimensionsIndex, 0); i <= toIndex && (dim = dimensions[i]); i++) {
              prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
              dim.secondarySize = Math.min(self.getItemSecondarySize(i, data[i]), self.scrollSecondarySize);
              dim.secondaryPos = prevDimension.secondaryPos + prevDimension.secondarySize;
  
              if (i === 0 || dim.secondaryPos + dim.secondarySize > self.scrollSecondarySize) {
                dim.secondaryPos = 0;
                dim.primarySize = self.getItemPrimarySize(i, data[i]);
                dim.primaryPos = prevDimension.primaryPos + prevDimension.rowPrimarySize;
  
                dim.rowStartIndex = i;
                dim.rowPrimarySize = dim.primarySize;
              } else {
                dim.primarySize = self.getItemPrimarySize(i, data[i]);
                dim.primaryPos = prevDimension.primaryPos;
                dim.rowStartIndex = prevDimension.rowStartIndex;
  
                dimensions[dim.rowStartIndex].rowPrimarySize = dim.rowPrimarySize = Math.max(
                  dimensions[dim.rowStartIndex].rowPrimarySize,
                  dim.primarySize
                );
                dim.rowPrimarySize = Math.max(dim.primarySize, dim.rowPrimarySize);
              }
            }
          }
  
          this.getContentSize = function () {
            var dim = dimensions[dimensionsIndex] || EMPTY_DIMENSION;
            return (
              (dim.primaryPos + dim.primarySize || 0) +
              this.getEstimatedPrimaryPos(data.length - dimensionsIndex - 1) +
              repeaterBeforeSize +
              repeaterAfterSize
            );
          };
          this.onDestroy = function () {
            dimensions.length = 0;
          };
  
          this.onRefreshData = function () {
            var i;
            var ii;
            // Make sure dimensions has as many items as data.length.
            // This is to be sure we don't have to allocate objects while scrolling.
            for (i = dimensions.length, ii = data.length; i < ii; i++) {
              dimensions.push({});
            }
            dimensionsIndex = -1;
          };
          this.onRefreshLayout = function () {
            dimensionsIndex = -1;
          };
          this.getDimensions = function (index) {
            index = Math.min(index, data.length - 1);
  
            if (dimensionsIndex < index) {
              // Once we start asking for dimensions near the end of the list, go ahead and calculate
              // everything. This is to make sure when the user gets to the end of the list, the
              // scroll height of the list is 100% accurate (not estimated anymore).
              if (index > data.length * 0.9) {
                calculateDimensions(data.length - 1);
                dimensionsIndex = data.length - 1;
                scrollViewSetDimensions();
              } else {
                calculateDimensions(index);
                dimensionsIndex = index;
                debouncedScrollViewSetDimensions();
              }
            }
            return dimensions[index];
          };
  
          var oldRenderStartIndex = -1;
          var oldScrollValue = -1;
          this.updateRenderRange = function (scrollValue, scrollValueEnd) {
            var i;
            var len;
            var dim;
  
            // Calculate more dimensions than we estimate we'll need, to be sure.
            this.getDimensions(this.getEstimatedIndex(scrollValueEnd) * 2);
  
            // -- Calculate renderStartIndex
            // base case: start at 0
            if (oldRenderStartIndex === -1 || scrollValue === 0) {
              i = 0;
              // scrolling down
            } else if (scrollValue >= oldScrollValue) {
              for (i = oldRenderStartIndex, len = data.length; i < len; i++) {
                if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize >= scrollValue) {
                  break;
                }
              }
              // scrolling up
            } else {
              for (i = oldRenderStartIndex; i >= 0; i--) {
                if ((dim = this.getDimensions(i)) && dim.primaryPos <= scrollValue) {
                  // when grid view, make sure the render starts at the beginning of a row.
                  i = isGridView ? dim.rowStartIndex : i;
                  break;
                }
              }
            }
  
            renderStartIndex = Math.min(Math.max(0, i), data.length - 1);
            renderBeforeBoundary = renderStartIndex !== -1 ? this.getDimensions(renderStartIndex).primaryPos : -1;
  
            // -- Calculate renderEndIndex
            var lastRowDim;
            for (i = renderStartIndex + 1, len = data.length; i < len; i++) {
              if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize > scrollValueEnd) {
                // Go all the way to the end of the row if we're in a grid
                if (isGridView) {
                  lastRowDim = dim;
                  while (i < len - 1 && (dim = this.getDimensions(i + 1)).primaryPos === lastRowDim.primaryPos) {
                    i++;
                  }
                }
                break;
              }
            }
  
            renderEndIndex = Math.min(i, data.length - 1);
            renderAfterBoundary =
              renderEndIndex !== -1
                ? (dim = this.getDimensions(renderEndIndex)).primaryPos + (dim.rowPrimarySize || dim.primarySize)
                : -1;
  
            oldScrollValue = scrollValue;
            oldRenderStartIndex = renderStartIndex;
          };
        }
      };
    }
  
    /**
     * @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.
     *
     * If there is any dynamic content inside the ion-content, be sure to call `.resize()` with {@link ionic.service:$ionicScrollDelegate}
     * after the content has been added.
     *
     * 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.
     * 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. See {@link ionic.provider:$ionicConfigProvider} to set this as the global default.
     * @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. Has access to 'scrollLeft' and 'scrollTop' locals.
     * @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',
      '$ionicConfig',
      function ($timeout, $controller, $ionicBind, $ionicConfig) {
        return {
          restrict: 'E',
          require: '^?ionNavView',
          scope: true,
          priority: 800,
          compile: function (element, attr) {
            var innerElement;
            var scrollCtrl;
  
            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('<div class="scroll"></div>');
              innerElement.append(element.contents());
              element.append(innerElement);
            } else {
              element.addClass('scroll-content-false');
            }
  
            var nativeScrolling =
              attr.overflowScroll !== 'false' && (attr.overflowScroll === 'true' || !$ionicConfig.scrolling.jsScrolling());
  
            // collection-repeat requires JS scrolling
            if (nativeScrolling) {
              nativeScrolling = !element[0].querySelector('[collection-repeat]');
            }
            return { pre: prelink };
            function prelink($scope, $element, $attr) {
              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 (isDefined($attr.padding)) {
                $scope.$watch($attr.padding, function (newVal) {
                  (innerElement || $element).toggleClass('padding', !!newVal);
                });
              }
  
              if ($attr.scroll === 'false') {
                //do nothing
              } else {
                var scrollViewOptions = {};
  
                // determined in compile phase above
                if (nativeScrolling) {
                  // use native scrolling
                  $element.addClass('overflow-scroll');
  
                  scrollViewOptions = {
                    el: $element[0],
                    delegateHandle: attr.delegateHandle,
                    startX: $scope.$eval($scope.startX) || 0,
                    startY: $scope.$eval($scope.startY) || 0,
                    nativeScrolling: true,
                  };
                } else {
                  // Use JS scrolling
                  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: onScrollComplete,
                  };
                }
  
                // init scroll controller with appropriate options
                scrollCtrl = $controller('$ionicScroll', {
                  $scope: $scope,
                  scrollViewOptions: scrollViewOptions,
                });
  
                $scope.scrollCtrl = scrollCtrl;
  
                $scope.$on('$destroy', function () {
                  if (scrollViewOptions) {
                    scrollViewOptions.scrollingComplete = noop;
                    delete scrollViewOptions.el;
                  }
                  innerElement = null;
                  $element = null;
                  attr.$$element = null;
                });
              }
  
              function onScrollComplete() {
                $scope.$onScrollComplete({
                  scrollTop: scrollCtrl.scrollView.__scrollTop,
                  scrollLeft: scrollCtrl.scrollView.__scrollLeft,
                });
              }
            }
          },
        };
      },
    ]);
  
    /**
     * @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
     * <ion-side-menus>
     *   <!-- Center content -->
     *   <ion-side-menu-content>
     *   </ion-side-menu-content>
     *
     *   <!-- Left menu -->
     *   <ion-side-menu expose-aside-when="large">
     *   </ion-side-menu>
     * </ion-side-menus>
     * ```
     * 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) {
            var prevInnerWidth = $window.innerWidth;
            var prevInnerHeight = $window.innerHeight;
  
            ionic.on(
              'resize',
              function () {
                if (prevInnerWidth === $window.innerWidth && prevInnerHeight === $window.innerHeight) {
                  return;
                }
                prevInnerWidth = $window.innerWidth;
                prevInnerHeight = $window.innerHeight;
                onResize();
              },
              $window
            );
  
            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(checkAsideExpose);
              },
              300,
              false
            );
  
            $scope.$evalAsync(checkAsideExpose);
          },
        };
      },
    ]);
  
    var GESTURE_DIRECTIVES =
      'onHold onTap onDoubleTap onTouch onRelease onDragStart onDrag onDragEnd 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
     * <button on-hold="onHold()" class="button">Test</button>
     * ```
     */
  
    /**
     * @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
     * <button on-tap="onTap()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onDoubleTap
     * @module ionic
     * @restrict A
     *
     * @description
     * Double tap touch at a location.
     *
     * @usage
     * ```html
     * <button on-double-tap="onDoubleTap()" class="button">Test</button>
     * ```
     */
  
    /**
     * @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
     * <button on-touch="onTouch()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onRelease
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when the user ends a touch.
     *
     * @usage
     * ```html
     * <button on-release="onRelease()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onDragStart
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when a drag gesture has started.
     *
     * @usage
     * ```html
     * <button on-drag-start="onDragStart()" class="button">Test</button>
     * ```
     */
  
    /**
     * @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
     * <button on-drag="onDrag()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onDragEnd
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when a drag gesture has ended.
     *
     * @usage
     * ```html
     * <button on-drag-end="onDragEnd()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onDragUp
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when the element is dragged up.
     *
     * @usage
     * ```html
     * <button on-drag-up="onDragUp()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onDragRight
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when the element is dragged to the right.
     *
     * @usage
     * ```html
     * <button on-drag-right="onDragRight()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onDragDown
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when the element is dragged down.
     *
     * @usage
     * ```html
     * <button on-drag-down="onDragDown()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onDragLeft
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when the element is dragged to the left.
     *
     * @usage
     * ```html
     * <button on-drag-left="onDragLeft()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onSwipe
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when a moving touch has a high velocity in any direction.
     *
     * @usage
     * ```html
     * <button on-swipe="onSwipe()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onSwipeUp
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when a moving touch has a high velocity moving up.
     *
     * @usage
     * ```html
     * <button on-swipe-up="onSwipeUp()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onSwipeRight
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when a moving touch has a high velocity moving to the right.
     *
     * @usage
     * ```html
     * <button on-swipe-right="onSwipeRight()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onSwipeDown
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when a moving touch has a high velocity moving down.
     *
     * @usage
     * ```html
     * <button on-swipe-down="onSwipeDown()" class="button">Test</button>
     * ```
     */
  
    /**
     * @ngdoc directive
     * @name onSwipeLeft
     * @module ionic
     * @restrict A
     *
     * @description
     * Called when a moving touch has a high velocity moving to the left.
     *
     * @usage
     * ```html
     * <button on-swipe-left="onSwipeLeft()" class="button">Test</button>
     * ```
     */
  
    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
       * <ion-header-bar align-title="left" class="bar-positive">
       *   <div class="buttons">
       *     <button class="button" ng-click="doSomething()">Left Button</button>
       *   </div>
       *   <h1 class="title">Title!</h1>
       *   <div class="buttons">
       *     <button class="button">Right Button</button>
       *   </div>
       * </ion-header-bar>
       * <ion-content class="has-header">
       *   Some content!
       * </ion-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
       * <ion-content class="has-footer">
       *   Some content!
       * </ion-content>
       * <ion-footer-bar align-title="left" class="bar-assertive">
       *   <div class="buttons">
       *     <button class="button">Left Button</button>
       *   </div>
       *   <h1 class="title">Title!</h1>
       *   <div class="buttons" ng-click="doSomething()">
       *     <button class="button">Right Button</button>
       *   </div>
       * </ion-footer-bar>
       * ```
       */
      .directive('ionFooterBar', headerFooterBarDirective(false));
  
    function tapScrollToTopDirective() {
      //eslint-disable-line no-unused-vars
      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) {
              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.$emit('$ionicSubheader', $scope.$hasSubheader);
                    }
                  );
                  $scope.$on('$destroy', function () {
                    delete $scope.$hasHeader;
                    delete $scope.$hasSubheader;
                  });
                  ctrl.align();
                  $scope.$on('$ionicHeader.align', function () {
                    ionic.requestAnimationFrame(function () {
                      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);
                  });
                  ctrl.align();
                  $scope.$on('$ionicFooter.align', function () {
                    ionic.requestAnimationFrame(function () {
                      ctrl.align();
                    });
                  });
                }
              }
            },
          };
        },
      ];
    }
  
    /**
     * @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=} spinner The {@link ionic.directive:ionSpinner} to show while loading. The SVG
     * {@link ionic.directive:ionSpinner} is now the default, replacing rotating font icons.
     * @param {string=} icon The icon to show while loading. Default: 'ion-load-d'.  This is depreciated
     * in favor of the SVG {@link ionic.directive:ionSpinner}.
     * @param {boolean=} immediate-check Whether to check the infinite scroll bounds immediately on load.
     *
     * @usage
     * ```html
     * <ion-content ng-controller="MyController">
     *   <ion-list>
     *   ....
     *   ....
     *   </ion-list>
     *
     *   <ion-infinite-scroll
     *     on-infinite="loadMore()"
     *     distance="1%">
     *   </ion-infinite-scroll>
     * </ion-content>
     * ```
     * ```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
     * <ion-infinite-scroll
     *   ng-if="moreDataCanBeLoaded()"
     *   icon="ion-loading-c"
     *   on-infinite="loadMoreData()">
     * </ion-infinite-scroll>
     * ```
     */
    IonicModule.directive('ionInfiniteScroll', [
      '$timeout',
      function ($timeout) {
        return {
          restrict: 'E',
          require: ['?^$ionicScroll', 'ionInfiniteScroll'],
          template: function ($element, $attrs) {
            if ($attrs.icon) return '<i class="icon {{icon()}} icon-refreshing {{scrollingType}}"></i>';
            return '<ion-spinner icon="{{spinner()}}"></ion-spinner>';
          },
          scope: true,
          controller: '$ionInfiniteScroll',
          link: function ($scope, $element, $attrs, ctrls) {
            var infiniteScrollCtrl = ctrls[1];
            var scrollCtrl = (infiniteScrollCtrl.scrollCtrl = ctrls[0]);
            var jsScrolling = (infiniteScrollCtrl.jsScrolling = !scrollCtrl.isNative());
  
            // if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling
            if (jsScrolling) {
              infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
              $scope.scrollingType = 'js-scrolling';
              //bind to JS scroll events
              scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds);
            } else {
              // grabbing the scrollable element, to determine dimensions, and current scroll pos
              var scrollEl = ionic.DomUtil.getParentOrSelfWithClass($element[0].parentNode, 'overflow-scroll');
              infiniteScrollCtrl.scrollEl = scrollEl;
              // if there's no scroll controller, and no overflow scroll div, infinite scroll wont work
              if (!scrollEl) {
                throw 'Infinite scroll must be used inside a scrollable div';
              }
              //bind to native scroll events
              infiniteScrollCtrl.scrollEl.addEventListener('scroll', infiniteScrollCtrl.checkBounds);
            }
  
            // Optionally check bounds on start after scrollView is fully rendered
            var doImmediateCheck = isDefined($attrs.immediateCheck) ? $scope.$eval($attrs.immediateCheck) : true;
            if (doImmediateCheck) {
              $timeout(function () {
                infiniteScrollCtrl.checkBounds();
              });
            }
          },
        };
      },
    ]);
  
    /**
     * @ngdoc directive
     * @name ionInput
     * @parent ionic.directive:ionList
     * @module ionic
     * @restrict E
     * Creates a text input group that can easily be focused
     *
     * @usage
     *
     * ```html
     * <ion-list>
     *   <ion-input>
     *     <input type="text" placeholder="First Name">
     *   </ion-input>
     *
     *   <ion-input>
     *     <ion-label>Username</ion-label>
     *     <input type="text">
     *   </ion-input>
     * </ion-list>
     * ```
     */
  
    var labelIds = -1;
  
    IonicModule.directive('ionInput', [
      function () {
        return {
          restrict: 'E',
          controller: [
            '$scope',
            '$element',
            function ($scope, $element) {
              this.$scope = $scope;
              this.$element = $element;
  
              this.setInputAriaLabeledBy = function (id) {
                var inputs = $element[0].querySelectorAll('input,textarea');
                inputs.length && inputs[0].setAttribute('aria-labelledby', id);
              };
  
              this.focus = function () {
                var inputs = $element[0].querySelectorAll('input,textarea');
                inputs.length && inputs[0].focus();
              };
            },
          ],
        };
      },
    ]);
  
    /**
     * @ngdoc directive
     * @name ionLabel
     * @parent ionic.directive:ionList
     * @module ionic
     * @restrict E
     *
     * New in Ionic 1.2. It is strongly recommended that you use `<ion-label>` in place
     * of any `<label>` elements for maximum cross-browser support and performance.
     *
     * Creates a label for a form input.
     *
     * @usage
     *
     * ```html
     * <ion-list>
     *   <ion-input>
     *     <ion-label>Username</ion-label>
     *     <input type="text">
     *   </ion-input>
     * </ion-list>
     * ```
     */
    IonicModule.directive('ionLabel', [
      function () {
        return {
          restrict: 'E',
          require: '?^ionInput',
          compile: function () {
            return function link($scope, $element, $attrs, ionInputCtrl) {
              var element = $element[0];
  
              $element.addClass('input-label');
  
              $element.attr('aria-label', $element.text());
              var id = element.id || '_label-' + ++labelIds;
  
              if (!element.id) {
                $element.attr('id', id);
              }
  
              if (ionInputCtrl) {
                ionInputCtrl.setInputAriaLabeledBy(id);
  
                $element.on('click', function () {
                  ionInputCtrl.focus();
                });
              }
            };
          },
        };
      },
    ]);
  
    /**
     * Input label adds accessibility to <span class="input-label">.
     */
    IonicModule.directive('inputLabel', [
      function () {
        return {
          restrict: 'C',
          require: '?^ionInput',
          compile: function () {
            return function link($scope, $element, $attrs, ionInputCtrl) {
              var element = $element[0];
  
              $element.attr('aria-label', $element.text());
              var id = element.id || '_label-' + ++labelIds;
  
              if (!element.id) {
                $element.attr('id', id);
              }
  
              if (ionInputCtrl) {
                ionInputCtrl.setInputAriaLabeledBy(id);
              }
            };
          },
        };
      },
    ]);
  
    /**
     * @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
     * <ion-list>
     *   <ion-item>Hello!</ion-item>
     *   <ion-item href="#/detail">
     *     Link to detail page
     *   </ion-item>
     * </ion-list>
     * ```
     */
    IonicModule.directive('ionItem', [
      '$$rAF',
      function ($$rAF) {
        return {
          restrict: 'E',
          controller: [
            '$scope',
            '$element',
            function ($scope, $element) {
              this.$scope = $scope;
              this.$element = $element;
            },
          ],
          scope: true,
          compile: function ($element, $attrs) {
            var isAnchor = isDefined($attrs.href) || isDefined($attrs.ngHref) || 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 ? '<a></a>' : '<div></div>');
              innerElement.addClass('item-content');
  
              if (isDefined($attrs.href) || isDefined($attrs.ngHref)) {
                innerElement.attr('ng-href', '{{$href()}}');
                if (isDefined($attrs.target)) {
                  innerElement.attr('target', '{{$target()}}');
                }
              }
  
              innerElement.append($element.contents());
  
              $element.addClass('item item-complex').append(innerElement);
            } else {
              $element.addClass('item');
            }
  
            return function link($scope, $element, $attrs) {
              $scope.$href = function () {
                return $attrs.href || $attrs.ngHref;
              };
              $scope.$target = function () {
                return $attrs.target;
              };
  
              var content = $element[0].querySelector('.item-content');
              if (content) {
                $scope.$on('$collectionRepeatLeave', function () {
                  if (content && content.$$ionicOptionsOpen) {
                    content.style[ionic.CSS.TRANSFORM] = '';
                    content.style[ionic.CSS.TRANSITION] = 'none';
                    $$rAF(function () {
                      content.style[ionic.CSS.TRANSITION] = '';
                    });
                    content.$$ionicOptionsOpen = false;
                  }
                });
              }
            };
          },
        };
      },
    ]);
  
    var ITEM_TPL_DELETE_BUTTON = '<div class="item-left-edit item-delete enable-pointer-events">' + '</div>';
    /**
     * @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
     * <ion-list show-delete="shouldShowDelete">
     *   <ion-item>
     *     <ion-delete-button class="ion-minus-circled"></ion-delete-button>
     *     Hello, list item!
     *   </ion-item>
     * </ion-list>
     * <ion-toggle ng-model="shouldShowDelete">
     *   Show Delete?
     * </ion-toggle>
     * ```
     */
    IonicModule.directive('ionDeleteButton', function () {
      function stopPropagation(ev) {
        ev.stopPropagation();
      }
  
      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');
  
            //Don't bubble click up to main .item
            $element.on('click', stopPropagation);
  
            init();
            $scope.$on('$ionic.reconnectScope', init);
            function init() {
              listCtrl = listCtrl || $element.controller('ionList');
              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 = jqLite(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 = '<div class="item-options invisible">' + '</div>';
    /**
     * @ngdoc directive
     * @name ionOptionButton
     * @parent ionic.directive:ionItem
     * @module ionic
     * @restrict E
     * @description
     * 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
     * <ion-list>
     *   <ion-item>
     *     I love kittens!
     *     <ion-option-button class="button-positive">Share</ion-option-button>
     *     <ion-option-button class="button-assertive">Edit</ion-option-button>
     *   </ion-item>
     * </ion-list>
     * ```
     */
    IonicModule.directive('ionOptionButton', [
      function () {
        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.prepend(itemCtrl.optionsContainer);
              }
              itemCtrl.optionsContainer.prepend($element);
  
              itemCtrl.$element.addClass('item-right-editable');
  
              //Don't bubble click up to main .item
              $element.on('click', stopPropagation);
            };
          },
        };
      },
    ]);
  
    var ITEM_TPL_REORDER_BUTTON =
      '<div data-prevent-scroll="true" class="item-right-edit item-reorder enable-pointer-events">' + '</div>';
  
    /**
     * @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
     * <ion-list ng-controller="MyCtrl" show-reorder="true">
     *   <ion-item ng-repeat="item in items">
     *     Item {{item}}
     *     <ion-reorder-button class="ion-navicon"
     *                         on-reorder="moveItem(item, $fromIndex, $toIndex)">
     *     </ion-reorder-button>
     *   </ion-item>
     * </ion-list>
     * ```
     * ```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/ionic-team/ionic-plugins-keyboard).
     * - On Android not in fullscreen mode, i.e. you have
     *   `<preference name="Fullscreen" value="false" />` 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
     *  <ion-footer-bar align-title="left" keyboard-attach class="bar-assertive">
     *    <h1 class="title">Title!</h1>
     *  </ion-footer-bar>
     * ```
     */
  
    IonicModule.directive('keyboardAttach', function () {
      return function (scope, element) {
        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 && 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
     * <ion-list>
     *   <ion-item ng-repeat="item in items">
     *     {% raw %}Hello, {{item}}!{% endraw %}
     *   </ion-item>
     * </ion-list>
     * ```
     *
     * Advanced Usage: Thumbnails, Delete buttons, Reordering, Swiping
     *
     * ```html
     * <ion-list ng-controller="MyCtrl"
     *           show-delete="shouldShowDelete"
     *           show-reorder="shouldShowReorder"
     *           can-swipe="listCanSwipe">
     *   <ion-item ng-repeat="item in items"
     *             class="item-thumbnail-left">
     *
     *     {% raw %}<img ng-src="{{item.img}}">
     *     <h2>{{item.title}}</h2>
     *     <p>{{item.description}}</p>{% endraw %}
     *     <ion-option-button class="button-positive"
     *                        ng-click="share(item)">
     *       Share
     *     </ion-option-button>
     *     <ion-option-button class="button-info"
     *                        ng-click="edit(item)">
     *       Edit
     *     </ion-option-button>
     *     <ion-delete-button class="ion-minus-circled"
     *                        ng-click="items.splice($index, 1)">
     *     </ion-delete-button>
     *     <ion-reorder-button class="ion-navicon"
     *                         on-reorder="reorderItem(item, $fromIndex, $toIndex)">
     *     </ion-reorder-button>
     *
     *   </ion-item>
     * </ion-list>
     * ```
     *
     *```javascript
     * app.controller('MyCtrl', function($scope) {
     *  $scope.shouldShowDelete = false;
     *  $scope.shouldShowReorder = false;
     *  $scope.listCanSwipe = true
     * });
     *```
     *
     * @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('<div class="list">').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
     * <a menu-close href="#/home" class="item">Home</a>
     * ```
     *
     * Note that if your destination state uses a resolve and that resolve asynchronously
     * takes longer than a standard transition (300ms), you'll need to set the
     * `nextViewOptions` manually as your resolve completes.
     *
     * ```js
     * $ionicHistory.nextViewOptions({
     *  historyRoot: true,
     *  disableAnimate: true,
     *  expire: 300
     * });
     * ```
     */
    IonicModule.directive('menuClose', [
      '$ionicHistory',
      '$timeout',
      function ($ionicHistory, $timeout) {
        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,
                });
                // if no transition in 300ms, reset nextViewOptions
                // the expire should take care of it, but will be cancelled in some
                // cases. This directive is an exception to the rules of history.js
                $timeout(function () {
                  $ionicHistory.nextViewOptions({
                    historyRoot: false,
                    disableAnimate: false,
                  });
                }, 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
     * <ion-nav-bar>
     *   <ion-nav-buttons side="left">
     *    <!-- Toggle left side menu -->
     *    <button menu-toggle="left" class="button button-icon icon ion-navicon"></button>
     *   </ion-nav-buttons>
     *   <ion-nav-buttons side="right">
     *    <!-- Toggle right side menu -->
     *    <button menu-toggle="right" class="button button-icon icon ion-navicon"></button>
     *   </ion-nav-buttons>
     * </ion-nav-bar>
     * ```
     *
     * ### Button Hidden On Child Views
     * By default, the menu toggle button will only appear on a root
     * level side-menu page. Navigating in to child views will hide the menu-
     * toggle button. They can be made visible on child pages by setting the
     * enable-menu-with-back-views attribute of the {@link ionic.directive:ionSideMenus}
     * directive to true.
     *
     * ```html
     * <ion-side-menus enable-menu-with-back-views="true">
     * ```
     */
    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:
            '<div class="modal-backdrop">' +
            '<div class="modal-backdrop-bg"></div>' +
            '<div class="modal-wrapper" ng-transclude></div>' +
            '</div>',
        };
      },
    ]);
  
    IonicModule.directive('ionModalView', function () {
      return {
        restrict: 'E',
        compile: function (element) {
          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
     * <ion-nav-bar>
     *   <ion-nav-back-button>
     *   </ion-nav-back-button>
     * </ion-nav-bar>
     * ```
     *
     * With custom inner markup, and automatically adds a default click action:
     *
     * ```html
     * <ion-nav-bar>
     *   <ion-nav-back-button class="button-clear">
     *     <i class="ion-arrow-left-c"></i> Back
     *   </ion-nav-back-button>
     * </ion-nav-bar>
     * ```
     *
     * With custom inner markup and custom click action, using {@link ionic.service:$ionicHistory}:
     *
     * ```html
     * <ion-nav-bar ng-controller="MyCtrl">
     *   <ion-nav-back-button class="button-clear"
     *     ng-click="myGoBack()">
     *     <i class="ion-arrow-left-c"></i> Back
     *   </ion-nav-back-button>
     * </ion-nav-bar>
     * ```
     * ```js
     * function MyCtrl($scope, $ionicHistory) {
     *   $scope.myGoBack = function() {
     *     $ionicHistory.goBack();
     *   };
     * }
     * ```
     */
    IonicModule.directive('ionNavBackButton', [
      '$ionicConfig',
      '$document',
      function ($ionicConfig, $document) {
        return {
          restrict: 'E',
          require: '^ionNavBar',
          compile: function (tElement, tAttrs) {
            // clone the back button, but as a <div>
            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()');
            }
  
            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 = '<i class="icon ' + defaultIcon + '"></i> ' + 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 += '<span class="default-title">' + $ionicConfig.backButton.text() + '</span>';
              }
              if (!hasPreviousTitle && $ionicConfig.backButton.previousTitleText()) {
                buttonTextEle.innerHTML += '<span class="previous-title"></span>';
              }
              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
     * `<ion-nav-bar>`, 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
     * <body ng-app="starter">
     *   <!-- The nav bar that will be updated as we navigate -->
     *   <ion-nav-bar class="bar-positive">
     *   </ion-nav-bar>
     *
     *   <!-- where the initial view template will be rendered -->
     *   <ion-nav-view>
     *     <ion-view>
     *       <ion-content>Hello!</ion-content>
     *     </ion-view>
     *   </ion-nav-view>
     * </body>
     * ```
     *
     * @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.
     *
     */
    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
     * <ion-nav-bar>
     * </ion-nav-bar>
     * <ion-nav-view>
     *   <ion-view>
     *     <ion-nav-buttons side="primary">
     *       <button class="button" ng-click="doSomething()">
     *         I'm a button on the primary of the navbar!
     *       </button>
     *     </ion-nav-buttons>
     *     <ion-content>
     *       Some super content here!
     *     </ion-content>
     *   </ion-view>
     * </ion-nav-view>
     * ```
     *
     * @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
     * <a nav-direction="forward" href="#/home">Home</a>
     * ```
     */
    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
     * <ion-nav-bar>
     * </ion-nav-bar>
     * <ion-nav-view>
     *   <ion-view>
     *     <ion-nav-title>
     *       <img src="logo.svg">
     *     </ion-nav-title>
     *     <ion-content>
     *       Some super content here!
     *     </ion-content>
     *   </ion-view>
     * </ion-nav-view>
     * ```
     *
     */
    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
     * <a nav-transition="none" href="#/home">Home</a>
     * ```
     */
    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 if it matches the index state,
     * and then try to load home.html into the `<ion-nav-view>`.
     *
     * 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 `<script type="text/ng-template">` syntax.
     * So here is one way to put home.html into our app:
     *
     * ```html
     * <script id="home" type="text/ng-template">
     *   <!-- The title of the ion-view will be shown on the navbar -->
     *   <ion-view view-title="Home">
     *     <ion-content ng-controller="HomeCtrl">
     *       <!-- The content of the page -->
     *       <a href="#/music">Go to music page!</a>
     *     </ion-content>
     *   </ion-view>
     * </script>
     * ```
     *
     * 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.
     *
     * By default, 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. This can be configured using the
     * {@link ionic.provider:$ionicConfigProvider}:
     *
     * ```js
     * $ionicConfigProvider.views.forwardCache(true);
     * ```
     *
     * #### 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
     * <ion-view cache-view="false" view-title="My Title!">
     *   ...
     * </ion-view>
     * ```
     *
     *
     * ## 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:
     *
     * <iframe width="560" height="315" src="//www.youtube.com/embed/dqJRoh8MnBo"
     * frameborder="0" allowfullscreen></iframe>
     *
     * Note: We do not recommend using [resolve](https://github.com/angular-ui/ui-router/wiki#resolve)
     * of AngularUI Router. The recommended approach is to execute any logic needed before beginning the state transition.
     *
     * @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 = noop;
          };
        },
      ])
  
      .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) {
          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: '<div class="popover-backdrop">' + '<div class="popover-wrapper" ng-transclude></div>' + '</div>',
        };
      },
    ]);
  
    IonicModule.directive('ionPopoverView', function () {
      return {
        restrict: 'E',
        compile: function (element) {
          element.append(jqLite('<div class="popover-arrow">'));
          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 [AngularJS radio](http://docs.angularjs.org/api/ng/input/input[radio]).
     *
     * @usage
     * ```html
     * <ion-radio ng-model="choice" ng-value="'A'">Choose A</ion-radio>
     * <ion-radio ng-model="choice" ng-value="'B'">Choose B</ion-radio>
     * <ion-radio ng-model="choice" ng-value="'C'">Choose C</ion-radio>
     * ```
     *
     * @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:
          '<label class="item item-radio">' +
          '<input type="radio" name="radio-group">' +
          '<div class="radio-content">' +
          '<div class="item-content disable-pointer-events" ng-transclude></div>' +
          '<i class="radio-icon disable-pointer-events icon ion-checkmark"></i>' +
          '</div>' +
          '</label>',
  
        compile: function (element, attr) {
          if (attr.icon) {
            var iconElm = element.find('i');
            iconElm.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,
              'ng-required': attr.ngRequired,
              required: attr.required,
            },
            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
     * <ion-content ng-controller="MyController">
     *   <ion-refresher
     *     pulling-text="Pull to refresh..."
     *     on-refresh="doRefresh()">
     *   </ion-refresher>
     *   <ion-list>
     *     <ion-item ng-repeat="item in items"></ion-item>
     *   </ion-list>
     * </ion-content>
     * ```
     * ```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-text The text to display while the user is pulling down.
     * @param {string=} pulling-icon The icon to display while the user is pulling down.
     * Default: 'ion-android-arrow-down'.
     * @param {string=} spinner The {@link ionic.directive:ionSpinner} icon to display
     * after user lets go of the refresher. The SVG {@link ionic.directive:ionSpinner}
     * is now the default, replacing rotating font icons. Set to `none` to disable both the
     * spinner and the icon.
     * @param {string=} refreshing-icon The font icon to display after user lets go of the
     * refresher. This is deprecated in favor of the SVG {@link ionic.directive:ionSpinner}.
     * @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', [
      function () {
        return {
          restrict: 'E',
          replace: true,
          require: ['?^$ionicScroll', 'ionRefresher'],
          controller: '$ionicRefresher',
          template:
            '<div class="scroll-refresher invisible" collection-repeat-ignore>' +
            '<div class="ionic-refresher-content" ' +
            'ng-class="{\'ionic-refresher-with-text\': pullingText || refreshingText}">' +
            '<div class="icon-pulling" ng-class="{\'pulling-rotation-disabled\':disablePullingRotation}">' +
            '<i class="icon {{pullingIcon}}"></i>' +
            '</div>' +
            '<div class="text-pulling" ng-bind-html="pullingText"></div>' +
            '<div class="icon-refreshing">' +
            '<ion-spinner ng-if="showSpinner" icon="{{spinner}}"></ion-spinner>' +
            '<i ng-if="showIcon" class="icon {{refreshingIcon}}"></i>' +
            '</div>' +
            '<div class="text-refreshing" ng-bind-html="refreshingText"></div>' +
            '</div>' +
            '</div>',
          link: function ($scope, $element, $attrs, ctrls) {
            // JS Scrolling uses the scroll controller
            var scrollCtrl = ctrls[0],
              refresherCtrl = ctrls[1];
            if (!scrollCtrl || scrollCtrl.isNative()) {
              // Kick off native scrolling
              refresherCtrl.init();
            } else {
              $element[0].classList.add('js-scrolling');
              scrollCtrl._setRefresher($scope, $element[0], refresherCtrl.getRefresherDomMethods());
  
              $scope.$on('scroll.refreshComplete', function () {
                $scope.$evalAsync(function () {
                  if (scrollCtrl.scrollView) {
                    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
     * <ion-scroll zooming="true" direction="xy" style="width: 500px; height: 500px">
     *   <div style="width: 5000px; height: 5000px; background: url('https://upload.wikimedia.org/wikipedia/commons/a/ad/Europe_geological_map-en.jpg') repeat"></div>
     *  </ion-scroll>
     * ```
     *
     * 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 {expression=} on-scroll-complete Called whenever the scrolling paging is completed.
     * @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',
      '$ionicConfig',
      function ($timeout, $controller, $ionicBind, $ionicConfig) {
        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('<div class="scroll"></div>');
            innerElement.append(element.contents());
            element.append(innerElement);
  
            var nativeScrolling =
              attr.overflowScroll !== 'false' && (attr.overflowScroll === 'true' || !$ionicConfig.scrolling.jsScrolling());
  
            return { pre: prelink };
            function prelink($scope, $element, $attr) {
              $ionicBind($scope, $attr, {
                direction: '@',
                paging: '@',
                $onScroll: '&onScroll',
                $onScrollComplete: '&onScrollComplete',
                scroll: '@',
                scrollbarX: '@',
                scrollbarY: '@',
                zooming: '@',
                minZoom: '@',
                maxZoom: '@',
              });
              $scope.direction = $scope.direction || 'y';
  
              if (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;
  
              if (nativeScrolling) {
                $element.addClass('overflow-scroll');
              }
  
              $element.addClass('scroll-' + $scope.direction);
  
              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,
                nativeScrolling: nativeScrolling,
                scrollingComplete: onScrollComplete,
              };
  
              if (isPaging) {
                scrollViewOptions.speedMultiplier = 0.8;
                scrollViewOptions.bouncing = false;
              }
  
              var scrollCtrl = $controller('$ionicScroll', {
                $scope: $scope,
                scrollViewOptions: scrollViewOptions,
              });
  
              function onScrollComplete() {
                $scope.$onScrollComplete &&
                  $scope.$onScrollComplete({
                    scrollTop: scrollCtrl.scrollView.__scrollTop,
                    scrollLeft: scrollCtrl.scrollView.__scrollLeft,
                  });
              }
            }
          },
        };
      },
    ]);
  
    /**
     * @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
     * <ion-side-menu
     *   side="left"
     *   width="myWidthValue + 20"
     *   is-enabled="shouldLeftSideMenuBeEnabled()">
     * </ion-side-menu>
     * ```
     * 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
     * <ion-side-menu-content
     *   edge-drag-threshold="true"
     *   drag-content="true">
     * </ion-side-menu-content>
     * ```
     * 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 () {},
                endDrag: function () {},
                setCanScroll: function (canScroll) {
                  var c = $element[0].querySelector('.scroll');
  
                  if (!c) {
                    return;
                  }
  
                  var content = angular.element(c.parentElement);
                  if (!content) {
                    return;
                  }
  
                  // freeze our scroll container if we have one
                  var scrollScope = content.scope();
                  scrollScope.scrollCtrl && scrollScope.scrollCtrl.freezeScrollShut(!canScroll);
                },
                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)';
                }),
                setMarginLeftAndRight: ionic.animationFrameThrottle(function (amountLeft, amountRight) {
                  amountLeft = (amountLeft && parseInt(amountLeft, 10)) || 0;
                  amountRight = (amountRight && parseInt(amountRight, 10)) || 0;
  
                  var amount = amountLeft + amountRight;
  
                  if (amount > 0) {
                    $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amountLeft + 'px,0,0)';
                    $element[0].style.width = $window.innerWidth - amount + 'px';
                    content.offsetX = amountLeft;
                  } else {
                    $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
                    $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 };
              gestureOpts.prevent_default_directions = ['left', 'right'];
              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.
       *
       * "Burger Icon" toggles can be added to the header with the {@link ionic.directive:menuToggle}
       * attribute directive. Clicking the toggle will open and close the side menu like the `menu-close`
       * directive. The side menu will automatically hide on child pages, but can be overridden with the
       * enable-menu-with-back-views attribute mentioned below.
       *
       * 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 `<ion-side-menus>` parent element. This will encompass all pages that have a
       * side menu, and have at least 2 child elements: 1 `<ion-side-menu-content>` for the center content,
       * and one or more `<ion-side-menu>` directives for each side menu(left/right) that you wish to place.
       *
       * ```html
       * <ion-side-menus>
       *   <!-- Left menu -->
       *   <ion-side-menu side="left">
       *   </ion-side-menu>
       *
       *   <ion-side-menu-content>
       *   <!-- Main content, usually <ion-nav-view> -->
       *   </ion-side-menu-content>
       *
       *   <!-- Right menu -->
       *   <ion-side-menu side="right">
       *   </ion-side-menu>
       *
       * </ion-side-menus>
       * ```
       * ```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
     * @codepen AjgEB
     * @deprecated will be removed in the next Ionic release in favor of the new ion-slides component.
     * Don't depend on the internal behavior of this widget.
     * @delegate ionic.service:$ionicSlideBoxDelegate
     * @restrict E
     * @description
     * The Slide Box is a multi-page container where each page can be swiped or dragged between:
     *
     *
     * @usage
     * ```html
     * <ion-slide-box on-slide-changed="slideHasChanged($index)">
     *   <ion-slide>
     *     <div class="box blue"><h1>BLUE</h1></div>
     *   </ion-slide>
     *   <ion-slide>
     *     <div class="box yellow"><h1>YELLOW</h1></div>
     *   </ion-slide>
     *   <ion-slide>
     *     <div class="box pink"><h1>PINK</h1></div>
     *   </ion-slide>
     * </ion-slide-box>
     * ```
     *
     * @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. Accepts expressions via `show-pager="{{shouldShow()}}"`. Defaults to true.
     * @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 index to.
     */
    IonicModule.directive('ionSlideBox', [
      '$animate',
      '$timeout',
      '$compile',
      '$ionicSlideBoxDelegate',
      '$ionicHistory',
      '$ionicScrollDelegate',
      function ($animate, $timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory, $ionicScrollDelegate) {
        return {
          restrict: 'E',
          replace: true,
          transclude: true,
          scope: {
            autoPlay: '=',
            doesContinue: '@',
            slideInterval: '@',
            showPager: '@',
            pagerClick: '&',
            disableScroll: '@',
            onSlideChanged: '&',
            activeSlide: '=?',
            bounce: '@',
          },
          controller: [
            '$scope',
            '$element',
            '$attrs',
            function ($scope, $element, $attrs) {
              var _this = this;
  
              var continuous = $scope.$eval($scope.doesContinue) === true;
              var bouncing = $scope.$eval($scope.bounce) !== false; //Default to 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,
                bouncing: bouncing,
                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 () {});
                },
                onDrag: function () {
                  freezeAllScrolls(true);
                },
                onDragEnd: function () {
                  freezeAllScrolls(false);
                },
              });
  
              function freezeAllScrolls(shouldFreeze) {
                if (shouldFreeze && !_this.isScrollFreeze) {
                  $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);
                } else if (!shouldFreeze && _this.isScrollFreeze) {
                  $ionicScrollDelegate.freezeAllScrolls(false);
                }
                _this.isScrollFreeze = shouldFreeze;
              }
  
              slider.enableSlide($scope.$eval($attrs.disableScroll) !== true);
  
              $scope.$watch('activeSlide', function (nv) {
                if (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', function () {
                deregisterInstance();
                slider.kill();
              });
  
              this.slidesCount = function () {
                return slider.slidesCount();
              };
  
              this.onPagerClick = function (index) {
                $scope.pagerClick({ index: index });
              };
  
              $timeout(function () {
                slider.load();
              });
            },
          ],
          template: '<div class="slider">' + '<div class="slider-slides" ng-transclude>' + '</div>' + '</div>',
  
          link: function ($scope, $element, $attr) {
            // Disable ngAnimate for slidebox and its children
            $animate.enabled($element, false);
  
            // if showPager is undefined, show the pager
            if (!isDefined($attr.showPager)) {
              $scope.showPager = true;
              getPager().toggleClass('hide', !true);
            }
  
            $attr.$observe('showPager', function (show) {
              if (show === undefined) return;
              show = $scope.$eval(show);
              getPager().toggleClass('hide', !show);
            });
  
            var pager;
            function getPager() {
              if (!pager) {
                var childScope = $scope.$new();
                pager = jqLite('<ion-pager></ion-pager>');
                $element.append(pager);
                pager = $compile(pager)(childScope);
              }
              return pager;
            }
          },
        };
      },
    ])
      .directive('ionSlide', function () {
        return {
          restrict: 'E',
          require: '?^ionSlideBox',
          compile: function (element) {
            element.addClass('slider-slide');
          },
        };
      })
  
      .directive('ionPager', function () {
        return {
          restrict: 'E',
          replace: true,
          require: '^ionSlideBox',
          template:
            '<div class="slider-pager"><span class="slider-pager-page" ng-repeat="slide in numSlides() track by $index" ng-class="{active: $index == currentSlide}" ng-click="pagerClick($index)"><i class="icon ion-record"></i></span></div>',
          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 ionSlides
     * @module ionic
     * @restrict E
     * @description
     * The Slides component is a powerful multi-page container where each page can be swiped or dragged between.
     *
     * Note: this is a new version of the Ionic Slide Box based on the [Swiper](http://www.idangero.us/swiper/#.Vmc1J-ODFBc) widget from
     * [idangerous](http://www.idangero.us/).
     *
     * ![SlideBox](http://ionicframework.com.s3.amazonaws.com/docs/controllers/slideBox.gif)
     *
     * @usage
     * ```html
     * <ion-content scroll="false">
     *   <ion-slides  options="options" slider="data.slider">
     *     <ion-slide-page>
     *       <div class="box blue"><h1>BLUE</h1></div>
     *     </ion-slide-page>
     *     <ion-slide-page>
     *       <div class="box yellow"><h1>YELLOW</h1></div>
     *     </ion-slide-page>
     *     <ion-slide-page>
     *       <div class="box pink"><h1>PINK</h1></div>
     *     </ion-slide-page>
     *   </ion-slides>
     * </ion-content>
     * ```
     *
     * ```js
     * $scope.options = {
     *   loop: false,
     *   effect: 'fade',
     *   speed: 500,
     * }
     *
     * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){
     *   // data.slider is the instance of Swiper
     *   $scope.slider = data.slider;
     * });
     *
     * $scope.$on("$ionicSlides.slideChangeStart", function(event, data){
     *   console.log('Slide change is beginning');
     * });
     *
     * $scope.$on("$ionicSlides.slideChangeEnd", function(event, data){
     *   // note: the indexes are 0-based
     *   $scope.activeIndex = data.slider.activeIndex;
     *   $scope.previousIndex = data.slider.previousIndex;
     * });
     *
     * ```
     *
     * ## Slide Events
     *
     * The slides component dispatches events when the active slide changes
     *
     * <table class="table">
     *   <tr>
     *     <td><code>$ionicSlides.slideChangeStart</code></td>
     *     <td>This event is emitted when a slide change begins</td>
     *   </tr>
     *   <tr>
     *     <td><code>$ionicSlides.slideChangeEnd</code></td>
     *     <td>This event is emitted when a slide change completes</td>
     *   </tr>
     *   <tr>
     *     <td><code>$ionicSlides.sliderInitialized</code></td>
     *     <td>This event is emitted when the slider is initialized. It provides access to an instance of the slider.</td>
     *   </tr>
     * </table>
     *
     *
     * ## Updating Slides Dynamically
     * When applying data to the slider at runtime, typically everything will work as expected.
     *
     * In the event that the slides are looped, use the `updateLoop` method on the slider to ensure the slides update correctly.
     *
     * ```
     * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){
     *   // grab an instance of the slider
     *   $scope.slider = data.slider;
     * });
     *
     * function dataChangeHandler(){
     *   // call this function when data changes, such as an HTTP request, etc
     *   if ( $scope.slider ){
     *     $scope.slider.updateLoop();
     *   }
     * }
     * ```
     *
     */
    IonicModule.directive('ionSlides', [
      '$animate',
      '$timeout',
      '$compile',
      function ($animate, $timeout, $compile) {
        return {
          restrict: 'E',
          transclude: true,
          scope: {
            options: '=',
            slider: '=',
          },
          template:
            '<div class="swiper-container">' +
            '<div class="swiper-wrapper" ng-transclude>' +
            '</div>' +
            '<div ng-hide="!showPager" class="swiper-pagination"></div>' +
            '</div>',
          controller: [
            '$scope',
            '$element',
            function ($scope, $element) {
              var _this = this;
  
              this.update = function () {
                $timeout(function () {
                  if (!_this.__slider) {
                    return;
                  }
  
                  _this.__slider.update();
                  if (_this._options.loop) {
                    _this.__slider.createLoop();
                  }
  
                  var slidesLength = _this.__slider.slides.length;
  
                  // Don't allow pager to show with > 10 slides
                  if (slidesLength > 10) {
                    $scope.showPager = false;
                  }
  
                  // When slide index is greater than total then slide to last index
                  if (_this.__slider.activeIndex > slidesLength - 1) {
                    _this.__slider.slideTo(slidesLength - 1);
                  }
                });
              };
  
              this.rapidUpdate = ionic.debounce(function () {
                _this.update();
              }, 50);
  
              this.getSlider = function () {
                return _this.__slider;
              };
  
              var options = $scope.options || {};
  
              var newOptions = angular.extend(
                {
                  pagination: $element.children().children()[1],
                  paginationClickable: true,
                  lazyLoading: true,
                  preloadImages: false,
                },
                options
              );
  
              this._options = newOptions;
  
              $timeout(function () {
                var slider = new ionic.views.Swiper($element.children()[0], newOptions, $scope, $compile);
  
                $scope.$emit('$ionicSlides.sliderInitialized', { slider: slider });
  
                _this.__slider = slider;
                $scope.slider = _this.__slider;
  
                $scope.$on('$destroy', function () {
                  slider.destroy();
                  _this.__slider = null;
                });
              });
  
              $timeout(function () {
                // if it's a loop, render the slides again just incase
                _this.rapidUpdate();
              }, 200);
            },
          ],
  
          link: function ($scope) {
            $scope.showPager = true;
            // Disable ngAnimate for slidebox and its children
            //$animate.enabled(false, $element);
          },
        };
      },
    ]).directive('ionSlidePage', [
      function () {
        return {
          restrict: 'E',
          require: '?^ionSlides',
          transclude: true,
          replace: true,
          template: '<div class="swiper-slide" ng-transclude></div>',
          link: function ($scope, $element, $attr, ionSlidesCtrl) {
            ionSlidesCtrl.rapidUpdate();
  
            $scope.$on('$destroy', function () {
              ionSlidesCtrl.rapidUpdate();
            });
          },
        };
      },
    ]);
  
    /**
     * @ngdoc directive
     * @name ionSpinner
     * @module ionic
     * @restrict E
     *
     * @description
     * The `ionSpinner` directive provides a variety of animated spinners.
     * Spinners enables you to give your users feedback that the app is
     * processing/thinking/waiting/chillin' out, or whatever you'd like it to indicate.
     * By default, the {@link ionic.directive:ionRefresher} feature uses this spinner, rather
     * than rotating font icons (previously included in [ionicons](http://ionicons.com/)).
     * While font icons are great for simple or stationary graphics, they're not suited to
     * provide great animations, which is why Ionic uses SVG instead.
     *
     * Ionic offers ten spinners out of the box, and by default, it will use the appropriate spinner
     * for the platform on which it's running. Under the hood, the `ionSpinner` directive dynamically
     * builds the required SVG element, which allows Ionic to provide all ten of the animated SVGs
     * within 3KB.
     *
     * <style>
     * .spinner-table {
     *   max-width: 280px;
     * }
     * .spinner-table tbody > tr > th, .spinner-table tbody > tr > td {
     *   vertical-align: middle;
     *   width: 42px;
     *   height: 42px;
     * }
     * .spinner {
     *   stroke: #444;
     *   fill: #444; }
     *   .spinner svg {
     *     width: 28px;
     *     height: 28px; }
     *   .spinner.spinner-inverse {
     *     stroke: #fff;
     *     fill: #fff; }
     *
     * .spinner-android {
     *   stroke: #4b8bf4; }
     *
     * .spinner-ios, .spinner-ios-small {
     *   stroke: #69717d; }
     *
     * .spinner-spiral .stop1 {
     *   stop-color: #fff;
     *   stop-opacity: 0; }
     * .spinner-spiral.spinner-inverse .stop1 {
     *   stop-color: #000; }
     * .spinner-spiral.spinner-inverse .stop2 {
     *   stop-color: #fff; }
     * </style>
     *
     * <script src="http://code.ionicframework.com/nightly/js/ionic.bundle.min.js"></script>
     * <table class="table spinner-table" ng-app="ionic">
     *  <tr>
     *    <th>
     *      <code>android</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="android"></ion-spinner>
     *    </td>
     *  </tr>
     *  <tr>
     *    <th>
     *      <code>ios</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="ios"></ion-spinner>
     *    </td>
     *  </tr>
     *  <tr>
     *    <th>
     *      <code>ios-small</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="ios-small"></ion-spinner>
     *    </td>
     *  </tr>
     *  <tr>
     *    <th>
     *      <code>bubbles</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="bubbles"></ion-spinner>
     *    </td>
     *  </tr>
     *  <tr>
     *    <th>
     *      <code>circles</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="circles"></ion-spinner>
     *    </td>
     *  </tr>
     *  <tr>
     *    <th>
     *      <code>crescent</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="crescent"></ion-spinner>
     *    </td>
     *  </tr>
     *  <tr>
     *    <th>
     *      <code>dots</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="dots"></ion-spinner>
     *    </td>
     *  </tr>
     *  <tr>
     *    <th>
     *      <code>lines</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="lines"></ion-spinner>
     *    </td>
     *  </tr>
     *  <tr>
     *    <th>
     *      <code>ripple</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="ripple"></ion-spinner>
     *    </td>
     *  </tr>
     *  <tr>
     *    <th>
     *      <code>spiral</code>
     *    </th>
     *    <td>
     *      <ion-spinner icon="spiral"></ion-spinner>
     *    </td>
     *  </tr>
     * </table>
     *
     * Each spinner uses SVG with SMIL animations, however, the Android spinner also uses JavaScript
     * so it also works on Android 4.0-4.3. Additionally, each spinner can be styled with CSS,
     * and scaled to any size.
     *
     *
     * @usage
     * The following code would use the default spinner for the platform it's running from. If it's neither
     * iOS or Android, it'll default to use `ios`.
     *
     * ```html
     * <ion-spinner></ion-spinner>
     * ```
     *
     * By setting the `icon` attribute, you can specify which spinner to use, no matter what
     * the platform is.
     *
     * ```html
     * <ion-spinner icon="spiral"></ion-spinner>
     * ```
     *
     * ## Spinner Colors
     * Like with most of Ionic's other components, spinners can also be styled using
     * Ionic's standard color naming convention. For example:
     *
     * ```html
     * <ion-spinner class="spinner-energized"></ion-spinner>
     * ```
     *
     *
     * ## Styling SVG with CSS
     * One cool thing about SVG is its ability to be styled with CSS! Some of the properties
     * have different names, for example, SVG uses the term `stroke` instead of `border`, and
     * `fill` instead of `background-color`.
     *
     * ```css
     * .spinner svg {
     *   width: 28px;
     *   height: 28px;
     *   stroke: #444;
     *   fill: #444;
     * }
     * ```
     *
     */
    IonicModule.directive('ionSpinner', function () {
      return {
        restrict: 'E',
        controller: '$ionicSpinner',
        link: function ($scope, $element, $attrs, ctrl) {
          var spinnerName = ctrl.init();
          $element.addClass('spinner spinner-' + spinnerName);
  
          $element.on('$destroy', function onDestroy() {
            ctrl.stop();
          });
        },
      };
    });
  
    /**
     * @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
     * <ion-tab
     *   title="Tab!"
     *   icon="my-icon"
     *   href="#/tab/tab-link"
     *   on-select="onTabSelected()"
     *   on-deselect="onTabDeselected()">
     * </ion-tab>
     * ```
     * 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()}.
     * @param {expression=} hidden Whether the tab is to be hidden or not.
     * @param {expression=} disabled Whether the tab is to be disabled or not.
     */
    IonicModule.directive('ionTab', [
      '$compile',
      '$ionicConfig',
      '$ionicBind',
      '$ionicViewSwitcher',
      function ($compile, $ionicConfig, $ionicBind, $ionicViewSwitcher) {
        //Returns ' key="value"' if value exists
        function attrStr(k, v) {
          return 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 =
              '<ion-tab-nav' +
              attrStr('ng-click', attr.ngClick) +
              attrStr('title', attr.title) +
              attrStr('icon', attr.icon) +
              attrStr('icon-on', attr.iconOn) +
              attrStr('icon-off', attr.iconOff) +
              attrStr('badge', attr.badge) +
              attrStr('badge-style', attr.badgeStyle) +
              attrStr('hidden', attr.hidden) +
              attrStr('disabled', attr.disabled) +
              attrStr('class', attr['class']) +
              '></ion-tab-nav>';
  
            //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;
              $scope.$tabSelected = 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();
                tabContentEle.innerHTML = '';
                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:
            "<a ng-class=\"{'has-badge':badge, 'tab-hidden':isHidden(), 'tab-item-active': isTabActive()}\" " +
            ' ng-disabled="disabled()" class="tab-item">' +
            '<span class="badge {{badgeStyle}}" ng-if="badge">{{badge}}</span>' +
            '<i class="icon {{getIcon()}}" ng-if="getIcon()"></i>' +
            '<span class="tab-title" ng-bind-html="title"></span>' +
            '</a>',
          scope: {
            title: '@',
            icon: '@',
            iconOn: '@',
            iconOff: '@',
            badge: '=',
            hidden: '@',
            disabled: '&',
            badgeStyle: '@',
            class: '@',
          },
          link: function ($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;
            };
  
            $scope.getIcon = function () {
              if (tabsCtrl.selectedTab() === tabCtrl.$scope) {
                // active
                return $scope.iconOn || $scope.icon;
              } else {
                // inactive
                return $scope.iconOff || $scope.icon;
              }
            };
          },
        };
      },
    ]);
  
    /**
     * @ngdoc directive
     * @name ionTabs
     * @module ionic
     * @delegate ionic.service:$ionicTabsDelegate
     * @restrict E
     * @codepen odqCz
     *
     * @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) to the element to define
     * its look and feel.
     *
     * For iOS, tabs will appear at the bottom of the screen. For Android, tabs will be at the top
     * of the screen, below the nav-bar. This follows each OS's design specification, but can be
     * configured with the {@link ionic.provider:$ionicConfigProvider}.
     *
     * 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
     * <ion-tabs class="tabs-positive tabs-icon-top">
     *
     *   <ion-tab title="Home" icon-on="ion-ios-filing" icon-off="ion-ios-filing-outline">
     *     <!-- Tab 1 content -->
     *   </ion-tab>
     *
     *   <ion-tab title="About" icon-on="ion-ios-clock" icon-off="ion-ios-clock-outline">
     *     <!-- Tab 2 content -->
     *   </ion-tab>
     *
     *   <ion-tab title="Settings" icon-on="ion-ios-gear" icon-off="ion-ios-gear-outline">
     *     <!-- Tab 3 content -->
     *   </ion-tab>
     *
     * </ion-tabs>
     * ```
     *
     * @param {string=} delegate-handle The handle used to identify these tabs
     * with {@link ionic.service:$ionicTabsDelegate}.
     */
  
    IonicModule.directive('ionTabs', [
      '$ionicTabsDelegate',
      '$ionicConfig',
      function ($ionicTabsDelegate, $ionicConfig) {
        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('<div class="tab-nav tabs">');
            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.$emit('$ionicTabs.top', $scope.$hasTabsTop);
                }
              );
  
              function emitLifecycleEvent(ev, data) {
                ev.stopPropagation();
                var previousSelectedTab = tabsCtrl.previousSelectedTab();
                if (previousSelectedTab) {
                  previousSelectedTab.$broadcast(ev.name.replace('NavView', 'Tabs'), data);
                }
              }
  
              $scope.$on('$ionicNavView.beforeLeave', emitLifecycleEvent);
              $scope.$on('$ionicNavView.afterLeave', emitLifecycleEvent);
              $scope.$on('$ionicNavView.leave', emitLifecycleEvent);
  
              $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 ionTitle
     * @module ionic
     * @restrict E
     *
     * Used for titles in header and nav bars. New in 1.2
     *
     * Identical to <div class="title"> but with future compatibility for Ionic 2
     *
     * @usage
     *
     * ```html
     * <ion-nav-bar>
     *   <ion-title>Hello</ion-title>
     * <ion-nav-bar>
     * ```
     */
    IonicModule.directive('ionTitle', [
      function () {
        return {
          restrict: 'E',
          compile: function (element) {
            element.addClass('title');
          },
        };
      },
    ]);
  
    /**
     * @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
     * <ion-toggle ng-model="airplaneMode" toggle-class="toggle-calm">Airplane Mode</ion-toggle>
     * ```
     */
    IonicModule.directive('ionToggle', [
      '$timeout',
      '$ionicConfig',
      function ($timeout, $ionicConfig) {
        return {
          restrict: 'E',
          replace: true,
          require: '?ngModel',
          transclude: true,
          template:
            '<div class="item item-toggle">' +
            '<div ng-transclude></div>' +
            '<label class="toggle">' +
            '<input type="checkbox">' +
            '<div class="track">' +
            '<div class="handle"></div>' +
            '</div>' +
            '</label>' +
            '</div>',
  
          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,
                'ng-required': attr.ngRequired,
                required: attr.required,
              },
              function (value, name) {
                if (isDefined(value)) {
                  input.attr(name, value);
                }
              }
            );
  
            if (attr.toggleClass) {
              element[0].getElementsByTagName('label')[0].classList.add(attr.toggleClass);
            }
  
            element.addClass('toggle-' + $ionicConfig.form.toggle());
  
            return function ($scope, $element) {
              var el = $element[0].getElementsByTagName('label')[0];
              var checkbox = el.children[0];
              var track = el.children[1];
              var 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 (ngModelController) {
                    ngModelController.$setViewValue(checkbox.checked);
                    $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 exits its parent {@link ionic.directive:ionNavView}, the view also emits view
     * information, such as its title, whether the back button should be displayed or not, whether the
     * corresponding {@link ionic.directive:ionNavBar} should be displayed 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.provider:$ionicConfigProvider}, 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
     * <ion-nav-bar></ion-nav-bar>
     * <ion-nav-view>
     *   <ion-view view-title="My Page">
     *     <ion-content>
     *       Hello!
     *     </ion-content>
     *   </ion-view>
     * </ion-nav-view>
     * ```
     *
     * ## 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.
     *
     * Life cycle events are emitted upwards from the transitioning view's scope. In some cases, it is
     * desirable for a child/nested view to be notified of the event.
     * For this use case, `$ionicParentView` life cycle events are broadcast downwards.
     *
     * <table class="table">
     *  <tr>
     *   <td><code>$ionicView.loaded</code></td>
     *   <td>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.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicView.enter</code></td>
     *   <td>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.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicView.leave</code></td>
     *   <td>The view has finished leaving and is no longer the
     * active view. This event will fire, whether it is cached or destroyed.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicView.beforeEnter</code></td>
     *   <td>The view is about to enter and become the active view.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicView.beforeLeave</code></td>
     *   <td>The view is about to leave and no longer be the active view.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicView.afterEnter</code></td>
     *   <td>The view has fully entered and is now the active view.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicView.afterLeave</code></td>
     *   <td>The view has finished leaving and is no longer the active view.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicView.unloaded</code></td>
     *   <td>The view's controller has been destroyed and its element has been
     * removed from the DOM.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicParentView.enter</code></td>
     *   <td>The parent view has fully entered and is now the active view.
     * This event will fire, whether it was the first load or a cached view.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicParentView.leave</code></td>
     *   <td>The parent view has finished leaving and is no longer the
     * active view. This event will fire, whether it is cached or destroyed.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicParentView.beforeEnter</code></td>
     *   <td>The parent view is about to enter and become the active view.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicParentView.beforeLeave</code></td>
     *   <td>The parent view is about to leave and no longer be the active view.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicParentView.afterEnter</code></td>
     *   <td>The parent view has fully entered and is now the active view.</td>
     *  </tr>
     *  <tr>
     *   <td><code>$ionicParentView.afterLeave</code></td>
     *   <td>The parent view has finished leaving and is no longer the active view.</td>
     *  </tr>
     * </table>
     *
     * ## LifeCycle Event Usage
     *
     * Below is an example of how to listen to life cycle events and
     * access state parameter data
     *
     * ```js
     * $scope.$on("$ionicView.beforeEnter", function(event, data){
     *    // handle event
     *    console.log("State Params: ", data.stateParams);
     * });
     *
     * $scope.$on("$ionicView.enter", function(event, data){
     *    // handle event
     *    console.log("State Params: ", data.stateParams);
     * });
     *
     * $scope.$on("$ionicView.afterEnter", function(event, data){
     *    // handle event
     *    console.log("State Params: ", data.stateParams);
     * });
     * ```
     *
     * ## 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=} can-swipe-back If this view should be allowed to use the swipe to go back gesture or not.
     * This does not enable the swipe to go back feature if it is not available for the platform it's running
     * from, or there isn't a previous view. 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();
          };
        },
      };
    });
  })();
  