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();
        };
      }
    };
  });
  
  })();