text: /*!
   * ionic.bundle.js is a concatenation of:
   * ionic.js, angular.js, angular-animate.js,
   * angular-sanitize.js, angular-ui-router.js,
   * and ionic-angular.js
   */
  
  /*!
   * Copyright 2014 Drifty Co.
   * http://drifty.com/
   *
   * Ionic, v1.0.0-beta.14
   * A powerful HTML5 mobile app framework.
   * http://ionicframework.com/
   *
   * By @maxlynch, @benjsperry, @adamdbradley <3
   *
   * Licensed under the MIT license. Please see LICENSE for more information.
   *
   */
  
  /*!
   * Copyright 2014 Drifty Co.
   * http://drifty.com/
   *
   * Ionic, v1.0.0-beta.14
   * A powerful HTML5 mobile app framework.
   * http://ionicframework.com/
   *
   * By @maxlynch, @benjsperry, @adamdbradley <3
   *
   * Licensed under the MIT license. Please see LICENSE for more information.
   *
   */
  
  (function() {
  
  // Create global ionic obj and its namespaces
  // build processes may have already created an ionic obj
  window.ionic = window.ionic || {};
  window.ionic.views = {};
  window.ionic.version = '1.0.0-beta.14';
  
  (function (ionic) {
  
    ionic.DelegateService = function(methodNames) {
  
      if (methodNames.indexOf('$getByHandle') > -1) {
        throw new Error("Method '$getByHandle' is implicitly added to each delegate service. Do not list it as a method.");
      }
  
      function trueFn() { return true; }
  
      return ['$log', function($log) {
  
        /*
         * Creates a new object that will have all the methodNames given,
         * and call them on the given the controller instance matching given
         * handle.
         * The reason we don't just let $getByHandle return the controller instance
         * itself is that the controller instance might not exist yet.
         *
         * We want people to be able to do
         * `var instance = $ionicScrollDelegate.$getByHandle('foo')` on controller
         * instantiation, but on controller instantiation a child directive
         * may not have been compiled yet!
         *
         * So this is our way of solving this problem: we create an object
         * that will only try to fetch the controller with given handle
         * once the methods are actually called.
         */
        function DelegateInstance(instances, handle) {
          this._instances = instances;
          this.handle = handle;
        }
        methodNames.forEach(function(methodName) {
          DelegateInstance.prototype[methodName] = instanceMethodCaller(methodName);
        });
  
  
        /**
         * The delegate service (eg $ionicNavBarDelegate) is just an instance
         * with a non-defined handle, a couple extra methods for registering
         * and narrowing down to a specific handle.
         */
        function DelegateService() {
          this._instances = [];
        }
        DelegateService.prototype = DelegateInstance.prototype;
        DelegateService.prototype._registerInstance = function(instance, handle, filterFn) {
          var instances = this._instances;
          instance.$$delegateHandle = handle;
          instance.$$filterFn = filterFn || trueFn;
          instances.push(instance);
  
          return function deregister() {
            var index = instances.indexOf(instance);
            if (index !== -1) {
              instances.splice(index, 1);
            }
          };
        };
        DelegateService.prototype.$getByHandle = function(handle) {
          return new DelegateInstance(this._instances, handle);
        };
  
        return new DelegateService();
  
        function instanceMethodCaller(methodName) {
          return function caller() {
            var handle = this.handle;
            var args = arguments;
            var foundInstancesCount = 0;
            var returnValue;
  
            this._instances.forEach(function(instance) {
              if ((!handle || handle == instance.$$delegateHandle) && instance.$$filterFn(instance)) {
                foundInstancesCount++;
                var ret = instance[methodName].apply(instance, args);
                //Only return the value from the first call
                if (foundInstancesCount === 1) {
                  returnValue = ret;
                }
              }
            });
  
            if (!foundInstancesCount && handle) {
              return $log.warn(
                'Delegate for handle "' + handle + '" could not find a ' +
                'corresponding element with delegate-handle="' + handle + '"! ' +
                methodName + '() was not called!\n' +
                'Possible cause: If you are calling ' + methodName + '() immediately, and ' +
                'your element with delegate-handle="' + handle + '" is a child of your ' +
                'controller, then your element may not be compiled yet. Put a $timeout ' +
                'around your call to ' + methodName + '() and try again.'
              );
            }
            return returnValue;
          };
        }
  
      }];
    };
  
  })(window.ionic);
  
  (function(window, document, ionic) {
  
    var readyCallbacks = [];
    var isDomReady = document.readyState === 'complete' || document.readyState === 'interactive';
  
    function domReady() {
      isDomReady = true;
      for (var x = 0; x < readyCallbacks.length; x++) {
        ionic.requestAnimationFrame(readyCallbacks[x]);
      }
      readyCallbacks = [];
      document.removeEventListener('DOMContentLoaded', domReady);
    }
    if (!isDomReady) {
      document.addEventListener('DOMContentLoaded', domReady);
    }
  
  
    // From the man himself, Mr. Paul Irish.
    // The requestAnimationFrame polyfill
    // Put it on window just to preserve its context
    // without having to use .call
    window._rAF = (function() {
      return window.requestAnimationFrame       ||
             window.webkitRequestAnimationFrame ||
             window.mozRequestAnimationFrame    ||
             function(callback) {
               window.setTimeout(callback, 16);
             };
    })();
  
    var cancelAnimationFrame = window.cancelAnimationFrame ||
      window.webkitCancelAnimationFrame ||
      window.mozCancelAnimationFrame ||
      window.webkitCancelRequestAnimationFrame;
  
    /**
    * @ngdoc utility
    * @name ionic.DomUtil
    * @module ionic
    */
    ionic.DomUtil = {
      //Call with proper context
      /**
       * @ngdoc method
       * @name ionic.DomUtil#requestAnimationFrame
       * @alias ionic.requestAnimationFrame
       * @description Calls [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame), or a polyfill if not available.
       * @param {function} callback The function to call when the next frame
       * happens.
       */
      requestAnimationFrame: function(cb) {
        return window._rAF(cb);
      },
  
      cancelAnimationFrame: function(requestId) {
        cancelAnimationFrame(requestId);
      },
  
      /**
       * @ngdoc method
       * @name ionic.DomUtil#animationFrameThrottle
       * @alias ionic.animationFrameThrottle
       * @description
       * When given a callback, if that callback is called 100 times between
       * animation frames, adding Throttle will make it only run the last of
       * the 100 calls.
       *
       * @param {function} callback a function which will be throttled to
       * requestAnimationFrame
       * @returns {function} A function which will then call the passed in callback.
       * The passed in callback will receive the context the returned function is
       * called with.
       */
      animationFrameThrottle: function(cb) {
        var args, isQueued, context;
        return function() {
          args = arguments;
          context = this;
          if (!isQueued) {
            isQueued = true;
            ionic.requestAnimationFrame(function() {
              cb.apply(context, args);
              isQueued = false;
            });
          }
        };
      },
  
      contains: function(parentNode, otherNode) {
        var current = otherNode;
        while (current) {
          if (current === parentNode) return true;
          current = current.parentNode;
        }
      },
  
      /**
       * @ngdoc method
       * @name ionic.DomUtil#getPositionInParent
       * @description
       * Find an element's scroll offset within its container.
       * @param {DOMElement} element The element to find the offset of.
       * @returns {object} A position object with the following properties:
       *   - `{number}` `left` The left offset of the element.
       *   - `{number}` `top` The top offset of the element.
       */
      getPositionInParent: function(el) {
        return {
          left: el.offsetLeft,
          top: el.offsetTop
        };
      },
  
      /**
       * @ngdoc method
       * @name ionic.DomUtil#ready
       * @description
       * Call a function when the DOM is ready, or if it is already ready
       * call the function immediately.
       * @param {function} callback The function to be called.
       */
      ready: function(cb) {
        if (isDomReady) {
          ionic.requestAnimationFrame(cb);
        } else {
          readyCallbacks.push(cb);
        }
      },
  
      /**
       * @ngdoc method
       * @name ionic.DomUtil#getTextBounds
       * @description
       * Get a rect representing the bounds of the given textNode.
       * @param {DOMElement} textNode The textNode to find the bounds of.
       * @returns {object} An object representing the bounds of the node. Properties:
       *   - `{number}` `left` The left position of the textNode.
       *   - `{number}` `right` The right position of the textNode.
       *   - `{number}` `top` The top position of the textNode.
       *   - `{number}` `bottom` The bottom position of the textNode.
       *   - `{number}` `width` The width of the textNode.
       *   - `{number}` `height` The height of the textNode.
       */
      getTextBounds: function(textNode) {
        if (document.createRange) {
          var range = document.createRange();
          range.selectNodeContents(textNode);
          if (range.getBoundingClientRect) {
            var rect = range.getBoundingClientRect();
            if (rect) {
              var sx = window.scrollX;
              var sy = window.scrollY;
  
              return {
                top: rect.top + sy,
                left: rect.left + sx,
                right: rect.left + sx + rect.width,
                bottom: rect.top + sy + rect.height,
                width: rect.width,
                height: rect.height
              };
            }
          }
        }
        return null;
      },
  
      /**
       * @ngdoc method
       * @name ionic.DomUtil#getChildIndex
       * @description
       * Get the first index of a child node within the given element of the
       * specified type.
       * @param {DOMElement} element The element to find the index of.
       * @param {string} type The nodeName to match children of element against.
       * @returns {number} The index, or -1, of a child with nodeName matching type.
       */
      getChildIndex: function(element, type) {
        if (type) {
          var ch = element.parentNode.children;
          var c;
          for (var i = 0, k = 0, j = ch.length; i < j; i++) {
            c = ch[i];
            if (c.nodeName && c.nodeName.toLowerCase() == type) {
              if (c == element) {
                return k;
              }
              k++;
            }
          }
        }
        return Array.prototype.slice.call(element.parentNode.children).indexOf(element);
      },
  
      /**
       * @private
       */
      swapNodes: function(src, dest) {
        dest.parentNode.insertBefore(src, dest);
      },
  
      elementIsDescendant: function(el, parent, stopAt) {
        var current = el;
        do {
          if (current === parent) return true;
          current = current.parentNode;
        } while (current && current !== stopAt);
        return false;
      },
  
      /**
       * @ngdoc method
       * @name ionic.DomUtil#getParentWithClass
       * @param {DOMElement} element
       * @param {string} className
       * @returns {DOMElement} The closest parent of element matching the
       * className, or null.
       */
      getParentWithClass: function(e, className, depth) {
        depth = depth || 10;
        while (e.parentNode && depth--) {
          if (e.parentNode.classList && e.parentNode.classList.contains(className)) {
            return e.parentNode;
          }
          e = e.parentNode;
        }
        return null;
      },
      /**
       * @ngdoc method
       * @name ionic.DomUtil#getParentOrSelfWithClass
       * @param {DOMElement} element
       * @param {string} className
       * @returns {DOMElement} The closest parent or self matching the
       * className, or null.
       */
      getParentOrSelfWithClass: function(e, className, depth) {
        depth = depth || 10;
        while (e && depth--) {
          if (e.classList && e.classList.contains(className)) {
            return e;
          }
          e = e.parentNode;
        }
        return null;
      },
  
      /**
       * @ngdoc method
       * @name ionic.DomUtil#rectContains
       * @param {number} x
       * @param {number} y
       * @param {number} x1
       * @param {number} y1
       * @param {number} x2
       * @param {number} y2
       * @returns {boolean} Whether {x,y} fits within the rectangle defined by
       * {x1,y1,x2,y2}.
       */
      rectContains: function(x, y, x1, y1, x2, y2) {
        if (x < x1 || x > x2) return false;
        if (y < y1 || y > y2) return false;
        return true;
      },
  
      /**
       * @ngdoc method
       * @name ionic.DomUtil#blurAll
       * @description
       * Blurs any currently focused input element
       * @returns {DOMElement} The element blurred or null
       */
      blurAll: function() {
        if (document.activeElement && document.activeElement != document.body) {
          document.activeElement.blur();
          return document.activeElement;
        }
        return null;
      },
  
      cachedAttr: function(ele, key, value) {
        ele = ele && ele.length && ele[0] || ele;
        if (ele && ele.setAttribute) {
          var dataKey = '$attr-' + key;
          if (arguments.length > 2) {
            if (ele[dataKey] !== value) {
              ele.setAttribute(key, value);
              ele[dataKey] = value;
            }
          } else if (typeof ele[dataKey] == 'undefined') {
            ele[dataKey] = ele.getAttribute(key);
          }
          return ele[dataKey];
        }
      },
  
      cachedStyles: function(ele, styles) {
        ele = ele && ele.length && ele[0] || ele;
        if (ele && ele.style) {
          for (var prop in styles) {
            if (ele['$style-' + prop] !== styles[prop]) {
              ele.style[prop] = ele['$style-' + prop] = styles[prop];
            }
          }
        }
      }
  
    };
  
    //Shortcuts
    ionic.requestAnimationFrame = ionic.DomUtil.requestAnimationFrame;
    ionic.cancelAnimationFrame = ionic.DomUtil.cancelAnimationFrame;
    ionic.animationFrameThrottle = ionic.DomUtil.animationFrameThrottle;
  
  })(window, document, ionic);
  
  /**
   * ion-events.js
   *
   * Author: Max Lynch <max@drifty.com>
   *
   * Framework events handles various mobile browser events, and
   * detects special events like tap/swipe/etc. and emits them
   * as custom events that can be used in an app.
   *
   * Portions lovingly adapted from github.com/maker/ratchet and github.com/alexgibson/tap.js - thanks guys!
   */
  
  (function(ionic) {
  
    // Custom event polyfill
    ionic.CustomEvent = (function() {
      if( typeof window.CustomEvent === 'function' ) return CustomEvent;
  
      var customEvent = function(event, params) {
        var evt;
        params = params || {
          bubbles: false,
          cancelable: false,
          detail: undefined
        };
        try {
          evt = document.createEvent("CustomEvent");
          evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
        } catch (error) {
          // fallback for browsers that don't support createEvent('CustomEvent')
          evt = document.createEvent("Event");
          for (var param in params) {
            evt[param] = params[param];
          }
          evt.initEvent(event, params.bubbles, params.cancelable);
        }
        return evt;
      };
      customEvent.prototype = window.Event.prototype;
      return customEvent;
    })();
  
  
    /**
     * @ngdoc utility
     * @name ionic.EventController
     * @module ionic
     */
    ionic.EventController = {
      VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'],
  
      /**
       * @ngdoc method
       * @name ionic.EventController#trigger
       * @alias ionic.trigger
       * @param {string} eventType The event to trigger.
       * @param {object} data The data for the event. Hint: pass in
       * `{target: targetElement}`
       * @param {boolean=} bubbles Whether the event should bubble up the DOM.
       * @param {boolean=} cancelable Whether the event should be cancelable.
       */
      // Trigger a new event
      trigger: function(eventType, data, bubbles, cancelable) {
        var event = new ionic.CustomEvent(eventType, {
          detail: data,
          bubbles: !!bubbles,
          cancelable: !!cancelable
        });
  
        // Make sure to trigger the event on the given target, or dispatch it from
        // the window if we don't have an event target
        data && data.target && data.target.dispatchEvent && data.target.dispatchEvent(event) || window.dispatchEvent(event);
      },
  
      /**
       * @ngdoc method
       * @name ionic.EventController#on
       * @alias ionic.on
       * @description Listen to an event on an element.
       * @param {string} type The event to listen for.
       * @param {function} callback The listener to be called.
       * @param {DOMElement} element The element to listen for the event on.
       */
      on: function(type, callback, element) {
        var e = element || window;
  
        // Bind a gesture if it's a virtual event
        for(var i = 0, j = this.VIRTUALIZED_EVENTS.length; i < j; i++) {
          if(type == this.VIRTUALIZED_EVENTS[i]) {
            var gesture = new ionic.Gesture(element);
            gesture.on(type, callback);
            return gesture;
          }
        }
  
        // Otherwise bind a normal event
        e.addEventListener(type, callback);
      },
  
      /**
       * @ngdoc method
       * @name ionic.EventController#off
       * @alias ionic.off
       * @description Remove an event listener.
       * @param {string} type
       * @param {function} callback
       * @param {DOMElement} element
       */
      off: function(type, callback, element) {
        element.removeEventListener(type, callback);
      },
  
      /**
       * @ngdoc method
       * @name ionic.EventController#onGesture
       * @alias ionic.onGesture
       * @description Add an event listener for a gesture on an element.
       *
       * Available eventTypes (from [hammer.js](http://eightmedia.github.io/hammer.js/)):
       *
       * `hold`, `tap`, `doubletap`, `drag`, `dragstart`, `dragend`, `dragup`, `dragdown`, <br/>
       * `dragleft`, `dragright`, `swipe`, `swipeup`, `swipedown`, `swipeleft`, `swiperight`, <br/>
       * `transform`, `transformstart`, `transformend`, `rotate`, `pinch`, `pinchin`, `pinchout`, </br>
       * `touch`, `release`
       *
       * @param {string} eventType The gesture event to listen for.
       * @param {function(e)} callback The function to call when the gesture
       * happens.
       * @param {DOMElement} element The angular element to listen for the event on.
       * @param {object} options object.
       * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on).
       */
      onGesture: function(type, callback, element, options) {
        var gesture = new ionic.Gesture(element, options);
        gesture.on(type, callback);
        return gesture;
      },
  
      /**
       * @ngdoc method
       * @name ionic.EventController#offGesture
       * @alias ionic.offGesture
       * @description Remove an event listener for a gesture created on an element.
       * @param {ionic.Gesture} gesture The gesture that should be removed.
       * @param {string} eventType The gesture event to remove the listener for.
       * @param {function(e)} callback The listener to remove.
  
       */
      offGesture: function(gesture, type, callback) {
        gesture && gesture.off(type, callback);
      },
  
      handlePopState: function(event) {}
    };
  
  
    // Map some convenient top-level functions for event handling
    ionic.on = function() { ionic.EventController.on.apply(ionic.EventController, arguments); };
    ionic.off = function() { ionic.EventController.off.apply(ionic.EventController, arguments); };
    ionic.trigger = ionic.EventController.trigger;//function() { ionic.EventController.trigger.apply(ionic.EventController.trigger, arguments); };
    ionic.onGesture = function() { return ionic.EventController.onGesture.apply(ionic.EventController.onGesture, arguments); };
    ionic.offGesture = function() { return ionic.EventController.offGesture.apply(ionic.EventController.offGesture, arguments); };
  
  })(window.ionic);
  
  /**
    * Simple gesture controllers with some common gestures that emit
    * gesture events.
    *
    * Ported from github.com/EightMedia/hammer.js Gestures - thanks!
    */
  (function(ionic) {
  
    /**
     * ionic.Gestures
     * use this to create instances
     * @param   {HTMLElement}   element
     * @param   {Object}        options
     * @returns {ionic.Gestures.Instance}
     * @constructor
     */
    ionic.Gesture = function(element, options) {
      return new ionic.Gestures.Instance(element, options || {});
    };
  
    ionic.Gestures = {};
  
    // default settings
    ionic.Gestures.defaults = {
      // add css to the element to prevent the browser from doing
      // its native behavior. this doesnt prevent the scrolling,
      // but cancels the contextmenu, tap highlighting etc
      // set to false to disable this
      stop_browser_behavior: 'disable-user-behavior'
    };
  
    // detect touchevents
    ionic.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
    ionic.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  
    // dont use mouseevents on mobile devices
    ionic.Gestures.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i;
    ionic.Gestures.NO_MOUSEEVENTS = ionic.Gestures.HAS_TOUCHEVENTS && window.navigator.userAgent.match(ionic.Gestures.MOBILE_REGEX);
  
    // eventtypes per touchevent (start, move, end)
    // are filled by ionic.Gestures.event.determineEventTypes on setup
    ionic.Gestures.EVENT_TYPES = {};
  
    // direction defines
    ionic.Gestures.DIRECTION_DOWN = 'down';
    ionic.Gestures.DIRECTION_LEFT = 'left';
    ionic.Gestures.DIRECTION_UP = 'up';
    ionic.Gestures.DIRECTION_RIGHT = 'right';
  
    // pointer type
    ionic.Gestures.POINTER_MOUSE = 'mouse';
    ionic.Gestures.POINTER_TOUCH = 'touch';
    ionic.Gestures.POINTER_PEN = 'pen';
  
    // touch event defines
    ionic.Gestures.EVENT_START = 'start';
    ionic.Gestures.EVENT_MOVE = 'move';
    ionic.Gestures.EVENT_END = 'end';
  
    // hammer document where the base events are added at
    ionic.Gestures.DOCUMENT = window.document;
  
    // plugins namespace
    ionic.Gestures.plugins = {};
  
    // if the window events are set...
    ionic.Gestures.READY = false;
  
    /**
     * setup events to detect gestures on the document
     */
    function setup() {
      if(ionic.Gestures.READY) {
        return;
      }
  
      // find what eventtypes we add listeners to
      ionic.Gestures.event.determineEventTypes();
  
      // Register all gestures inside ionic.Gestures.gestures
      for(var name in ionic.Gestures.gestures) {
        if(ionic.Gestures.gestures.hasOwnProperty(name)) {
          ionic.Gestures.detection.register(ionic.Gestures.gestures[name]);
        }
      }
  
      // Add touch events on the document
      ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_MOVE, ionic.Gestures.detection.detect);
      ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_END, ionic.Gestures.detection.detect);
  
      // ionic.Gestures is ready...!
      ionic.Gestures.READY = true;
    }
  
    /**
     * create new hammer instance
     * all methods should return the instance itself, so it is chainable.
     * @param   {HTMLElement}       element
     * @param   {Object}            [options={}]
     * @returns {ionic.Gestures.Instance}
     * @name Gesture.Instance
     * @constructor
     */
    ionic.Gestures.Instance = function(element, options) {
      var self = this;
  
      // A null element was passed into the instance, which means
      // whatever lookup was done to find this element failed to find it
      // so we can't listen for events on it.
      if(element === null) {
        console.error('Null element passed to gesture (element does not exist). Not listening for gesture');
        return;
      }
  
      // setup ionic.GesturesJS window events and register all gestures
      // this also sets up the default options
      setup();
  
      this.element = element;
  
      // start/stop detection option
      this.enabled = true;
  
      // merge options
      this.options = ionic.Gestures.utils.extend(
          ionic.Gestures.utils.extend({}, ionic.Gestures.defaults),
          options || {});
  
      // add some css to the element to prevent the browser from doing its native behavoir
      if(this.options.stop_browser_behavior) {
        ionic.Gestures.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
      }
  
      // start detection on touchstart
      ionic.Gestures.event.onTouch(element, ionic.Gestures.EVENT_START, function(ev) {
        if(self.enabled) {
          ionic.Gestures.detection.startDetect(self, ev);
        }
      });
  
      // return instance
      return this;
    };
  
  
    ionic.Gestures.Instance.prototype = {
      /**
       * bind events to the instance
       * @param   {String}      gesture
       * @param   {Function}    handler
       * @returns {ionic.Gestures.Instance}
       */
      on: function onEvent(gesture, handler){
        var gestures = gesture.split(' ');
        for(var t=0; t<gestures.length; t++) {
          this.element.addEventListener(gestures[t], handler, false);
        }
        return this;
      },
  
  
      /**
       * unbind events to the instance
       * @param   {String}      gesture
       * @param   {Function}    handler
       * @returns {ionic.Gestures.Instance}
       */
      off: function offEvent(gesture, handler){
        var gestures = gesture.split(' ');
        for(var t=0; t<gestures.length; t++) {
          this.element.removeEventListener(gestures[t], handler, false);
        }
        return this;
      },
  
  
      /**
       * trigger gesture event
       * @param   {String}      gesture
       * @param   {Object}      eventData
       * @returns {ionic.Gestures.Instance}
       */
      trigger: function triggerEvent(gesture, eventData){
        // create DOM event
        var event = ionic.Gestures.DOCUMENT.createEvent('Event');
        event.initEvent(gesture, true, true);
        event.gesture = eventData;
  
        // trigger on the target if it is in the instance element,
        // this is for event delegation tricks
        var element = this.element;
        if(ionic.Gestures.utils.hasParent(eventData.target, element)) {
          element = eventData.target;
        }
  
        element.dispatchEvent(event);
        return this;
      },
  
  
      /**
       * enable of disable hammer.js detection
       * @param   {Boolean}   state
       * @returns {ionic.Gestures.Instance}
       */
      enable: function enable(state) {
        this.enabled = state;
        return this;
      }
    };
  
    /**
     * this holds the last move event,
     * used to fix empty touchend issue
     * see the onTouch event for an explanation
     * type {Object}
     */
    var last_move_event = null;
  
  
    /**
     * when the mouse is hold down, this is true
     * type {Boolean}
     */
    var enable_detect = false;
  
  
    /**
     * when touch events have been fired, this is true
     * type {Boolean}
     */
    var touch_triggered = false;
  
  
    ionic.Gestures.event = {
      /**
       * simple addEventListener
       * @param   {HTMLElement}   element
       * @param   {String}        type
       * @param   {Function}      handler
       */
      bindDom: function(element, type, handler) {
        var types = type.split(' ');
        for(var t=0; t<types.length; t++) {
          element.addEventListener(types[t], handler, false);
        }
      },
  
  
      /**
       * touch events with mouse fallback
       * @param   {HTMLElement}   element
       * @param   {String}        eventType        like ionic.Gestures.EVENT_MOVE
       * @param   {Function}      handler
       */
      onTouch: function onTouch(element, eventType, handler) {
        var self = this;
  
        this.bindDom(element, ionic.Gestures.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
          var sourceEventType = ev.type.toLowerCase();
  
          // onmouseup, but when touchend has been fired we do nothing.
          // this is for touchdevices which also fire a mouseup on touchend
          if(sourceEventType.match(/mouse/) && touch_triggered) {
            return;
          }
  
          // mousebutton must be down or a touch event
          else if( sourceEventType.match(/touch/) ||   // touch events are always on screen
            sourceEventType.match(/pointerdown/) || // pointerevents touch
            (sourceEventType.match(/mouse/) && ev.which === 1)   // mouse is pressed
            ){
              enable_detect = true;
            }
  
          // mouse isn't pressed
          else if(sourceEventType.match(/mouse/) && ev.which !== 1) {
            enable_detect = false;
          }
  
  
          // we are in a touch event, set the touch triggered bool to true,
          // this for the conflicts that may occur on ios and android
          if(sourceEventType.match(/touch|pointer/)) {
            touch_triggered = true;
          }
  
          // count the total touches on the screen
          var count_touches = 0;
  
          // when touch has been triggered in this detection session
          // and we are now handling a mouse event, we stop that to prevent conflicts
          if(enable_detect) {
            // update pointerevent
            if(ionic.Gestures.HAS_POINTEREVENTS && eventType != ionic.Gestures.EVENT_END) {
              count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
            }
            // touch
            else if(sourceEventType.match(/touch/)) {
              count_touches = ev.touches.length;
            }
            // mouse
            else if(!touch_triggered) {
              count_touches = sourceEventType.match(/up/) ? 0 : 1;
            }
  
            // if we are in a end event, but when we remove one touch and
            // we still have enough, set eventType to move
            if(count_touches > 0 && eventType == ionic.Gestures.EVENT_END) {
              eventType = ionic.Gestures.EVENT_MOVE;
            }
            // no touches, force the end event
            else if(!count_touches) {
              eventType = ionic.Gestures.EVENT_END;
            }
  
            // store the last move event
            if(count_touches || last_move_event === null) {
              last_move_event = ev;
            }
  
            // trigger the handler
            handler.call(ionic.Gestures.detection, self.collectEventData(element, eventType, self.getTouchList(last_move_event, eventType), ev));
  
            // remove pointerevent from list
            if(ionic.Gestures.HAS_POINTEREVENTS && eventType == ionic.Gestures.EVENT_END) {
              count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
            }
          }
  
          //debug(sourceEventType +" "+ eventType);
  
          // on the end we reset everything
          if(!count_touches) {
            last_move_event = null;
            enable_detect = false;
            touch_triggered = false;
            ionic.Gestures.PointerEvent.reset();
          }
        });
      },
  
  
      /**
       * we have different events for each device/browser
       * determine what we need and set them in the ionic.Gestures.EVENT_TYPES constant
       */
      determineEventTypes: function determineEventTypes() {
        // determine the eventtype we want to set
        var types;
  
        // pointerEvents magic
        if(ionic.Gestures.HAS_POINTEREVENTS) {
          types = ionic.Gestures.PointerEvent.getEvents();
        }
        // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
        else if(ionic.Gestures.NO_MOUSEEVENTS) {
          types = [
            'touchstart',
            'touchmove',
            'touchend touchcancel'];
        }
        // for non pointer events browsers and mixed browsers,
        // like chrome on windows8 touch laptop
        else {
          types = [
            'touchstart mousedown',
            'touchmove mousemove',
            'touchend touchcancel mouseup'];
        }
  
        ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_START]  = types[0];
        ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_MOVE]   = types[1];
        ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_END]    = types[2];
      },
  
  
      /**
       * create touchlist depending on the event
       * @param   {Object}    ev
       * @param   {String}    eventType   used by the fakemultitouch plugin
       */
      getTouchList: function getTouchList(ev/*, eventType*/) {
        // get the fake pointerEvent touchlist
        if(ionic.Gestures.HAS_POINTEREVENTS) {
          return ionic.Gestures.PointerEvent.getTouchList();
        }
        // get the touchlist
        else if(ev.touches) {
          return ev.touches;
        }
        // make fake touchlist from mouse position
        else {
          ev.identifier = 1;
          return [ev];
        }
      },
  
  
      /**
       * collect event data for ionic.Gestures js
       * @param   {HTMLElement}   element
       * @param   {String}        eventType        like ionic.Gestures.EVENT_MOVE
       * @param   {Object}        eventData
       */
      collectEventData: function collectEventData(element, eventType, touches, ev) {
  
        // find out pointerType
        var pointerType = ionic.Gestures.POINTER_TOUCH;
        if(ev.type.match(/mouse/) || ionic.Gestures.PointerEvent.matchType(ionic.Gestures.POINTER_MOUSE, ev)) {
          pointerType = ionic.Gestures.POINTER_MOUSE;
        }
  
        return {
          center      : ionic.Gestures.utils.getCenter(touches),
                      timeStamp   : new Date().getTime(),
                      target      : ev.target,
                      touches     : touches,
                      eventType   : eventType,
                      pointerType : pointerType,
                      srcEvent    : ev,
  
                      /**
                       * prevent the browser default actions
                       * mostly used to disable scrolling of the browser
                       */
                      preventDefault: function() {
                        if(this.srcEvent.preventManipulation) {
                          this.srcEvent.preventManipulation();
                        }
  
                        if(this.srcEvent.preventDefault) {
                          // this.srcEvent.preventDefault();
                        }
                      },
  
                      /**
                       * stop bubbling the event up to its parents
                       */
                      stopPropagation: function() {
                        this.srcEvent.stopPropagation();
                      },
  
                      /**
                       * immediately stop gesture detection
                       * might be useful after a swipe was detected
                       * @return {*}
                       */
                      stopDetect: function() {
                        return ionic.Gestures.detection.stopDetect();
                      }
        };
      }
    };
  
    ionic.Gestures.PointerEvent = {
      /**
       * holds all pointers
       * type {Object}
       */
      pointers: {},
  
      /**
       * get a list of pointers
       * @returns {Array}     touchlist
       */
      getTouchList: function() {
        var self = this;
        var touchlist = [];
  
        // we can use forEach since pointerEvents only is in IE10
        Object.keys(self.pointers).sort().forEach(function(id) {
          touchlist.push(self.pointers[id]);
        });
        return touchlist;
      },
  
      /**
       * update the position of a pointer
       * @param   {String}   type             ionic.Gestures.EVENT_END
       * @param   {Object}   pointerEvent
       */
      updatePointer: function(type, pointerEvent) {
        if(type == ionic.Gestures.EVENT_END) {
          this.pointers = {};
        }
        else {
          pointerEvent.identifier = pointerEvent.pointerId;
          this.pointers[pointerEvent.pointerId] = pointerEvent;
        }
  
        return Object.keys(this.pointers).length;
      },
  
      /**
       * check if ev matches pointertype
       * @param   {String}        pointerType     ionic.Gestures.POINTER_MOUSE
       * @param   {PointerEvent}  ev
       */
      matchType: function(pointerType, ev) {
        if(!ev.pointerType) {
          return false;
        }
  
        var types = {};
        types[ionic.Gestures.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == ionic.Gestures.POINTER_MOUSE);
        types[ionic.Gestures.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == ionic.Gestures.POINTER_TOUCH);
        types[ionic.Gestures.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == ionic.Gestures.POINTER_PEN);
        return types[pointerType];
      },
  
  
      /**
       * get events
       */
      getEvents: function() {
        return [
          'pointerdown MSPointerDown',
        'pointermove MSPointerMove',
        'pointerup pointercancel MSPointerUp MSPointerCancel'
          ];
      },
  
      /**
       * reset the list
       */
      reset: function() {
        this.pointers = {};
      }
    };
  
  
    ionic.Gestures.utils = {
      /**
       * extend method,
       * also used for cloning when dest is an empty object
       * @param   {Object}    dest
       * @param   {Object}    src
       * @param	{Boolean}	merge		do a merge
       * @returns {Object}    dest
       */
      extend: function extend(dest, src, merge) {
        for (var key in src) {
          if(dest[key] !== undefined && merge) {
            continue;
          }
          dest[key] = src[key];
        }
        return dest;
      },
  
  
      /**
       * find if a node is in the given parent
       * used for event delegation tricks
       * @param   {HTMLElement}   node
       * @param   {HTMLElement}   parent
       * @returns {boolean}       has_parent
       */
      hasParent: function(node, parent) {
        while(node){
          if(node == parent) {
            return true;
          }
          node = node.parentNode;
        }
        return false;
      },
  
  
      /**
       * get the center of all the touches
       * @param   {Array}     touches
       * @returns {Object}    center
       */
      getCenter: function getCenter(touches) {
        var valuesX = [], valuesY = [];
  
        for(var t= 0,len=touches.length; t<len; t++) {
          valuesX.push(touches[t].pageX);
          valuesY.push(touches[t].pageY);
        }
  
        return {
          pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
            pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
        };
      },
  
  
      /**
       * calculate the velocity between two points
       * @param   {Number}    delta_time
       * @param   {Number}    delta_x
       * @param   {Number}    delta_y
       * @returns {Object}    velocity
       */
      getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
        return {
          x: Math.abs(delta_x / delta_time) || 0,
          y: Math.abs(delta_y / delta_time) || 0
        };
      },
  
  
      /**
       * calculate the angle between two coordinates
       * @param   {Touch}     touch1
       * @param   {Touch}     touch2
       * @returns {Number}    angle
       */
      getAngle: function getAngle(touch1, touch2) {
        var y = touch2.pageY - touch1.pageY,
        x = touch2.pageX - touch1.pageX;
        return Math.atan2(y, x) * 180 / Math.PI;
      },
  
  
      /**
       * angle to direction define
       * @param   {Touch}     touch1
       * @param   {Touch}     touch2
       * @returns {String}    direction constant, like ionic.Gestures.DIRECTION_LEFT
       */
      getDirection: function getDirection(touch1, touch2) {
        var x = Math.abs(touch1.pageX - touch2.pageX),
        y = Math.abs(touch1.pageY - touch2.pageY);
  
        if(x >= y) {
          return touch1.pageX - touch2.pageX > 0 ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
        }
        else {
          return touch1.pageY - touch2.pageY > 0 ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
        }
      },
  
  
      /**
       * calculate the distance between two touches
       * @param   {Touch}     touch1
       * @param   {Touch}     touch2
       * @returns {Number}    distance
       */
      getDistance: function getDistance(touch1, touch2) {
        var x = touch2.pageX - touch1.pageX,
        y = touch2.pageY - touch1.pageY;
        return Math.sqrt((x*x) + (y*y));
      },
  
  
      /**
       * calculate the scale factor between two touchLists (fingers)
       * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
       * @param   {Array}     start
       * @param   {Array}     end
       * @returns {Number}    scale
       */
      getScale: function getScale(start, end) {
        // need two fingers...
        if(start.length >= 2 && end.length >= 2) {
          return this.getDistance(end[0], end[1]) /
            this.getDistance(start[0], start[1]);
        }
        return 1;
      },
  
  
      /**
       * calculate the rotation degrees between two touchLists (fingers)
       * @param   {Array}     start
       * @param   {Array}     end
       * @returns {Number}    rotation
       */
      getRotation: function getRotation(start, end) {
        // need two fingers
        if(start.length >= 2 && end.length >= 2) {
          return this.getAngle(end[1], end[0]) -
            this.getAngle(start[1], start[0]);
        }
        return 0;
      },
  
  
      /**
       * boolean if the direction is vertical
       * @param    {String}    direction
       * @returns  {Boolean}   is_vertical
       */
      isVertical: function isVertical(direction) {
        return (direction == ionic.Gestures.DIRECTION_UP || direction == ionic.Gestures.DIRECTION_DOWN);
      },
  
  
      /**
       * stop browser default behavior with css class
       * @param   {HtmlElement}   element
       * @param   {Object}        css_class
       */
      stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_class) {
        // changed from making many style changes to just adding a preset classname
        // less DOM manipulations, less code, and easier to control in the CSS side of things
        // hammer.js doesn't come with CSS, but ionic does, which is why we prefer this method
        if(element && element.classList) {
          element.classList.add(css_class);
          element.onselectstart = function() {
            return false;
          };
        }
      }
    };
  
  
    ionic.Gestures.detection = {
      // contains all registred ionic.Gestures.gestures in the correct order
      gestures: [],
  
      // data of the current ionic.Gestures.gesture detection session
      current: null,
  
      // the previous ionic.Gestures.gesture session data
      // is a full clone of the previous gesture.current object
      previous: null,
  
      // when this becomes true, no gestures are fired
      stopped: false,
  
  
      /**
       * start ionic.Gestures.gesture detection
       * @param   {ionic.Gestures.Instance}   inst
       * @param   {Object}            eventData
       */
      startDetect: function startDetect(inst, eventData) {
        // already busy with a ionic.Gestures.gesture detection on an element
        if(this.current) {
          return;
        }
  
        this.stopped = false;
  
        this.current = {
          inst        : inst, // reference to ionic.GesturesInstance we're working for
          startEvent  : ionic.Gestures.utils.extend({}, eventData), // start eventData for distances, timing etc
          lastEvent   : false, // last eventData
          name        : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
        };
  
        this.detect(eventData);
      },
  
  
      /**
       * ionic.Gestures.gesture detection
       * @param   {Object}    eventData
       */
      detect: function detect(eventData) {
        if(!this.current || this.stopped) {
          return;
        }
  
        // extend event data with calculations about scale, distance etc
        eventData = this.extendEventData(eventData);
  
        // instance options
        var inst_options = this.current.inst.options;
  
        // call ionic.Gestures.gesture handlers
        for(var g=0,len=this.gestures.length; g<len; g++) {
          var gesture = this.gestures[g];
  
          // only when the instance options have enabled this gesture
          if(!this.stopped && inst_options[gesture.name] !== false) {
            // if a handler returns false, we stop with the detection
            if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
              this.stopDetect();
              break;
            }
          }
        }
  
        // store as previous event event
        if(this.current) {
          this.current.lastEvent = eventData;
        }
  
        // endevent, but not the last touch, so dont stop
        if(eventData.eventType == ionic.Gestures.EVENT_END && !eventData.touches.length-1) {
          this.stopDetect();
        }
  
        return eventData;
      },
  
  
      /**
       * clear the ionic.Gestures.gesture vars
       * this is called on endDetect, but can also be used when a final ionic.Gestures.gesture has been detected
       * to stop other ionic.Gestures.gestures from being fired
       */
      stopDetect: function stopDetect() {
        // clone current data to the store as the previous gesture
        // used for the double tap gesture, since this is an other gesture detect session
        this.previous = ionic.Gestures.utils.extend({}, this.current);
  
        // reset the current
        this.current = null;
  
        // stopped!
        this.stopped = true;
      },
  
  
      /**
       * extend eventData for ionic.Gestures.gestures
       * @param   {Object}   ev
       * @returns {Object}   ev
       */
      extendEventData: function extendEventData(ev) {
        var startEv = this.current.startEvent;
  
        // if the touches change, set the new touches over the startEvent touches
        // this because touchevents don't have all the touches on touchstart, or the
        // user must place his fingers at the EXACT same time on the screen, which is not realistic
        // but, sometimes it happens that both fingers are touching at the EXACT same time
        if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
          // extend 1 level deep to get the touchlist with the touch objects
          startEv.touches = [];
          for(var i=0,len=ev.touches.length; i<len; i++) {
            startEv.touches.push(ionic.Gestures.utils.extend({}, ev.touches[i]));
          }
        }
  
        var delta_time = ev.timeStamp - startEv.timeStamp,
            delta_x = ev.center.pageX - startEv.center.pageX,
            delta_y = ev.center.pageY - startEv.center.pageY,
            velocity = ionic.Gestures.utils.getVelocity(delta_time, delta_x, delta_y);
  
        ionic.Gestures.utils.extend(ev, {
          deltaTime   : delta_time,
  
          deltaX      : delta_x,
          deltaY      : delta_y,
  
          velocityX   : velocity.x,
          velocityY   : velocity.y,
  
          distance    : ionic.Gestures.utils.getDistance(startEv.center, ev.center),
          angle       : ionic.Gestures.utils.getAngle(startEv.center, ev.center),
          direction   : ionic.Gestures.utils.getDirection(startEv.center, ev.center),
  
          scale       : ionic.Gestures.utils.getScale(startEv.touches, ev.touches),
          rotation    : ionic.Gestures.utils.getRotation(startEv.touches, ev.touches),
  
          startEvent  : startEv
        });
  
        return ev;
      },
  
  
      /**
       * register new gesture
       * @param   {Object}    gesture object, see gestures.js for documentation
       * @returns {Array}     gestures
       */
      register: function register(gesture) {
        // add an enable gesture options if there is no given
        var options = gesture.defaults || {};
        if(options[gesture.name] === undefined) {
          options[gesture.name] = true;
        }
  
        // extend ionic.Gestures default options with the ionic.Gestures.gesture options
        ionic.Gestures.utils.extend(ionic.Gestures.defaults, options, true);
  
        // set its index
        gesture.index = gesture.index || 1000;
  
        // add ionic.Gestures.gesture to the list
        this.gestures.push(gesture);
  
        // sort the list by index
        this.gestures.sort(function(a, b) {
          if (a.index < b.index) {
            return -1;
          }
          if (a.index > b.index) {
            return 1;
          }
          return 0;
        });
  
        return this.gestures;
      }
    };
  
  
    ionic.Gestures.gestures = ionic.Gestures.gestures || {};
  
    /**
     * Custom gestures
     * ==============================
     *
     * Gesture object
     * --------------------
     * The object structure of a gesture:
     *
     * { name: 'mygesture',
     *   index: 1337,
     *   defaults: {
     *     mygesture_option: true
     *   }
     *   handler: function(type, ev, inst) {
     *     // trigger gesture event
     *     inst.trigger(this.name, ev);
     *   }
     * }
  
     * @param   {String}    name
     * this should be the name of the gesture, lowercase
     * it is also being used to disable/enable the gesture per instance config.
     *
     * @param   {Number}    [index=1000]
     * the index of the gesture, where it is going to be in the stack of gestures detection
     * like when you build an gesture that depends on the drag gesture, it is a good
     * idea to place it after the index of the drag gesture.
     *
     * @param   {Object}    [defaults={}]
     * the default settings of the gesture. these are added to the instance settings,
     * and can be overruled per instance. you can also add the name of the gesture,
     * but this is also added by default (and set to true).
     *
     * @param   {Function}  handler
     * this handles the gesture detection of your custom gesture and receives the
     * following arguments:
     *
     *      @param  {Object}    eventData
     *      event data containing the following properties:
     *          timeStamp   {Number}        time the event occurred
     *          target      {HTMLElement}   target element
     *          touches     {Array}         touches (fingers, pointers, mouse) on the screen
     *          pointerType {String}        kind of pointer that was used. matches ionic.Gestures.POINTER_MOUSE|TOUCH
     *          center      {Object}        center position of the touches. contains pageX and pageY
     *          deltaTime   {Number}        the total time of the touches in the screen
     *          deltaX      {Number}        the delta on x axis we haved moved
     *          deltaY      {Number}        the delta on y axis we haved moved
     *          velocityX   {Number}        the velocity on the x
     *          velocityY   {Number}        the velocity on y
     *          angle       {Number}        the angle we are moving
     *          direction   {String}        the direction we are moving. matches ionic.Gestures.DIRECTION_UP|DOWN|LEFT|RIGHT
     *          distance    {Number}        the distance we haved moved
     *          scale       {Number}        scaling of the touches, needs 2 touches
     *          rotation    {Number}        rotation of the touches, needs 2 touches *
     *          eventType   {String}        matches ionic.Gestures.EVENT_START|MOVE|END
     *          srcEvent    {Object}        the source event, like TouchStart or MouseDown *
     *          startEvent  {Object}        contains the same properties as above,
     *                                      but from the first touch. this is used to calculate
     *                                      distances, deltaTime, scaling etc
     *
     *      @param  {ionic.Gestures.Instance}    inst
     *      the instance we are doing the detection for. you can get the options from
     *      the inst.options object and trigger the gesture event by calling inst.trigger
     *
     *
     * Handle gestures
     * --------------------
     * inside the handler you can get/set ionic.Gestures.detectionic.current. This is the current
     * detection sessionic. It has the following properties
     *      @param  {String}    name
     *      contains the name of the gesture we have detected. it has not a real function,
     *      only to check in other gestures if something is detected.
     *      like in the drag gesture we set it to 'drag' and in the swipe gesture we can
     *      check if the current gesture is 'drag' by accessing ionic.Gestures.detectionic.current.name
     *
     *      readonly
     *      @param  {ionic.Gestures.Instance}    inst
     *      the instance we do the detection for
     *
     *      readonly
     *      @param  {Object}    startEvent
     *      contains the properties of the first gesture detection in this sessionic.
     *      Used for calculations about timing, distance, etc.
     *
     *      readonly
     *      @param  {Object}    lastEvent
     *      contains all the properties of the last gesture detect in this sessionic.
     *
     * after the gesture detection session has been completed (user has released the screen)
     * the ionic.Gestures.detectionic.current object is copied into ionic.Gestures.detectionic.previous,
     * this is usefull for gestures like doubletap, where you need to know if the
     * previous gesture was a tap
     *
     * options that have been set by the instance can be received by calling inst.options
     *
     * You can trigger a gesture event by calling inst.trigger("mygesture", event).
     * The first param is the name of your gesture, the second the event argument
     *
     *
     * Register gestures
     * --------------------
     * When an gesture is added to the ionic.Gestures.gestures object, it is auto registered
     * at the setup of the first ionic.Gestures instance. You can also call ionic.Gestures.detectionic.register
     * manually and pass your gesture object as a param
     *
     */
  
    /**
     * Hold
     * Touch stays at the same place for x time
     * events  hold
     */
    ionic.Gestures.gestures.Hold = {
      name: 'hold',
      index: 10,
      defaults: {
        hold_timeout	: 500,
        hold_threshold	: 1
      },
      timer: null,
      handler: function holdGesture(ev, inst) {
        switch(ev.eventType) {
          case ionic.Gestures.EVENT_START:
            // clear any running timers
            clearTimeout(this.timer);
  
            // set the gesture so we can check in the timeout if it still is
            ionic.Gestures.detection.current.name = this.name;
  
            // set timer and if after the timeout it still is hold,
            // we trigger the hold event
            this.timer = setTimeout(function() {
              if(ionic.Gestures.detection.current.name == 'hold') {
                ionic.tap.cancelClick();
                inst.trigger('hold', ev);
              }
            }, inst.options.hold_timeout);
            break;
  
            // when you move or end we clear the timer
          case ionic.Gestures.EVENT_MOVE:
            if(ev.distance > inst.options.hold_threshold) {
              clearTimeout(this.timer);
            }
            break;
  
          case ionic.Gestures.EVENT_END:
            clearTimeout(this.timer);
            break;
        }
      }
    };
  
  
    /**
     * Tap/DoubleTap
     * Quick touch at a place or double at the same place
     * events  tap, doubletap
     */
    ionic.Gestures.gestures.Tap = {
      name: 'tap',
      index: 100,
      defaults: {
        tap_max_touchtime	: 250,
        tap_max_distance	: 10,
        tap_always			: true,
        doubletap_distance	: 20,
        doubletap_interval	: 300
      },
      handler: function tapGesture(ev, inst) {
        if(ev.eventType == ionic.Gestures.EVENT_END && ev.srcEvent.type != 'touchcancel') {
          // previous gesture, for the double tap since these are two different gesture detections
          var prev = ionic.Gestures.detection.previous,
          did_doubletap = false;
  
          // when the touchtime is higher then the max touch time
          // or when the moving distance is too much
          if(ev.deltaTime > inst.options.tap_max_touchtime ||
              ev.distance > inst.options.tap_max_distance) {
                return;
              }
  
          // check if double tap
          if(prev && prev.name == 'tap' &&
              (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
              ev.distance < inst.options.doubletap_distance) {
                inst.trigger('doubletap', ev);
                did_doubletap = true;
              }
  
          // do a single tap
          if(!did_doubletap || inst.options.tap_always) {
            ionic.Gestures.detection.current.name = 'tap';
            inst.trigger('tap', ev);
          }
        }
      }
    };
  
  
    /**
     * Swipe
     * triggers swipe events when the end velocity is above the threshold
     * events  swipe, swipeleft, swiperight, swipeup, swipedown
     */
    ionic.Gestures.gestures.Swipe = {
      name: 'swipe',
      index: 40,
      defaults: {
        // set 0 for unlimited, but this can conflict with transform
        swipe_max_touches  : 1,
        swipe_velocity     : 0.4
      },
      handler: function swipeGesture(ev, inst) {
        if(ev.eventType == ionic.Gestures.EVENT_END) {
          // max touches
          if(inst.options.swipe_max_touches > 0 &&
              ev.touches.length > inst.options.swipe_max_touches) {
                return;
              }
  
          // when the distance we moved is too small we skip this gesture
          // or we can be already in dragging
          if(ev.velocityX > inst.options.swipe_velocity ||
              ev.velocityY > inst.options.swipe_velocity) {
                // trigger swipe events
                inst.trigger(this.name, ev);
                inst.trigger(this.name + ev.direction, ev);
              }
        }
      }
    };
  
  
    /**
     * Drag
     * Move with x fingers (default 1) around on the page. Blocking the scrolling when
     * moving left and right is a good practice. When all the drag events are blocking
     * you disable scrolling on that area.
     * events  drag, drapleft, dragright, dragup, dragdown
     */
    ionic.Gestures.gestures.Drag = {
      name: 'drag',
      index: 50,
      defaults: {
        drag_min_distance : 10,
        // Set correct_for_drag_min_distance to true to make the starting point of the drag
        // be calculated from where the drag was triggered, not from where the touch started.
        // Useful to avoid a jerk-starting drag, which can make fine-adjustments
        // through dragging difficult, and be visually unappealing.
        correct_for_drag_min_distance : true,
        // set 0 for unlimited, but this can conflict with transform
        drag_max_touches  : 1,
        // prevent default browser behavior when dragging occurs
        // be careful with it, it makes the element a blocking element
        // when you are using the drag gesture, it is a good practice to set this true
        drag_block_horizontal   : true,
        drag_block_vertical     : true,
        // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
        // It disallows vertical directions if the initial direction was horizontal, and vice versa.
        drag_lock_to_axis       : false,
        // drag lock only kicks in when distance > drag_lock_min_distance
        // This way, locking occurs only when the distance has become large enough to reliably determine the direction
        drag_lock_min_distance : 25
      },
      triggered: false,
      handler: function dragGesture(ev, inst) {
  
        if (ev.srcEvent.type == 'touchstart' || ev.srcEvent.type == 'touchend') {
          this.preventedFirstMove = false;
  
        } else if (!this.preventedFirstMove && ev.srcEvent.type == 'touchmove') {
          ev.srcEvent.preventDefault();
          this.preventedFirstMove = true;
        }
  
        // current gesture isnt drag, but dragged is true
        // this means an other gesture is busy. now call dragend
        if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
          inst.trigger(this.name +'end', ev);
          this.triggered = false;
          return;
        }
  
        // max touches
        if(inst.options.drag_max_touches > 0 &&
            ev.touches.length > inst.options.drag_max_touches) {
              return;
            }
  
        switch(ev.eventType) {
          case ionic.Gestures.EVENT_START:
            this.triggered = false;
            break;
  
          case ionic.Gestures.EVENT_MOVE:
            // when the distance we moved is too small we skip this gesture
            // or we can be already in dragging
            if(ev.distance < inst.options.drag_min_distance &&
                ionic.Gestures.detection.current.name != this.name) {
                  return;
                }
  
            // we are dragging!
            if(ionic.Gestures.detection.current.name != this.name) {
              ionic.Gestures.detection.current.name = this.name;
              if (inst.options.correct_for_drag_min_distance) {
                // When a drag is triggered, set the event center to drag_min_distance pixels from the original event center.
                // Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0.
                // It might be useful to save the original start point somewhere
                var factor = Math.abs(inst.options.drag_min_distance/ev.distance);
                ionic.Gestures.detection.current.startEvent.center.pageX += ev.deltaX * factor;
                ionic.Gestures.detection.current.startEvent.center.pageY += ev.deltaY * factor;
  
                // recalculate event data using new start point
                ev = ionic.Gestures.detection.extendEventData(ev);
              }
            }
  
            // lock drag to axis?
            if(ionic.Gestures.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
              ev.drag_locked_to_axis = true;
            }
            var last_direction = ionic.Gestures.detection.current.lastEvent.direction;
            if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
              // keep direction on the axis that the drag gesture started on
              if(ionic.Gestures.utils.isVertical(last_direction)) {
                ev.direction = (ev.deltaY < 0) ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
              }
              else {
                ev.direction = (ev.deltaX < 0) ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
              }
            }
  
            // first time, trigger dragstart event
            if(!this.triggered) {
              inst.trigger(this.name +'start', ev);
              this.triggered = true;
            }
  
            // trigger normal event
            inst.trigger(this.name, ev);
  
            // direction event, like dragdown
            inst.trigger(this.name + ev.direction, ev);
  
            // block the browser events
            if( (inst.options.drag_block_vertical && ionic.Gestures.utils.isVertical(ev.direction)) ||
                (inst.options.drag_block_horizontal && !ionic.Gestures.utils.isVertical(ev.direction))) {
                  ev.preventDefault();
                }
            break;
  
          case ionic.Gestures.EVENT_END:
            // trigger dragend
            if(this.triggered) {
              inst.trigger(this.name +'end', ev);
            }
  
            this.triggered = false;
            break;
        }
      }
    };
  
  
    /**
     * Transform
     * User want to scale or rotate with 2 fingers
     * events  transform, pinch, pinchin, pinchout, rotate
     */
    ionic.Gestures.gestures.Transform = {
      name: 'transform',
      index: 45,
      defaults: {
        // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
        transform_min_scale     : 0.01,
        // rotation in degrees
        transform_min_rotation  : 1,
        // prevent default browser behavior when two touches are on the screen
        // but it makes the element a blocking element
        // when you are using the transform gesture, it is a good practice to set this true
        transform_always_block  : false
      },
      triggered: false,
      handler: function transformGesture(ev, inst) {
        // current gesture isnt drag, but dragged is true
        // this means an other gesture is busy. now call dragend
        if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
          inst.trigger(this.name +'end', ev);
          this.triggered = false;
          return;
        }
  
        // atleast multitouch
        if(ev.touches.length < 2) {
          return;
        }
  
        // prevent default when two fingers are on the screen
        if(inst.options.transform_always_block) {
          ev.preventDefault();
        }
  
        switch(ev.eventType) {
          case ionic.Gestures.EVENT_START:
            this.triggered = false;
            break;
  
          case ionic.Gestures.EVENT_MOVE:
            var scale_threshold = Math.abs(1-ev.scale);
            var rotation_threshold = Math.abs(ev.rotation);
  
            // when the distance we moved is too small we skip this gesture
            // or we can be already in dragging
            if(scale_threshold < inst.options.transform_min_scale &&
                rotation_threshold < inst.options.transform_min_rotation) {
                  return;
                }
  
            // we are transforming!
            ionic.Gestures.detection.current.name = this.name;
  
            // first time, trigger dragstart event
            if(!this.triggered) {
              inst.trigger(this.name +'start', ev);
              this.triggered = true;
            }
  
            inst.trigger(this.name, ev); // basic transform event
  
            // trigger rotate event
            if(rotation_threshold > inst.options.transform_min_rotation) {
              inst.trigger('rotate', ev);
            }
  
            // trigger pinch event
            if(scale_threshold > inst.options.transform_min_scale) {
              inst.trigger('pinch', ev);
              inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
            }
            break;
  
          case ionic.Gestures.EVENT_END:
            // trigger dragend
            if(this.triggered) {
              inst.trigger(this.name +'end', ev);
            }
  
            this.triggered = false;
            break;
        }
      }
    };
  
  
    /**
     * Touch
     * Called as first, tells the user has touched the screen
     * events  touch
     */
    ionic.Gestures.gestures.Touch = {
      name: 'touch',
      index: -Infinity,
      defaults: {
        // call preventDefault at touchstart, and makes the element blocking by
        // disabling the scrolling of the page, but it improves gestures like
        // transforming and dragging.
        // be careful with using this, it can be very annoying for users to be stuck
        // on the page
        prevent_default: false,
  
        // disable mouse events, so only touch (or pen!) input triggers events
        prevent_mouseevents: false
      },
      handler: function touchGesture(ev, inst) {
        if(inst.options.prevent_mouseevents && ev.pointerType == ionic.Gestures.POINTER_MOUSE) {
          ev.stopDetect();
          return;
        }
  
        if(inst.options.prevent_default) {
          ev.preventDefault();
        }
  
        if(ev.eventType ==  ionic.Gestures.EVENT_START) {
          inst.trigger(this.name, ev);
        }
      }
    };
  
  
    /**
     * Release
     * Called as last, tells the user has released the screen
     * events  release
     */
    ionic.Gestures.gestures.Release = {
      name: 'release',
      index: Infinity,
      handler: function releaseGesture(ev, inst) {
        if(ev.eventType ==  ionic.Gestures.EVENT_END) {
          inst.trigger(this.name, ev);
        }
      }
    };
  })(window.ionic);
  
  (function(window, document, ionic) {
  
    function getParameterByName(name) {
      name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
      var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
      results = regex.exec(location.search);
      return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
    }
  
    var IOS = 'ios';
    var ANDROID = 'android';
    var WINDOWS_PHONE = 'windowsphone';
    var requestAnimationFrame = ionic.requestAnimationFrame;
  
    /**
     * @ngdoc utility
     * @name ionic.Platform
     * @module ionic
     * @description
     * A set of utility methods that can be used to retrieve the device ready state and
     * various other information such as what kind of platform the app is currently installed on.
     *
     * @usage
     * ```js
     * angular.module('PlatformApp', ['ionic'])
     * .controller('PlatformCtrl', function($scope) {
     *
     *   ionic.Platform.ready(function(){
     *     // will execute when device is ready, or immediately if the device is already ready.
     *   });
     *
     *   var deviceInformation = ionic.Platform.device();
     *
     *   var isWebView = ionic.Platform.isWebView();
     *   var isIPad = ionic.Platform.isIPad();
     *   var isIOS = ionic.Platform.isIOS();
     *   var isAndroid = ionic.Platform.isAndroid();
     *   var isWindowsPhone = ionic.Platform.isWindowsPhone();
     *
     *   var currentPlatform = ionic.Platform.platform();
     *   var currentPlatformVersion = ionic.Platform.version();
     *
     *   ionic.Platform.exit(); // stops the app
     * });
     * ```
     */
    var self = ionic.Platform = {
  
      // Put navigator on platform so it can be mocked and set
      // the browser does not allow window.navigator to be set
      navigator: window.navigator,
  
      /**
       * @ngdoc property
       * @name ionic.Platform#isReady
       * @returns {boolean} Whether the device is ready.
       */
      isReady: false,
      /**
       * @ngdoc property
       * @name ionic.Platform#isFullScreen
       * @returns {boolean} Whether the device is fullscreen.
       */
      isFullScreen: false,
      /**
       * @ngdoc property
       * @name ionic.Platform#platforms
       * @returns {Array(string)} An array of all platforms found.
       */
      platforms: null,
      /**
       * @ngdoc property
       * @name ionic.Platform#grade
       * @returns {string} What grade the current platform is.
       */
      grade: null,
      ua: navigator.userAgent,
  
      /**
       * @ngdoc method
       * @name ionic.Platform#ready
       * @description
       * Trigger a callback once the device is ready, or immediately
       * if the device is already ready. This method can be run from
       * anywhere and does not need to be wrapped by any additonal methods.
       * When the app is within a WebView (Cordova), it'll fire
       * the callback once the device is ready. If the app is within
       * a web browser, it'll fire the callback after `window.load`.
       * Please remember that Cordova features (Camera, FileSystem, etc) still
       * will not work in a web browser.
       * @param {function} callback The function to call.
       */
      ready: function(cb) {
        // run through tasks to complete now that the device is ready
        if (self.isReady) {
          cb();
        } else {
          // the platform isn't ready yet, add it to this array
          // which will be called once the platform is ready
          readyCallbacks.push(cb);
        }
      },
  
      /**
       * @private
       */
      detect: function() {
        self._checkPlatforms();
  
        requestAnimationFrame(function() {
          // only add to the body class if we got platform info
          for (var i = 0; i < self.platforms.length; i++) {
            document.body.classList.add('platform-' + self.platforms[i]);
          }
        });
      },
  
      /**
       * @ngdoc method
       * @name ionic.Platform#setGrade
       * @description Set the grade of the device: 'a', 'b', or 'c'. 'a' is the best
       * (most css features enabled), 'c' is the worst.  By default, sets the grade
       * depending on the current device.
       * @param {string} grade The new grade to set.
       */
      setGrade: function(grade) {
        var oldGrade = self.grade;
        self.grade = grade;
        requestAnimationFrame(function() {
          if (oldGrade) {
            document.body.classList.remove('grade-' + oldGrade);
          }
          document.body.classList.add('grade-' + grade);
        });
      },
  
      /**
       * @ngdoc method
       * @name ionic.Platform#device
       * @description Return the current device (given by cordova).
       * @returns {object} The device object.
       */
      device: function() {
        return window.device || {};
      },
  
      _checkPlatforms: function() {
        self.platforms = [];
        var grade = 'a';
  
        if (self.isWebView()) {
          self.platforms.push('webview');
          self.platforms.push('cordova');
        } else {
          self.platforms.push('browser');
        }
        if (self.isIPad()) self.platforms.push('ipad');
  
        var platform = self.platform();
        if (platform) {
          self.platforms.push(platform);
  
          var version = self.version();
          if (version) {
            var v = version.toString();
            if (v.indexOf('.') > 0) {
              v = v.replace('.', '_');
            } else {
              v += '_0';
            }
            self.platforms.push(platform + v.split('_')[0]);
            self.platforms.push(platform + v);
  
            if (self.isAndroid() && version < 4.4) {
              grade = (version < 4 ? 'c' : 'b');
            } else if (self.isWindowsPhone()) {
              grade = 'b';
            }
          }
        }
  
        self.setGrade(grade);
      },
  
      /**
       * @ngdoc method
       * @name ionic.Platform#isWebView
       * @returns {boolean} Check if we are running within a WebView (such as Cordova).
       */
      isWebView: function() {
        return !(!window.cordova && !window.PhoneGap && !window.phonegap);
      },
      /**
       * @ngdoc method
       * @name ionic.Platform#isIPad
       * @returns {boolean} Whether we are running on iPad.
       */
      isIPad: function() {
        if (/iPad/i.test(self.navigator.platform)) {
          return true;
        }
        return /iPad/i.test(self.ua);
      },
      /**
       * @ngdoc method
       * @name ionic.Platform#isIOS
       * @returns {boolean} Whether we are running on iOS.
       */
      isIOS: function() {
        return self.is(IOS);
      },
      /**
       * @ngdoc method
       * @name ionic.Platform#isAndroid
       * @returns {boolean} Whether we are running on Android.
       */
      isAndroid: function() {
        return self.is(ANDROID);
      },
      /**
       * @ngdoc method
       * @name ionic.Platform#isWindowsPhone
       * @returns {boolean} Whether we are running on Windows Phone.
       */
      isWindowsPhone: function() {
        return self.is(WINDOWS_PHONE);
      },
  
      /**
       * @ngdoc method
       * @name ionic.Platform#platform
       * @returns {string} The name of the current platform.
       */
      platform: function() {
        // singleton to get the platform name
        if (platformName === null) self.setPlatform(self.device().platform);
        return platformName;
      },
  
      /**
       * @private
       */
      setPlatform: function(n) {
        if (typeof n != 'undefined' && n !== null && n.length) {
          platformName = n.toLowerCase();
        } else if (getParameterByName('ionicplatform')) {
          platformName = getParameterByName('ionicplatform');
        } else if (self.ua.indexOf('Android') > 0) {
          platformName = ANDROID;
        } else if (/iPhone|iPad|iPod/.test(self.ua)) {
          platformName = IOS;
        } else if (self.ua.indexOf('Windows Phone') > -1) {
          platformName = WINDOWS_PHONE;
        } else {
          platformName = self.navigator.platform && navigator.platform.toLowerCase().split(' ')[0] || '';
        }
      },
  
      /**
       * @ngdoc method
       * @name ionic.Platform#version
       * @returns {number} The version of the current device platform.
       */
      version: function() {
        // singleton to get the platform version
        if (platformVersion === null) self.setVersion(self.device().version);
        return platformVersion;
      },
  
      /**
       * @private
       */
      setVersion: function(v) {
        if (typeof v != 'undefined' && v !== null) {
          v = v.split('.');
          v = parseFloat(v[0] + '.' + (v.length > 1 ? v[1] : 0));
          if (!isNaN(v)) {
            platformVersion = v;
            return;
          }
        }
  
        platformVersion = 0;
  
        // fallback to user-agent checking
        var pName = self.platform();
        var versionMatch = {
          'android': /Android (\d+).(\d+)?/,
          'ios': /OS (\d+)_(\d+)?/,
          'windowsphone': /Windows Phone (\d+).(\d+)?/
        };
        if (versionMatch[pName]) {
          v = self.ua.match(versionMatch[pName]);
          if (v &&  v.length > 2) {
            platformVersion = parseFloat(v[1] + '.' + v[2]);
          }
        }
      },
  
      // Check if the platform is the one detected by cordova
      is: function(type) {
        type = type.toLowerCase();
        // check if it has an array of platforms
        if (self.platforms) {
          for (var x = 0; x < self.platforms.length; x++) {
            if (self.platforms[x] === type) return true;
          }
        }
        // exact match
        var pName = self.platform();
        if (pName) {
          return pName === type.toLowerCase();
        }
  
        // A quick hack for to check userAgent
        return self.ua.toLowerCase().indexOf(type) >= 0;
      },
  
      /**
       * @ngdoc method
       * @name ionic.Platform#exitApp
       * @description Exit the app.
       */
      exitApp: function() {
        self.ready(function() {
          navigator.app && navigator.app.exitApp && navigator.app.exitApp();
        });
      },
  
      /**
       * @ngdoc method
       * @name ionic.Platform#showStatusBar
       * @description Shows or hides the device status bar (in Cordova).
       * @param {boolean} shouldShow Whether or not to show the status bar.
       */
      showStatusBar: function(val) {
        // Only useful when run within cordova
        self._showStatusBar = val;
        self.ready(function() {
          // run this only when or if the platform (cordova) is ready
          requestAnimationFrame(function() {
            if (self._showStatusBar) {
              // they do not want it to be full screen
              window.StatusBar && window.StatusBar.show();
              document.body.classList.remove('status-bar-hide');
            } else {
              // it should be full screen
              window.StatusBar && window.StatusBar.hide();
              document.body.classList.add('status-bar-hide');
            }
          });
        });
      },
  
      /**
       * @ngdoc method
       * @name ionic.Platform#fullScreen
       * @description
       * Sets whether the app is fullscreen or not (in Cordova).
       * @param {boolean=} showFullScreen Whether or not to set the app to fullscreen. Defaults to true.
       * @param {boolean=} showStatusBar Whether or not to show the device's status bar. Defaults to false.
       */
      fullScreen: function(showFullScreen, showStatusBar) {
        // showFullScreen: default is true if no param provided
        self.isFullScreen = (showFullScreen !== false);
  
        // add/remove the fullscreen classname to the body
        ionic.DomUtil.ready(function() {
          // run this only when or if the DOM is ready
          requestAnimationFrame(function() {
            // fixing pane height before we adjust this
            var panes = document.getElementsByClassName('pane');
            for (var i = 0; i < panes.length; i++) {
              panes[i].style.height = panes[i].offsetHeight + "px";
            }
            if (self.isFullScreen) {
              document.body.classList.add('fullscreen');
            } else {
              document.body.classList.remove('fullscreen');
            }
          });
          // showStatusBar: default is false if no param provided
          self.showStatusBar((showStatusBar === true));
        });
      }
  
    };
  
    var platformName = null, // just the name, like iOS or Android
    platformVersion = null, // a float of the major and minor, like 7.1
    readyCallbacks = [],
    windowLoadListenderAttached;
  
    // setup listeners to know when the device is ready to go
    function onWindowLoad() {
      if (self.isWebView()) {
        // the window and scripts are fully loaded, and a cordova/phonegap
        // object exists then let's listen for the deviceready
        document.addEventListener("deviceready", onPlatformReady, false);
      } else {
        // the window and scripts are fully loaded, but the window object doesn't have the
        // cordova/phonegap object, so its just a browser, not a webview wrapped w/ cordova
        onPlatformReady();
      }
      if (windowLoadListenderAttached) {
        window.removeEventListener("load", onWindowLoad, false);
      }
    }
    if (document.readyState === 'complete') {
      onWindowLoad();
    } else {
      windowLoadListenderAttached = true;
      window.addEventListener("load", onWindowLoad, false);
    }
  
    function onPlatformReady() {
      // the device is all set to go, init our own stuff then fire off our event
      self.isReady = true;
      self.detect();
      for (var x = 0; x < readyCallbacks.length; x++) {
        // fire off all the callbacks that were added before the platform was ready
        readyCallbacks[x]();
      }
      readyCallbacks = [];
      ionic.trigger('platformready', { target: document });
  
      requestAnimationFrame(function() {
        document.body.classList.add('platform-ready');
      });
    }
  
  })(this, document, ionic);
  
  (function(document, ionic) {
    'use strict';
  
    // Ionic CSS polyfills
    ionic.CSS = {};
  
    (function() {
  
      // transform
      var i, keys = ['webkitTransform', 'transform', '-webkit-transform', 'webkit-transform',
                     '-moz-transform', 'moz-transform', 'MozTransform', 'mozTransform', 'msTransform'];
  
      for (i = 0; i < keys.length; i++) {
        if (document.documentElement.style[keys[i]] !== undefined) {
          ionic.CSS.TRANSFORM = keys[i];
          break;
        }
      }
  
      // transition
      keys = ['webkitTransition', 'mozTransition', 'msTransition', 'transition'];
      for (i = 0; i < keys.length; i++) {
        if (document.documentElement.style[keys[i]] !== undefined) {
          ionic.CSS.TRANSITION = keys[i];
          break;
        }
      }
  
      // The only prefix we care about is webkit for transitions.
      var isWebkit = ionic.CSS.TRANSITION.indexOf('webkit') > -1;
  
      // transition duration
      ionic.CSS.TRANSITION_DURATION = (isWebkit ? '-webkit-' : '') + 'transition-duration';
  
      // To be sure transitionend works everywhere, include *both* the webkit and non-webkit events
      ionic.CSS.TRANSITIONEND = (isWebkit ?  'webkitTransitionEnd ' : '') + 'transitionend';
    })();
  
    // classList polyfill for them older Androids
    // https://gist.github.com/devongovett/1381839
    if (!("classList" in document.documentElement) && Object.defineProperty && typeof HTMLElement !== 'undefined') {
      Object.defineProperty(HTMLElement.prototype, 'classList', {
        get: function() {
          var self = this;
          function update(fn) {
            return function() {
              var x, classes = self.className.split(/\s+/);
  
              for (x = 0; x < arguments.length; x++) {
                fn(classes, classes.indexOf(arguments[x]), arguments[x]);
              }
  
              self.className = classes.join(" ");
            };
          }
  
          return {
            add: update(function(classes, index, value) {
              ~index || classes.push(value);
            }),
  
            remove: update(function(classes, index) {
              ~index && classes.splice(index, 1);
            }),
  
            toggle: update(function(classes, index, value) {
              ~index ? classes.splice(index, 1) : classes.push(value);
            }),
  
            contains: function(value) {
              return !!~self.className.split(/\s+/).indexOf(value);
            },
  
            item: function(i) {
              return self.className.split(/\s+/)[i] || null;
            }
          };
  
        }
      });
    }
  
  })(document, ionic);
  
  
  /**
   * @ngdoc page
   * @name tap
   * @module ionic
   * @description
   * On touch devices such as a phone or tablet, some browsers implement a 300ms delay between
   * the time the user stops touching the display and the moment the browser executes the
   * click. This delay was initially introduced so the browser can know whether the user wants to
   * double-tap to zoom in on the webpage.  Basically, the browser waits roughly 300ms to see if
   * the user is double-tapping, or just tapping on the display once.
   *
   * Out of the box, Ionic automatically removes the 300ms delay in order to make Ionic apps
   * feel more "native" like. Resultingly, other solutions such as
   * [fastclick](https://github.com/ftlabs/fastclick) and Angular's
   * [ngTouch](https://docs.angularjs.org/api/ngTouch) should not be included, to avoid conflicts.
   *
   * Some browsers already remove the delay with certain settings, such as the CSS property
   * `touch-events: none` or with specific meta tag viewport values. However, each of these
   * browsers still handle clicks differently, such as when to fire off or cancel the event
   * (like scrolling when the target is a button, or holding a button down).
   * For browsers that already remove the 300ms delay, consider Ionic's tap system as a way to
   * normalize how clicks are handled across the various devices so there's an expected response
   * no matter what the device, platform or version. Additionally, Ionic will prevent
   * ghostclicks which even browsers that remove the delay still experience.
   *
   * In some cases, third-party libraries may also be working with touch events which can interfere
   * with the tap system. For example, mapping libraries like Google or Leaflet Maps often implement
   * a touch detection system which conflicts with Ionic's tap system.
   *
   * ### Disabling the tap system
   *
   * To disable the tap for an element and all of its children elements,
   * add the attribute `data-tap-disabled="true"`.
   *
   * ```html
   * <div data-tap-disabled="true">
   *     <div id="google-map"></div>
   * </div>
   * ```
   *
   * ### Additional Notes:
   *
   * - Ionic tap  works with Ionic's JavaScript scrolling
   * - Elements can come and go from the DOM and Ionic tap doesn't keep adding and removing
   *   listeners
   * - No "tap delay" after the first "tap" (you can tap as fast as you want, they all click)
   * - Minimal events listeners, only being added to document
   * - Correct focus in/out on each input type (select, textearea, range) on each platform/device
   * - Shows and hides virtual keyboard correctly for each platform/device
   * - Works with labels surrounding inputs
   * - Does not fire off a click if the user moves the pointer too far
   * - Adds and removes an 'activated' css class
   * - Multiple [unit tests](https://github.com/driftyco/ionic/blob/master/test/unit/utils/tap.unit.js) for each scenario
   *
   */
  /*
  
   IONIC TAP
   ---------------
   - Both touch and mouse events are added to the document.body on DOM ready
   - If a touch event happens, it does not use mouse event listeners
   - On touchend, if the distance between start and end was small, trigger a click
   - In the triggered click event, add a 'isIonicTap' property
   - The triggered click receives the same x,y coordinates as as the end event
   - On document.body click listener (with useCapture=true), only allow clicks with 'isIonicTap'
   - Triggering clicks with mouse events work the same as touch, except with mousedown/mouseup
   - Tapping inputs is disabled during scrolling
  */
  
  var tapDoc; // the element which the listeners are on (document.body)
  var tapActiveEle; // the element which is active (probably has focus)
  var tapEnabledTouchEvents;
  var tapMouseResetTimer;
  var tapPointerMoved;
  var tapPointerStart;
  var tapTouchFocusedInput;
  var tapLastTouchTarget;
  var tapTouchMoveListener = 'touchmove';
  
  // how much the coordinates can be off between start/end, but still a click
  var TAP_RELEASE_TOLERANCE = 12; // default tolerance
  var TAP_RELEASE_BUTTON_TOLERANCE = 50; // button elements should have a larger tolerance
  
  var tapEventListeners = {
    'click': tapClickGateKeeper,
  
    'mousedown': tapMouseDown,
    'mouseup': tapMouseUp,
    'mousemove': tapMouseMove,
  
    'touchstart': tapTouchStart,
    'touchend': tapTouchEnd,
    'touchcancel': tapTouchCancel,
    'touchmove': tapTouchMove,
  
    'pointerdown': tapTouchStart,
    'pointerup': tapTouchEnd,
    'pointercancel': tapTouchCancel,
    'pointermove': tapTouchMove,
  
    'MSPointerDown': tapTouchStart,
    'MSPointerUp': tapTouchEnd,
    'MSPointerCancel': tapTouchCancel,
    'MSPointerMove': tapTouchMove,
  
    'focusin': tapFocusIn,
    'focusout': tapFocusOut
  };
  
  ionic.tap = {
  
    register: function(ele) {
      tapDoc = ele;
  
      tapEventListener('click', true, true);
      tapEventListener('mouseup');
      tapEventListener('mousedown');
  
      if (window.navigator.pointerEnabled) {
        tapEventListener('pointerdown');
        tapEventListener('pointerup');
        tapEventListener('pointcancel');
        tapTouchMoveListener = 'pointermove';
  
      } else if (window.navigator.msPointerEnabled) {
        tapEventListener('MSPointerDown');
        tapEventListener('MSPointerUp');
        tapEventListener('MSPointerCancel');
        tapTouchMoveListener = 'MSPointerMove';
  
      } else {
        tapEventListener('touchstart');
        tapEventListener('touchend');
        tapEventListener('touchcancel');
      }
  
      tapEventListener('focusin');
      tapEventListener('focusout');
  
      return function() {
        for (var type in tapEventListeners) {
          tapEventListener(type, false);
        }
        tapDoc = null;
        tapActiveEle = null;
        tapEnabledTouchEvents = false;
        tapPointerMoved = false;
        tapPointerStart = null;
      };
    },
  
    ignoreScrollStart: function(e) {
      return (e.defaultPrevented) ||  // defaultPrevented has been assigned by another component handling the event
             (/^(file|range)$/i).test(e.target.type) ||
             (e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-scroll')) == 'true' || // manually set within an elements attributes
             (!!(/^(object|embed)$/i).test(e.target.tagName)) ||  // flash/movie/object touches should not try to scroll
             ionic.tap.isElementTapDisabled(e.target); // check if this element, or an ancestor, has `data-tap-disabled` attribute
    },
  
    isTextInput: function(ele) {
      return !!ele &&
             (ele.tagName == 'TEXTAREA' ||
              ele.contentEditable === 'true' ||
              (ele.tagName == 'INPUT' && !(/^(radio|checkbox|range|file|submit|reset)$/i).test(ele.type)));
    },
  
    isDateInput: function(ele) {
      return !!ele &&
              (ele.tagName == 'INPUT' && (/^(date|time|datetime-local|month|week)$/i).test(ele.type));
    },
  
    isLabelWithTextInput: function(ele) {
      var container = tapContainingElement(ele, false);
  
      return !!container &&
             ionic.tap.isTextInput(tapTargetElement(container));
    },
  
    containsOrIsTextInput: function(ele) {
      return ionic.tap.isTextInput(ele) || ionic.tap.isLabelWithTextInput(ele);
    },
  
    cloneFocusedInput: function(container, scrollIntance) {
      if (ionic.tap.hasCheckedClone) return;
      ionic.tap.hasCheckedClone = true;
  
      ionic.requestAnimationFrame(function() {
        var focusInput = container.querySelector(':focus');
        if (ionic.tap.isTextInput(focusInput)) {
          var clonedInput = focusInput.cloneNode(true);
          
          clonedInput.value = focusInput.value;
          clonedInput.classList.add('cloned-text-input');
          clonedInput.readOnly = true;
          if (focusInput.isContentEditable) {
            clonedInput.contentEditable = focusInput.contentEditable;
            clonedInput.innerHTML = focusInput.innerHTML;
          }
          focusInput.parentElement.insertBefore(clonedInput, focusInput);
          focusInput.classList.add('previous-input-focus');
  
          clonedInput.scrollTop = focusInput.scrollTop;
        }
      });
    },
  
    hasCheckedClone: false,
  
    removeClonedInputs: function(container, scrollIntance) {
      ionic.tap.hasCheckedClone = false;
  
      ionic.requestAnimationFrame(function() {
        var clonedInputs = container.querySelectorAll('.cloned-text-input');
        var previousInputFocus = container.querySelectorAll('.previous-input-focus');
        var x;
  
        for (x = 0; x < clonedInputs.length; x++) {
          clonedInputs[x].parentElement.removeChild(clonedInputs[x]);
        }
  
        for (x = 0; x < previousInputFocus.length; x++) {
          previousInputFocus[x].classList.remove('previous-input-focus');
          previousInputFocus[x].style.top = '';
          previousInputFocus[x].focus();
        }
      });
    },
  
    requiresNativeClick: function(ele) {
      if (!ele || ele.disabled || (/^(file|range)$/i).test(ele.type) || (/^(object|video)$/i).test(ele.tagName) || ionic.tap.isLabelContainingFileInput(ele)) {
        return true;
      }
      return ionic.tap.isElementTapDisabled(ele);
    },
  
    isLabelContainingFileInput: function(ele) {
      var lbl = tapContainingElement(ele);
      if (lbl.tagName !== 'LABEL') return false;
      var fileInput = lbl.querySelector('input[type=file]');
      if (fileInput && fileInput.disabled === false) return true;
      return false;
    },
  
    isElementTapDisabled: function(ele) {
      if (ele && ele.nodeType === 1) {
        var element = ele;
        while (element) {
          if ((element.dataset ? element.dataset.tapDisabled : element.getAttribute('data-tap-disabled')) == 'true') {
            return true;
          }
          element = element.parentElement;
        }
      }
      return false;
    },
  
    setTolerance: function(releaseTolerance, releaseButtonTolerance) {
      TAP_RELEASE_TOLERANCE = releaseTolerance;
      TAP_RELEASE_BUTTON_TOLERANCE = releaseButtonTolerance;
    },
  
    cancelClick: function() {
      // used to cancel any simulated clicks which may happen on a touchend/mouseup
      // gestures uses this method within its tap and hold events
      tapPointerMoved = true;
    },
  
    pointerCoord: function(event) {
      // This method can get coordinates for both a mouse click
      // or a touch depending on the given event
      var c = { x:0, y:0 };
      if (event) {
        var touches = event.touches && event.touches.length ? event.touches : [event];
        var e = (event.changedTouches && event.changedTouches[0]) || touches[0];
        if (e) {
          c.x = e.clientX || e.pageX || 0;
          c.y = e.clientY || e.pageY || 0;
        }
      }
      return c;
    }
  
  };
  
  function tapEventListener(type, enable, useCapture) {
    if (enable !== false) {
      tapDoc.addEventListener(type, tapEventListeners[type], useCapture);
    } else {
      tapDoc.removeEventListener(type, tapEventListeners[type]);
    }
  }
  
  function tapClick(e) {
    // simulate a normal click by running the element's click method then focus on it
    var container = tapContainingElement(e.target);
    var ele = tapTargetElement(container);
  
    if (ionic.tap.requiresNativeClick(ele) || tapPointerMoved) return false;
  
    var c = ionic.tap.pointerCoord(e);
  
    //console.log('tapClick', e.type, ele.tagName, '('+c.x+','+c.y+')');
    triggerMouseEvent('click', ele, c.x, c.y);
  
    // if it's an input, focus in on the target, otherwise blur
    tapHandleFocus(ele);
  }
  
  function triggerMouseEvent(type, ele, x, y) {
    // using initMouseEvent instead of MouseEvent for our Android friends
    var clickEvent = document.createEvent("MouseEvents");
    clickEvent.initMouseEvent(type, true, true, window, 1, 0, 0, x, y, false, false, false, false, 0, null);
    clickEvent.isIonicTap = true;
    ele.dispatchEvent(clickEvent);
  }
  
  function tapClickGateKeeper(e) {
    if (e.target.type == 'submit' && e.detail === 0) {
      // do not prevent click if it came from an "Enter" or "Go" keypress submit
      return;
    }
  
    // do not allow through any click events that were not created by ionic.tap
    if ((ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) ||
        (!e.isIonicTap && !ionic.tap.requiresNativeClick(e.target))) {
      //console.log('clickPrevent', e.target.tagName);
      e.stopPropagation();
  
      if (!ionic.tap.isLabelWithTextInput(e.target)) {
        // labels clicks from native should not preventDefault othersize keyboard will not show on input focus
        e.preventDefault();
      }
      return false;
    }
  }
  
  // MOUSE
  function tapMouseDown(e) {
    if (e.isIonicTap || tapIgnoreEvent(e)) return;
  
    if (tapEnabledTouchEvents) {
      console.log('mousedown', 'stop event');
      e.stopPropagation();
  
      if ((!ionic.tap.isTextInput(e.target) || tapLastTouchTarget !== e.target) && !(/^(select|option)$/i).test(e.target.tagName)) {
        // If you preventDefault on a text input then you cannot move its text caret/cursor.
        // Allow through only the text input default. However, without preventDefault on an
        // input the 300ms delay can change focus on inputs after the keyboard shows up.
        // The focusin event handles the chance of focus changing after the keyboard shows.
        e.preventDefault();
      }
  
      return false;
    }
  
    tapPointerMoved = false;
    tapPointerStart = ionic.tap.pointerCoord(e);
  
    tapEventListener('mousemove');
    ionic.activator.start(e);
  }
  
  function tapMouseUp(e) {
    if (tapEnabledTouchEvents) {
      e.stopPropagation();
      e.preventDefault();
      return false;
    }
  
    if (tapIgnoreEvent(e) || (/^(select|option)$/i).test(e.target.tagName)) return false;
  
    if (!tapHasPointerMoved(e)) {
      tapClick(e);
    }
    tapEventListener('mousemove', false);
    ionic.activator.end();
    tapPointerMoved = false;
  }
  
  function tapMouseMove(e) {
    if (tapHasPointerMoved(e)) {
      tapEventListener('mousemove', false);
      ionic.activator.end();
      tapPointerMoved = true;
      return false;
    }
  }
  
  
  // TOUCH
  function tapTouchStart(e) {
    if (tapIgnoreEvent(e)) return;
  
    tapPointerMoved = false;
  
    tapEnableTouchEvents();
    tapPointerStart = ionic.tap.pointerCoord(e);
  
    tapEventListener(tapTouchMoveListener);
    ionic.activator.start(e);
  
    if (ionic.Platform.isIOS() && ionic.tap.isLabelWithTextInput(e.target)) {
      // if the tapped element is a label, which has a child input
      // then preventDefault so iOS doesn't ugly auto scroll to the input
      // but do not prevent default on Android or else you cannot move the text caret
      // and do not prevent default on Android or else no virtual keyboard shows up
  
      var textInput = tapTargetElement(tapContainingElement(e.target));
      if (textInput !== tapActiveEle) {
        // don't preventDefault on an already focused input or else iOS's text caret isn't usable
        e.preventDefault();
      }
    }
  }
  
  function tapTouchEnd(e) {
    if (tapIgnoreEvent(e)) return;
  
    tapEnableTouchEvents();
    if (!tapHasPointerMoved(e)) {
      tapClick(e);
  
      if ((/^(select|option)$/i).test(e.target.tagName)) {
        e.preventDefault();
      }
    }
  
    tapLastTouchTarget = e.target;
    tapTouchCancel();
  }
  
  function tapTouchMove(e) {
    if (tapHasPointerMoved(e)) {
      tapPointerMoved = true;
      tapEventListener(tapTouchMoveListener, false);
      ionic.activator.end();
      return false;
    }
  }
  
  function tapTouchCancel(e) {
    tapEventListener(tapTouchMoveListener, false);
    ionic.activator.end();
    tapPointerMoved = false;
  }
  
  function tapEnableTouchEvents() {
    tapEnabledTouchEvents = true;
    clearTimeout(tapMouseResetTimer);
    tapMouseResetTimer = setTimeout(function() {
      tapEnabledTouchEvents = false;
    }, 2000);
  }
  
  function tapIgnoreEvent(e) {
    if (e.isTapHandled) return true;
    e.isTapHandled = true;
  
    if (ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) {
      e.preventDefault();
      return true;
    }
  }
  
  function tapHandleFocus(ele) {
    tapTouchFocusedInput = null;
  
    var triggerFocusIn = false;
  
    if (ele.tagName == 'SELECT') {
      // trick to force Android options to show up
      triggerMouseEvent('mousedown', ele, 0, 0);
      ele.focus && ele.focus();
      triggerFocusIn = true;
  
    } else if (tapActiveElement() === ele) {
      // already is the active element and has focus
      triggerFocusIn = true;
  
    } else if ((/^(input|textarea)$/i).test(ele.tagName) || ele.isContentEditable) {
      triggerFocusIn = true;
      ele.focus && ele.focus();
      ele.value = ele.value;
      if (tapEnabledTouchEvents) {
        tapTouchFocusedInput = ele;
      }
  
    } else {
      tapFocusOutActive();
    }
  
    if (triggerFocusIn) {
      tapActiveElement(ele);
      ionic.trigger('ionic.focusin', {
        target: ele
      }, true);
    }
  }
  
  function tapFocusOutActive() {
    var ele = tapActiveElement();
    if (ele && ((/^(input|textarea|select)$/i).test(ele.tagName) || ele.isContentEditable)) {
      console.log('tapFocusOutActive', ele.tagName);
      ele.blur();
    }
    tapActiveElement(null);
  }
  
  function tapFocusIn(e) {
    // Because a text input doesn't preventDefault (so the caret still works) there's a chance
    // that it's mousedown event 300ms later will change the focus to another element after
    // the keyboard shows up.
  
    if (tapEnabledTouchEvents &&
        ionic.tap.isTextInput(tapActiveElement()) &&
        ionic.tap.isTextInput(tapTouchFocusedInput) &&
        tapTouchFocusedInput !== e.target) {
  
      // 1) The pointer is from touch events
      // 2) There is an active element which is a text input
      // 3) A text input was just set to be focused on by a touch event
      // 4) A new focus has been set, however the target isn't the one the touch event wanted
      console.log('focusin', 'tapTouchFocusedInput');
      tapTouchFocusedInput.focus();
      tapTouchFocusedInput = null;
    }
    ionic.scroll.isScrolling = false;
  }
  
  function tapFocusOut() {
    tapActiveElement(null);
  }
  
  function tapActiveElement(ele) {
    if (arguments.length) {
      tapActiveEle = ele;
    }
    return tapActiveEle || document.activeElement;
  }
  
  function tapHasPointerMoved(endEvent) {
    if (!endEvent || endEvent.target.nodeType !== 1 || !tapPointerStart || (tapPointerStart.x === 0 && tapPointerStart.y === 0)) {
      return false;
    }
    var endCoordinates = ionic.tap.pointerCoord(endEvent);
  
    var hasClassList = !!(endEvent.target.classList && endEvent.target.classList.contains &&
      typeof endEvent.target.classList.contains === 'function');
    var releaseTolerance = hasClassList && endEvent.target.classList.contains('button') ?
      TAP_RELEASE_BUTTON_TOLERANCE :
      TAP_RELEASE_TOLERANCE;
  
    return Math.abs(tapPointerStart.x - endCoordinates.x) > releaseTolerance ||
           Math.abs(tapPointerStart.y - endCoordinates.y) > releaseTolerance;
  }
  
  function tapContainingElement(ele, allowSelf) {
    var climbEle = ele;
    for (var x = 0; x < 6; x++) {
      if (!climbEle) break;
      if (climbEle.tagName === 'LABEL') return climbEle;
      climbEle = climbEle.parentElement;
    }
    if (allowSelf !== false) return ele;
  }
  
  function tapTargetElement(ele) {
    if (ele && ele.tagName === 'LABEL') {
      if (ele.control) return ele.control;
  
      // older devices do not support the "control" property
      if (ele.querySelector) {
        var control = ele.querySelector('input,textarea,select');
        if (control) return control;
      }
    }
    return ele;
  }
  
  ionic.DomUtil.ready(function() {
    var ng = typeof angular !== 'undefined' ? angular : null;
    //do nothing for e2e tests
    if (!ng || (ng && !ng.scenario)) {
      ionic.tap.register(document);
    }
  });
  
  (function(document, ionic) {
    'use strict';
  
    var queueElements = {};   // elements that should get an active state in XX milliseconds
    var activeElements = {};  // elements that are currently active
    var keyId = 0;            // a counter for unique keys for the above ojects
    var ACTIVATED_CLASS = 'activated';
  
    ionic.activator = {
  
      start: function(e) {
        var self = this;
  
        var hitX = ionic.tap.pointerCoord(e).x;
        if (hitX > 0 && hitX < 45) {
          return;
        }
  
        // when an element is touched/clicked, it climbs up a few
        // parents to see if it is an .item or .button element
        ionic.requestAnimationFrame(function() {
          if ((ionic.scroll && ionic.scroll.isScrolling) || ionic.tap.requiresNativeClick(e.target)) return;
          var ele = e.target;
          var eleToActivate;
  
          for (var x = 0; x < 6; x++) {
            if (!ele || ele.nodeType !== 1) break;
            if (eleToActivate && ele.classList.contains('item')) {
              eleToActivate = ele;
              break;
            }
            if (ele.tagName == 'A' || ele.tagName == 'BUTTON' || ele.hasAttribute('ng-click')) {
              eleToActivate = ele;
              break;
            }
            if (ele.classList.contains('button')) {
              eleToActivate = ele;
              break;
            }
            // no sense climbing past these
            if (ele.tagName == 'ION-CONTENT' || ele.classList.contains('pane') || ele.tagName == 'BODY') {
              break;
            }
            ele = ele.parentElement;
          }
  
          if (eleToActivate) {
            // queue that this element should be set to active
            queueElements[keyId] = eleToActivate;
  
            // on the next frame, set the queued elements to active
            ionic.requestAnimationFrame(activateElements);
  
            keyId = (keyId > 29 ? 0 : keyId + 1);
          }
  
        });
      },
  
      end: function() {
        // clear out any active/queued elements after XX milliseconds
        setTimeout(clear, 200);
      }
  
    };
  
    function clear() {
      // clear out any elements that are queued to be set to active
      queueElements = {};
  
      // in the next frame, remove the active class from all active elements
      ionic.requestAnimationFrame(deactivateElements);
    }
  
    function activateElements() {
      // activate all elements in the queue
      for (var key in queueElements) {
        if (queueElements[key]) {
          queueElements[key].classList.add(ACTIVATED_CLASS);
          activeElements[key] = queueElements[key];
        }
      }
      queueElements = {};
    }
  
    function deactivateElements() {
      if (ionic.transition && ionic.transition.isActive) {
        setTimeout(deactivateElements, 400);
        return;
      }
  
      for (var key in activeElements) {
        if (activeElements[key]) {
          activeElements[key].classList.remove(ACTIVATED_CLASS);
          delete activeElements[key];
        }
      }
    }
  
  })(document, ionic);
  
  (function(ionic) {
  
    /* for nextUid() function below */
    var uid = ['0','0','0'];
  
    /**
     * Various utilities used throughout Ionic
     *
     * Some of these are adopted from underscore.js and backbone.js, both also MIT licensed.
     */
    ionic.Utils = {
  
      arrayMove: function(arr, old_index, new_index) {
        if (new_index >= arr.length) {
          var k = new_index - arr.length;
          while ((k--) + 1) {
            arr.push(undefined);
          }
        }
        arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
        return arr;
      },
  
      /**
       * Return a function that will be called with the given context
       */
      proxy: function(func, context) {
        var args = Array.prototype.slice.call(arguments, 2);
        return function() {
          return func.apply(context, args.concat(Array.prototype.slice.call(arguments)));
        };
      },
  
      /**
       * Only call a function once in the given interval.
       *
       * @param func {Function} the function to call
       * @param wait {int} how long to wait before/after to allow function calls
       * @param immediate {boolean} whether to call immediately or after the wait interval
       */
       debounce: function(func, wait, immediate) {
        var timeout, args, context, timestamp, result;
        return function() {
          context = this;
          args = arguments;
          timestamp = new Date();
          var later = function() {
            var last = (new Date()) - timestamp;
            if (last < wait) {
              timeout = setTimeout(later, wait - last);
            } else {
              timeout = null;
              if (!immediate) result = func.apply(context, args);
            }
          };
          var callNow = immediate && !timeout;
          if (!timeout) {
            timeout = setTimeout(later, wait);
          }
          if (callNow) result = func.apply(context, args);
          return result;
        };
      },
  
      /**
       * Throttle the given fun, only allowing it to be
       * called at most every `wait` ms.
       */
      throttle: function(func, wait, options) {
        var context, args, result;
        var timeout = null;
        var previous = 0;
        options || (options = {});
        var later = function() {
          previous = options.leading === false ? 0 : Date.now();
          timeout = null;
          result = func.apply(context, args);
        };
        return function() {
          var now = Date.now();
          if (!previous && options.leading === false) previous = now;
          var remaining = wait - (now - previous);
          context = this;
          args = arguments;
          if (remaining <= 0) {
            clearTimeout(timeout);
            timeout = null;
            previous = now;
            result = func.apply(context, args);
          } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
          }
          return result;
        };
      },
       // Borrowed from Backbone.js's extend
       // Helper function to correctly set up the prototype chain, for subclasses.
       // Similar to `goog.inherits`, but uses a hash of prototype properties and
       // class properties to be extended.
      inherit: function(protoProps, staticProps) {
        var parent = this;
        var child;
  
        // The constructor function for the new subclass is either defined by you
        // (the "constructor" property in your `extend` definition), or defaulted
        // by us to simply call the parent's constructor.
        if (protoProps && protoProps.hasOwnProperty('constructor')) {
          child = protoProps.constructor;
        } else {
          child = function() { return parent.apply(this, arguments); };
        }
  
        // Add static properties to the constructor function, if supplied.
        ionic.extend(child, parent, staticProps);
  
        // Set the prototype chain to inherit from `parent`, without calling
        // `parent`'s constructor function.
        var Surrogate = function() { this.constructor = child; };
        Surrogate.prototype = parent.prototype;
        child.prototype = new Surrogate();
  
        // Add prototype properties (instance properties) to the subclass,
        // if supplied.
        if (protoProps) ionic.extend(child.prototype, protoProps);
  
        // Set a convenience property in case the parent's prototype is needed
        // later.
        child.__super__ = parent.prototype;
  
        return child;
      },
  
      // Extend adapted from Underscore.js
      extend: function(obj) {
         var args = Array.prototype.slice.call(arguments, 1);
         for (var i = 0; i < args.length; i++) {
           var source = args[i];
           if (source) {
             for (var prop in source) {
               obj[prop] = source[prop];
             }
           }
         }
         return obj;
      },
  
      /**
       * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric
       * characters such as '012ABC'. The reason why we are not using simply a number counter is that
       * the number string gets longer over time, and it can also overflow, where as the nextId
       * will grow much slower, it is a string, and it will never overflow.
       *
       * @returns an unique alpha-numeric string
       */
      nextUid: function() {
        var index = uid.length;
        var digit;
  
        while (index) {
          index--;
          digit = uid[index].charCodeAt(0);
          if (digit == 57 /*'9'*/) {
            uid[index] = 'A';
            return uid.join('');
          }
          if (digit == 90  /*'Z'*/) {
            uid[index] = '0';
          } else {
            uid[index] = String.fromCharCode(digit + 1);
            return uid.join('');
          }
        }
        uid.unshift('0');
        return uid.join('');
      },
  
      disconnectScope: function disconnectScope(scope) {
        if (!scope) return;
  
        if (scope.$root === scope) {
          return; // we can't disconnect the root node;
        }
        var parent = scope.$parent;
        scope.$$disconnected = true;
        scope.$broadcast('$ionic.disconnectScope');
  
        // See Scope.$destroy
        if (parent.$$childHead === scope) {
          parent.$$childHead = scope.$$nextSibling;
        }
        if (parent.$$childTail === scope) {
          parent.$$childTail = scope.$$prevSibling;
        }
        if (scope.$$prevSibling) {
          scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
        }
        if (scope.$$nextSibling) {
          scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
        }
        scope.$$nextSibling = scope.$$prevSibling = null;
      },
  
      reconnectScope: function reconnectScope(scope) {
        if (!scope) return;
  
        if (scope.$root === scope) {
          return; // we can't disconnect the root node;
        }
        if (!scope.$$disconnected) {
          return;
        }
        var parent = scope.$parent;
        scope.$$disconnected = false;
        scope.$broadcast('$ionic.reconnectScope');
        // See Scope.$new for this logic...
        scope.$$prevSibling = parent.$$childTail;
        if (parent.$$childHead) {
          parent.$$childTail.$$nextSibling = scope;
          parent.$$childTail = scope;
        } else {
          parent.$$childHead = parent.$$childTail = scope;
        }
      },
  
      isScopeDisconnected: function(scope) {
        var climbScope = scope;
        while (climbScope) {
          if (climbScope.$$disconnected) return true;
          climbScope = climbScope.$parent;
        }
        return false;
      }
    };
  
    // Bind a few of the most useful functions to the ionic scope
    ionic.inherit = ionic.Utils.inherit;
    ionic.extend = ionic.Utils.extend;
    ionic.throttle = ionic.Utils.throttle;
    ionic.proxy = ionic.Utils.proxy;
    ionic.debounce = ionic.Utils.debounce;
  
  })(window.ionic);
  
  /**
   * @ngdoc page
   * @name keyboard
   * @module ionic
   * @description
   * On both Android and iOS, Ionic will attempt to prevent the keyboard from
   * obscuring inputs and focusable elements when it appears by scrolling them
   * into view.  In order for this to work, any focusable elements must be within
   * a [Scroll View](http://ionicframework.com/docs/api/directive/ionScroll/)
   * or a directive such as [Content](http://ionicframework.com/docs/api/directive/ionContent/)
   * that has a Scroll View.
   *
   * It will also attempt to prevent the native overflow scrolling on focus,
   * which can cause layout issues such as pushing headers up and out of view.
   *
   * The keyboard fixes work best in conjunction with the
   * [Ionic Keyboard Plugin](https://github.com/driftyco/ionic-plugins-keyboard),
   * although it will perform reasonably well without.  However, if you are using
   * Cordova there is no reason not to use the plugin.
   *
   * ### Hide when keyboard shows
   *
   * To hide an element when the keyboard is open, add the class `hide-on-keyboard-open`.
   *
   * ```html
   * <div class="hide-on-keyboard-open">
   *   <div id="google-map"></div>
   * </div>
   * ```
   * ----------
   *
   * ### Plugin Usage
   * Information on using the plugin can be found at
   * [https://github.com/driftyco/ionic-plugins-keyboard](https://github.com/driftyco/ionic-plugins-keyboard).
   *
   * ----------
   *
   * ### Android Notes
   * - If your app is running in fullscreen, i.e. you have
   *   `<preference name="Fullscreen" value="true" />` in your `config.xml` file
   *   you will need to set `ionic.Platform.isFullScreen = true` manually.
   *
   * - You can configure the behavior of the web view when the keyboard shows by setting
   *   [android:windowSoftInputMode](http://developer.android.com/reference/android/R.attr.html#windowSoftInputMode)
   *   to either `adjustPan`, `adjustResize` or `adjustNothing` in your app's
   *   activity in `AndroidManifest.xml`. `adjustResize` is the recommended setting
   *   for Ionic, but if for some reason you do use `adjustPan` you will need to
   *   set `ionic.Platform.isFullScreen = true`.
   *
   *   ```xml
   *   <activity android:windowSoftInputMode="adjustResize">
   *
   *   ```
   *
   * ### iOS Notes
   * - If the content of your app (including the header) is being pushed up and
   *   out of view on input focus, try setting `cordova.plugins.Keyboard.disableScroll(true)`.
   *   This does **not** disable scrolling in the Ionic scroll view, rather it
   *   disables the native overflow scrolling that happens automatically as a
   *   result of focusing on inputs below the keyboard.
   *
   */
  
  var keyboardViewportHeight = getViewportHeight();
  var keyboardIsOpen;
  var keyboardActiveElement;
  var keyboardFocusOutTimer;
  var keyboardFocusInTimer;
  var keyboardPollHeightTimer;
  var keyboardLastShow = 0;
  
  var KEYBOARD_OPEN_CSS = 'keyboard-open';
  var SCROLL_CONTAINER_CSS = 'scroll';
  
  ionic.keyboard = {
    isOpen: false,
    height: null,
    landscape: false,
  
    hide: function() {
      clearTimeout(keyboardFocusInTimer);
      clearTimeout(keyboardFocusOutTimer);
      clearTimeout(keyboardPollHeightTimer);
  
      ionic.keyboard.isOpen = false;
  
      ionic.trigger('resetScrollView', {
        target: keyboardActiveElement
      }, true);
  
      ionic.requestAnimationFrame(function(){
        document.body.classList.remove(KEYBOARD_OPEN_CSS);
      });
  
      // the keyboard is gone now, remove the touchmove that disables native scroll
      if (window.navigator.msPointerEnabled) {
        document.removeEventListener("MSPointerMove", keyboardPreventDefault);
      } else {
        document.removeEventListener('touchmove', keyboardPreventDefault);
      }
      document.removeEventListener('keydown', keyboardOnKeyDown);
  
      if( keyboardHasPlugin() ) {
        cordova.plugins.Keyboard.close();
      }
    },
  
    show: function() {
      if( keyboardHasPlugin() ) {
        cordova.plugins.Keyboard.show();
      }
    }
  };
  
  function keyboardInit() {
    if( keyboardHasPlugin() ) {
      window.addEventListener('native.keyboardshow', keyboardNativeShow);
      window.addEventListener('native.keyboardhide', keyboardFocusOut);
  
      //deprecated
      window.addEventListener('native.showkeyboard', keyboardNativeShow);
      window.addEventListener('native.hidekeyboard', keyboardFocusOut);
  
    } else {
      document.body.addEventListener('focusout', keyboardFocusOut);
    }
  
    document.body.addEventListener('ionic.focusin', keyboardBrowserFocusIn);
    document.body.addEventListener('focusin', keyboardBrowserFocusIn);
  
    document.body.addEventListener('orientationchange', keyboardOrientationChange);
  
    if (window.navigator.msPointerEnabled) {
      document.removeEventListener("MSPointerDown", keyboardInit);
    } else {
      document.removeEventListener('touchstart', keyboardInit);
    }
  }
  
  function keyboardNativeShow(e) {
    clearTimeout(keyboardFocusOutTimer);
    ionic.keyboard.height = e.keyboardHeight;
  }
  
  function keyboardBrowserFocusIn(e) {
    if( !e.target || e.target.readOnly || !ionic.tap.isTextInput(e.target) || ionic.tap.isDateInput(e.target) || !keyboardIsWithinScroll(e.target) ) return;
  
    document.addEventListener('keydown', keyboardOnKeyDown, false);
  
    document.body.scrollTop = 0;
    document.body.querySelector('.scroll-content').scrollTop = 0;
  
    keyboardActiveElement = e.target;
  
    keyboardSetShow(e);
  }
  
  function keyboardSetShow(e) {
    clearTimeout(keyboardFocusInTimer);
    clearTimeout(keyboardFocusOutTimer);
  
    keyboardFocusInTimer = setTimeout(function(){
      if ( keyboardLastShow + 350 > Date.now() ) return;
      console.log('keyboardSetShow');
      keyboardLastShow = Date.now();
      var keyboardHeight;
      var elementBounds = keyboardActiveElement.getBoundingClientRect();
      var count = 0;
  
      keyboardPollHeightTimer = setInterval(function(){
  
        keyboardHeight = keyboardGetHeight();
        if (count > 10){
          clearInterval(keyboardPollHeightTimer);
          //waited long enough, just guess
          keyboardHeight = 275;
        }
        if (keyboardHeight){
          clearInterval(keyboardPollHeightTimer);
          keyboardShow(e.target, elementBounds.top, elementBounds.bottom, keyboardViewportHeight, keyboardHeight);
        }
        count++;
  
      }, 100);
    }, 32);
  }
  
  function keyboardShow(element, elementTop, elementBottom, viewportHeight, keyboardHeight) {
    var details = {
      target: element,
      elementTop: Math.round(elementTop),
      elementBottom: Math.round(elementBottom),
      keyboardHeight: keyboardHeight,
      viewportHeight: viewportHeight
    };
  
    details.hasPlugin = keyboardHasPlugin();
  
    details.contentHeight = viewportHeight - keyboardHeight;
  
    console.log('keyboardShow', keyboardHeight, details.contentHeight);
  
    // figure out if the element is under the keyboard
    details.isElementUnderKeyboard = (details.elementBottom > details.contentHeight);
  
    ionic.keyboard.isOpen = true;
  
    // send event so the scroll view adjusts
    keyboardActiveElement = element;
    ionic.trigger('scrollChildIntoView', details, true);
  
    ionic.requestAnimationFrame(function(){
      document.body.classList.add(KEYBOARD_OPEN_CSS);
    });
  
    // any showing part of the document that isn't within the scroll the user
    // could touchmove and cause some ugly changes to the app, so disable
    // any touchmove events while the keyboard is open using e.preventDefault()
    if (window.navigator.msPointerEnabled) {
      document.addEventListener("MSPointerMove", keyboardPreventDefault, false);
    } else {
      document.addEventListener('touchmove', keyboardPreventDefault, false);
    }
  
    return details;
  }
  
  function keyboardFocusOut(e) {
    clearTimeout(keyboardFocusOutTimer);
  
    keyboardFocusOutTimer = setTimeout(ionic.keyboard.hide, 350);
  }
  
  function keyboardUpdateViewportHeight() {
    if( getViewportHeight() > keyboardViewportHeight ) {
      keyboardViewportHeight = getViewportHeight();
    }
  }
  
  function keyboardOnKeyDown(e) {
    if( ionic.scroll.isScrolling ) {
      keyboardPreventDefault(e);
    }
  }
  
  function keyboardPreventDefault(e) {
    if( e.target.tagName !== 'TEXTAREA' ) {
      e.preventDefault();
    }
  }
  
  function keyboardOrientationChange() {
    var updatedViewportHeight = getViewportHeight();
  
    //too slow, have to wait for updated height
    if (updatedViewportHeight === keyboardViewportHeight){
      var count = 0;
      var pollViewportHeight = setInterval(function(){
        //give up
        if (count > 10){
          clearInterval(pollViewportHeight);
        }
  
        updatedViewportHeight = getViewportHeight();
  
        if (updatedViewportHeight !== keyboardViewportHeight){
          if (updatedViewportHeight < keyboardViewportHeight){
            ionic.keyboard.landscape = true;
          } else {
            ionic.keyboard.landscape = false;
          }
          keyboardViewportHeight = updatedViewportHeight;
          clearInterval(pollViewportHeight);
        }
        count++;
  
      }, 50);
    } else {
      keyboardViewportHeight = updatedViewportHeight;
    }
  }
  
  function keyboardGetHeight() {
    // check if we are already have a keyboard height from the plugin
    if ( ionic.keyboard.height ) {
      return ionic.keyboard.height;
    }
  
    if ( ionic.Platform.isAndroid() ){
      //should be using the plugin, no way to know how big the keyboard is, so guess
      if ( ionic.Platform.isFullScreen ){
        return 275;
      }
      //otherwise, wait for the screen to resize
      if ( getViewportHeight() < keyboardViewportHeight ){
        return keyboardViewportHeight - getViewportHeight();
      } else {
        return 0;
      }
    }
  
    // fallback for when its the webview without the plugin
    // or for just the standard web browser
    if( ionic.Platform.isIOS() ) {
      if ( ionic.keyboard.landscape ){
        return 206;
      }
  
      if (!ionic.Platform.isWebView()){
        return 216;
      }
  
      return 260;
    }
  
    // safe guess
    return 275;
  }
  
  function getViewportHeight() {
    return window.innerHeight || screen.height;
  }
  
  function keyboardIsWithinScroll(ele) {
    while(ele) {
      if(ele.classList.contains(SCROLL_CONTAINER_CSS)) {
        return true;
      }
      ele = ele.parentElement;
    }
    return false;
  }
  
  function keyboardHasPlugin() {
    return !!(window.cordova && cordova.plugins && cordova.plugins.Keyboard);
  }
  
  ionic.Platform.ready(function() {
    keyboardUpdateViewportHeight();
  
    // Android sometimes reports bad innerHeight on window.load
    // try it again in a lil bit to play it safe
    setTimeout(keyboardUpdateViewportHeight, 999);
  
    // only initialize the adjustments for the virtual keyboard
    // if a touchstart event happens
    if (window.navigator.msPointerEnabled) {
      document.addEventListener("MSPointerDown", keyboardInit, false);
    } else {
      document.addEventListener('touchstart', keyboardInit, false);
    }
  });
  
  
  
  var viewportTag;
  var viewportProperties = {};
  
  ionic.viewport = {
    orientation: function() {
      // 0 = Portrait
      // 90 = Landscape
      // not using window.orientation because each device has a different implementation
      return (window.innerWidth > window.innerHeight ? 90 : 0);
    }
  };
  
  function viewportLoadTag() {
    var x;
  
    for (x = 0; x < document.head.children.length; x++) {
      if (document.head.children[x].name == 'viewport') {
        viewportTag = document.head.children[x];
        break;
      }
    }
  
    if (viewportTag) {
      var props = viewportTag.content.toLowerCase().replace(/\s+/g, '').split(',');
      var keyValue;
      for (x = 0; x < props.length; x++) {
        if (props[x]) {
          keyValue = props[x].split('=');
          viewportProperties[ keyValue[0] ] = (keyValue.length > 1 ? keyValue[1] : '_');
        }
      }
      viewportUpdate();
    }
  }
  
  function viewportUpdate() {
    // unit tests in viewport.unit.js
  
    var initWidth = viewportProperties.width;
    var initHeight = viewportProperties.height;
    var p = ionic.Platform;
    var version = p.version();
    var DEVICE_WIDTH = 'device-width';
    var DEVICE_HEIGHT = 'device-height';
    var orientation = ionic.viewport.orientation();
  
    // Most times we're removing the height and adding the width
    // So this is the default to start with, then modify per platform/version/oreintation
    delete viewportProperties.height;
    viewportProperties.width = DEVICE_WIDTH;
  
    if (p.isIPad()) {
      // iPad
  
      if (version > 7) {
        // iPad >= 7.1
        // https://issues.apache.org/jira/browse/CB-4323
        delete viewportProperties.width;
  
      } else {
        // iPad <= 7.0
  
        if (p.isWebView()) {
          // iPad <= 7.0 WebView
  
          if (orientation == 90) {
            // iPad <= 7.0 WebView Landscape
            viewportProperties.height = '0';
  
          } else if (version == 7) {
            // iPad <= 7.0 WebView Portait
            viewportProperties.height = DEVICE_HEIGHT;
          }
        } else {
          // iPad <= 6.1 Browser
          if (version < 7) {
            viewportProperties.height = '0';
          }
        }
      }
  
    } else if (p.isIOS()) {
      // iPhone
  
      if (p.isWebView()) {
        // iPhone WebView
  
        if (version > 7) {
          // iPhone >= 7.1 WebView
          delete viewportProperties.width;
  
        } else if (version < 7) {
          // iPhone <= 6.1 WebView
          // if height was set it needs to get removed with this hack for <= 6.1
          if (initHeight) viewportProperties.height = '0';
  
        } else if (version == 7) {
          //iPhone == 7.0 WebView
          viewportProperties.height = DEVICE_HEIGHT;
        }
  
      } else {
        // iPhone Browser
  
        if (version < 7) {
          // iPhone <= 6.1 Browser
          // if height was set it needs to get removed with this hack for <= 6.1
          if (initHeight) viewportProperties.height = '0';
        }
      }
  
    }
  
    // only update the viewport tag if there was a change
    if (initWidth !== viewportProperties.width || initHeight !== viewportProperties.height) {
      viewportTagUpdate();
    }
  }
  
  function viewportTagUpdate() {
    var key, props = [];
    for (key in viewportProperties) {
      if (viewportProperties[key]) {
        props.push(key + (viewportProperties[key] == '_' ? '' : '=' + viewportProperties[key]));
      }
    }
  
    viewportTag.content = props.join(', ');
  }
  
  ionic.Platform.ready(function() {
    viewportLoadTag();
  
    window.addEventListener("orientationchange", function() {
      setTimeout(viewportUpdate, 1000);
    }, false);
  });
  
  (function(ionic) {
  'use strict';
    ionic.views.View = function() {
      this.initialize.apply(this, arguments);
    };
  
    ionic.views.View.inherit = ionic.inherit;
  
    ionic.extend(ionic.views.View.prototype, {
      initialize: function() {}
    });
  
  })(window.ionic);
  
  /*
   * Scroller
   * http://github.com/zynga/scroller
   *
   * Copyright 2011, Zynga Inc.
   * Licensed under the MIT License.
   * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
   *
   * Based on the work of: Unify Project (unify-project.org)
   * http://unify-project.org
   * Copyright 2011, Deutsche Telekom AG
   * License: MIT + Apache (V2)
   */
  
  /* jshint eqnull: true */
  
  /**
   * Generic animation class with support for dropped frames both optional easing and duration.
   *
   * Optional duration is useful when the lifetime is defined by another condition than time
   * e.g. speed of an animating object, etc.
   *
   * Dropped frame logic allows to keep using the same updater logic independent from the actual
   * rendering. This eases a lot of cases where it might be pretty complex to break down a state
   * based on the pure time difference.
   */
  var zyngaCore = { effect: {} };
  (function(global) {
    var time = Date.now || function() {
      return +new Date();
    };
    var desiredFrames = 60;
    var millisecondsPerSecond = 1000;
    var running = {};
    var counter = 1;
  
    zyngaCore.effect.Animate = {
  
      /**
       * A requestAnimationFrame wrapper / polyfill.
       *
       * @param callback {Function} The callback to be invoked before the next repaint.
       * @param root {HTMLElement} The root element for the repaint
       */
      requestAnimationFrame: (function() {
  
        // Check for request animation Frame support
        var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame;
        var isNative = !!requestFrame;
  
        if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
          isNative = false;
        }
  
        if (isNative) {
          return function(callback, root) {
            requestFrame(callback, root);
          };
        }
  
        var TARGET_FPS = 60;
        var requests = {};
        var requestCount = 0;
        var rafHandle = 1;
        var intervalHandle = null;
        var lastActive = +new Date();
  
        return function(callback, root) {
          var callbackHandle = rafHandle++;
  
          // Store callback
          requests[callbackHandle] = callback;
          requestCount++;
  
          // Create timeout at first request
          if (intervalHandle === null) {
  
            intervalHandle = setInterval(function() {
  
              var time = +new Date();
              var currentRequests = requests;
  
              // Reset data structure before executing callbacks
              requests = {};
              requestCount = 0;
  
              for(var key in currentRequests) {
                if (currentRequests.hasOwnProperty(key)) {
                  currentRequests[key](time);
                  lastActive = time;
                }
              }
  
              // Disable the timeout when nothing happens for a certain
              // period of time
              if (time - lastActive > 2500) {
                clearInterval(intervalHandle);
                intervalHandle = null;
              }
  
            }, 1000 / TARGET_FPS);
          }
  
          return callbackHandle;
        };
  
      })(),
  
  
      /**
       * Stops the given animation.
       *
       * @param id {Integer} Unique animation ID
       * @return {Boolean} Whether the animation was stopped (aka, was running before)
       */
      stop: function(id) {
        var cleared = running[id] != null;
        if (cleared) {
          running[id] = null;
        }
  
        return cleared;
      },
  
  
      /**
       * Whether the given animation is still running.
       *
       * @param id {Integer} Unique animation ID
       * @return {Boolean} Whether the animation is still running
       */
      isRunning: function(id) {
        return running[id] != null;
      },
  
  
      /**
       * Start the animation.
       *
       * @param stepCallback {Function} Pointer to function which is executed on every step.
       *   Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
       * @param verifyCallback {Function} Executed before every animation step.
       *   Signature of the method should be `function() { return continueWithAnimation; }`
       * @param completedCallback {Function}
       *   Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
       * @param duration {Integer} Milliseconds to run the animation
       * @param easingMethod {Function} Pointer to easing function
       *   Signature of the method should be `function(percent) { return modifiedValue; }`
       * @param root {Element} Render root, when available. Used for internal
       *   usage of requestAnimationFrame.
       * @return {Integer} Identifier of animation. Can be used to stop it any time.
       */
      start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
  
        var start = time();
        var lastFrame = start;
        var percent = 0;
        var dropCounter = 0;
        var id = counter++;
  
        if (!root) {
          root = document.body;
        }
  
        // Compacting running db automatically every few new animations
        if (id % 20 === 0) {
          var newRunning = {};
          for (var usedId in running) {
            newRunning[usedId] = true;
          }
          running = newRunning;
        }
  
        // This is the internal step method which is called every few milliseconds
        var step = function(virtual) {
  
          // Normalize virtual value
          var render = virtual !== true;
  
          // Get current time
          var now = time();
  
          // Verification is executed before next animation step
          if (!running[id] || (verifyCallback && !verifyCallback(id))) {
  
            running[id] = null;
            completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false);
            return;
  
          }
  
          // For the current rendering to apply let's update omitted steps in memory.
          // This is important to bring internal state variables up-to-date with progress in time.
          if (render) {
  
            var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
            for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
              step(true);
              dropCounter++;
            }
  
          }
  
          // Compute percent value
          if (duration) {
            percent = (now - start) / duration;
            if (percent > 1) {
              percent = 1;
            }
          }
  
          // Execute step callback, then...
          var value = easingMethod ? easingMethod(percent) : percent;
          if ((stepCallback(value, now, render) === false || percent === 1) && render) {
            running[id] = null;
            completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null);
          } else if (render) {
            lastFrame = now;
            zyngaCore.effect.Animate.requestAnimationFrame(step, root);
          }
        };
  
        // Mark as running
        running[id] = true;
  
        // Init first step
        zyngaCore.effect.Animate.requestAnimationFrame(step, root);
  
        // Return unique animation ID
        return id;
      }
    };
  })(this);
  
  /*
   * Scroller
   * http://github.com/zynga/scroller
   *
   * Copyright 2011, Zynga Inc.
   * Licensed under the MIT License.
   * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
   *
   * Based on the work of: Unify Project (unify-project.org)
   * http://unify-project.org
   * Copyright 2011, Deutsche Telekom AG
   * License: MIT + Apache (V2)
   */
  
  var Scroller;
  
  (function(ionic) {
    var NOOP = function(){};
  
    // Easing Equations (c) 2003 Robert Penner, all rights reserved.
    // Open source under the BSD License.
  
    /**
     * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
    **/
    var easeOutCubic = function(pos) {
      return (Math.pow((pos - 1), 3) + 1);
    };
  
    /**
     * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
    **/
    var easeInOutCubic = function(pos) {
      if ((pos /= 0.5) < 1) {
        return 0.5 * Math.pow(pos, 3);
      }
  
      return 0.5 * (Math.pow((pos - 2), 3) + 2);
    };
  
  
  /**
   * ionic.views.Scroll
   * A powerful scroll view with support for bouncing, pull to refresh, and paging.
   * @param   {Object}        options options for the scroll view
   * @class A scroll view system
   * @memberof ionic.views
   */
  ionic.views.Scroll = ionic.views.View.inherit({
    initialize: function(options) {
      var self = this;
  
      self.__container = options.el;
      self.__content = options.el.firstElementChild;
  
      //Remove any scrollTop attached to these elements; they are virtual scroll now
      //This also stops on-load-scroll-to-window.location.hash that the browser does
      setTimeout(function() {
        if (self.__container && self.__content) {
          self.__container.scrollTop = 0;
          self.__content.scrollTop = 0;
        }
      });
  
      self.options = {
  
        /** Disable scrolling on x-axis by default */
        scrollingX: false,
        scrollbarX: true,
  
        /** Enable scrolling on y-axis */
        scrollingY: true,
        scrollbarY: true,
  
        startX: 0,
        startY: 0,
  
        /** The amount to dampen mousewheel events */
        wheelDampen: 6,
  
        /** The minimum size the scrollbars scale to while scrolling */
        minScrollbarSizeX: 5,
        minScrollbarSizeY: 5,
  
        /** Scrollbar fading after scrolling */
        scrollbarsFade: true,
        scrollbarFadeDelay: 300,
        /** The initial fade delay when the pane is resized or initialized */
        scrollbarResizeFadeDelay: 1000,
  
        /** Enable animations for deceleration, snap back, zooming and scrolling */
        animating: true,
  
        /** duration for animations triggered by scrollTo/zoomTo */
        animationDuration: 250,
  
        /** The velocity required to make the scroll view "slide" after touchend */
        decelVelocityThreshold: 4,
  
        /** The velocity required to make the scroll view "slide" after touchend when using paging */
        decelVelocityThresholdPaging: 4,
  
        /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
        bouncing: true,
  
        /** Enable locking to the main axis if user moves only slightly on one of them at start */
        locking: true,
  
        /** Enable pagination mode (switching between full page content panes) */
        paging: false,
  
        /** Enable snapping of content to a configured pixel grid */
        snapping: false,
  
        /** Enable zooming of content via API, fingers and mouse wheel */
        zooming: false,
  
        /** Minimum zoom level */
        minZoom: 0.5,
  
        /** Maximum zoom level */
        maxZoom: 3,
  
        /** Multiply or decrease scrolling speed **/
        speedMultiplier: 1,
  
        deceleration: 0.97,
  
        /** Whether to prevent default on a scroll operation to capture drag events **/
        preventDefault: false,
  
        /** Callback that is fired on the later of touch end or deceleration end,
          provided that another scrolling action has not begun. Used to know
          when to fade out a scrollbar. */
        scrollingComplete: NOOP,
  
        /** This configures the amount of change applied to deceleration when reaching boundaries  **/
        penetrationDeceleration : 0.03,
  
        /** This configures the amount of change applied to acceleration when reaching boundaries  **/
        penetrationAcceleration : 0.08,
  
        // The ms interval for triggering scroll events
        scrollEventInterval: 10,
  
        freeze: false,
  
        getContentWidth: function() {
          return Math.max(self.__content.scrollWidth, self.__content.offsetWidth);
        },
        getContentHeight: function() {
          return Math.max(self.__content.scrollHeight, self.__content.offsetHeight + (self.__content.offsetTop * 2));
        }
      };
  
      for (var key in options) {
        self.options[key] = options[key];
      }
  
      self.hintResize = ionic.debounce(function() {
        self.resize();
      }, 1000, true);
  
      self.onScroll = function() {
  
        if (!ionic.scroll.isScrolling) {
          setTimeout(self.setScrollStart, 50);
        } else {
          clearTimeout(self.scrollTimer);
          self.scrollTimer = setTimeout(self.setScrollStop, 80);
        }
  
      };
  
      self.freeze = function(shouldFreeze) {
        if (arguments.length) {
          self.options.freeze = shouldFreeze;
        }
        return self.options.freeze;
      };
  
      self.setScrollStart = function() {
        ionic.scroll.isScrolling = Math.abs(ionic.scroll.lastTop - self.__scrollTop) > 1;
        clearTimeout(self.scrollTimer);
        self.scrollTimer = setTimeout(self.setScrollStop, 80);
      };
  
      self.setScrollStop = function() {
        ionic.scroll.isScrolling = false;
        ionic.scroll.lastTop = self.__scrollTop;
      };
  
      self.triggerScrollEvent = ionic.throttle(function() {
        self.onScroll();
        ionic.trigger('scroll', {
          scrollTop: self.__scrollTop,
          scrollLeft: self.__scrollLeft,
          target: self.__container
        });
      }, self.options.scrollEventInterval);
  
      self.triggerScrollEndEvent = function() {
        ionic.trigger('scrollend', {
          scrollTop: self.__scrollTop,
          scrollLeft: self.__scrollLeft,
          target: self.__container
        });
      };
  
      self.__scrollLeft = self.options.startX;
      self.__scrollTop = self.options.startY;
  
      // Get the render update function, initialize event handlers,
      // and calculate the size of the scroll container
      self.__callback = self.getRenderFn();
      self.__initEventHandlers();
      self.__createScrollbars();
  
    },
  
    run: function() {
      this.resize();
  
      // Fade them out
      this.__fadeScrollbars('out', this.options.scrollbarResizeFadeDelay);
    },
  
  
  
    /*
    ---------------------------------------------------------------------------
      INTERNAL FIELDS :: STATUS
    ---------------------------------------------------------------------------
    */
  
    /** Whether only a single finger is used in touch handling */
    __isSingleTouch: false,
  
    /** Whether a touch event sequence is in progress */
    __isTracking: false,
  
    /** Whether a deceleration animation went to completion. */
    __didDecelerationComplete: false,
  
    /**
     * Whether a gesture zoom/rotate event is in progress. Activates when
     * a gesturestart event happens. This has higher priority than dragging.
     */
    __isGesturing: false,
  
    /**
     * Whether the user has moved by such a distance that we have enabled
     * dragging mode. Hint: It's only enabled after some pixels of movement to
     * not interrupt with clicks etc.
     */
    __isDragging: false,
  
    /**
     * Not touching and dragging anymore, and smoothly animating the
     * touch sequence using deceleration.
     */
    __isDecelerating: false,
  
    /**
     * Smoothly animating the currently configured change
     */
    __isAnimating: false,
  
  
  
    /*
    ---------------------------------------------------------------------------
      INTERNAL FIELDS :: DIMENSIONS
    ---------------------------------------------------------------------------
    */
  
    /** Available outer left position (from document perspective) */
    __clientLeft: 0,
  
    /** Available outer top position (from document perspective) */
    __clientTop: 0,
  
    /** Available outer width */
    __clientWidth: 0,
  
    /** Available outer height */
    __clientHeight: 0,
  
    /** Outer width of content */
    __contentWidth: 0,
  
    /** Outer height of content */
    __contentHeight: 0,
  
    /** Snapping width for content */
    __snapWidth: 100,
  
    /** Snapping height for content */
    __snapHeight: 100,
  
    /** Height to assign to refresh area */
    __refreshHeight: null,
  
    /** Whether the refresh process is enabled when the event is released now */
    __refreshActive: false,
  
    /** Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
    __refreshActivate: null,
  
    /** Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
    __refreshDeactivate: null,
  
    /** Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
    __refreshStart: null,
  
    /** Zoom level */
    __zoomLevel: 1,
  
    /** Scroll position on x-axis */
    __scrollLeft: 0,
  
    /** Scroll position on y-axis */
    __scrollTop: 0,
  
    /** Maximum allowed scroll position on x-axis */
    __maxScrollLeft: 0,
  
    /** Maximum allowed scroll position on y-axis */
    __maxScrollTop: 0,
  
    /* Scheduled left position (final position when animating) */
    __scheduledLeft: 0,
  
    /* Scheduled top position (final position when animating) */
    __scheduledTop: 0,
  
    /* Scheduled zoom level (final scale when animating) */
    __scheduledZoom: 0,
  
  
  
    /*
    ---------------------------------------------------------------------------
      INTERNAL FIELDS :: LAST POSITIONS
    ---------------------------------------------------------------------------
    */
  
    /** Left position of finger at start */
    __lastTouchLeft: null,
  
    /** Top position of finger at start */
    __lastTouchTop: null,
  
    /** Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
    __lastTouchMove: null,
  
    /** List of positions, uses three indexes for each state: left, top, timestamp */
    __positions: null,
  
  
  
    /*
    ---------------------------------------------------------------------------
      INTERNAL FIELDS :: DECELERATION SUPPORT
    ---------------------------------------------------------------------------
    */
  
    /** Minimum left scroll position during deceleration */
    __minDecelerationScrollLeft: null,
  
    /** Minimum top scroll position during deceleration */
    __minDecelerationScrollTop: null,
  
    /** Maximum left scroll position during deceleration */
    __maxDecelerationScrollLeft: null,
  
    /** Maximum top scroll position during deceleration */
    __maxDecelerationScrollTop: null,
  
    /** Current factor to modify horizontal scroll position with on every step */
    __decelerationVelocityX: null,
  
    /** Current factor to modify vertical scroll position with on every step */
    __decelerationVelocityY: null,
  
  
    /** the browser-specific property to use for transforms */
    __transformProperty: null,
    __perspectiveProperty: null,
  
    /** scrollbar indicators */
    __indicatorX: null,
    __indicatorY: null,
  
    /** Timeout for scrollbar fading */
    __scrollbarFadeTimeout: null,
  
    /** whether we've tried to wait for size already */
    __didWaitForSize: null,
    __sizerTimeout: null,
  
    __initEventHandlers: function() {
      var self = this;
  
      // Event Handler
      var container = self.__container;
  
      self.scrollChildIntoView = function(e) {
  
        //distance from bottom of scrollview to top of viewport
        var scrollBottomOffsetToTop;
  
        if ( !self.isScrolledIntoView ) {
          // shrink scrollview so we can actually scroll if the input is hidden
          // if it isn't shrink so we can scroll to inputs under the keyboard
          if ((ionic.Platform.isIOS() || ionic.Platform.isFullScreen)){
  
            // if there are things below the scroll view account for them and
            // subtract them from the keyboard height when resizing
            scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
            var scrollBottomOffsetToBottom = e.detail.viewportHeight - scrollBottomOffsetToTop;
            var keyboardOffset = Math.max(0, e.detail.keyboardHeight - scrollBottomOffsetToBottom);
            container.style.height = (container.clientHeight - keyboardOffset) + "px";
            container.style.overflow = "visible";
            //update scroll view
            self.resize();
          }
          self.isScrolledIntoView = true;
        }
  
        //If the element is positioned under the keyboard...
        if ( e.detail.isElementUnderKeyboard ) {
          var delay;
          // Wait on android for web view to resize
          if ( ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen ) {
            // android y u resize so slow
            if ( ionic.Platform.version() < 4.4) {
              delay = 500;
            } else {
              // probably overkill for chrome
              delay = 350;
            }
          } else {
            delay = 80;
          }
  
          //Put element in middle of visible screen
          //Wait for android to update view height and resize() to reset scroll position
          ionic.scroll.isScrolling = true;
          setTimeout(function(){
            //middle of the scrollview, where we want to scroll to
            var scrollMidpointOffset = container.clientHeight * 0.5;
  
            scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
            //distance from top of focused element to the bottom of the scroll view
            var elementTopOffsetToScrollBottom = e.detail.elementTop - scrollBottomOffsetToTop;
  
            var scrollTop = elementTopOffsetToScrollBottom  + scrollMidpointOffset;
  
            if (scrollTop > 0){
              ionic.tap.cloneFocusedInput(container, self);
              self.scrollBy(0, scrollTop, true);
              self.onScroll();
            }
          }, delay);
        }
  
        //Only the first scrollView parent of the element that broadcasted this event
        //(the active element that needs to be shown) should receive this event
        e.stopPropagation();
      };
  
      self.resetScrollView = function(e) {
        //return scrollview to original height once keyboard has hidden
        if (self.isScrolledIntoView) {
          self.isScrolledIntoView = false;
          container.style.height = "";
          container.style.overflow = "";
          self.resize();
          ionic.scroll.isScrolling = false;
        }
      };
  
      //Broadcasted when keyboard is shown on some platforms.
      //See js/utils/keyboard.js
      container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);
      container.addEventListener('resetScrollView', self.resetScrollView);
  
      function getEventTouches(e) {
        return e.touches && e.touches.length ? e.touches : [{
          pageX: e.pageX,
          pageY: e.pageY
        }];
      }
  
      self.touchStart = function(e) {
        self.startCoordinates = ionic.tap.pointerCoord(e);
  
        if ( ionic.tap.ignoreScrollStart(e) ) {
          return;
        }
  
        self.__isDown = true;
  
        if ( ionic.tap.containsOrIsTextInput(e.target) || e.target.tagName === 'SELECT' ) {
          // do not start if the target is a text input
          // if there is a touchmove on this input, then we can start the scroll
          self.__hasStarted = false;
          return;
        }
  
        self.__isSelectable = true;
        self.__enableScrollY = true;
        self.__hasStarted = true;
        self.doTouchStart(getEventTouches(e), e.timeStamp);
        e.preventDefault();
      };
  
      self.touchMove = function(e) {
        if (self.options.freeze || !self.__isDown ||
          (!self.__isDown && e.defaultPrevented) ||
          (e.target.tagName === 'TEXTAREA' && e.target.parentElement.querySelector(':focus')) ) {
          return;
        }
  
        if ( !self.__hasStarted && ( ionic.tap.containsOrIsTextInput(e.target) || e.target.tagName === 'SELECT' ) ) {
          // the target is a text input and scroll has started
          // since the text input doesn't start on touchStart, do it here
          self.__hasStarted = true;
          self.doTouchStart(getEventTouches(e), e.timeStamp);
          e.preventDefault();
          return;
        }
  
        if (self.startCoordinates) {
          // we have start coordinates, so get this touch move's current coordinates
          var currentCoordinates = ionic.tap.pointerCoord(e);
  
          if ( self.__isSelectable &&
              ionic.tap.isTextInput(e.target) &&
              Math.abs(self.startCoordinates.x - currentCoordinates.x) > 20 ) {
            // user slid the text input's caret on its x axis, disable any future y scrolling
            self.__enableScrollY = false;
            self.__isSelectable = true;
          }
  
          if ( self.__enableScrollY && Math.abs(self.startCoordinates.y - currentCoordinates.y) > 10 ) {
            // user scrolled the entire view on the y axis
            // disabled being able to select text on an input
            // hide the input which has focus, and show a cloned one that doesn't have focus
            self.__isSelectable = false;
            ionic.tap.cloneFocusedInput(container, self);
          }
        }
  
        self.doTouchMove(getEventTouches(e), e.timeStamp, e.scale);
        self.__isDown = true;
      };
  
      self.touchMoveBubble = function(e) {
        if(self.__isDown && self.options.preventDefault) {
          e.preventDefault();
        }
      };
  
      self.touchEnd = function(e) {
        if (!self.__isDown) return;
  
        self.doTouchEnd(e, e.timeStamp);
        self.__isDown = false;
        self.__hasStarted = false;
        self.__isSelectable = true;
        self.__enableScrollY = true;
  
        if ( !self.__isDragging && !self.__isDecelerating && !self.__isAnimating ) {
          ionic.tap.removeClonedInputs(container, self);
        }
      };
  
      self.mouseWheel = ionic.animationFrameThrottle(function(e) {
        var scrollParent = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'ionic-scroll');
        if (!self.options.freeze && scrollParent === self.__container) {
  
          self.hintResize();
          self.scrollBy(
            (e.wheelDeltaX || e.deltaX || 0) / self.options.wheelDampen,
            (-e.wheelDeltaY || e.deltaY || 0) / self.options.wheelDampen
          );
  
          self.__fadeScrollbars('in');
          clearTimeout(self.__wheelHideBarTimeout);
          self.__wheelHideBarTimeout = setTimeout(function() {
            self.__fadeScrollbars('out');
          }, 100);
        }
      });
  
      if ('ontouchstart' in window) {
        // Touch Events
        container.addEventListener("touchstart", self.touchStart, false);
        if(self.options.preventDefault) container.addEventListener("touchmove", self.touchMoveBubble, false);
        document.addEventListener("touchmove", self.touchMove, false);
        document.addEventListener("touchend", self.touchEnd, false);
        document.addEventListener("touchcancel", self.touchEnd, false);
  
      } else if (window.navigator.pointerEnabled) {
        // Pointer Events
        container.addEventListener("pointerdown", self.touchStart, false);
        if(self.options.preventDefault) container.addEventListener("pointermove", self.touchMoveBubble, false);
        document.addEventListener("pointermove", self.touchMove, false);
        document.addEventListener("pointerup", self.touchEnd, false);
        document.addEventListener("pointercancel", self.touchEnd, false);
        document.addEventListener("wheel", self.mouseWheel, false);
  
      } else if (window.navigator.msPointerEnabled) {
        // IE10, WP8 (Pointer Events)
        container.addEventListener("MSPointerDown", self.touchStart, false);
        if(self.options.preventDefault) container.addEventListener("MSPointerMove", self.touchMoveBubble, false);
        document.addEventListener("MSPointerMove", self.touchMove, false);
        document.addEventListener("MSPointerUp", self.touchEnd, false);
        document.addEventListener("MSPointerCancel", self.touchEnd, false);
        document.addEventListener("wheel", self.mouseWheel, false);
  
      } else {
        // Mouse Events
        var mousedown = false;
  
        self.mouseDown = function(e) {
          if ( ionic.tap.ignoreScrollStart(e) || e.target.tagName === 'SELECT' ) {
            return;
          }
          self.doTouchStart(getEventTouches(e), e.timeStamp);
  
          if ( !ionic.tap.isTextInput(e.target) ) {
            e.preventDefault();
          }
          mousedown = true;
        };
  
        self.mouseMove = function(e) {
          if (self.options.freeze || !mousedown || (!mousedown && e.defaultPrevented)) {
            return;
          }
  
          self.doTouchMove(getEventTouches(e), e.timeStamp);
  
          mousedown = true;
        };
  
        self.mouseMoveBubble = function(e) {
          if (mousedown && self.options.preventDefault) {
            e.preventDefault();
          }
        };
  
        self.mouseUp = function(e) {
          if (!mousedown) {
            return;
          }
  
          self.doTouchEnd(e, e.timeStamp);
  
          mousedown = false;
        };
  
        container.addEventListener("mousedown", self.mouseDown, false);
        if(self.options.preventDefault) container.addEventListener("mousemove", self.mouseMoveBubble, false);
        document.addEventListener("mousemove", self.mouseMove, false);
        document.addEventListener("mouseup", self.mouseUp, false);
        document.addEventListener('mousewheel', self.mouseWheel, false);
        document.addEventListener('wheel', self.mouseWheel, false);
      }
    },
  
    __cleanup: function() {
      var self = this;
      var container = self.__container;
  
      container.removeEventListener('touchstart', self.touchStart);
      container.removeEventListener('touchmove', self.touchMoveBubble);
      document.removeEventListener('touchmove', self.touchMove);
      document.removeEventListener('touchend', self.touchEnd);
      document.removeEventListener('touchcancel', self.touchCancel);
  
      container.removeEventListener("pointerdown", self.touchStart);
      container.removeEventListener("pointermove", self.touchMoveBubble);
      document.removeEventListener("pointermove", self.touchMove);
      document.removeEventListener("pointerup", self.touchEnd);
      document.removeEventListener("pointercancel", self.touchEnd);
  
      container.removeEventListener("MSPointerDown", self.touchStart);
      container.removeEventListener("MSPointerMove", self.touchMoveBubble);
      document.removeEventListener("MSPointerMove", self.touchMove);
      document.removeEventListener("MSPointerUp", self.touchEnd);
      document.removeEventListener("MSPointerCancel", self.touchEnd);
  
      container.removeEventListener("mousedown", self.mouseDown);
      container.removeEventListener("mousemove", self.mouseMoveBubble);
      document.removeEventListener("mousemove", self.mouseMove);
      document.removeEventListener("mouseup", self.mouseUp);
      document.removeEventListener('mousewheel', self.mouseWheel);
      document.removeEventListener('wheel', self.mouseWheel);
  
      container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView);
      container.removeEventListener('resetScrollView', self.resetScrollView);
  
      ionic.tap.removeClonedInputs(container, self);
  
      delete self.__container;
      delete self.__content;
      delete self.__indicatorX;
      delete self.__indicatorY;
      delete self.options.el;
  
      self.__callback = self.scrollChildIntoView = self.resetScrollView = NOOP;
  
      self.mouseMove = self.mouseDown = self.mouseUp = self.mouseWheel =
        self.touchStart = self.touchMove = self.touchEnd = self.touchCancel = NOOP;
  
      self.resize = self.scrollTo = self.zoomTo =
        self.__scrollingComplete = NOOP;
      container = null;
    },
  
    /** Create a scroll bar div with the given direction **/
    __createScrollbar: function(direction) {
      var bar = document.createElement('div'),
        indicator = document.createElement('div');
  
      indicator.className = 'scroll-bar-indicator scroll-bar-fade-out';
  
      if (direction == 'h') {
        bar.className = 'scroll-bar scroll-bar-h';
      } else {
        bar.className = 'scroll-bar scroll-bar-v';
      }
  
      bar.appendChild(indicator);
      return bar;
    },
  
    __createScrollbars: function() {
      var self = this;
      var indicatorX, indicatorY;
  
      if (self.options.scrollingX) {
        indicatorX = {
          el: self.__createScrollbar('h'),
          sizeRatio: 1
        };
        indicatorX.indicator = indicatorX.el.children[0];
  
        if (self.options.scrollbarX) {
          self.__container.appendChild(indicatorX.el);
        }
        self.__indicatorX = indicatorX;
      }
  
      if (self.options.scrollingY) {
        indicatorY = {
          el: self.__createScrollbar('v'),
          sizeRatio: 1
        };
        indicatorY.indicator = indicatorY.el.children[0];
  
        if (self.options.scrollbarY) {
          self.__container.appendChild(indicatorY.el);
        }
        self.__indicatorY = indicatorY;
      }
    },
  
    __resizeScrollbars: function() {
      var self = this;
  
      // Update horiz bar
      if (self.__indicatorX) {
        var width = Math.max(Math.round(self.__clientWidth * self.__clientWidth / (self.__contentWidth)), 20);
        if (width > self.__contentWidth) {
          width = 0;
        }
        if (width !== self.__indicatorX.size) {
          ionic.requestAnimationFrame(function(){
            self.__indicatorX.indicator.style.width = width + 'px';
          });
        }
        self.__indicatorX.size = width;
        self.__indicatorX.minScale = self.options.minScrollbarSizeX / width;
        self.__indicatorX.maxPos = self.__clientWidth - width;
        self.__indicatorX.sizeRatio = self.__maxScrollLeft ? self.__indicatorX.maxPos / self.__maxScrollLeft : 1;
      }
  
      // Update vert bar
      if (self.__indicatorY) {
        var height = Math.max(Math.round(self.__clientHeight * self.__clientHeight / (self.__contentHeight)), 20);
        if (height > self.__contentHeight) {
          height = 0;
        }
        if (height !== self.__indicatorY.size) {
          ionic.requestAnimationFrame(function(){
            self.__indicatorY && (self.__indicatorY.indicator.style.height = height + 'px');
          });
        }
        self.__indicatorY.size = height;
        self.__indicatorY.minScale = self.options.minScrollbarSizeY / height;
        self.__indicatorY.maxPos = self.__clientHeight - height;
        self.__indicatorY.sizeRatio = self.__maxScrollTop ? self.__indicatorY.maxPos / self.__maxScrollTop : 1;
      }
    },
  
    /**
     * Move and scale the scrollbars as the page scrolls.
     */
    __repositionScrollbars: function() {
      var self = this, width, heightScale,
          widthDiff, heightDiff,
          x, y,
          xstop = 0, ystop = 0;
  
      if (self.__indicatorX) {
        // Handle the X scrollbar
  
        // Don't go all the way to the right if we have a vertical scrollbar as well
        if (self.__indicatorY) xstop = 10;
  
        x = Math.round(self.__indicatorX.sizeRatio * self.__scrollLeft) || 0,
  
        // The the difference between the last content X position, and our overscrolled one
        widthDiff = self.__scrollLeft - (self.__maxScrollLeft - xstop);
  
        if (self.__scrollLeft < 0) {
  
          widthScale = Math.max(self.__indicatorX.minScale,
              (self.__indicatorX.size - Math.abs(self.__scrollLeft)) / self.__indicatorX.size);
  
          // Stay at left
          x = 0;
  
          // Make sure scale is transformed from the left/center origin point
          self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'left center';
        } else if (widthDiff > 0) {
  
          widthScale = Math.max(self.__indicatorX.minScale,
              (self.__indicatorX.size - widthDiff) / self.__indicatorX.size);
  
          // Stay at the furthest x for the scrollable viewport
          x = self.__indicatorX.maxPos - xstop;
  
          // Make sure scale is transformed from the right/center origin point
          self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'right center';
  
        } else {
  
          // Normal motion
          x = Math.min(self.__maxScrollLeft, Math.max(0, x));
          widthScale = 1;
  
        }
  
        var translate3dX = 'translate3d(' + x + 'px, 0, 0) scaleX(' + widthScale + ')';
        if (self.__indicatorX.transformProp !== translate3dX) {
          self.__indicatorX.indicator.style[self.__transformProperty] = translate3dX;
          self.__indicatorX.transformProp = translate3dX;
        }
      }
  
      if (self.__indicatorY) {
  
        y = Math.round(self.__indicatorY.sizeRatio * self.__scrollTop) || 0;
  
        // Don't go all the way to the right if we have a vertical scrollbar as well
        if (self.__indicatorX) ystop = 10;
  
        heightDiff = self.__scrollTop - (self.__maxScrollTop - ystop);
  
        if (self.__scrollTop < 0) {
  
          heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - Math.abs(self.__scrollTop)) / self.__indicatorY.size);
  
          // Stay at top
          y = 0;
  
          // Make sure scale is transformed from the center/top origin point
          if (self.__indicatorY.originProp !== 'center top') {
            self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center top';
            self.__indicatorY.originProp = 'center top';
          }
  
        } else if (heightDiff > 0) {
  
          heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - heightDiff) / self.__indicatorY.size);
  
          // Stay at bottom of scrollable viewport
          y = self.__indicatorY.maxPos - ystop;
  
          // Make sure scale is transformed from the center/bottom origin point
          if (self.__indicatorY.originProp !== 'center bottom') {
            self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center bottom';
            self.__indicatorY.originProp = 'center bottom';
          }
  
        } else {
  
          // Normal motion
          y = Math.min(self.__maxScrollTop, Math.max(0, y));
          heightScale = 1;
  
        }
  
        var translate3dY = 'translate3d(0,' + y + 'px, 0) scaleY(' + heightScale + ')';
        if (self.__indicatorY.transformProp !== translate3dY) {
          self.__indicatorY.indicator.style[self.__transformProperty] = translate3dY;
          self.__indicatorY.transformProp = translate3dY;
        }
      }
    },
  
    __fadeScrollbars: function(direction, delay) {
      var self = this;
  
      if (!self.options.scrollbarsFade) {
        return;
      }
  
      var className = 'scroll-bar-fade-out';
  
      if (self.options.scrollbarsFade === true) {
        clearTimeout(self.__scrollbarFadeTimeout);
  
        if (direction == 'in') {
          if (self.__indicatorX) { self.__indicatorX.indicator.classList.remove(className); }
          if (self.__indicatorY) { self.__indicatorY.indicator.classList.remove(className); }
        } else {
          self.__scrollbarFadeTimeout = setTimeout(function() {
            if (self.__indicatorX) { self.__indicatorX.indicator.classList.add(className); }
            if (self.__indicatorY) { self.__indicatorY.indicator.classList.add(className); }
          }, delay || self.options.scrollbarFadeDelay);
        }
      }
    },
  
    __scrollingComplete: function() {
      this.options.scrollingComplete();
      ionic.tap.removeClonedInputs(this.__container, this);
      this.__fadeScrollbars('out');
    },
  
    resize: function(continueScrolling) {
      var self = this;
      if (!self.__container || !self.options) return;
  
      // Update Scroller dimensions for changed content
      // Add padding to bottom of content
      self.setDimensions(
        self.__container.clientWidth,
        self.__container.clientHeight,
        self.options.getContentWidth(),
        self.options.getContentHeight(),
        continueScrolling
      );
    },
    /*
    ---------------------------------------------------------------------------
      PUBLIC API
    ---------------------------------------------------------------------------
    */
  
    getRenderFn: function() {
      var self = this;
  
      var content = self.__content;
  
      var docStyle = document.documentElement.style;
  
      var engine;
      if ('MozAppearance' in docStyle) {
        engine = 'gecko';
      } else if ('WebkitAppearance' in docStyle) {
        engine = 'webkit';
      } else if (typeof navigator.cpuClass === 'string') {
        engine = 'trident';
      }
  
      var vendorPrefix = {
        trident: 'ms',
        gecko: 'Moz',
        webkit: 'Webkit',
        presto: 'O'
      }[engine];
  
      var helperElem = document.createElement("div");
      var undef;
  
      var perspectiveProperty = vendorPrefix + "Perspective";
      var transformProperty = vendorPrefix + "Transform";
      var transformOriginProperty = vendorPrefix + 'TransformOrigin';
  
      self.__perspectiveProperty = transformProperty;
      self.__transformProperty = transformProperty;
      self.__transformOriginProperty = transformOriginProperty;
  
      if (helperElem.style[perspectiveProperty] !== undef) {
  
        return function(left, top, zoom, wasResize) {
          var translate3d = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0) scale(' + zoom + ')';
          if (translate3d !== self.contentTransform) {
            content.style[transformProperty] = translate3d;
            self.contentTransform = translate3d;
          }
          self.__repositionScrollbars();
          if (!wasResize) {
            self.triggerScrollEvent();
          }
        };
  
      } else if (helperElem.style[transformProperty] !== undef) {
  
        return function(left, top, zoom, wasResize) {
          content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px) scale(' + zoom + ')';
          self.__repositionScrollbars();
          if (!wasResize) {
            self.triggerScrollEvent();
          }
        };
  
      } else {
  
        return function(left, top, zoom, wasResize) {
          content.style.marginLeft = left ? (-left/zoom) + 'px' : '';
          content.style.marginTop = top ? (-top/zoom) + 'px' : '';
          content.style.zoom = zoom || '';
          self.__repositionScrollbars();
          if (!wasResize) {
            self.triggerScrollEvent();
          }
        };
  
      }
    },
  
  
    /**
     * Configures the dimensions of the client (outer) and content (inner) elements.
     * Requires the available space for the outer element and the outer size of the inner element.
     * All values which are falsy (null or zero etc.) are ignored and the old value is kept.
     *
     * @param clientWidth {Integer} Inner width of outer element
     * @param clientHeight {Integer} Inner height of outer element
     * @param contentWidth {Integer} Outer width of inner element
     * @param contentHeight {Integer} Outer height of inner element
     */
    setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight, continueScrolling) {
      var self = this;
  
      if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) {
        // this scrollview isn't rendered, don't bother
        return;
      }
  
      // Only update values which are defined
      if (clientWidth === +clientWidth) {
        self.__clientWidth = clientWidth;
      }
  
      if (clientHeight === +clientHeight) {
        self.__clientHeight = clientHeight;
      }
  
      if (contentWidth === +contentWidth) {
        self.__contentWidth = contentWidth;
      }
  
      if (contentHeight === +contentHeight) {
        self.__contentHeight = contentHeight;
      }
  
      // Refresh maximums
      self.__computeScrollMax();
      self.__resizeScrollbars();
  
      // Refresh scroll position
      if (!continueScrolling) {
        self.scrollTo(self.__scrollLeft, self.__scrollTop, true, null, true);
      }
  
    },
  
  
    /**
     * Sets the client coordinates in relation to the document.
     *
     * @param left {Integer} Left position of outer element
     * @param top {Integer} Top position of outer element
     */
    setPosition: function(left, top) {
      this.__clientLeft = left || 0;
      this.__clientTop = top || 0;
    },
  
  
    /**
     * Configures the snapping (when snapping is active)
     *
     * @param width {Integer} Snapping width
     * @param height {Integer} Snapping height
     */
    setSnapSize: function(width, height) {
      this.__snapWidth = width;
      this.__snapHeight = height;
    },
  
  
    /**
     * Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
     * the user event is released during visibility of this zone. This was introduced by some apps on iOS like
     * the official Twitter client.
     *
     * @param height {Integer} Height of pull-to-refresh zone on top of rendered list
     * @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
     * @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
     * @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
     * @param showCallback {Function} Callback to execute when the refresher should be shown. This is for showing the refresher during a negative scrollTop.
     * @param hideCallback {Function} Callback to execute when the refresher should be hidden. This is for hiding the refresher when it's behind the nav bar.
     * @param tailCallback {Function} Callback to execute just before the refresher returns to it's original state. This is for zooming out the refresher.
     * @param pullProgressCallback Callback to state the progress while pulling to refresh
     */
    activatePullToRefresh: function(height, refresherMethods) {
      var self = this;
  
      self.__refreshHeight = height;
      self.__refreshActivate = function() {ionic.requestAnimationFrame(refresherMethods.activate);};
      self.__refreshDeactivate = function() {ionic.requestAnimationFrame(refresherMethods.deactivate);};
      self.__refreshStart = function() {ionic.requestAnimationFrame(refresherMethods.start);};
      self.__refreshShow = function() {ionic.requestAnimationFrame(refresherMethods.show);};
      self.__refreshHide = function() {ionic.requestAnimationFrame(refresherMethods.hide);};
      self.__refreshTail = function() {ionic.requestAnimationFrame(refresherMethods.tail);};
      self.__refreshTailTime = 100;
      self.__minSpinTime = 600;
    },
  
  
    /**
     * Starts pull-to-refresh manually.
     */
    triggerPullToRefresh: function() {
      // Use publish instead of scrollTo to allow scrolling to out of boundary position
      // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
      this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);
  
      var d = new Date();
      this.refreshStartTime = d.getTime();
  
      if (this.__refreshStart) {
        this.__refreshStart();
      }
    },
  
  
    /**
     * Signalizes that pull-to-refresh is finished.
     */
    finishPullToRefresh: function() {
      var self = this;
      // delay to make sure the spinner has a chance to spin for a split second before it's dismissed
      var d = new Date();
      var delay = 0;
      if (self.refreshStartTime + self.__minSpinTime > d.getTime()) {
        delay = self.refreshStartTime + self.__minSpinTime - d.getTime();
      }
      setTimeout(function() {
        if (self.__refreshTail) {
          self.__refreshTail();
        }
        setTimeout(function() {
          self.__refreshActive = false;
          if (self.__refreshDeactivate) {
            self.__refreshDeactivate();
          }
          if (self.__refreshHide) {
            self.__refreshHide();
          }
  
          self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
        },self.__refreshTailTime);
      },delay);
    },
  
  
    /**
     * Returns the scroll position and zooming values
     *
     * @return {Map} `left` and `top` scroll position and `zoom` level
     */
    getValues: function() {
      return {
        left: this.__scrollLeft,
        top: this.__scrollTop,
        zoom: this.__zoomLevel
      };
    },
  
  
    /**
     * Returns the maximum scroll values
     *
     * @return {Map} `left` and `top` maximum scroll values
     */
    getScrollMax: function() {
      return {
        left: this.__maxScrollLeft,
        top: this.__maxScrollTop
      };
    },
  
  
    /**
     * Zooms to the given level. Supports optional animation. Zooms
     * the center when no coordinates are given.
     *
     * @param level {Number} Level to zoom to
     * @param animate {Boolean} Whether to use animation
     * @param originLeft {Number} Zoom in at given left coordinate
     * @param originTop {Number} Zoom in at given top coordinate
     */
    zoomTo: function(level, animate, originLeft, originTop) {
      var self = this;
  
      if (!self.options.zooming) {
        throw new Error("Zooming is not enabled!");
      }
  
      // Stop deceleration
      if (self.__isDecelerating) {
        zyngaCore.effect.Animate.stop(self.__isDecelerating);
        self.__isDecelerating = false;
      }
  
      var oldLevel = self.__zoomLevel;
  
      // Normalize input origin to center of viewport if not defined
      if (originLeft == null) {
        originLeft = self.__clientWidth / 2;
      }
  
      if (originTop == null) {
        originTop = self.__clientHeight / 2;
      }
  
      // Limit level according to configuration
      level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
  
      // Recompute maximum values while temporary tweaking maximum scroll ranges
      self.__computeScrollMax(level);
  
      // Recompute left and top coordinates based on new zoom level
      var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft;
      var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop;
  
      // Limit x-axis
      if (left > self.__maxScrollLeft) {
        left = self.__maxScrollLeft;
      } else if (left < 0) {
        left = 0;
      }
  
      // Limit y-axis
      if (top > self.__maxScrollTop) {
        top = self.__maxScrollTop;
      } else if (top < 0) {
        top = 0;
      }
  
      // Push values out
      self.__publish(left, top, level, animate);
  
    },
  
  
    /**
     * Zooms the content by the given factor.
     *
     * @param factor {Number} Zoom by given factor
     * @param animate {Boolean} Whether to use animation
     * @param originLeft {Number} Zoom in at given left coordinate
     * @param originTop {Number} Zoom in at given top coordinate
     */
    zoomBy: function(factor, animate, originLeft, originTop) {
      this.zoomTo(this.__zoomLevel * factor, animate, originLeft, originTop);
    },
  
  
    /**
     * Scrolls to the given position. Respect limitations and snapping automatically.
     *
     * @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
     * @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
     * @param animate {Boolean} Whether the scrolling should happen using an animation
     * @param zoom {Number} Zoom level to go to
     */
    scrollTo: function(left, top, animate, zoom, wasResize) {
      var self = this;
  
      // Stop deceleration
      if (self.__isDecelerating) {
        zyngaCore.effect.Animate.stop(self.__isDecelerating);
        self.__isDecelerating = false;
      }
  
      // Correct coordinates based on new zoom level
      if (zoom != null && zoom !== self.__zoomLevel) {
  
        if (!self.options.zooming) {
          throw new Error("Zooming is not enabled!");
        }
  
        left *= zoom;
        top *= zoom;
  
        // Recompute maximum values while temporary tweaking maximum scroll ranges
        self.__computeScrollMax(zoom);
  
      } else {
  
        // Keep zoom when not defined
        zoom = self.__zoomLevel;
  
      }
  
      if (!self.options.scrollingX) {
  
        left = self.__scrollLeft;
  
      } else {
  
        if (self.options.paging) {
          left = Math.round(left / self.__clientWidth) * self.__clientWidth;
        } else if (self.options.snapping) {
          left = Math.round(left / self.__snapWidth) * self.__snapWidth;
        }
  
      }
  
      if (!self.options.scrollingY) {
  
        top = self.__scrollTop;
  
      } else {
  
        if (self.options.paging) {
          top = Math.round(top / self.__clientHeight) * self.__clientHeight;
        } else if (self.options.snapping) {
          top = Math.round(top / self.__snapHeight) * self.__snapHeight;
        }
  
      }
  
      // Limit for allowed ranges
      left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
      top = Math.max(Math.min(self.__maxScrollTop, top), 0);
  
      // Don't animate when no change detected, still call publish to make sure
      // that rendered position is really in-sync with internal data
      if (left === self.__scrollLeft && top === self.__scrollTop) {
        animate = false;
      }
  
      // Publish new values
      self.__publish(left, top, zoom, animate, wasResize);
  
    },
  
  
    /**
     * Scroll by the given offset
     *
     * @param left {Number} Scroll x-axis by given offset
     * @param top {Number} Scroll y-axis by given offset
     * @param animate {Boolean} Whether to animate the given change
     */
    scrollBy: function(left, top, animate) {
      var self = this;
  
      var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
      var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
  
      self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
    },
  
  
  
    /*
    ---------------------------------------------------------------------------
      EVENT CALLBACKS
    ---------------------------------------------------------------------------
    */
  
    /**
     * Mouse wheel handler for zooming support
     */
    doMouseZoom: function(wheelDelta, timeStamp, pageX, pageY) {
      var change = wheelDelta > 0 ? 0.97 : 1.03;
      return this.zoomTo(this.__zoomLevel * change, false, pageX - this.__clientLeft, pageY - this.__clientTop);
    },
  
    /**
     * Touch start handler for scrolling support
     */
    doTouchStart: function(touches, timeStamp) {
      var self = this;
  
      // remember if the deceleration was just stopped
      self.__decStopped = !!(self.__isDecelerating || self.__isAnimating);
  
      self.hintResize();
  
      if (timeStamp instanceof Date) {
        timeStamp = timeStamp.valueOf();
      }
      if (typeof timeStamp !== "number") {
        timeStamp = Date.now();
      }
  
      // Reset interruptedAnimation flag
      self.__interruptedAnimation = true;
  
      // Stop deceleration
      if (self.__isDecelerating) {
        zyngaCore.effect.Animate.stop(self.__isDecelerating);
        self.__isDecelerating = false;
        self.__interruptedAnimation = true;
      }
  
      // Stop animation
      if (self.__isAnimating) {
        zyngaCore.effect.Animate.stop(self.__isAnimating);
        self.__isAnimating = false;
        self.__interruptedAnimation = true;
      }
  
      // Use center point when dealing with two fingers
      var currentTouchLeft, currentTouchTop;
      var isSingleTouch = touches.length === 1;
      if (isSingleTouch) {
        currentTouchLeft = touches[0].pageX;
        currentTouchTop = touches[0].pageY;
      } else {
        currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
        currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
      }
  
      // Store initial positions
      self.__initialTouchLeft = currentTouchLeft;
      self.__initialTouchTop = currentTouchTop;
  
      // Store initial touchList for scale calculation
      self.__initialTouches = touches;
  
      // Store current zoom level
      self.__zoomLevelStart = self.__zoomLevel;
  
      // Store initial touch positions
      self.__lastTouchLeft = currentTouchLeft;
      self.__lastTouchTop = currentTouchTop;
  
      // Store initial move time stamp
      self.__lastTouchMove = timeStamp;
  
      // Reset initial scale
      self.__lastScale = 1;
  
      // Reset locking flags
      self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
      self.__enableScrollY = !isSingleTouch && self.options.scrollingY;
  
      // Reset tracking flag
      self.__isTracking = true;
  
      // Reset deceleration complete flag
      self.__didDecelerationComplete = false;
  
      // Dragging starts directly with two fingers, otherwise lazy with an offset
      self.__isDragging = !isSingleTouch;
  
      // Some features are disabled in multi touch scenarios
      self.__isSingleTouch = isSingleTouch;
  
      // Clearing data structure
      self.__positions = [];
  
    },
  
  
    /**
     * Touch move handler for scrolling support
     */
    doTouchMove: function(touches, timeStamp, scale) {
      if (timeStamp instanceof Date) {
        timeStamp = timeStamp.valueOf();
      }
      if (typeof timeStamp !== "number") {
        timeStamp = Date.now();
      }
  
      var self = this;
  
      // Ignore event when tracking is not enabled (event might be outside of element)
      if (!self.__isTracking) {
        return;
      }
  
      var currentTouchLeft, currentTouchTop;
  
      // Compute move based around of center of fingers
      if (touches.length === 2) {
        currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
        currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
  
        // Calculate scale when not present and only when touches are used
        if (!scale && self.options.zooming) {
          scale = self.__getScale(self.__initialTouches, touches);
        }
      } else {
        currentTouchLeft = touches[0].pageX;
        currentTouchTop = touches[0].pageY;
      }
  
      var positions = self.__positions;
  
      // Are we already is dragging mode?
      if (self.__isDragging) {
          self.__decStopped = false;
  
        // Compute move distance
        var moveX = currentTouchLeft - self.__lastTouchLeft;
        var moveY = currentTouchTop - self.__lastTouchTop;
  
        // Read previous scroll position and zooming
        var scrollLeft = self.__scrollLeft;
        var scrollTop = self.__scrollTop;
        var level = self.__zoomLevel;
  
        // Work with scaling
        if (scale != null && self.options.zooming) {
  
          var oldLevel = level;
  
          // Recompute level based on previous scale and new scale
          level = level / self.__lastScale * scale;
  
          // Limit level according to configuration
          level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
  
          // Only do further compution when change happened
          if (oldLevel !== level) {
  
            // Compute relative event position to container
            var currentTouchLeftRel = currentTouchLeft - self.__clientLeft;
            var currentTouchTopRel = currentTouchTop - self.__clientTop;
  
            // Recompute left and top coordinates based on new zoom level
            scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel;
            scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel;
  
            // Recompute max scroll values
            self.__computeScrollMax(level);
  
          }
        }
  
        if (self.__enableScrollX) {
  
          scrollLeft -= moveX * self.options.speedMultiplier;
          var maxScrollLeft = self.__maxScrollLeft;
  
          if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
  
            // Slow down on the edges
            if (self.options.bouncing) {
  
              scrollLeft += (moveX / 2  * self.options.speedMultiplier);
  
            } else if (scrollLeft > maxScrollLeft) {
  
              scrollLeft = maxScrollLeft;
  
            } else {
  
              scrollLeft = 0;
  
            }
          }
        }
  
        // Compute new vertical scroll position
        if (self.__enableScrollY) {
  
          scrollTop -= moveY * self.options.speedMultiplier;
          var maxScrollTop = self.__maxScrollTop;
  
          if (scrollTop > maxScrollTop || scrollTop < 0) {
  
            // Slow down on the edges
            if (self.options.bouncing || (self.__refreshHeight && scrollTop < 0)) {
  
              scrollTop += (moveY / 2 * self.options.speedMultiplier);
  
              // Support pull-to-refresh (only when only y is scrollable)
              if (!self.__enableScrollX && self.__refreshHeight != null) {
  
                // hide the refresher when it's behind the header bar in case of header transparency
                if (scrollTop < 0) {
                  self.__refreshHidden = false;
                  self.__refreshShow();
                } else {
                  self.__refreshHide();
                  self.__refreshHidden = true;
                }
  
                if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {
  
                  self.__refreshActive = true;
                  if (self.__refreshActivate) {
                    self.__refreshActivate();
                  }
  
                } else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {
  
                  self.__refreshActive = false;
                  if (self.__refreshDeactivate) {
                    self.__refreshDeactivate();
                  }
  
                }
              }
  
            } else if (scrollTop > maxScrollTop) {
  
              scrollTop = maxScrollTop;
  
            } else {
  
              scrollTop = 0;
  
            }
          } else if (self.__refreshHeight && !self.__refreshHidden) {
            // if a positive scroll value and the refresher is still not hidden, hide it
            self.__refreshHide();
            self.__refreshHidden = true;
          }
        }
  
        // Keep list from growing infinitely (holding min 10, max 20 measure points)
        if (positions.length > 60) {
          positions.splice(0, 30);
        }
  
        // Track scroll movement for decleration
        positions.push(scrollLeft, scrollTop, timeStamp);
  
        // Sync scroll position
        self.__publish(scrollLeft, scrollTop, level);
  
      // Otherwise figure out whether we are switching into dragging mode now.
      } else {
  
        var minimumTrackingForScroll = self.options.locking ? 3 : 0;
        var minimumTrackingForDrag = 5;
  
        var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
        var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);
  
        self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
        self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;
  
        positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);
  
        self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
        if (self.__isDragging) {
          self.__interruptedAnimation = false;
          self.__fadeScrollbars('in');
        }
  
      }
  
      // Update last touch positions and time stamp for next event
      self.__lastTouchLeft = currentTouchLeft;
      self.__lastTouchTop = currentTouchTop;
      self.__lastTouchMove = timeStamp;
      self.__lastScale = scale;
  
    },
  
  
    /**
     * Touch end handler for scrolling support
     */
    doTouchEnd: function(e, timeStamp) {
      if (timeStamp instanceof Date) {
        timeStamp = timeStamp.valueOf();
      }
      if (typeof timeStamp !== "number") {
        timeStamp = Date.now();
      }
  
      var self = this;
  
      // Ignore event when tracking is not enabled (no touchstart event on element)
      // This is required as this listener ('touchmove') sits on the document and not on the element itself.
      if (!self.__isTracking) {
        return;
      }
  
      // Not touching anymore (when two finger hit the screen there are two touch end events)
      self.__isTracking = false;
  
      // Be sure to reset the dragging flag now. Here we also detect whether
      // the finger has moved fast enough to switch into a deceleration animation.
      if (self.__isDragging) {
  
        // Reset dragging flag
        self.__isDragging = false;
  
        // Start deceleration
        // Verify that the last move detected was in some relevant time frame
        if (self.__isSingleTouch && self.options.animating && (timeStamp - self.__lastTouchMove) <= 100) {
  
          // Then figure out what the scroll position was about 100ms ago
          var positions = self.__positions;
          var endPos = positions.length - 1;
          var startPos = endPos;
  
          // Move pointer to position measured 100ms ago
          for (var i = endPos; i > 0 && positions[i] > (self.__lastTouchMove - 100); i -= 3) {
            startPos = i;
          }
  
          // If start and stop position is identical in a 100ms timeframe,
          // we cannot compute any useful deceleration.
          if (startPos !== endPos) {
  
            // Compute relative movement between these two points
            var timeOffset = positions[endPos] - positions[startPos];
            var movedLeft = self.__scrollLeft - positions[startPos - 2];
            var movedTop = self.__scrollTop - positions[startPos - 1];
  
            // Based on 50ms compute the movement to apply for each render step
            self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60);
            self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);
  
            // How much velocity is required to start the deceleration
            var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? self.options.decelVelocityThresholdPaging : self.options.decelVelocityThreshold;
  
            // Verify that we have enough velocity to start deceleration
            if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {
  
              // Deactivate pull-to-refresh when decelerating
              if (!self.__refreshActive) {
                self.__startDeceleration(timeStamp);
              }
            }
          } else {
            self.__scrollingComplete();
          }
        } else if ((timeStamp - self.__lastTouchMove) > 100) {
          self.__scrollingComplete();
        }
  
      } else if (self.__decStopped) {
        // the deceleration was stopped
        // user flicked the scroll fast, and stop dragging, then did a touchstart to stop the srolling
        // tell the touchend event code to do nothing, we don't want to actually send a click
        e.isTapHandled = true;
        self.__decStopped = false;
      }
  
      // If this was a slower move it is per default non decelerated, but this
      // still means that we want snap back to the bounds which is done here.
      // This is placed outside the condition above to improve edge case stability
      // e.g. touchend fired without enabled dragging. This should normally do not
      // have modified the scroll positions or even showed the scrollbars though.
      if (!self.__isDecelerating) {
  
        if (self.__refreshActive && self.__refreshStart) {
  
          // Use publish instead of scrollTo to allow scrolling to out of boundary position
          // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
          self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);
  
          var d = new Date();
          self.refreshStartTime = d.getTime();
  
          if (self.__refreshStart) {
            self.__refreshStart();
          }
          // for iOS-ey style scrolling
          if (!ionic.Platform.isAndroid())self.__startDeceleration();
        } else {
  
          if (self.__interruptedAnimation || self.__isDragging) {
            self.__scrollingComplete();
          }
          self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);
  
          // Directly signalize deactivation (nothing todo on refresh?)
          if (self.__refreshActive) {
  
            self.__refreshActive = false;
            if (self.__refreshDeactivate) {
              self.__refreshDeactivate();
            }
  
          }
        }
      }
  
      // Fully cleanup list
      self.__positions.length = 0;
  
    },
  
  
  
    /*
    ---------------------------------------------------------------------------
      PRIVATE API
    ---------------------------------------------------------------------------
    */
  
    /**
     * Applies the scroll position to the content element
     *
     * @param left {Number} Left scroll position
     * @param top {Number} Top scroll position
     * @param animate {Boolean} Whether animation should be used to move to the new coordinates
     */
    __publish: function(left, top, zoom, animate, wasResize) {
  
      var self = this;
  
      // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
      var wasAnimating = self.__isAnimating;
      if (wasAnimating) {
        zyngaCore.effect.Animate.stop(wasAnimating);
        self.__isAnimating = false;
      }
  
      if (animate && self.options.animating) {
  
        // Keep scheduled positions for scrollBy/zoomBy functionality
        self.__scheduledLeft = left;
        self.__scheduledTop = top;
        self.__scheduledZoom = zoom;
  
        var oldLeft = self.__scrollLeft;
        var oldTop = self.__scrollTop;
        var oldZoom = self.__zoomLevel;
  
        var diffLeft = left - oldLeft;
        var diffTop = top - oldTop;
        var diffZoom = zoom - oldZoom;
  
        var step = function(percent, now, render) {
  
          if (render) {
  
            self.__scrollLeft = oldLeft + (diffLeft * percent);
            self.__scrollTop = oldTop + (diffTop * percent);
            self.__zoomLevel = oldZoom + (diffZoom * percent);
  
            // Push values out
            if (self.__callback) {
              self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel, wasResize);
            }
  
          }
        };
  
        var verify = function(id) {
          return self.__isAnimating === id;
        };
  
        var completed = function(renderedFramesPerSecond, animationId, wasFinished) {
          if (animationId === self.__isAnimating) {
            self.__isAnimating = false;
          }
          if (self.__didDecelerationComplete || wasFinished) {
            self.__scrollingComplete();
          }
  
          if (self.options.zooming) {
            self.__computeScrollMax();
          }
        };
  
        // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
        self.__isAnimating = zyngaCore.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);
  
      } else {
  
        self.__scheduledLeft = self.__scrollLeft = left;
        self.__scheduledTop = self.__scrollTop = top;
        self.__scheduledZoom = self.__zoomLevel = zoom;
  
        // Push values out
        if (self.__callback) {
          self.__callback(left, top, zoom, wasResize);
        }
  
        // Fix max scroll ranges
        if (self.options.zooming) {
          self.__computeScrollMax();
        }
      }
    },
  
  
    /**
     * Recomputes scroll minimum values based on client dimensions and content dimensions.
     */
    __computeScrollMax: function(zoomLevel) {
      var self = this;
  
      if (zoomLevel == null) {
        zoomLevel = self.__zoomLevel;
      }
  
      self.__maxScrollLeft = Math.max((self.__contentWidth * zoomLevel) - self.__clientWidth, 0);
      self.__maxScrollTop = Math.max((self.__contentHeight * zoomLevel) - self.__clientHeight, 0);
  
      if (!self.__didWaitForSize && !self.__maxScrollLeft && !self.__maxScrollTop) {
        self.__didWaitForSize = true;
        self.__waitForSize();
      }
    },
  
  
    /**
     * If the scroll view isn't sized correctly on start, wait until we have at least some size
     */
    __waitForSize: function() {
      var self = this;
  
      clearTimeout(self.__sizerTimeout);
  
      var sizer = function() {
        self.resize(true);
      };
  
      sizer();
      self.__sizerTimeout = setTimeout(sizer, 500);
    },
  
    /*
    ---------------------------------------------------------------------------
      ANIMATION (DECELERATION) SUPPORT
    ---------------------------------------------------------------------------
    */
  
    /**
     * Called when a touch sequence end and the speed of the finger was high enough
     * to switch into deceleration mode.
     */
    __startDeceleration: function(timeStamp) {
      var self = this;
  
      if (self.options.paging) {
  
        var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0);
        var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0);
        var clientWidth = self.__clientWidth;
        var clientHeight = self.__clientHeight;
  
        // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
        // Each page should have exactly the size of the client area.
        self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
        self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
        self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
        self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;
  
      } else {
  
        self.__minDecelerationScrollLeft = 0;
        self.__minDecelerationScrollTop = 0;
        self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
        self.__maxDecelerationScrollTop = self.__maxScrollTop;
        if (self.__refreshActive) self.__minDecelerationScrollTop = self.__refreshHeight * -1;
      }
  
      // Wrap class method
      var step = function(percent, now, render) {
        self.__stepThroughDeceleration(render);
      };
  
      // How much velocity is required to keep the deceleration running
      self.__minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1;
  
      // Detect whether it's still worth to continue animating steps
      // If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
      var verify = function() {
        var shouldContinue = Math.abs(self.__decelerationVelocityX) >= self.__minVelocityToKeepDecelerating ||
          Math.abs(self.__decelerationVelocityY) >= self.__minVelocityToKeepDecelerating;
        if (!shouldContinue) {
          self.__didDecelerationComplete = true;
  
          //Make sure the scroll values are within the boundaries after a bounce,
          //not below 0 or above maximum
          if (self.options.bouncing && !self.__refreshActive) {
            self.scrollTo(
              Math.min( Math.max(self.__scrollLeft, 0), self.__maxScrollLeft ),
              Math.min( Math.max(self.__scrollTop, 0), self.__maxScrollTop ),
              self.__refreshActive
            );
          }
        }
        return shouldContinue;
      };
  
      var completed = function(renderedFramesPerSecond, animationId, wasFinished) {
        self.__isDecelerating = false;
        if (self.__didDecelerationComplete) {
          self.__scrollingComplete();
        }
  
        // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
        if (self.options.paging) {
          self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
        }
      };
  
      // Start animation and switch on flag
      self.__isDecelerating = zyngaCore.effect.Animate.start(step, verify, completed);
  
    },
  
  
    /**
     * Called on every step of the animation
     *
     * @param inMemory {Boolean} Whether to not render the current step, but keep it in memory only. Used internally only!
     */
    __stepThroughDeceleration: function(render) {
      var self = this;
  
  
      //
      // COMPUTE NEXT SCROLL POSITION
      //
  
      // Add deceleration to scroll position
      var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;// * self.options.deceleration);
      var scrollTop = self.__scrollTop + self.__decelerationVelocityY;// * self.options.deceleration);
  
  
      //
      // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
      //
  
      if (!self.options.bouncing) {
  
        var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
        if (scrollLeftFixed !== scrollLeft) {
          scrollLeft = scrollLeftFixed;
          self.__decelerationVelocityX = 0;
        }
  
        var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
        if (scrollTopFixed !== scrollTop) {
          scrollTop = scrollTopFixed;
          self.__decelerationVelocityY = 0;
        }
  
      }
  
  
      //
      // UPDATE SCROLL POSITION
      //
  
      if (render) {
  
        self.__publish(scrollLeft, scrollTop, self.__zoomLevel);
  
      } else {
  
        self.__scrollLeft = scrollLeft;
        self.__scrollTop = scrollTop;
  
      }
  
  
      //
      // SLOW DOWN
      //
  
      // Slow down velocity on every iteration
      if (!self.options.paging) {
  
        // This is the factor applied to every iteration of the animation
        // to slow down the process. This should emulate natural behavior where
        // objects slow down when the initiator of the movement is removed
        var frictionFactor = self.options.deceleration;
  
        self.__decelerationVelocityX *= frictionFactor;
        self.__decelerationVelocityY *= frictionFactor;
  
      }
  
  
      //
      // BOUNCING SUPPORT
      //
  
      if (self.options.bouncing) {
  
        var scrollOutsideX = 0;
        var scrollOutsideY = 0;
  
        // This configures the amount of change applied to deceleration/acceleration when reaching boundaries
        var penetrationDeceleration = self.options.penetrationDeceleration;
        var penetrationAcceleration = self.options.penetrationAcceleration;
  
        // Check limits
        if (scrollLeft < self.__minDecelerationScrollLeft) {
          scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
        } else if (scrollLeft > self.__maxDecelerationScrollLeft) {
          scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
        }
  
        if (scrollTop < self.__minDecelerationScrollTop) {
          scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
        } else if (scrollTop > self.__maxDecelerationScrollTop) {
          scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
        }
  
        // Slow down until slow enough, then flip back to snap position
        if (scrollOutsideX !== 0) {
          var isHeadingOutwardsX = scrollOutsideX * self.__decelerationVelocityX <= self.__minDecelerationScrollLeft;
          if (isHeadingOutwardsX) {
            self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
          }
          var isStoppedX = Math.abs(self.__decelerationVelocityX) <= self.__minVelocityToKeepDecelerating;
          //If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds
          if (!isHeadingOutwardsX || isStoppedX) {
            self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
          }
        }
  
        if (scrollOutsideY !== 0) {
          var isHeadingOutwardsY = scrollOutsideY * self.__decelerationVelocityY <= self.__minDecelerationScrollTop;
          if (isHeadingOutwardsY) {
            self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
          }
          var isStoppedY = Math.abs(self.__decelerationVelocityY) <= self.__minVelocityToKeepDecelerating;
          //If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds
          if (!isHeadingOutwardsY || isStoppedY) {
            self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
          }
        }
      }
    },
  
  
    /**
     * calculate the distance between two touches
     * @param   {Touch}     touch1
     * @param   {Touch}     touch2
     * @returns {Number}    distance
     */
    __getDistance: function getDistance(touch1, touch2) {
      var x = touch2.pageX - touch1.pageX,
      y = touch2.pageY - touch1.pageY;
      return Math.sqrt((x*x) + (y*y));
    },
  
  
    /**
     * calculate the scale factor between two touchLists (fingers)
     * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
     * @param   {Array}     start
     * @param   {Array}     end
     * @returns {Number}    scale
     */
    __getScale: function getScale(start, end) {
      // need two fingers...
      if (start.length >= 2 && end.length >= 2) {
        return this.__getDistance(end[0], end[1]) /
          this.__getDistance(start[0], start[1]);
      }
      return 1;
    }
  });
  
  ionic.scroll = {
    isScrolling: false,
    lastTop: 0
  };
  
  })(ionic);
  
  (function(ionic) {
  'use strict';
  
    var ITEM_CLASS = 'item';
    var ITEM_CONTENT_CLASS = 'item-content';
    var ITEM_SLIDING_CLASS = 'item-sliding';
    var ITEM_OPTIONS_CLASS = 'item-options';
    var ITEM_PLACEHOLDER_CLASS = 'item-placeholder';
    var ITEM_REORDERING_CLASS = 'item-reordering';
    var ITEM_REORDER_BTN_CLASS = 'item-reorder';
  
    var DragOp = function() {};
    DragOp.prototype = {
      start: function(e) {
      },
      drag: function(e) {
      },
      end: function(e) {
      },
      isSameItem: function(item) {
        return false;
      }
    };
  
    var SlideDrag = function(opts) {
      this.dragThresholdX = opts.dragThresholdX || 10;
      this.el = opts.el;
      this.item = opts.item;
      this.canSwipe = opts.canSwipe;
    };
  
    SlideDrag.prototype = new DragOp();
  
    SlideDrag.prototype.start = function(e) {
      var content, buttons, offsetX, buttonsWidth;
  
      if (!this.canSwipe()) {
        return;
      }
  
      if (e.target.classList.contains(ITEM_CONTENT_CLASS)) {
        content = e.target;
      } else if (e.target.classList.contains(ITEM_CLASS)) {
        content = e.target.querySelector('.' + ITEM_CONTENT_CLASS);
      } else {
        content = ionic.DomUtil.getParentWithClass(e.target, ITEM_CONTENT_CLASS);
      }
  
      // If we don't have a content area as one of our children (or ourselves), skip
      if (!content) {
        return;
      }
  
      // Make sure we aren't animating as we slide
      content.classList.remove(ITEM_SLIDING_CLASS);
  
      // Grab the starting X point for the item (for example, so we can tell whether it is open or closed to start)
      offsetX = parseFloat(content.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]) || 0;
  
      // Grab the buttons
      buttons = content.parentNode.querySelector('.' + ITEM_OPTIONS_CLASS);
      if (!buttons) {
        return;
      }
      buttons.classList.remove('invisible');
  
      buttonsWidth = buttons.offsetWidth;
  
      this._currentDrag = {
        buttons: buttons,
        buttonsWidth: buttonsWidth,
        content: content,
        startOffsetX: offsetX
      };
    };
  
    /**
     * Check if this is the same item that was previously dragged.
     */
    SlideDrag.prototype.isSameItem = function(op) {
      if (op._lastDrag && this._currentDrag) {
        return this._currentDrag.content == op._lastDrag.content;
      }
      return false;
    };
  
    SlideDrag.prototype.clean = function(isInstant) {
      var lastDrag = this._lastDrag;
  
      if (!lastDrag || !lastDrag.content) return;
  
      lastDrag.content.style[ionic.CSS.TRANSITION] = '';
      lastDrag.content.style[ionic.CSS.TRANSFORM] = '';
      if (isInstant) {
        lastDrag.content.style[ionic.CSS.TRANSITION] = 'none';
        makeInvisible();
        ionic.requestAnimationFrame(function() {
          lastDrag.content.style[ionic.CSS.TRANSITION] = '';
        });
      } else {
        ionic.requestAnimationFrame(function() {
          setTimeout(makeInvisible, 250);
        });
      }
      function makeInvisible() {
        lastDrag.buttons && lastDrag.buttons.classList.add('invisible');
      }
    };
  
    SlideDrag.prototype.drag = ionic.animationFrameThrottle(function(e) {
      var buttonsWidth;
  
      // We really aren't dragging
      if (!this._currentDrag) {
        return;
      }
  
      // Check if we should start dragging. Check if we've dragged past the threshold,
      // or we are starting from the open state.
      if (!this._isDragging &&
          ((Math.abs(e.gesture.deltaX) > this.dragThresholdX) ||
          (Math.abs(this._currentDrag.startOffsetX) > 0))) {
        this._isDragging = true;
      }
  
      if (this._isDragging) {
        buttonsWidth = this._currentDrag.buttonsWidth;
  
        // Grab the new X point, capping it at zero
        var newX = Math.min(0, this._currentDrag.startOffsetX + e.gesture.deltaX);
  
        // If the new X position is past the buttons, we need to slow down the drag (rubber band style)
        if (newX < -buttonsWidth) {
          // Calculate the new X position, capped at the top of the buttons
          newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4)));
        }
  
        this._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + newX + 'px, 0, 0)';
        this._currentDrag.content.style[ionic.CSS.TRANSITION] = 'none';
      }
    });
  
    SlideDrag.prototype.end = function(e, doneCallback) {
      var self = this;
  
      // There is no drag, just end immediately
      if (!self._currentDrag) {
        doneCallback && doneCallback();
        return;
      }
  
      // If we are currently dragging, we want to snap back into place
      // The final resting point X will be the width of the exposed buttons
      var restingPoint = -self._currentDrag.buttonsWidth;
  
      // Check if the drag didn't clear the buttons mid-point
      // and we aren't moving fast enough to swipe open
      if (e.gesture.deltaX > -(self._currentDrag.buttonsWidth / 2)) {
  
        // If we are going left but too slow, or going right, go back to resting
        if (e.gesture.direction == "left" && Math.abs(e.gesture.velocityX) < 0.3) {
          restingPoint = 0;
  
        } else if (e.gesture.direction == "right") {
          restingPoint = 0;
        }
  
      }
  
      ionic.requestAnimationFrame(function() {
        if (restingPoint === 0) {
          self._currentDrag.content.style[ionic.CSS.TRANSFORM] = '';
          var buttons = self._currentDrag.buttons;
          setTimeout(function() {
            buttons && buttons.classList.add('invisible');
          }, 250);
        } else {
          self._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + restingPoint + 'px,0,0)';
        }
        self._currentDrag.content.style[ionic.CSS.TRANSITION] = '';
  
  
        // Kill the current drag
        if (!self._lastDrag) {
          self._lastDrag = {};
        }
        ionic.extend(self._lastDrag, self._currentDrag);
        if (self._currentDrag) {
          self._currentDrag.buttons = null;
          self._currentDrag.content = null;
        }
        self._currentDrag = null;
  
        // We are done, notify caller
        doneCallback && doneCallback();
      });
    };
  
    var ReorderDrag = function(opts) {
      var self = this;
  
      self.dragThresholdY = opts.dragThresholdY || 0;
      self.onReorder = opts.onReorder;
      self.listEl = opts.listEl;
      self.el = self.item = opts.el;
      self.scrollEl = opts.scrollEl;
      self.scrollView = opts.scrollView;
      // Get the True Top of the list el http://www.quirksmode.org/js/findpos.html
      self.listElTrueTop = 0;
      if (self.listEl.offsetParent) {
        var obj = self.listEl;
        do {
          self.listElTrueTop += obj.offsetTop;
          obj = obj.offsetParent;
        } while (obj);
      }
    };
  
    ReorderDrag.prototype = new DragOp();
  
    ReorderDrag.prototype._moveElement = function(e) {
      var y = e.gesture.center.pageY +
        this.scrollView.getValues().top -
        (this._currentDrag.elementHeight / 2) -
        this.listElTrueTop;
      this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(0, ' + y + 'px, 0)';
    };
  
    ReorderDrag.prototype.deregister = function() {
      this.listEl = this.el = this.scrollEl = this.scrollView = null;
    };
  
    ReorderDrag.prototype.start = function(e) {
      var content;
  
      var startIndex = ionic.DomUtil.getChildIndex(this.el, this.el.nodeName.toLowerCase());
      var elementHeight = this.el.scrollHeight;
      var placeholder = this.el.cloneNode(true);
  
      placeholder.classList.add(ITEM_PLACEHOLDER_CLASS);
  
      this.el.parentNode.insertBefore(placeholder, this.el);
      this.el.classList.add(ITEM_REORDERING_CLASS);
  
      this._currentDrag = {
        elementHeight: elementHeight,
        startIndex: startIndex,
        placeholder: placeholder,
        scrollHeight: scroll,
        list: placeholder.parentNode
      };
  
      this._moveElement(e);
    };
  
    ReorderDrag.prototype.drag = ionic.animationFrameThrottle(function(e) {
      // We really aren't dragging
      var self = this;
      if (!this._currentDrag) {
        return;
      }
  
      var scrollY = 0;
      var pageY = e.gesture.center.pageY;
      var offset = this.listElTrueTop;
  
      //If we have a scrollView, check scroll boundaries for dragged element and scroll if necessary
      if (this.scrollView) {
  
        var container = this.scrollView.__container;
        scrollY = this.scrollView.getValues().top;
  
        var containerTop = container.offsetTop;
        var pixelsPastTop = containerTop - pageY + this._currentDrag.elementHeight / 2;
        var pixelsPastBottom = pageY + this._currentDrag.elementHeight / 2 - containerTop - container.offsetHeight;
  
        if (e.gesture.deltaY < 0 && pixelsPastTop > 0 && scrollY > 0) {
          this.scrollView.scrollBy(null, -pixelsPastTop);
          //Trigger another drag so the scrolling keeps going
          ionic.requestAnimationFrame(function() {
            self.drag(e);
          });
        }
        if (e.gesture.deltaY > 0 && pixelsPastBottom > 0) {
          if (scrollY < this.scrollView.getScrollMax().top) {
            this.scrollView.scrollBy(null, pixelsPastBottom);
            //Trigger another drag so the scrolling keeps going
            ionic.requestAnimationFrame(function() {
              self.drag(e);
            });
          }
        }
      }
  
      // Check if we should start dragging. Check if we've dragged past the threshold,
      // or we are starting from the open state.
      if (!this._isDragging && Math.abs(e.gesture.deltaY) > this.dragThresholdY) {
        this._isDragging = true;
      }
  
      if (this._isDragging) {
        this._moveElement(e);
  
        this._currentDrag.currentY = scrollY + pageY - offset;
  
        // this._reorderItems();
      }
    });
  
    // When an item is dragged, we need to reorder any items for sorting purposes
    ReorderDrag.prototype._getReorderIndex = function() {
      var self = this;
  
      var placeholder = self._currentDrag.placeholder;
      var siblings = Array.prototype.slice.call(self._currentDrag.placeholder.parentNode.children)
        .filter(function(el) {
          return el.nodeName === self.el.nodeName && el !== self.el;
        });
  
      var dragOffsetTop = self._currentDrag.currentY;
      var el;
      for (var i = 0, len = siblings.length; i < len; i++) {
        el = siblings[i];
        if (i === len - 1) {
          if (dragOffsetTop > el.offsetTop) {
            return i;
          }
        } else if (i === 0) {
          if (dragOffsetTop < el.offsetTop + el.offsetHeight) {
            return i;
          }
        } else if (dragOffsetTop > el.offsetTop - el.offsetHeight / 2 &&
                   dragOffsetTop < el.offsetTop + el.offsetHeight) {
          return i;
        }
      }
      return self._currentDrag.startIndex;
    };
  
    ReorderDrag.prototype.end = function(e, doneCallback) {
      if (!this._currentDrag) {
        doneCallback && doneCallback();
        return;
      }
  
      var placeholder = this._currentDrag.placeholder;
      var finalIndex = this._getReorderIndex();
  
      // Reposition the element
      this.el.classList.remove(ITEM_REORDERING_CLASS);
      this.el.style[ionic.CSS.TRANSFORM] = '';
  
      placeholder.parentNode.insertBefore(this.el, placeholder);
      placeholder.parentNode.removeChild(placeholder);
  
      this.onReorder && this.onReorder(this.el, this._currentDrag.startIndex, finalIndex);
  
      this._currentDrag = {
        placeholder: null,
        content: null
      };
      this._currentDrag = null;
      doneCallback && doneCallback();
    };
  
  
  
    /**
     * The ListView handles a list of items. It will process drag animations, edit mode,
     * and other operations that are common on mobile lists or table views.
     */
    ionic.views.ListView = ionic.views.View.inherit({
      initialize: function(opts) {
        var self = this;
  
        opts = ionic.extend({
          onReorder: function(el, oldIndex, newIndex) {},
          virtualRemoveThreshold: -200,
          virtualAddThreshold: 200,
          canSwipe: function() {
            return true;
          }
        }, opts);
  
        ionic.extend(self, opts);
  
        if (!self.itemHeight && self.listEl) {
          self.itemHeight = self.listEl.children[0] && parseInt(self.listEl.children[0].style.height, 10);
        }
  
        self.onRefresh = opts.onRefresh || function() {};
        self.onRefreshOpening = opts.onRefreshOpening || function() {};
        self.onRefreshHolding = opts.onRefreshHolding || function() {};
  
        window.ionic.onGesture('release', function(e) {
          self._handleEndDrag(e);
        }, self.el);
  
        window.ionic.onGesture('drag', function(e) {
          self._handleDrag(e);
        }, self.el);
        // Start the drag states
        self._initDrag();
      },
  
      /**
       * Be sure to cleanup references.
       */
      deregister: function() {
        this.el = this.listEl = this.scrollEl = this.scrollView = null;
  
        // ensure no scrolls have been left frozen
        if (this.isScrollFreeze) {
          self.scrollView.freeze(false);
        }
      },
  
      /**
       * Called to tell the list to stop refreshing. This is useful
       * if you are refreshing the list and are done with refreshing.
       */
      stopRefreshing: function() {
        var refresher = this.el.querySelector('.list-refresher');
        refresher.style.height = '0';
      },
  
      /**
       * If we scrolled and have virtual mode enabled, compute the window
       * of active elements in order to figure out the viewport to render.
       */
      didScroll: function(e) {
        var self = this;
  
        if (self.isVirtual) {
          var itemHeight = self.itemHeight;
  
          // TODO: This would be inaccurate if we are windowed
          var totalItems = self.listEl.children.length;
  
          // Grab the total height of the list
          var scrollHeight = e.target.scrollHeight;
  
          // Get the viewport height
          var viewportHeight = self.el.parentNode.offsetHeight;
  
          // scrollTop is the current scroll position
          var scrollTop = e.scrollTop;
  
          // High water is the pixel position of the first element to include (everything before
          // that will be removed)
          var highWater = Math.max(0, e.scrollTop + self.virtualRemoveThreshold);
  
          // Low water is the pixel position of the last element to include (everything after
          // that will be removed)
          var lowWater = Math.min(scrollHeight, Math.abs(e.scrollTop) + viewportHeight + self.virtualAddThreshold);
  
          // Compute how many items per viewport size can show
          var itemsPerViewport = Math.floor((lowWater - highWater) / itemHeight);
  
          // Get the first and last elements in the list based on how many can fit
          // between the pixel range of lowWater and highWater
          var first = parseInt(Math.abs(highWater / itemHeight), 10);
          var last = parseInt(Math.abs(lowWater / itemHeight), 10);
  
          // Get the items we need to remove
          self._virtualItemsToRemove = Array.prototype.slice.call(self.listEl.children, 0, first);
  
          // Grab the nodes we will be showing
          var nodes = Array.prototype.slice.call(self.listEl.children, first, first + itemsPerViewport);
  
          self.renderViewport && self.renderViewport(highWater, lowWater, first, last);
        }
      },
  
      didStopScrolling: function(e) {
        if (this.isVirtual) {
          for (var i = 0; i < this._virtualItemsToRemove.length; i++) {
            var el = this._virtualItemsToRemove[i];
            //el.parentNode.removeChild(el);
            this.didHideItem && this.didHideItem(i);
          }
          // Once scrolling stops, check if we need to remove old items
  
        }
      },
  
      /**
       * Clear any active drag effects on the list.
       */
      clearDragEffects: function(isInstant) {
        if (this._lastDragOp) {
          this._lastDragOp.clean && this._lastDragOp.clean(isInstant);
          this._lastDragOp.deregister && this._lastDragOp.deregister();
          this._lastDragOp = null;
        }
      },
  
      _initDrag: function() {
        // Store the last one
        if (this._lastDragOp) {
          this._lastDragOp.deregister && this._lastDragOp.deregister();
        }
        this._lastDragOp = this._dragOp;
  
        this._dragOp = null;
      },
  
      // Return the list item from the given target
      _getItem: function(target) {
        while (target) {
          if (target.classList && target.classList.contains(ITEM_CLASS)) {
            return target;
          }
          target = target.parentNode;
        }
        return null;
      },
  
  
      _startDrag: function(e) {
        var self = this;
  
        var didStart = false;
  
        self._isDragging = false;
  
        var lastDragOp = self._lastDragOp;
        var item;
  
        // If we have an open SlideDrag and we're scrolling the list. Clear it.
        if (self._didDragUpOrDown && lastDragOp instanceof SlideDrag) {
            lastDragOp.clean && lastDragOp.clean();
        }
  
        // Check if this is a reorder drag
        if (ionic.DomUtil.getParentOrSelfWithClass(e.target, ITEM_REORDER_BTN_CLASS) && (e.gesture.direction == 'up' || e.gesture.direction == 'down')) {
          item = self._getItem(e.target);
  
          if (item) {
            self._dragOp = new ReorderDrag({
              listEl: self.el,
              el: item,
              scrollEl: self.scrollEl,
              scrollView: self.scrollView,
              onReorder: function(el, start, end) {
                self.onReorder && self.onReorder(el, start, end);
              }
            });
            self._dragOp.start(e);
            e.preventDefault();
          }
        }
  
        // Or check if this is a swipe to the side drag
        else if (!self._didDragUpOrDown && (e.gesture.direction == 'left' || e.gesture.direction == 'right') && Math.abs(e.gesture.deltaX) > 5) {
  
          // Make sure this is an item with buttons
          item = self._getItem(e.target);
          if (item && item.querySelector('.item-options')) {
            self._dragOp = new SlideDrag({
              el: self.el,
              item: item,
              canSwipe: self.canSwipe
            });
            self._dragOp.start(e);
            e.preventDefault();
            self.isScrollFreeze = self.scrollView.freeze(true);
          }
        }
  
        // If we had a last drag operation and this is a new one on a different item, clean that last one
        if (lastDragOp && self._dragOp && !self._dragOp.isSameItem(lastDragOp) && e.defaultPrevented) {
          lastDragOp.clean && lastDragOp.clean();
        }
      },
  
  
      _handleEndDrag: function(e) {
        var self = this;
  
        if (self.scrollView) {
          self.isScrollFreeze = self.scrollView.freeze(false);
        }
  
        self._didDragUpOrDown = false;
  
        if (!self._dragOp) {
          return;
        }
  
        self._dragOp.end(e, function() {
          self._initDrag();
        });
      },
  
      /**
       * Process the drag event to move the item to the left or right.
       */
      _handleDrag: function(e) {
        var self = this, content, buttons;
  
        if (Math.abs(e.gesture.deltaY) > 5) {
          self._didDragUpOrDown = true;
        }
  
        // If we get a drag event, make sure we aren't in another drag, then check if we should
        // start one
        if (!self.isDragging && !self._dragOp) {
          self._startDrag(e);
        }
  
        // No drag still, pass it up
        if (!self._dragOp) {
          return;
        }
  
        e.gesture.srcEvent.preventDefault();
        self._dragOp.drag(e);
      }
  
    });
  
  })(ionic);
  
  (function(ionic) {
  'use strict';
  
    ionic.views.Modal = ionic.views.View.inherit({
      initialize: function(opts) {
        opts = ionic.extend({
          focusFirstInput: false,
          unfocusOnHide: true,
          focusFirstDelay: 600,
          backdropClickToClose: true,
          hardwareBackButtonClose: true,
        }, opts);
  
        ionic.extend(this, opts);
  
        this.el = opts.el;
      },
      show: function() {
        var self = this;
  
        if(self.focusFirstInput) {
          // Let any animations run first
          window.setTimeout(function() {
            var input = self.el.querySelector('input, textarea');
            input && input.focus && input.focus();
          }, self.focusFirstDelay);
        }
      },
      hide: function() {
        // Unfocus all elements
        if(this.unfocusOnHide) {
          var inputs = this.el.querySelectorAll('input, textarea');
          // Let any animations run first
          window.setTimeout(function() {
            for(var i = 0; i < inputs.length; i++) {
              inputs[i].blur && inputs[i].blur();
            }
          });
        }
      }
    });
  
  })(ionic);
  
  (function(ionic) {
  'use strict';
  
    /**
     * The side menu view handles one of the side menu's in a Side Menu Controller
     * configuration.
     * It takes a DOM reference to that side menu element.
     */
    ionic.views.SideMenu = ionic.views.View.inherit({
      initialize: function(opts) {
        this.el = opts.el;
        this.isEnabled = (typeof opts.isEnabled === 'undefined') ? true : opts.isEnabled;
        this.setWidth(opts.width);
      },
      getFullWidth: function() {
        return this.width;
      },
      setWidth: function(width) {
        this.width = width;
        this.el.style.width = width + 'px';
      },
      setIsEnabled: function(isEnabled) {
        this.isEnabled = isEnabled;
      },
      bringUp: function() {
        if(this.el.style.zIndex !== '0') {
          this.el.style.zIndex = '0';
        }
      },
      pushDown: function() {
        if(this.el.style.zIndex !== '-1') {
          this.el.style.zIndex = '-1';
        }
      }
    });
  
    ionic.views.SideMenuContent = ionic.views.View.inherit({
      initialize: function(opts) {
        ionic.extend(this, {
          animationClass: 'menu-animated',
          onDrag: function(e) {},
          onEndDrag: function(e) {}
        }, opts);
  
        ionic.onGesture('drag', ionic.proxy(this._onDrag, this), this.el);
        ionic.onGesture('release', ionic.proxy(this._onEndDrag, this), this.el);
      },
      _onDrag: function(e) {
        this.onDrag && this.onDrag(e);
      },
      _onEndDrag: function(e) {
        this.onEndDrag && this.onEndDrag(e);
      },
      disableAnimation: function() {
        this.el.classList.remove(this.animationClass);
      },
      enableAnimation: function() {
        this.el.classList.add(this.animationClass);
      },
      getTranslateX: function() {
        return parseFloat(this.el.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]);
      },
      setTranslateX: ionic.animationFrameThrottle(function(x) {
        this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(' + x + 'px, 0, 0)';
      })
    });
  
  })(ionic);
  
  /*
   * Adapted from Swipe.js 2.0
   *
   * Brad Birdsall
   * Copyright 2013, MIT License
   *
  */
  
  (function(ionic) {
  'use strict';
  
  ionic.views.Slider = ionic.views.View.inherit({
    initialize: function (options) {
      var slider = this;
  
      // utilities
      var noop = function() {}; // simple no operation function
      var offloadFn = function(fn) { setTimeout(fn || noop, 0); }; // offload a functions execution
  
      // check browser capabilities
      var browser = {
        addEventListener: !!window.addEventListener,
        touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch,
        transitions: (function(temp) {
          var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition'];
          for ( var i in props ) if (temp.style[ props[i] ] !== undefined) return true;
          return false;
        })(document.createElement('swipe'))
      };
  
  
      var container = options.el;
  
      // quit if no root element
      if (!container) return;
      var element = container.children[0];
      var slides, slidePos, width, length;
      options = options || {};
      var index = parseInt(options.startSlide, 10) || 0;
      var speed = options.speed || 300;
      options.continuous = options.continuous !== undefined ? options.continuous : true;
  
      function setup() {
  
        // cache slides
        slides = element.children;
        length = slides.length;
  
        // set continuous to false if only one slide
        if (slides.length < 2) options.continuous = false;
  
        //special case if two slides
        if (browser.transitions && options.continuous && slides.length < 3) {
          element.appendChild(slides[0].cloneNode(true));
          element.appendChild(element.children[1].cloneNode(true));
          slides = element.children;
        }
  
        // create an array to store current positions of each slide
        slidePos = new Array(slides.length);
  
        // determine width of each slide
        width = container.offsetWidth || container.getBoundingClientRect().width;
  
        element.style.width = (slides.length * width) + 'px';
  
        // stack elements
        var pos = slides.length;
        while(pos--) {
  
          var slide = slides[pos];
  
          slide.style.width = width + 'px';
          slide.setAttribute('data-index', pos);
  
          if (browser.transitions) {
            slide.style.left = (pos * -width) + 'px';
            move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
          }
  
        }
  
        // reposition elements before and after index
        if (options.continuous && browser.transitions) {
          move(circle(index-1), -width, 0);
          move(circle(index+1), width, 0);
        }
  
        if (!browser.transitions) element.style.left = (index * -width) + 'px';
  
        container.style.visibility = 'visible';
  
        options.slidesChanged && options.slidesChanged();
      }
  
      function prev() {
  
        if (options.continuous) slide(index-1);
        else if (index) slide(index-1);
  
      }
  
      function next() {
  
        if (options.continuous) slide(index+1);
        else if (index < slides.length - 1) slide(index+1);
  
      }
  
      function circle(index) {
  
        // a simple positive modulo using slides.length
        return (slides.length + (index % slides.length)) % slides.length;
  
      }
  
      function slide(to, slideSpeed) {
  
        // do nothing if already on requested slide
        if (index == to) return;
  
        if (browser.transitions) {
  
          var direction = Math.abs(index-to) / (index-to); // 1: backward, -1: forward
  
          // get the actual position of the slide
          if (options.continuous) {
            var natural_direction = direction;
            direction = -slidePos[circle(to)] / width;
  
            // if going forward but to < index, use to = slides.length + to
            // if going backward but to > index, use to = -slides.length + to
            if (direction !== natural_direction) to =  -direction * slides.length + to;
  
          }
  
          var diff = Math.abs(index-to) - 1;
  
          // move all the slides between index and to in the right direction
          while (diff--) move( circle((to > index ? to : index) - diff - 1), width * direction, 0);
  
          to = circle(to);
  
          move(index, width * direction, slideSpeed || speed);
          move(to, 0, slideSpeed || speed);
  
          if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place
  
        } else {
  
          to = circle(to);
          animate(index * -width, to * -width, slideSpeed || speed);
          //no fallback for a circular continuous if the browser does not accept transitions
        }
  
        index = to;
        offloadFn(options.callback && options.callback(index, slides[index]));
      }
  
      function move(index, dist, speed) {
  
        translate(index, dist, speed);
        slidePos[index] = dist;
  
      }
  
      function translate(index, dist, speed) {
  
        var slide = slides[index];
        var style = slide && slide.style;
  
        if (!style) return;
  
        style.webkitTransitionDuration =
        style.MozTransitionDuration =
        style.msTransitionDuration =
        style.OTransitionDuration =
        style.transitionDuration = speed + 'ms';
  
        style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)';
        style.msTransform =
        style.MozTransform =
        style.OTransform = 'translateX(' + dist + 'px)';
  
      }
  
      function animate(from, to, speed) {
  
        // if not an animation, just reposition
        if (!speed) {
  
          element.style.left = to + 'px';
          return;
  
        }
  
        var start = +new Date();
  
        var timer = setInterval(function() {
  
          var timeElap = +new Date() - start;
  
          if (timeElap > speed) {
  
            element.style.left = to + 'px';
  
            if (delay) begin();
  
            options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
  
            clearInterval(timer);
            return;
  
          }
  
          element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px';
  
        }, 4);
  
      }
  
      // setup auto slideshow
      var delay = options.auto || 0;
      var interval;
  
      function begin() {
  
        interval = setTimeout(next, delay);
  
      }
  
      function stop() {
  
        delay = options.auto || 0;
        clearTimeout(interval);
  
      }
  
  
      // setup initial vars
      var start = {};
      var delta = {};
      var isScrolling;
  
      // setup event capturing
      var events = {
  
        handleEvent: function(event) {
          if(event.type == 'mousedown' || event.type == 'mouseup' || event.type == 'mousemove') {
            event.touches = [{
              pageX: event.pageX,
              pageY: event.pageY
            }];
          }
  
          switch (event.type) {
            case 'mousedown': this.start(event); break;
            case 'touchstart': this.start(event); break;
            case 'touchmove': this.touchmove(event); break;
            case 'mousemove': this.touchmove(event); break;
            case 'touchend': offloadFn(this.end(event)); break;
            case 'mouseup': offloadFn(this.end(event)); break;
            case 'webkitTransitionEnd':
            case 'msTransitionEnd':
            case 'oTransitionEnd':
            case 'otransitionend':
            case 'transitionend': offloadFn(this.transitionEnd(event)); break;
            case 'resize': offloadFn(setup); break;
          }
  
          if (options.stopPropagation) event.stopPropagation();
  
        },
        start: function(event) {
  
          var touches = event.touches[0];
  
          // measure start values
          start = {
  
            // get initial touch coords
            x: touches.pageX,
            y: touches.pageY,
  
            // store time to determine touch duration
            time: +new Date()
  
          };
  
          // used for testing first move event
          isScrolling = undefined;
  
          // reset delta and end measurements
          delta = {};
  
          // attach touchmove and touchend listeners
          if(browser.touch) {
            element.addEventListener('touchmove', this, false);
            element.addEventListener('touchend', this, false);
          } else {
            element.addEventListener('mousemove', this, false);
            element.addEventListener('mouseup', this, false);
            document.addEventListener('mouseup', this, false);
          }
        },
        touchmove: function(event) {
  
          // ensure swiping with one touch and not pinching
          // ensure sliding is enabled
          if (event.touches.length > 1 ||
              event.scale && event.scale !== 1 ||
              slider.slideIsDisabled) {
            return;
          }
  
          if (options.disableScroll) event.preventDefault();
  
          var touches = event.touches[0];
  
          // measure change in x and y
          delta = {
            x: touches.pageX - start.x,
            y: touches.pageY - start.y
          };
  
          // determine if scrolling test has run - one time test
          if ( typeof isScrolling == 'undefined') {
            isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) );
          }
  
          // if user is not trying to scroll vertically
          if (!isScrolling) {
  
            // prevent native scrolling
            event.preventDefault();
  
            // stop slideshow
            stop();
  
            // increase resistance if first or last slide
            if (options.continuous) { // we don't add resistance at the end
  
              translate(circle(index-1), delta.x + slidePos[circle(index-1)], 0);
              translate(index, delta.x + slidePos[index], 0);
              translate(circle(index+1), delta.x + slidePos[circle(index+1)], 0);
  
            } else {
  
              delta.x =
                delta.x /
                  ( (!index && delta.x > 0 ||         // if first slide and sliding left
                    index == slides.length - 1 &&     // or if last slide and sliding right
                    delta.x < 0                       // and if sliding at all
                  ) ?
                  ( Math.abs(delta.x) / width + 1 )      // determine resistance level
                  : 1 );                                 // no resistance if false
  
              // translate 1:1
              translate(index-1, delta.x + slidePos[index-1], 0);
              translate(index, delta.x + slidePos[index], 0);
              translate(index+1, delta.x + slidePos[index+1], 0);
            }
  
          }
  
        },
        end: function(event) {
  
          // measure duration
          var duration = +new Date() - start.time;
  
          // determine if slide attempt triggers next/prev slide
          var isValidSlide =
                Number(duration) < 250 &&         // if slide duration is less than 250ms
                Math.abs(delta.x) > 20 ||         // and if slide amt is greater than 20px
                Math.abs(delta.x) > width/2;      // or if slide amt is greater than half the width
  
          // determine if slide attempt is past start and end
          var isPastBounds = (!index && delta.x > 0) ||      // if first slide and slide amt is greater than 0
                (index == slides.length - 1 && delta.x < 0); // or if last slide and slide amt is less than 0
  
          if (options.continuous) isPastBounds = false;
  
          // determine direction of swipe (true:right, false:left)
          var direction = delta.x < 0;
  
          // if not scrolling vertically
          if (!isScrolling) {
  
            if (isValidSlide && !isPastBounds) {
  
              if (direction) {
  
                if (options.continuous) { // we need to get the next in this direction in place
  
                  move(circle(index-1), -width, 0);
                  move(circle(index+2), width, 0);
  
                } else {
                  move(index-1, -width, 0);
                }
  
                move(index, slidePos[index]-width, speed);
                move(circle(index+1), slidePos[circle(index+1)]-width, speed);
                index = circle(index+1);
  
              } else {
                if (options.continuous) { // we need to get the next in this direction in place
  
                  move(circle(index+1), width, 0);
                  move(circle(index-2), -width, 0);
  
                } else {
                  move(index+1, width, 0);
                }
  
                move(index, slidePos[index]+width, speed);
                move(circle(index-1), slidePos[circle(index-1)]+width, speed);
                index = circle(index-1);
  
              }
  
              options.callback && options.callback(index, slides[index]);
  
            } else {
  
              if (options.continuous) {
  
                move(circle(index-1), -width, speed);
                move(index, 0, speed);
                move(circle(index+1), width, speed);
  
              } else {
  
                move(index-1, -width, speed);
                move(index, 0, speed);
                move(index+1, width, speed);
              }
  
            }
  
          }
  
          // kill touchmove and touchend event listeners until touchstart called again
          if(browser.touch) {
            element.removeEventListener('touchmove', events, false);
            element.removeEventListener('touchend', events, false);
          } else {
            element.removeEventListener('mousemove', events, false);
            element.removeEventListener('mouseup', events, false);
            document.removeEventListener('mouseup', events, false);
          }
  
        },
        transitionEnd: function(event) {
  
          if (parseInt(event.target.getAttribute('data-index'), 10) == index) {
  
            if (delay) begin();
  
            options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
  
          }
  
        }
  
      };
  
      // Public API
      this.update = function() {
        setTimeout(setup);
      };
      this.setup = function() {
        setup();
      };
  
      this.loop = function(value) {
        if (arguments.length) options.continuous = !!value;
        return options.continuous;
      };
  
      this.enableSlide = function(shouldEnable) {
        if (arguments.length) {
          this.slideIsDisabled = !shouldEnable;
        }
        return !this.slideIsDisabled;
      },
      this.slide = this.select = function(to, speed) {
        // cancel slideshow
        stop();
  
        slide(to, speed);
      };
  
      this.prev = this.previous = function() {
        // cancel slideshow
        stop();
  
        prev();
      };
  
      this.next = function() {
        // cancel slideshow
        stop();
  
        next();
      };
  
      this.stop = function() {
        // cancel slideshow
        stop();
      };
  
      this.start = function() {
        begin();
      };
  
      this.autoPlay = function(newDelay) {
        if (!delay || delay < 0) {
          stop();
        } else {
          delay = newDelay;
          begin();
        }
      };
  
      this.currentIndex = this.selected = function() {
        // return current index position
        return index;
      };
  
      this.slidesCount = this.count = function() {
        // return total number of slides
        return length;
      };
  
      this.kill = function() {
        // cancel slideshow
        stop();
  
        // reset element
        element.style.width = '';
        element.style.left = '';
  
        // reset slides
        var pos = slides.length;
        while(pos--) {
  
          var slide = slides[pos];
          slide.style.width = '';
          slide.style.left = '';
  
          if (browser.transitions) translate(pos, 0, 0);
  
        }
  
        // removed event listeners
        if (browser.addEventListener) {
  
          // remove current event listeners
          element.removeEventListener('touchstart', events, false);
          element.removeEventListener('webkitTransitionEnd', events, false);
          element.removeEventListener('msTransitionEnd', events, false);
          element.removeEventListener('oTransitionEnd', events, false);
          element.removeEventListener('otransitionend', events, false);
          element.removeEventListener('transitionend', events, false);
          window.removeEventListener('resize', events, false);
  
        }
        else {
  
          window.onresize = null;
  
        }
      };
  
      this.load = function() {
        // trigger setup
        setup();
  
        // start auto slideshow if applicable
        if (delay) begin();
  
  
        // add event listeners
        if (browser.addEventListener) {
  
          // set touchstart event on element
          if (browser.touch) {
            element.addEventListener('touchstart', events, false);
          } else {
            element.addEventListener('mousedown', events, false);
          }
  
          if (browser.transitions) {
            element.addEventListener('webkitTransitionEnd', events, false);
            element.addEventListener('msTransitionEnd', events, false);
            element.addEventListener('oTransitionEnd', events, false);
            element.addEventListener('otransitionend', events, false);
            element.addEventListener('transitionend', events, false);
          }
  
          // set resize event on window
          window.addEventListener('resize', events, false);
  
        } else {
  
          window.onresize = function () { setup(); }; // to play nice with old IE
  
        }
      };
  
    }
  });
  
  })(ionic);
  
  (function(ionic) {
  'use strict';
  
    ionic.views.Toggle = ionic.views.View.inherit({
      initialize: function(opts) {
        var self = this;
  
        this.el = opts.el;
        this.checkbox = opts.checkbox;
        this.track = opts.track;
        this.handle = opts.handle;
        this.openPercent = -1;
        this.onChange = opts.onChange || function() {};
  
        this.triggerThreshold = opts.triggerThreshold || 20;
  
        this.dragStartHandler = function(e) {
          self.dragStart(e);
        };
        this.dragHandler = function(e) {
          self.drag(e);
        };
        this.holdHandler = function(e) {
          self.hold(e);
        };
        this.releaseHandler = function(e) {
          self.release(e);
        };
  
        this.dragStartGesture = ionic.onGesture('dragstart', this.dragStartHandler, this.el);
        this.dragGesture = ionic.onGesture('drag', this.dragHandler, this.el);
        this.dragHoldGesture = ionic.onGesture('hold', this.holdHandler, this.el);
        this.dragReleaseGesture = ionic.onGesture('release', this.releaseHandler, this.el);
      },
  
      destroy: function() {
        ionic.offGesture(this.dragStartGesture, 'dragstart', this.dragStartGesture);
        ionic.offGesture(this.dragGesture, 'drag', this.dragGesture);
        ionic.offGesture(this.dragHoldGesture, 'hold', this.holdHandler);
        ionic.offGesture(this.dragReleaseGesture, 'release', this.releaseHandler);
      },
  
      tap: function(e) {
        if(this.el.getAttribute('disabled') !== 'disabled') {
          this.val( !this.checkbox.checked );
        }
      },
  
      dragStart: function(e) {
        if(this.checkbox.disabled) return;
  
        this._dragInfo = {
          width: this.el.offsetWidth,
          left: this.el.offsetLeft,
          right: this.el.offsetLeft + this.el.offsetWidth,
          triggerX: this.el.offsetWidth / 2,
          initialState: this.checkbox.checked
        };
  
        // Stop any parent dragging
        e.gesture.srcEvent.preventDefault();
  
        // Trigger hold styles
        this.hold(e);
      },
  
      drag: function(e) {
        var self = this;
        if(!this._dragInfo) { return; }
  
        // Stop any parent dragging
        e.gesture.srcEvent.preventDefault();
  
        ionic.requestAnimationFrame(function(amount) {
          if (!self._dragInfo) { return; }
  
          var slidePageLeft = self.track.offsetLeft + (self.handle.offsetWidth / 2);
          var slidePageRight = self.track.offsetLeft + self.track.offsetWidth - (self.handle.offsetWidth / 2);
          var dx = e.gesture.deltaX;
  
          var px = e.gesture.touches[0].pageX - self._dragInfo.left;
          var mx = self._dragInfo.width - self.triggerThreshold;
  
          // The initial state was on, so "tend towards" on
          if(self._dragInfo.initialState) {
            if(px < self.triggerThreshold) {
              self.setOpenPercent(0);
            } else if(px > self._dragInfo.triggerX) {
              self.setOpenPercent(100);
            }
          } else {
            // The initial state was off, so "tend towards" off
            if(px < self._dragInfo.triggerX) {
              self.setOpenPercent(0);
            } else if(px > mx) {
              self.setOpenPercent(100);
            }
          }
        });
      },
  
      endDrag: function(e) {
        this._dragInfo = null;
      },
  
      hold: function(e) {
        this.el.classList.add('dragging');
      },
      release: function(e) {
        this.el.classList.remove('dragging');
        this.endDrag(e);
      },
  
  
      setOpenPercent: function(openPercent) {
        // only make a change if the new open percent has changed
        if(this.openPercent < 0 || (openPercent < (this.openPercent - 3) || openPercent > (this.openPercent + 3) ) ) {
          this.openPercent = openPercent;
  
          if(openPercent === 0) {
            this.val(false);
          } else if(openPercent === 100) {
            this.val(true);
          } else {
            var openPixel = Math.round( (openPercent / 100) * this.track.offsetWidth - (this.handle.offsetWidth) );
            openPixel = (openPixel < 1 ? 0 : openPixel);
            this.handle.style[ionic.CSS.TRANSFORM] = 'translate3d(' + openPixel + 'px,0,0)';
          }
        }
      },
  
      val: function(value) {
        if(value === true || value === false) {
          if(this.handle.style[ionic.CSS.TRANSFORM] !== "") {
            this.handle.style[ionic.CSS.TRANSFORM] = "";
          }
          this.checkbox.checked = value;
          this.openPercent = (value ? 100 : 0);
          this.onChange && this.onChange();
        }
        return this.checkbox.checked;
      }
  
    });
  
  })(ionic);
  
  })();
  /*!
   * ionic.bundle.js is a concatenation of:
   * ionic.js, angular.js, angular-animate.js,
   * angular-sanitize.js, angular-ui-router.js,
   * and ionic-angular.js
   */
  
  /*
   AngularJS v1.3.13
   (c) 2010-2014 Google, Inc. http://angularjs.org
   License: MIT
  */
  (function(M,Y,t){'use strict';function S(b){return function(){var a=arguments[0],c;c="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.3.13/"+(b?b+"/":"")+a;for(a=1;a<arguments.length;a++){c=c+(1==a?"?":"&")+"p"+(a-1)+"=";var d=encodeURIComponent,e;e=arguments[a];e="function"==typeof e?e.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof e?"undefined":"string"!=typeof e?JSON.stringify(e):e;c+=d(e)}return Error(c)}}function Ta(b){if(null==b||Ua(b))return!1;var a=b.length;return b.nodeType===
  oa&&a?!0:F(b)||H(b)||0===a||"number"===typeof a&&0<a&&a-1 in b}function s(b,a,c){var d,e;if(b)if(G(b))for(d in b)"prototype"==d||"length"==d||"name"==d||b.hasOwnProperty&&!b.hasOwnProperty(d)||a.call(c,b[d],d,b);else if(H(b)||Ta(b)){var f="object"!==typeof b;d=0;for(e=b.length;d<e;d++)(f||d in b)&&a.call(c,b[d],d,b)}else if(b.forEach&&b.forEach!==s)b.forEach(a,c,b);else for(d in b)b.hasOwnProperty(d)&&a.call(c,b[d],d,b);return b}function Ed(b,a,c){for(var d=Object.keys(b).sort(),e=0;e<d.length;e++)a.call(c,
  b[d[e]],d[e]);return d}function lc(b){return function(a,c){b(c,a)}}function Fd(){return++ob}function mc(b,a){a?b.$$hashKey=a:delete b.$$hashKey}function x(b){for(var a=b.$$hashKey,c=1,d=arguments.length;c<d;c++){var e=arguments[c];if(e)for(var f=Object.keys(e),g=0,h=f.length;g<h;g++){var l=f[g];b[l]=e[l]}}mc(b,a);return b}function ba(b){return parseInt(b,10)}function Pb(b,a){return x(Object.create(b),a)}function z(){}function pa(b){return b}function ea(b){return function(){return b}}function B(b){return"undefined"===
  typeof b}function y(b){return"undefined"!==typeof b}function I(b){return null!==b&&"object"===typeof b}function F(b){return"string"===typeof b}function V(b){return"number"===typeof b}function qa(b){return"[object Date]"===Da.call(b)}function G(b){return"function"===typeof b}function pb(b){return"[object RegExp]"===Da.call(b)}function Ua(b){return b&&b.window===b}function Va(b){return b&&b.$evalAsync&&b.$watch}function Wa(b){return"boolean"===typeof b}function nc(b){return!(!b||!(b.nodeName||b.prop&&
  b.attr&&b.find))}function Gd(b){var a={};b=b.split(",");var c;for(c=0;c<b.length;c++)a[b[c]]=!0;return a}function ua(b){return Q(b.nodeName||b[0]&&b[0].nodeName)}function Xa(b,a){var c=b.indexOf(a);0<=c&&b.splice(c,1);return a}function Ea(b,a,c,d){if(Ua(b)||Va(b))throw Ka("cpws");if(a){if(b===a)throw Ka("cpi");c=c||[];d=d||[];if(I(b)){var e=c.indexOf(b);if(-1!==e)return d[e];c.push(b);d.push(a)}if(H(b))for(var f=a.length=0;f<b.length;f++)e=Ea(b[f],null,c,d),I(b[f])&&(c.push(b[f]),d.push(e)),a.push(e);
  else{var g=a.$$hashKey;H(a)?a.length=0:s(a,function(b,c){delete a[c]});for(f in b)b.hasOwnProperty(f)&&(e=Ea(b[f],null,c,d),I(b[f])&&(c.push(b[f]),d.push(e)),a[f]=e);mc(a,g)}}else if(a=b)H(b)?a=Ea(b,[],c,d):qa(b)?a=new Date(b.getTime()):pb(b)?(a=new RegExp(b.source,b.toString().match(/[^\/]*$/)[0]),a.lastIndex=b.lastIndex):I(b)&&(e=Object.create(Object.getPrototypeOf(b)),a=Ea(b,e,c,d));return a}function ra(b,a){if(H(b)){a=a||[];for(var c=0,d=b.length;c<d;c++)a[c]=b[c]}else if(I(b))for(c in a=a||{},
  b)if("$"!==c.charAt(0)||"$"!==c.charAt(1))a[c]=b[c];return a||b}function ga(b,a){if(b===a)return!0;if(null===b||null===a)return!1;if(b!==b&&a!==a)return!0;var c=typeof b,d;if(c==typeof a&&"object"==c)if(H(b)){if(!H(a))return!1;if((c=b.length)==a.length){for(d=0;d<c;d++)if(!ga(b[d],a[d]))return!1;return!0}}else{if(qa(b))return qa(a)?ga(b.getTime(),a.getTime()):!1;if(pb(b)&&pb(a))return b.toString()==a.toString();if(Va(b)||Va(a)||Ua(b)||Ua(a)||H(a))return!1;c={};for(d in b)if("$"!==d.charAt(0)&&!G(b[d])){if(!ga(b[d],
  a[d]))return!1;c[d]=!0}for(d in a)if(!c.hasOwnProperty(d)&&"$"!==d.charAt(0)&&a[d]!==t&&!G(a[d]))return!1;return!0}return!1}function Ya(b,a,c){return b.concat(Za.call(a,c))}function oc(b,a){var c=2<arguments.length?Za.call(arguments,2):[];return!G(a)||a instanceof RegExp?a:c.length?function(){return arguments.length?a.apply(b,Ya(c,arguments,0)):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}}function Hd(b,a){var c=a;"string"===typeof b&&"$"===b.charAt(0)&&"$"===b.charAt(1)?
  c=t:Ua(a)?c="$WINDOW":a&&Y===a?c="$DOCUMENT":Va(a)&&(c="$SCOPE");return c}function $a(b,a){if("undefined"===typeof b)return t;V(a)||(a=a?2:null);return JSON.stringify(b,Hd,a)}function pc(b){return F(b)?JSON.parse(b):b}function va(b){b=D(b).clone();try{b.empty()}catch(a){}var c=D("<div>").append(b).html();try{return b[0].nodeType===qb?Q(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+Q(b)})}catch(d){return Q(c)}}function qc(b){try{return decodeURIComponent(b)}catch(a){}}function rc(b){var a=
  {},c,d;s((b||"").split("&"),function(b){b&&(c=b.replace(/\+/g,"%20").split("="),d=qc(c[0]),y(d)&&(b=y(c[1])?qc(c[1]):!0,sc.call(a,d)?H(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Qb(b){var a=[];s(b,function(b,d){H(b)?s(b,function(b){a.push(Fa(d,!0)+(!0===b?"":"="+Fa(b,!0)))}):a.push(Fa(d,!0)+(!0===b?"":"="+Fa(b,!0)))});return a.length?a.join("&"):""}function rb(b){return Fa(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Fa(b,a){return encodeURIComponent(b).replace(/%40/gi,
  "@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,a?"%20":"+")}function Id(b,a){var c,d,e=sb.length;b=D(b);for(d=0;d<e;++d)if(c=sb[d]+a,F(c=b.attr(c)))return c;return null}function Jd(b,a){var c,d,e={};s(sb,function(a){a+="app";!c&&b.hasAttribute&&b.hasAttribute(a)&&(c=b,d=b.getAttribute(a))});s(sb,function(a){a+="app";var e;!c&&(e=b.querySelector("["+a.replace(":","\\:")+"]"))&&(c=e,d=e.getAttribute(a))});c&&(e.strictDi=null!==Id(c,"strict-di"),
  a(c,d?[d]:[],e))}function tc(b,a,c){I(c)||(c={});c=x({strictDi:!1},c);var d=function(){b=D(b);if(b.injector()){var d=b[0]===Y?"document":va(b);throw Ka("btstrpd",d.replace(/</,"&lt;").replace(/>/,"&gt;"));}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);c.debugInfoEnabled&&a.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);a.unshift("ng");d=ab(a,c.strictDi);d.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",
  d);c(b)(a)})}]);return d},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;M&&e.test(M.name)&&(c.debugInfoEnabled=!0,M.name=M.name.replace(e,""));if(M&&!f.test(M.name))return d();M.name=M.name.replace(f,"");ca.resumeBootstrap=function(b){s(b,function(b){a.push(b)});return d()};G(ca.resumeDeferredBootstrap)&&ca.resumeDeferredBootstrap()}function Kd(){M.name="NG_ENABLE_DEBUG_INFO!"+M.name;M.location.reload()}function Ld(b){b=ca.element(b).injector();if(!b)throw Ka("test");return b.get("$$testability")}
  function uc(b,a){a=a||"_";return b.replace(Md,function(b,d){return(d?a:"")+b.toLowerCase()})}function Nd(){var b;vc||((sa=M.jQuery)&&sa.fn.on?(D=sa,x(sa.fn,{scope:La.scope,isolateScope:La.isolateScope,controller:La.controller,injector:La.injector,inheritedData:La.inheritedData}),b=sa.cleanData,sa.cleanData=function(a){var c;if(Rb)Rb=!1;else for(var d=0,e;null!=(e=a[d]);d++)(c=sa._data(e,"events"))&&c.$destroy&&sa(e).triggerHandler("$destroy");b(a)}):D=R,ca.element=D,vc=!0)}function Sb(b,a,c){if(!b)throw Ka("areq",
  a||"?",c||"required");return b}function tb(b,a,c){c&&H(b)&&(b=b[b.length-1]);Sb(G(b),a,"not a function, got "+(b&&"object"===typeof b?b.constructor.name||"Object":typeof b));return b}function Ma(b,a){if("hasOwnProperty"===b)throw Ka("badname",a);}function wc(b,a,c){if(!a)return b;a=a.split(".");for(var d,e=b,f=a.length,g=0;g<f;g++)d=a[g],b&&(b=(e=b)[d]);return!c&&G(b)?oc(e,b):b}function ub(b){var a=b[0];b=b[b.length-1];var c=[a];do{a=a.nextSibling;if(!a)break;c.push(a)}while(a!==b);return D(c)}function ha(){return Object.create(null)}
  function Od(b){function a(a,b,c){return a[b]||(a[b]=c())}var c=S("$injector"),d=S("ng");b=a(b,"angular",Object);b.$$minErr=b.$$minErr||S;return a(b,"module",function(){var b={};return function(f,g,h){if("hasOwnProperty"===f)throw d("badname","module");g&&b.hasOwnProperty(f)&&(b[f]=null);return a(b,f,function(){function a(c,d,e,f){f||(f=b);return function(){f[e||"push"]([c,d,arguments]);return u}}if(!g)throw c("nomod",f);var b=[],d=[],e=[],q=a("$injector","invoke","push",d),u={_invokeQueue:b,_configBlocks:d,
  _runBlocks:e,requires:g,name:f,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),animation:a("$animateProvider","register"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:q,run:function(a){e.push(a);return this}};h&&q(h);return u})}})}function Pd(b){x(b,{bootstrap:tc,copy:Ea,extend:x,equals:ga,
  element:D,forEach:s,injector:ab,noop:z,bind:oc,toJson:$a,fromJson:pc,identity:pa,isUndefined:B,isDefined:y,isString:F,isFunction:G,isObject:I,isNumber:V,isElement:nc,isArray:H,version:Qd,isDate:qa,lowercase:Q,uppercase:vb,callbacks:{counter:0},getTestability:Ld,$$minErr:S,$$csp:bb,reloadWithDebugInfo:Kd});cb=Od(M);try{cb("ngLocale")}catch(a){cb("ngLocale",[]).provider("$locale",Rd)}cb("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:Sd});a.provider("$compile",xc).directive({a:Td,
  input:yc,textarea:yc,form:Ud,script:Vd,select:Wd,style:Xd,option:Yd,ngBind:Zd,ngBindHtml:$d,ngBindTemplate:ae,ngClass:be,ngClassEven:ce,ngClassOdd:de,ngCloak:ee,ngController:fe,ngForm:ge,ngHide:he,ngIf:ie,ngInclude:je,ngInit:ke,ngNonBindable:le,ngPluralize:me,ngRepeat:ne,ngShow:oe,ngStyle:pe,ngSwitch:qe,ngSwitchWhen:re,ngSwitchDefault:se,ngOptions:te,ngTransclude:ue,ngModel:ve,ngList:we,ngChange:xe,pattern:zc,ngPattern:zc,required:Ac,ngRequired:Ac,minlength:Bc,ngMinlength:Bc,maxlength:Cc,ngMaxlength:Cc,
  ngValue:ye,ngModelOptions:ze}).directive({ngInclude:Ae}).directive(wb).directive(Dc);a.provider({$anchorScroll:Be,$animate:Ce,$browser:De,$cacheFactory:Ee,$controller:Fe,$document:Ge,$exceptionHandler:He,$filter:Ec,$interpolate:Ie,$interval:Je,$http:Ke,$httpBackend:Le,$location:Me,$log:Ne,$parse:Oe,$rootScope:Pe,$q:Qe,$$q:Re,$sce:Se,$sceDelegate:Te,$sniffer:Ue,$templateCache:Ve,$templateRequest:We,$$testability:Xe,$timeout:Ye,$window:Ze,$$rAF:$e,$$asyncCallback:af,$$jqLite:bf})}])}function db(b){return b.replace(cf,
  function(a,b,d,e){return e?d.toUpperCase():d}).replace(df,"Moz$1")}function Fc(b){b=b.nodeType;return b===oa||!b||9===b}function Gc(b,a){var c,d,e=a.createDocumentFragment(),f=[];if(Tb.test(b)){c=c||e.appendChild(a.createElement("div"));d=(ef.exec(b)||["",""])[1].toLowerCase();d=ia[d]||ia._default;c.innerHTML=d[1]+b.replace(ff,"<$1></$2>")+d[2];for(d=d[0];d--;)c=c.lastChild;f=Ya(f,c.childNodes);c=e.firstChild;c.textContent=""}else f.push(a.createTextNode(b));e.textContent="";e.innerHTML="";s(f,function(a){e.appendChild(a)});
  return e}function R(b){if(b instanceof R)return b;var a;F(b)&&(b=U(b),a=!0);if(!(this instanceof R)){if(a&&"<"!=b.charAt(0))throw Ub("nosel");return new R(b)}if(a){a=Y;var c;b=(c=gf.exec(b))?[a.createElement(c[1])]:(c=Gc(b,a))?c.childNodes:[]}Hc(this,b)}function Vb(b){return b.cloneNode(!0)}function xb(b,a){a||yb(b);if(b.querySelectorAll)for(var c=b.querySelectorAll("*"),d=0,e=c.length;d<e;d++)yb(c[d])}function Ic(b,a,c,d){if(y(d))throw Ub("offargs");var e=(d=zb(b))&&d.events,f=d&&d.handle;if(f)if(a)s(a.split(" "),
  function(a){if(y(c)){var d=e[a];Xa(d||[],c);if(d&&0<d.length)return}b.removeEventListener(a,f,!1);delete e[a]});else for(a in e)"$destroy"!==a&&b.removeEventListener(a,f,!1),delete e[a]}function yb(b,a){var c=b.ng339,d=c&&Ab[c];d&&(a?delete d.data[a]:(d.handle&&(d.events.$destroy&&d.handle({},"$destroy"),Ic(b)),delete Ab[c],b.ng339=t))}function zb(b,a){var c=b.ng339,c=c&&Ab[c];a&&!c&&(b.ng339=c=++hf,c=Ab[c]={events:{},data:{},handle:t});return c}function Wb(b,a,c){if(Fc(b)){var d=y(c),e=!d&&a&&!I(a),
  f=!a;b=(b=zb(b,!e))&&b.data;if(d)b[a]=c;else{if(f)return b;if(e)return b&&b[a];x(b,a)}}}function Bb(b,a){return b.getAttribute?-1<(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+a+" "):!1}function Cb(b,a){a&&b.setAttribute&&s(a.split(" "),function(a){b.setAttribute("class",U((" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+U(a)+" "," ")))})}function Db(b,a){if(a&&b.setAttribute){var c=(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");
  s(a.split(" "),function(a){a=U(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});b.setAttribute("class",U(c))}}function Hc(b,a){if(a)if(a.nodeType)b[b.length++]=a;else{var c=a.length;if("number"===typeof c&&a.window!==a){if(c)for(var d=0;d<c;d++)b[b.length++]=a[d]}else b[b.length++]=a}}function Jc(b,a){return Eb(b,"$"+(a||"ngController")+"Controller")}function Eb(b,a,c){9==b.nodeType&&(b=b.documentElement);for(a=H(a)?a:[a];b;){for(var d=0,e=a.length;d<e;d++)if((c=D.data(b,a[d]))!==t)return c;b=b.parentNode||
  11===b.nodeType&&b.host}}function Kc(b){for(xb(b,!0);b.firstChild;)b.removeChild(b.firstChild)}function Lc(b,a){a||xb(b);var c=b.parentNode;c&&c.removeChild(b)}function jf(b,a){a=a||M;if("complete"===a.document.readyState)a.setTimeout(b);else D(a).on("load",b)}function Mc(b,a){var c=Fb[a.toLowerCase()];return c&&Nc[ua(b)]&&c}function kf(b,a){var c=b.nodeName;return("INPUT"===c||"TEXTAREA"===c)&&Oc[a]}function lf(b,a){var c=function(c,e){c.isDefaultPrevented=function(){return c.defaultPrevented};var f=
  a[e||c.type],g=f?f.length:0;if(g){if(B(c.immediatePropagationStopped)){var h=c.stopImmediatePropagation;c.stopImmediatePropagation=function(){c.immediatePropagationStopped=!0;c.stopPropagation&&c.stopPropagation();h&&h.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};1<g&&(f=ra(f));for(var l=0;l<g;l++)c.isImmediatePropagationStopped()||f[l].call(b,c)}};c.elem=b;return c}function bf(){this.$get=function(){return x(R,{hasClass:function(b,a){b.attr&&(b=b[0]);
  return Bb(b,a)},addClass:function(b,a){b.attr&&(b=b[0]);return Db(b,a)},removeClass:function(b,a){b.attr&&(b=b[0]);return Cb(b,a)}})}}function Na(b,a){var c=b&&b.$$hashKey;if(c)return"function"===typeof c&&(c=b.$$hashKey()),c;c=typeof b;return c="function"==c||"object"==c&&null!==b?b.$$hashKey=c+":"+(a||Fd)():c+":"+b}function eb(b,a){if(a){var c=0;this.nextUid=function(){return++c}}s(b,this.put,this)}function mf(b){return(b=b.toString().replace(Pc,"").match(Qc))?"function("+(b[1]||"").replace(/[\s\r\n]+/,
  " ")+")":"fn"}function ab(b,a){function c(a){return function(b,c){if(I(b))s(b,lc(a));else return a(b,c)}}function d(a,b){Ma(a,"service");if(G(b)||H(b))b=q.instantiate(b);if(!b.$get)throw Ga("pget",a);return n[a+"Provider"]=b}function e(a,b){return function(){var c=r.invoke(b,this);if(B(c))throw Ga("undef",a);return c}}function f(a,b,c){return d(a,{$get:!1!==c?e(a,b):b})}function g(a){var b=[],c;s(a,function(a){function d(a){var b,c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=q.get(e[0]);f[e[1]].apply(f,
  e[2])}}if(!m.get(a)){m.put(a,!0);try{F(a)?(c=cb(a),b=b.concat(g(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):G(a)?b.push(q.invoke(a)):H(a)?b.push(q.invoke(a)):tb(a,"module")}catch(e){throw H(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1==e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),Ga("modulerr",a,e.stack||e.message||e);}}});return b}function h(b,c){function d(a,e){if(b.hasOwnProperty(a)){if(b[a]===l)throw Ga("cdep",a+" <- "+k.join(" <- "));return b[a]}try{return k.unshift(a),
  b[a]=l,b[a]=c(a,e)}catch(f){throw b[a]===l&&delete b[a],f;}finally{k.shift()}}function e(b,c,f,g){"string"===typeof f&&(g=f,f=null);var h=[],k=ab.$$annotate(b,a,g),l,q,n;q=0;for(l=k.length;q<l;q++){n=k[q];if("string"!==typeof n)throw Ga("itkn",n);h.push(f&&f.hasOwnProperty(n)?f[n]:d(n,g))}H(b)&&(b=b[l]);return b.apply(c,h)}return{invoke:e,instantiate:function(a,b,c){var d=Object.create((H(a)?a[a.length-1]:a).prototype||null);a=e(a,d,b,c);return I(a)||G(a)?a:d},get:d,annotate:ab.$$annotate,has:function(a){return n.hasOwnProperty(a+
  "Provider")||b.hasOwnProperty(a)}}}a=!0===a;var l={},k=[],m=new eb([],!0),n={$provide:{provider:c(d),factory:c(f),service:c(function(a,b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:c(function(a,b){return f(a,ea(b),!1)}),constant:c(function(a,b){Ma(a,"constant");n[a]=b;u[a]=b}),decorator:function(a,b){var c=q.get(a+"Provider"),d=c.$get;c.$get=function(){var a=r.invoke(d,c);return r.invoke(b,null,{$delegate:a})}}}},q=n.$injector=h(n,function(a,b){ca.isString(b)&&k.push(b);
  throw Ga("unpr",k.join(" <- "));}),u={},r=u.$injector=h(u,function(a,b){var c=q.get(a+"Provider",b);return r.invoke(c.$get,c,t,a)});s(g(b),function(a){r.invoke(a||z)});return r}function Be(){var b=!0;this.disableAutoScrolling=function(){b=!1};this.$get=["$window","$location","$rootScope",function(a,c,d){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===ua(a))return b=a,!0});return b}function f(b){if(b){b.scrollIntoView();var c;c=g.yOffset;G(c)?c=c():nc(c)?(c=c[0],c="fixed"!==
  a.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):V(c)||(c=0);c&&(b=b.getBoundingClientRect().top,a.scrollBy(0,b-c))}else a.scrollTo(0,0)}function g(){var a=c.hash(),b;a?(b=h.getElementById(a))?f(b):(b=e(h.getElementsByName(a)))?f(b):"top"===a&&f(null):f(null)}var h=a.document;b&&d.$watch(function(){return c.hash()},function(a,b){a===b&&""===a||jf(function(){d.$evalAsync(g)})});return g}]}function af(){this.$get=["$$rAF","$timeout",function(b,a){return b.supported?function(a){return b(a)}:
  function(b){return a(b,0,!1)}}]}function nf(b,a,c,d){function e(a){try{a.apply(null,Za.call(arguments,1))}finally{if(v--,0===v)for(;w.length;)try{w.pop()()}catch(b){c.error(b)}}}function f(a,b){(function N(){s(L,function(a){a()});C=b(N,a)})()}function g(){h();l()}function h(){A=b.history.state;A=B(A)?null:A;ga(A,J)&&(A=J);J=A}function l(){if(E!==m.url()||O!==A)E=m.url(),O=A,s(W,function(a){a(m.url(),A)})}function k(a){try{return decodeURIComponent(a)}catch(b){return a}}var m=this,n=a[0],q=b.location,
  u=b.history,r=b.setTimeout,P=b.clearTimeout,p={};m.isMock=!1;var v=0,w=[];m.$$completeOutstandingRequest=e;m.$$incOutstandingRequestCount=function(){v++};m.notifyWhenNoOutstandingRequests=function(a){s(L,function(a){a()});0===v?a():w.push(a)};var L=[],C;m.addPollFn=function(a){B(C)&&f(100,r);L.push(a);return a};var A,O,E=q.href,T=a.find("base"),X=null;h();O=A;m.url=function(a,c,e){B(e)&&(e=null);q!==b.location&&(q=b.location);u!==b.history&&(u=b.history);if(a){var f=O===e;if(E===a&&(!d.history||f))return m;
  var g=E&&Ha(E)===Ha(a);E=a;O=e;!d.history||g&&f?(g||(X=a),c?q.replace(a):g?(c=q,e=a.indexOf("#"),a=-1===e?"":a.substr(e+1),c.hash=a):q.href=a):(u[c?"replaceState":"pushState"](e,"",a),h(),O=A);return m}return X||q.href.replace(/%27/g,"'")};m.state=function(){return A};var W=[],wa=!1,J=null;m.onUrlChange=function(a){if(!wa){if(d.history)D(b).on("popstate",g);D(b).on("hashchange",g);wa=!0}W.push(a);return a};m.$$checkUrlChange=l;m.baseHref=function(){var a=T.attr("href");return a?a.replace(/^(https?\:)?\/\/[^\/]*/,
  ""):""};var fa={},y="",da=m.baseHref();m.cookies=function(a,b){var d,e,f,g;if(a)b===t?n.cookie=encodeURIComponent(a)+"=;path="+da+";expires=Thu, 01 Jan 1970 00:00:00 GMT":F(b)&&(d=(n.cookie=encodeURIComponent(a)+"="+encodeURIComponent(b)+";path="+da).length+1,4096<d&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"));else{if(n.cookie!==y)for(y=n.cookie,d=y.split("; "),fa={},f=0;f<d.length;f++)e=d[f],g=e.indexOf("="),0<g&&(a=k(e.substring(0,g)),
  fa[a]===t&&(fa[a]=k(e.substring(g+1))));return fa}};m.defer=function(a,b){var c;v++;c=r(function(){delete p[c];e(a)},b||0);p[c]=!0;return c};m.defer.cancel=function(a){return p[a]?(delete p[a],P(a),e(z),!0):!1}}function De(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new nf(b,d,a,c)}]}function Ee(){this.$get=function(){function b(b,d){function e(a){a!=n&&(q?q==a&&(q=a.n):q=a,f(a.n,a.p),f(a,n),n=a,n.n=null)}function f(a,b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(b in a)throw S("$cacheFactory")("iid",
  b);var g=0,h=x({},d,{id:b}),l={},k=d&&d.capacity||Number.MAX_VALUE,m={},n=null,q=null;return a[b]={put:function(a,b){if(k<Number.MAX_VALUE){var c=m[a]||(m[a]={key:a});e(c)}if(!B(b))return a in l||g++,l[a]=b,g>k&&this.remove(q.key),b},get:function(a){if(k<Number.MAX_VALUE){var b=m[a];if(!b)return;e(b)}return l[a]},remove:function(a){if(k<Number.MAX_VALUE){var b=m[a];if(!b)return;b==n&&(n=b.p);b==q&&(q=b.n);f(b.n,b.p);delete m[a]}delete l[a];g--},removeAll:function(){l={};g=0;m={};n=q=null},destroy:function(){m=
  h=l=null;delete a[b]},info:function(){return x({},h,{size:g})}}}var a={};b.info=function(){var b={};s(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function Ve(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function xc(b,a){function c(a,b){var c=/^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/,d={};s(a,function(a,e){var f=a.match(c);if(!f)throw ja("iscp",b,e,a);d[e]={mode:f[1][0],collection:"*"===f[2],optional:"?"===f[3],attrName:f[4]||e}});return d}var d=
  {},e=/^\s*directive\:\s*([\w\-]+)\s+(.*)$/,f=/(([\w\-]+)(?:\:([^;]+))?;?)/,g=Gd("ngSrc,ngSrcset,src,srcset"),h=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,l=/^(on[a-z]+|formaction)$/;this.directive=function n(a,e){Ma(a,"directive");F(a)?(Sb(e,"directiveFactory"),d.hasOwnProperty(a)||(d[a]=[],b.factory(a+"Directive",["$injector","$exceptionHandler",function(b,e){var f=[];s(d[a],function(d,g){try{var h=b.invoke(d);G(h)?h={compile:ea(h)}:!h.compile&&h.link&&(h.compile=ea(h.link));h.priority=h.priority||0;h.index=
  g;h.name=h.name||a;h.require=h.require||h.controller&&h.name;h.restrict=h.restrict||"EA";I(h.scope)&&(h.$$isolateBindings=c(h.scope,h.name));f.push(h)}catch(l){e(l)}});return f}])),d[a].push(e)):s(a,lc(n));return this};this.aHrefSanitizationWhitelist=function(b){return y(b)?(a.aHrefSanitizationWhitelist(b),this):a.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(b){return y(b)?(a.imgSrcSanitizationWhitelist(b),this):a.imgSrcSanitizationWhitelist()};var k=!0;this.debugInfoEnabled=
  function(a){return y(a)?(k=a,this):k};this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$document","$sce","$animate","$$sanitizeUri",function(a,b,c,r,P,p,v,w,L,C,A){function O(a,b){try{a.addClass(b)}catch(c){}}function E(a,b,c,d,e){a instanceof D||(a=D(a));s(a,function(b,c){b.nodeType==qb&&b.nodeValue.match(/\S+/)&&(a[c]=D(b).wrap("<span></span>").parent()[0])});var f=T(a,b,a,c,d,e);E.$$addScopeClass(a);var g=null;return function(b,c,
  d){Sb(b,"scope");d=d||{};var e=d.parentBoundTranscludeFn,h=d.transcludeControllers;d=d.futureParentElement;e&&e.$$boundTransclude&&(e=e.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==ua(d)&&d.toString().match(/SVG/)?"svg":"html":"html");d="html"!==g?D(Xb(g,D("<div>").append(a).html())):c?La.clone.call(a):a;if(h)for(var l in h)d.data("$"+l+"Controller",h[l].instance);E.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,e);return d}}function T(a,b,c,d,e,f){function g(a,c,d,e){var f,l,k,q,n,p,w;if(r)for(w=
  Array(c.length),q=0;q<h.length;q+=3)f=h[q],w[f]=c[f];else w=c;q=0;for(n=h.length;q<n;)l=w[h[q++]],c=h[q++],f=h[q++],c?(c.scope?(k=a.$new(),E.$$addScopeInfo(D(l),k)):k=a,p=c.transcludeOnThisElement?X(a,c.transclude,e,c.elementTranscludeOnThisElement):!c.templateOnThisElement&&e?e:!e&&b?X(a,b):null,c(f,k,l,d,p)):f&&f(a,l.childNodes,t,e)}for(var h=[],l,k,q,n,r,p=0;p<a.length;p++){l=new Yb;k=W(a[p],[],l,0===p?d:t,e);(f=k.length?fa(k,a[p],l,b,c,null,[],[],f):null)&&f.scope&&E.$$addScopeClass(l.$$element);
  l=f&&f.terminal||!(q=a[p].childNodes)||!q.length?null:T(q,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||l)h.push(p,f,l),n=!0,r=r||f;f=null}return n?g:null}function X(a,b,c,d){return function(d,e,f,g,h){d||(d=a.$new(!1,h),d.$$transcluded=!0);return b(d,e,{parentBoundTranscludeFn:c,transcludeControllers:f,futureParentElement:g})}}function W(a,b,c,d,g){var h=c.$attr,l;switch(a.nodeType){case oa:da(b,ya(ua(a)),"E",d,g);for(var k,q,n,r=a.attributes,p=0,w=r&&r.length;p<
  w;p++){var P=!1,L=!1;k=r[p];l=k.name;q=U(k.value);k=ya(l);if(n=gb.test(k))l=l.replace(Sc,"").substr(8).replace(/_(.)/g,function(a,b){return b.toUpperCase()});var u=k.replace(/(Start|End)$/,"");B(u)&&k===u+"Start"&&(P=l,L=l.substr(0,l.length-5)+"end",l=l.substr(0,l.length-6));k=ya(l.toLowerCase());h[k]=l;if(n||!c.hasOwnProperty(k))c[k]=q,Mc(a,k)&&(c[k]=!0);Pa(a,b,q,k,n);da(b,k,"A",d,g,P,L)}a=a.className;I(a)&&(a=a.animVal);if(F(a)&&""!==a)for(;l=f.exec(a);)k=ya(l[2]),da(b,k,"C",d,g)&&(c[k]=U(l[3])),
  a=a.substr(l.index+l[0].length);break;case qb:M(b,a.nodeValue);break;case 8:try{if(l=e.exec(a.nodeValue))k=ya(l[1]),da(b,k,"M",d,g)&&(c[k]=U(l[2]))}catch(v){}}b.sort(N);return b}function wa(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw ja("uterdir",b,c);a.nodeType==oa&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return D(d)}function J(a,b,c){return function(d,e,f,g,h){e=wa(e[0],b,c);return a(d,e,f,g,h)}}function fa(a,
  d,e,f,g,l,k,n,r){function w(a,b,c,d){if(a){c&&(a=J(a,c,d));a.require=K.require;a.directiveName=x;if(T===K||K.$$isolateScope)a=Z(a,{isolateScope:!0});k.push(a)}if(b){c&&(b=J(b,c,d));b.require=K.require;b.directiveName=x;if(T===K||K.$$isolateScope)b=Z(b,{isolateScope:!0});n.push(b)}}function L(a,b,c,d){var e,f="data",g=!1,l=c,k;if(F(b)){k=b.match(h);b=b.substring(k[0].length);k[3]&&(k[1]?k[3]=null:k[1]=k[3]);"^"===k[1]?f="inheritedData":"^^"===k[1]&&(f="inheritedData",l=c.parent());"?"===k[2]&&(g=!0);
  e=null;d&&"data"===f&&(e=d[b])&&(e=e.instance);e=e||l[f]("$"+b+"Controller");if(!e&&!g)throw ja("ctreq",b,a);return e||null}H(b)&&(e=[],s(b,function(b){e.push(L(a,b,c,d))}));return e}function v(a,c,f,g,h){function l(a,b,c){var d;Va(a)||(c=b,b=a,a=t);z&&(d=O);c||(c=z?W.parent():W);return h(a,b,d,c,wa)}var r,w,u,A,O,fb,W,J;d===f?(J=e,W=e.$$element):(W=D(f),J=new Yb(W,e));T&&(A=c.$new(!0));h&&(fb=l,fb.$$boundTransclude=h);C&&(X={},O={},s(C,function(a){var b={$scope:a===T||a.$$isolateScope?A:c,$element:W,
  $attrs:J,$transclude:fb};u=a.controller;"@"==u&&(u=J[a.name]);b=p(u,b,!0,a.controllerAs);O[a.name]=b;z||W.data("$"+a.name+"Controller",b.instance);X[a.name]=b}));if(T){E.$$addScopeInfo(W,A,!0,!(ka&&(ka===T||ka===T.$$originalDirective)));E.$$addScopeClass(W,!0);g=X&&X[T.name];var xa=A;g&&g.identifier&&!0===T.bindToController&&(xa=g.instance);s(A.$$isolateBindings=T.$$isolateBindings,function(a,d){var e=a.attrName,f=a.optional,g,h,l,k;switch(a.mode){case "@":J.$observe(e,function(a){xa[d]=a});J.$$observers[e].$$scope=
  c;J[e]&&(xa[d]=b(J[e])(c));break;case "=":if(f&&!J[e])break;h=P(J[e]);k=h.literal?ga:function(a,b){return a===b||a!==a&&b!==b};l=h.assign||function(){g=xa[d]=h(c);throw ja("nonassign",J[e],T.name);};g=xa[d]=h(c);f=function(a){k(a,xa[d])||(k(a,g)?l(c,a=xa[d]):xa[d]=a);return g=a};f.$stateful=!0;f=a.collection?c.$watchCollection(J[e],f):c.$watch(P(J[e],f),null,h.literal);A.$on("$destroy",f);break;case "&":h=P(J[e]),xa[d]=function(a){return h(c,a)}}})}X&&(s(X,function(a){a()}),X=null);g=0;for(r=k.length;g<
  r;g++)w=k[g],$(w,w.isolateScope?A:c,W,J,w.require&&L(w.directiveName,w.require,W,O),fb);var wa=c;T&&(T.template||null===T.templateUrl)&&(wa=A);a&&a(wa,f.childNodes,t,h);for(g=n.length-1;0<=g;g--)w=n[g],$(w,w.isolateScope?A:c,W,J,w.require&&L(w.directiveName,w.require,W,O),fb)}r=r||{};for(var A=-Number.MAX_VALUE,O,C=r.controllerDirectives,X,T=r.newIsolateScopeDirective,ka=r.templateDirective,fa=r.nonTlbTranscludeDirective,da=!1,B=!1,z=r.hasElementTranscludeDirective,aa=e.$$element=D(d),K,x,N,Aa=f,
  Q,M=0,R=a.length;M<R;M++){K=a[M];var Pa=K.$$start,gb=K.$$end;Pa&&(aa=wa(d,Pa,gb));N=t;if(A>K.priority)break;if(N=K.scope)K.templateUrl||(I(N)?(Oa("new/isolated scope",T||O,K,aa),T=K):Oa("new/isolated scope",T,K,aa)),O=O||K;x=K.name;!K.templateUrl&&K.controller&&(N=K.controller,C=C||{},Oa("'"+x+"' controller",C[x],K,aa),C[x]=K);if(N=K.transclude)da=!0,K.$$tlb||(Oa("transclusion",fa,K,aa),fa=K),"element"==N?(z=!0,A=K.priority,N=aa,aa=e.$$element=D(Y.createComment(" "+x+": "+e[x]+" ")),d=aa[0],V(g,Za.call(N,
  0),d),Aa=E(N,f,A,l&&l.name,{nonTlbTranscludeDirective:fa})):(N=D(Vb(d)).contents(),aa.empty(),Aa=E(N,f));if(K.template)if(B=!0,Oa("template",ka,K,aa),ka=K,N=G(K.template)?K.template(aa,e):K.template,N=Tc(N),K.replace){l=K;N=Tb.test(N)?Uc(Xb(K.templateNamespace,U(N))):[];d=N[0];if(1!=N.length||d.nodeType!==oa)throw ja("tplrt",x,"");V(g,aa,d);R={$attr:{}};N=W(d,[],R);var ba=a.splice(M+1,a.length-(M+1));T&&y(N);a=a.concat(N).concat(ba);Rc(e,R);R=a.length}else aa.html(N);if(K.templateUrl)B=!0,Oa("template",
  ka,K,aa),ka=K,K.replace&&(l=K),v=S(a.splice(M,a.length-M),aa,e,g,da&&Aa,k,n,{controllerDirectives:C,newIsolateScopeDirective:T,templateDirective:ka,nonTlbTranscludeDirective:fa}),R=a.length;else if(K.compile)try{Q=K.compile(aa,e,Aa),G(Q)?w(null,Q,Pa,gb):Q&&w(Q.pre,Q.post,Pa,gb)}catch(of){c(of,va(aa))}K.terminal&&(v.terminal=!0,A=Math.max(A,K.priority))}v.scope=O&&!0===O.scope;v.transcludeOnThisElement=da;v.elementTranscludeOnThisElement=z;v.templateOnThisElement=B;v.transclude=Aa;r.hasElementTranscludeDirective=
  z;return v}function y(a){for(var b=0,c=a.length;b<c;b++)a[b]=Pb(a[b],{$$isolateScope:!0})}function da(b,e,f,g,h,l,k){if(e===h)return null;h=null;if(d.hasOwnProperty(e)){var q;e=a.get(e+"Directive");for(var r=0,p=e.length;r<p;r++)try{q=e[r],(g===t||g>q.priority)&&-1!=q.restrict.indexOf(f)&&(l&&(q=Pb(q,{$$start:l,$$end:k})),b.push(q),h=q)}catch(w){c(w)}}return h}function B(b){if(d.hasOwnProperty(b))for(var c=a.get(b+"Directive"),e=0,f=c.length;e<f;e++)if(b=c[e],b.multiElement)return!0;return!1}function Rc(a,
  b){var c=b.$attr,d=a.$attr,e=a.$$element;s(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});s(b,function(b,f){"class"==f?(O(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function S(a,b,c,d,e,f,g,h){var l=[],k,q,n=b[0],p=a.shift(),w=Pb(p,{templateUrl:null,transclude:null,replace:null,$$originalDirective:p}),
  P=G(p.templateUrl)?p.templateUrl(b,c):p.templateUrl,u=p.templateNamespace;b.empty();r(L.getTrustedResourceUrl(P)).then(function(r){var L,v;r=Tc(r);if(p.replace){r=Tb.test(r)?Uc(Xb(u,U(r))):[];L=r[0];if(1!=r.length||L.nodeType!==oa)throw ja("tplrt",p.name,P);r={$attr:{}};V(d,b,L);var A=W(L,[],r);I(p.scope)&&y(A);a=A.concat(a);Rc(c,r)}else L=n,b.html(r);a.unshift(w);k=fa(a,L,c,e,b,p,f,g,h);s(d,function(a,c){a==L&&(d[c]=b[0])});for(q=T(b[0].childNodes,e);l.length;){r=l.shift();v=l.shift();var C=l.shift(),
  E=l.shift(),A=b[0];if(!r.$$destroyed){if(v!==n){var J=v.className;h.hasElementTranscludeDirective&&p.replace||(A=Vb(L));V(C,D(v),A);O(D(A),J)}v=k.transcludeOnThisElement?X(r,k.transclude,E):E;k(q,r,A,d,v)}}l=null});return function(a,b,c,d,e){a=e;b.$$destroyed||(l?l.push(b,c,d,a):(k.transcludeOnThisElement&&(a=X(b,k.transclude,e)),k(q,b,c,d,a)))}}function N(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function Oa(a,b,c,d){if(b)throw ja("multidir",
  b.name,c.name,a,va(d));}function M(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=a.parent();var b=!!a.length;b&&E.$$addBindingClass(a);return function(a,c){var e=c.parent();b||E.$$addBindingClass(e);E.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function Xb(a,b){a=Q(a||"html");switch(a){case "svg":case "math":var c=Y.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function R(a,b){if("srcdoc"==
  b)return L.HTML;var c=ua(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=c&&("src"==b||"ngSrc"==b))return L.RESOURCE_URL}function Pa(a,c,d,e,f){var h=R(a,e);f=g[e]||f;var k=b(d,!0,h,f);if(k){if("multiple"===e&&"select"===ua(a))throw ja("selmulti",va(a));c.push({priority:100,compile:function(){return{pre:function(a,c,g){c=g.$$observers||(g.$$observers={});if(l.test(e))throw ja("nodomevents");var n=g[e];n!==d&&(k=n&&b(n,!0,h,f),d=n);k&&(g[e]=k(a),(c[e]||(c[e]=[])).$$inter=!0,(g.$$observers&&g.$$observers[e].$$scope||
  a).$watch(k,function(a,b){"class"===e&&a!=b?g.$updateClass(a,b):g.$set(e,a)}))}}}})}}function V(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]==d){a[g++]=c;h=g+e-1;for(var l=a.length;g<l;g++,h++)h<l?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=Y.createDocumentFragment();a.appendChild(d);D(c).data(D(d).data());sa?(Rb=!0,sa.cleanData([d])):delete D.cache[d[D.expando]];d=1;for(e=b.length;d<e;d++)f=b[d],D(f).remove(),
  a.appendChild(f),delete b[d];b[0]=c;b.length=1}function Z(a,b){return x(function(){return a.apply(null,arguments)},a,b)}function $(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,va(d))}}var Yb=function(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a};Yb.prototype={$normalize:ya,$addClass:function(a){a&&0<a.length&&C.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&C.removeClass(this.$$element,a)},$updateClass:function(a,
  b){var c=Vc(a,b);c&&c.length&&C.addClass(this.$$element,c);(c=Vc(b,a))&&c.length&&C.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=this.$$element[0],g=Mc(f,a),h=kf(f,a),f=a;g?(this.$$element.prop(a,b),e=g):h&&(this[h]=b,f=h);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=uc(a,"-"));g=ua(this.$$element);if("a"===g&&"href"===a||"img"===g&&"src"===a)this[a]=b=A(b,"src"===a);else if("img"===g&&"srcset"===a){for(var g="",h=U(b),l=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,l=/\s/.test(h)?
  l:/(,)/,h=h.split(l),l=Math.floor(h.length/2),k=0;k<l;k++)var q=2*k,g=g+A(U(h[q]),!0),g=g+(" "+U(h[q+1]));h=U(h[2*k]).split(/\s/);g+=A(U(h[0]),!0);2===h.length&&(g+=" "+U(h[1]));this[a]=b=g}!1!==d&&(null===b||b===t?this.$$element.removeAttr(e):this.$$element.attr(e,b));(a=this.$$observers)&&s(a[f],function(a){try{a(b)}catch(d){c(d)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers=ha()),e=d[a]||(d[a]=[]);e.push(b);v.$evalAsync(function(){!e.$$inter&&c.hasOwnProperty(a)&&b(c[a])});
  return function(){Xa(e,b)}}};var Aa=b.startSymbol(),ka=b.endSymbol(),Tc="{{"==Aa||"}}"==ka?pa:function(a){return a.replace(/\{\{/g,Aa).replace(/}}/g,ka)},gb=/^ngAttr[A-Z]/;E.$$addBindingInfo=k?function(a,b){var c=a.data("$binding")||[];H(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:z;E.$$addBindingClass=k?function(a){O(a,"ng-binding")}:z;E.$$addScopeInfo=k?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:z;E.$$addScopeClass=k?function(a,b){O(a,b?"ng-isolate-scope":
  "ng-scope")}:z;return E}]}function ya(b){return db(b.replace(Sc,""))}function Vc(b,a){var c="",d=b.split(/\s+/),e=a.split(/\s+/),f=0;a:for(;f<d.length;f++){for(var g=d[f],h=0;h<e.length;h++)if(g==e[h])continue a;c+=(0<c.length?" ":"")+g}return c}function Uc(b){b=D(b);var a=b.length;if(1>=a)return b;for(;a--;)8===b[a].nodeType&&pf.call(b,a,1);return b}function Fe(){var b={},a=!1,c=/^(\S+)(\s+as\s+(\w+))?$/;this.register=function(a,c){Ma(a,"controller");I(a)?x(b,a):b[a]=c};this.allowGlobals=function(){a=
  !0};this.$get=["$injector","$window",function(d,e){function f(a,b,c,d){if(!a||!I(a.$scope))throw S("$controller")("noscp",d,b);a.$scope[b]=c}return function(g,h,l,k){var m,n,q;l=!0===l;k&&F(k)&&(q=k);if(F(g)){k=g.match(c);if(!k)throw qf("ctrlfmt",g);n=k[1];q=q||k[3];g=b.hasOwnProperty(n)?b[n]:wc(h.$scope,n,!0)||(a?wc(e,n,!0):t);tb(g,n,!0)}if(l)return l=(H(g)?g[g.length-1]:g).prototype,m=Object.create(l||null),q&&f(h,q,m,n||g.name),x(function(){d.invoke(g,m,h,n);return m},{instance:m,identifier:q});
  m=d.instantiate(g,h,n);q&&f(h,q,m,n||g.name);return m}}]}function Ge(){this.$get=["$window",function(b){return D(b.document)}]}function He(){this.$get=["$log",function(b){return function(a,c){b.error.apply(b,arguments)}}]}function Zb(b,a){if(F(b)){var c=b.replace(rf,"").trim();if(c){var d=a("Content-Type");(d=d&&0===d.indexOf(Wc))||(d=(d=c.match(sf))&&tf[d[0]].test(c));d&&(b=pc(c))}}return b}function Xc(b){var a=ha(),c,d,e;if(!b)return a;s(b.split("\n"),function(b){e=b.indexOf(":");c=Q(U(b.substr(0,
  e)));d=U(b.substr(e+1));c&&(a[c]=a[c]?a[c]+", "+d:d)});return a}function Yc(b){var a=I(b)?b:t;return function(c){a||(a=Xc(b));return c?(c=a[Q(c)],void 0===c&&(c=null),c):a}}function Zc(b,a,c,d){if(G(d))return d(b,a,c);s(d,function(d){b=d(b,a,c)});return b}function Ke(){var b=this.defaults={transformResponse:[Zb],transformRequest:[function(a){return I(a)&&"[object File]"!==Da.call(a)&&"[object Blob]"!==Da.call(a)&&"[object FormData]"!==Da.call(a)?$a(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},
  post:ra($b),put:ra($b),patch:ra($b)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN"},a=!1;this.useApplyAsync=function(b){return y(b)?(a=!!b,this):a};var c=this.interceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(d,e,f,g,h,l){function k(a){function c(a){var b=x({},a);b.data=a.data?Zc(a.data,a.headers,a.status,e.transformResponse):a.data;a=a.status;return 200<=a&&300>a?b:h.reject(b)}function d(a){var b,c={};s(a,function(a,d){G(a)?(b=
  a(),null!=b&&(c[d]=b)):c[d]=a});return c}if(!ca.isObject(a))throw S("$http")("badreq",a);var e=x({method:"get",transformRequest:b.transformRequest,transformResponse:b.transformResponse},a);e.headers=function(a){var c=b.headers,e=x({},a.headers),f,g,c=x({},c.common,c[Q(a.method)]);a:for(f in c){a=Q(f);for(g in e)if(Q(g)===a)continue a;e[f]=c[f]}return d(e)}(a);e.method=vb(e.method);var f=[function(a){var d=a.headers,e=Zc(a.data,Yc(d),t,a.transformRequest);B(e)&&s(d,function(a,b){"content-type"===Q(b)&&
  delete d[b]});B(a.withCredentials)&&!B(b.withCredentials)&&(a.withCredentials=b.withCredentials);return m(a,e).then(c,c)},t],g=h.when(e);for(s(u,function(a){(a.request||a.requestError)&&f.unshift(a.request,a.requestError);(a.response||a.responseError)&&f.push(a.response,a.responseError)});f.length;){a=f.shift();var l=f.shift(),g=g.then(a,l)}g.success=function(a){g.then(function(b){a(b.data,b.status,b.headers,e)});return g};g.error=function(a){g.then(null,function(b){a(b.data,b.status,b.headers,e)});
  return g};return g}function m(c,f){function l(b,c,d,e){function f(){m(c,b,d,e)}O&&(200<=b&&300>b?O.put(X,[b,c,Xc(d),e]):O.remove(X));a?g.$applyAsync(f):(f(),g.$$phase||g.$apply())}function m(a,b,d,e){b=Math.max(b,0);(200<=b&&300>b?C.resolve:C.reject)({data:a,status:b,headers:Yc(d),config:c,statusText:e})}function w(a){m(a.data,a.status,ra(a.headers()),a.statusText)}function u(){var a=k.pendingRequests.indexOf(c);-1!==a&&k.pendingRequests.splice(a,1)}var C=h.defer(),A=C.promise,O,E,s=c.headers,X=n(c.url,
  c.params);k.pendingRequests.push(c);A.then(u,u);!c.cache&&!b.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(O=I(c.cache)?c.cache:I(b.cache)?b.cache:q);O&&(E=O.get(X),y(E)?E&&G(E.then)?E.then(w,w):H(E)?m(E[1],E[0],ra(E[2]),E[3]):m(E,200,{},"OK"):O.put(X,A));B(E)&&((E=$c(c.url)?e.cookies()[c.xsrfCookieName||b.xsrfCookieName]:t)&&(s[c.xsrfHeaderName||b.xsrfHeaderName]=E),d(c.method,X,f,l,s,c.timeout,c.withCredentials,c.responseType));return A}function n(a,b){if(!b)return a;var c=[];Ed(b,
  function(a,b){null===a||B(a)||(H(a)||(a=[a]),s(a,function(a){I(a)&&(a=qa(a)?a.toISOString():$a(a));c.push(Fa(b)+"="+Fa(a))}))});0<c.length&&(a+=(-1==a.indexOf("?")?"?":"&")+c.join("&"));return a}var q=f("$http"),u=[];s(c,function(a){u.unshift(F(a)?l.get(a):l.invoke(a))});k.pendingRequests=[];(function(a){s(arguments,function(a){k[a]=function(b,c){return k(x(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){s(arguments,function(a){k[a]=function(b,c,d){return k(x(d||{},{method:a,
  url:b,data:c}))}})})("post","put","patch");k.defaults=b;return k}]}function uf(){return new M.XMLHttpRequest}function Le(){this.$get=["$browser","$window","$document",function(b,a,c){return vf(b,uf,b.defer,a.angular.callbacks,c[0])}]}function vf(b,a,c,d,e){function f(a,b,c){var f=e.createElement("script"),m=null;f.type="text/javascript";f.src=a;f.async=!0;m=function(a){f.removeEventListener("load",m,!1);f.removeEventListener("error",m,!1);e.body.removeChild(f);f=null;var g=-1,u="unknown";a&&("load"!==
  a.type||d[b].called||(a={type:"error"}),u=a.type,g="error"===a.type?404:200);c&&c(g,u)};f.addEventListener("load",m,!1);f.addEventListener("error",m,!1);e.body.appendChild(f);return m}return function(e,h,l,k,m,n,q,u){function r(){v&&v();w&&w.abort()}function P(a,d,e,f,g){C!==t&&c.cancel(C);v=w=null;a(d,e,f,g);b.$$completeOutstandingRequest(z)}b.$$incOutstandingRequestCount();h=h||b.url();if("jsonp"==Q(e)){var p="_"+(d.counter++).toString(36);d[p]=function(a){d[p].data=a;d[p].called=!0};var v=f(h.replace("JSON_CALLBACK",
  "angular.callbacks."+p),p,function(a,b){P(k,a,d[p].data,"",b);d[p]=z})}else{var w=a();w.open(e,h,!0);s(m,function(a,b){y(a)&&w.setRequestHeader(b,a)});w.onload=function(){var a=w.statusText||"",b="response"in w?w.response:w.responseText,c=1223===w.status?204:w.status;0===c&&(c=b?200:"file"==Ba(h).protocol?404:0);P(k,c,b,w.getAllResponseHeaders(),a)};e=function(){P(k,-1,null,null,"")};w.onerror=e;w.onabort=e;q&&(w.withCredentials=!0);if(u)try{w.responseType=u}catch(L){if("json"!==u)throw L;}w.send(l||
  null)}if(0<n)var C=c(r,n);else n&&G(n.then)&&n.then(r)}}function Ie(){var b="{{",a="}}";this.startSymbol=function(a){return a?(b=a,this):b};this.endSymbol=function(b){return b?(a=b,this):a};this.$get=["$parse","$exceptionHandler","$sce",function(c,d,e){function f(a){return"\\\\\\"+a}function g(f,g,u,r){function P(c){return c.replace(k,b).replace(m,a)}function p(a){try{var b=a;a=u?e.getTrusted(u,b):e.valueOf(b);var c;if(r&&!y(a))c=a;else if(null==a)c="";else{switch(typeof a){case "string":break;case "number":a=
  ""+a;break;default:a=$a(a)}c=a}return c}catch(g){c=ac("interr",f,g.toString()),d(c)}}r=!!r;for(var v,w,L=0,C=[],A=[],O=f.length,E=[],s=[];L<O;)if(-1!=(v=f.indexOf(b,L))&&-1!=(w=f.indexOf(a,v+h)))L!==v&&E.push(P(f.substring(L,v))),L=f.substring(v+h,w),C.push(L),A.push(c(L,p)),L=w+l,s.push(E.length),E.push("");else{L!==O&&E.push(P(f.substring(L)));break}if(u&&1<E.length)throw ac("noconcat",f);if(!g||C.length){var X=function(a){for(var b=0,c=C.length;b<c;b++){if(r&&B(a[b]))return;E[s[b]]=a[b]}return E.join("")};
  return x(function(a){var b=0,c=C.length,e=Array(c);try{for(;b<c;b++)e[b]=A[b](a);return X(e)}catch(g){a=ac("interr",f,g.toString()),d(a)}},{exp:f,expressions:C,$$watchDelegate:function(a,b,c){var d;return a.$watchGroup(A,function(c,e){var f=X(c);G(b)&&b.call(this,f,c!==e?d:f,a);d=f},c)}})}}var h=b.length,l=a.length,k=new RegExp(b.replace(/./g,f),"g"),m=new RegExp(a.replace(/./g,f),"g");g.startSymbol=function(){return b};g.endSymbol=function(){return a};return g}]}function Je(){this.$get=["$rootScope",
  "$window","$q","$$q",function(b,a,c,d){function e(e,h,l,k){var m=a.setInterval,n=a.clearInterval,q=0,u=y(k)&&!k,r=(u?d:c).defer(),P=r.promise;l=y(l)?l:0;P.then(null,null,e);P.$$intervalId=m(function(){r.notify(q++);0<l&&q>=l&&(r.resolve(q),n(P.$$intervalId),delete f[P.$$intervalId]);u||b.$apply()},h);f[P.$$intervalId]=r;return P}var f={};e.cancel=function(b){return b&&b.$$intervalId in f?(f[b.$$intervalId].reject("canceled"),a.clearInterval(b.$$intervalId),delete f[b.$$intervalId],!0):!1};return e}]}
  function Rd(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January February March April May June July August September October November December".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),
  DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return 1===b?"one":"other"}}}}function bc(b){b=b.split("/");for(var a=b.length;a--;)b[a]=rb(b[a]);return b.join("/")}function ad(b,a){var c=Ba(b);a.$$protocol=
  c.protocol;a.$$host=c.hostname;a.$$port=ba(c.port)||wf[c.protocol]||null}function bd(b,a){var c="/"!==b.charAt(0);c&&(b="/"+b);var d=Ba(b);a.$$path=decodeURIComponent(c&&"/"===d.pathname.charAt(0)?d.pathname.substring(1):d.pathname);a.$$search=rc(d.search);a.$$hash=decodeURIComponent(d.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function za(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Ha(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function Gb(b){return b.replace(/(#.+)|#$/,
  "$1")}function cc(b){return b.substr(0,Ha(b).lastIndexOf("/")+1)}function dc(b,a){this.$$html5=!0;a=a||"";var c=cc(b);ad(b,this);this.$$parse=function(a){var b=za(c,a);if(!F(b))throw Hb("ipthprfx",a,c);bd(b,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Qb(this.$$search),b=this.$$hash?"#"+rb(this.$$hash):"";this.$$url=bc(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),
  !0;var f,g;(f=za(b,d))!==t?(g=f,g=(f=za(a,f))!==t?c+(za("/",f)||f):b+g):(f=za(c,d))!==t?g=c+f:c==d+"/"&&(g=c);g&&this.$$parse(g);return!!g}}function ec(b,a){var c=cc(b);ad(b,this);this.$$parse=function(d){d=za(b,d)||za(c,d);var e;"#"===d.charAt(0)?(e=za(a,d),B(e)&&(e=d)):e=this.$$html5?d:"";bd(e,this);d=this.$$path;var f=/^\/[A-Z]:(\/.*)/;0===e.indexOf(b)&&(e=e.replace(b,""));f.exec(e)||(d=(e=f.exec(d))?e[1]:d);this.$$path=d;this.$$compose()};this.$$compose=function(){var c=Qb(this.$$search),e=this.$$hash?
  "#"+rb(this.$$hash):"";this.$$url=bc(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+(this.$$url?a+this.$$url:"")};this.$$parseLinkUrl=function(a,c){return Ha(b)==Ha(a)?(this.$$parse(a),!0):!1}}function cd(b,a){this.$$html5=!0;ec.apply(this,arguments);var c=cc(b);this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;b==Ha(d)?f=d:(g=za(c,d))?f=b+a+g:c===d+"/"&&(f=c);f&&this.$$parse(f);return!!f};this.$$compose=function(){var c=Qb(this.$$search),e=this.$$hash?"#"+rb(this.$$hash):
  "";this.$$url=bc(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+a+this.$$url}}function Ib(b){return function(){return this[b]}}function dd(b,a){return function(c){if(B(c))return this[b];this[b]=a(c);this.$$compose();return this}}function Me(){var b="",a={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(a){return y(a)?(b=a,this):b};this.html5Mode=function(b){return Wa(b)?(a.enabled=b,this):I(b)?(Wa(b.enabled)&&(a.enabled=b.enabled),Wa(b.requireBase)&&(a.requireBase=b.requireBase),Wa(b.rewriteLinks)&&
  (a.rewriteLinks=b.rewriteLinks),this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(c,d,e,f,g){function h(a,b,c){var e=k.url(),f=k.$$state;try{d.url(a,b,c),k.$$state=d.state()}catch(g){throw k.url(e),k.$$state=f,g;}}function l(a,b){c.$broadcast("$locationChangeSuccess",k.absUrl(),a,k.$$state,b)}var k,m;m=d.baseHref();var n=d.url(),q;if(a.enabled){if(!m&&a.requireBase)throw Hb("nobase");q=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(m||"/");m=e.history?dc:cd}else q=
  Ha(n),m=ec;k=new m(q,"#"+b);k.$$parseLinkUrl(n,n);k.$$state=d.state();var u=/^\s*(javascript|mailto):/i;f.on("click",function(b){if(a.rewriteLinks&&!b.ctrlKey&&!b.metaKey&&!b.shiftKey&&2!=b.which&&2!=b.button){for(var e=D(b.target);"a"!==ua(e[0]);)if(e[0]===f[0]||!(e=e.parent())[0])return;var h=e.prop("href"),l=e.attr("href")||e.attr("xlink:href");I(h)&&"[object SVGAnimatedString]"===h.toString()&&(h=Ba(h.animVal).href);u.test(h)||!h||e.attr("target")||b.isDefaultPrevented()||!k.$$parseLinkUrl(h,
  l)||(b.preventDefault(),k.absUrl()!=d.url()&&(c.$apply(),g.angular["ff-684208-preventDefault"]=!0))}});Gb(k.absUrl())!=Gb(n)&&d.url(k.absUrl(),!0);var r=!0;d.onUrlChange(function(a,b){c.$evalAsync(function(){var d=k.absUrl(),e=k.$$state,f;k.$$parse(a);k.$$state=b;f=c.$broadcast("$locationChangeStart",a,d,b,e).defaultPrevented;k.absUrl()===a&&(f?(k.$$parse(d),k.$$state=e,h(d,!1,e)):(r=!1,l(d,e)))});c.$$phase||c.$digest()});c.$watch(function(){var a=Gb(d.url()),b=Gb(k.absUrl()),f=d.state(),g=k.$$replace,
  q=a!==b||k.$$html5&&e.history&&f!==k.$$state;if(r||q)r=!1,c.$evalAsync(function(){var b=k.absUrl(),d=c.$broadcast("$locationChangeStart",b,a,k.$$state,f).defaultPrevented;k.absUrl()===b&&(d?(k.$$parse(a),k.$$state=f):(q&&h(b,g,f===k.$$state?null:k.$$state),l(a,f)))});k.$$replace=!1});return k}]}function Ne(){var b=!0,a=this;this.debugEnabled=function(a){return y(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?
  "Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=c.console||{},e=b[a]||b.log||z;a=!1;try{a=!!e.apply}catch(l){}return a?function(){var a=[];s(arguments,function(b){a.push(d(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){b&&c.apply(a,arguments)}}()}}]}function ta(b,a){if("__defineGetter__"===
  b||"__defineSetter__"===b||"__lookupGetter__"===b||"__lookupSetter__"===b||"__proto__"===b)throw la("isecfld",a);return b}function ma(b,a){if(b){if(b.constructor===b)throw la("isecfn",a);if(b.window===b)throw la("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw la("isecdom",a);if(b===Object)throw la("isecobj",a);}return b}function fc(b){return b.constant}function hb(b,a,c,d,e){ma(b,e);ma(a,e);c=c.split(".");for(var f,g=0;1<c.length;g++){f=ta(c.shift(),e);var h=0===g&&a&&a[f]||
  b[f];h||(h={},b[f]=h);b=ma(h,e)}f=ta(c.shift(),e);ma(b[f],e);return b[f]=d}function Qa(b){return"constructor"==b}function ed(b,a,c,d,e,f,g){ta(b,f);ta(a,f);ta(c,f);ta(d,f);ta(e,f);var h=function(a){return ma(a,f)},l=g||Qa(b)?h:pa,k=g||Qa(a)?h:pa,m=g||Qa(c)?h:pa,n=g||Qa(d)?h:pa,q=g||Qa(e)?h:pa;return function(f,g){var h=g&&g.hasOwnProperty(b)?g:f;if(null==h)return h;h=l(h[b]);if(!a)return h;if(null==h)return t;h=k(h[a]);if(!c)return h;if(null==h)return t;h=m(h[c]);if(!d)return h;if(null==h)return t;
  h=n(h[d]);return e?null==h?t:h=q(h[e]):h}}function xf(b,a){return function(c,d){return b(c,d,ma,a)}}function yf(b,a,c){var d=a.expensiveChecks,e=d?zf:Af,f=e[b];if(f)return f;var g=b.split("."),h=g.length;if(a.csp)f=6>h?ed(g[0],g[1],g[2],g[3],g[4],c,d):function(a,b){var e=0,f;do f=ed(g[e++],g[e++],g[e++],g[e++],g[e++],c,d)(a,b),b=t,a=f;while(e<h);return f};else{var l="";d&&(l+="s = eso(s, fe);\nl = eso(l, fe);\n");var k=d;s(g,function(a,b){ta(a,c);var e=(b?"s":'((l&&l.hasOwnProperty("'+a+'"))?l:s)')+
  "."+a;if(d||Qa(a))e="eso("+e+", fe)",k=!0;l+="if(s == null) return undefined;\ns="+e+";\n"});l+="return s;";a=new Function("s","l","eso","fe",l);a.toString=ea(l);k&&(a=xf(a,c));f=a}f.sharedGetter=!0;f.assign=function(a,c,d){return hb(a,d,b,c,b)};return e[b]=f}function gc(b){return G(b.valueOf)?b.valueOf():Bf.call(b)}function Oe(){var b=ha(),a=ha();this.$get=["$filter","$sniffer",function(c,d){function e(a){var b=a;a.sharedGetter&&(b=function(b,c){return a(b,c)},b.literal=a.literal,b.constant=a.constant,
  b.assign=a.assign);return b}function f(a,b){for(var c=0,d=a.length;c<d;c++){var e=a[c];e.constant||(e.inputs?f(e.inputs,b):-1===b.indexOf(e)&&b.push(e))}return b}function g(a,b){return null==a||null==b?a===b:"object"===typeof a&&(a=gc(a),"object"===typeof a)?!1:a===b||a!==a&&b!==b}function h(a,b,c,d){var e=d.$$inputs||(d.$$inputs=f(d.inputs,[])),h;if(1===e.length){var l=g,e=e[0];return a.$watch(function(a){var b=e(a);g(b,l)||(h=d(a),l=b&&gc(b));return h},b,c)}for(var k=[],q=0,n=e.length;q<n;q++)k[q]=
  g;return a.$watch(function(a){for(var b=!1,c=0,f=e.length;c<f;c++){var l=e[c](a);if(b||(b=!g(l,k[c])))k[c]=l&&gc(l)}b&&(h=d(a));return h},b,c)}function l(a,b,c,d){var e,f;return e=a.$watch(function(a){return d(a)},function(a,c,d){f=a;G(b)&&b.apply(this,arguments);y(a)&&d.$$postDigest(function(){y(f)&&e()})},c)}function k(a,b,c,d){function e(a){var b=!0;s(a,function(a){y(a)||(b=!1)});return b}var f,g;return f=a.$watch(function(a){return d(a)},function(a,c,d){g=a;G(b)&&b.call(this,a,c,d);e(a)&&d.$$postDigest(function(){e(g)&&
  f()})},c)}function m(a,b,c,d){var e;return e=a.$watch(function(a){return d(a)},function(a,c,d){G(b)&&b.apply(this,arguments);e()},c)}function n(a,b){if(!b)return a;var c=a.$$watchDelegate,c=c!==k&&c!==l?function(c,d){var e=a(c,d);return b(e,c,d)}:function(c,d){var e=a(c,d),f=b(e,c,d);return y(e)?f:e};a.$$watchDelegate&&a.$$watchDelegate!==h?c.$$watchDelegate=a.$$watchDelegate:b.$stateful||(c.$$watchDelegate=h,c.inputs=[a]);return c}var q={csp:d.csp,expensiveChecks:!1},u={csp:d.csp,expensiveChecks:!0};
  return function(d,f,g){var v,w,L;switch(typeof d){case "string":L=d=d.trim();var C=g?a:b;v=C[L];v||(":"===d.charAt(0)&&":"===d.charAt(1)&&(w=!0,d=d.substring(2)),g=g?u:q,v=new hc(g),v=(new ib(v,c,g)).parse(d),v.constant?v.$$watchDelegate=m:w?(v=e(v),v.$$watchDelegate=v.literal?k:l):v.inputs&&(v.$$watchDelegate=h),C[L]=v);return n(v,f);case "function":return n(d,f);default:return n(z,f)}}}]}function Qe(){this.$get=["$rootScope","$exceptionHandler",function(b,a){return fd(function(a){b.$evalAsync(a)},
  a)}]}function Re(){this.$get=["$browser","$exceptionHandler",function(b,a){return fd(function(a){b.defer(a)},a)}]}function fd(b,a){function c(a,b,c){function d(b){return function(c){e||(e=!0,b.call(a,c))}}var e=!1;return[d(b),d(c)]}function d(){this.$$state={status:0}}function e(a,b){return function(c){b.call(a,c)}}function f(c){!c.processScheduled&&c.pending&&(c.processScheduled=!0,b(function(){var b,d,e;e=c.pending;c.processScheduled=!1;c.pending=t;for(var f=0,g=e.length;f<g;++f){d=e[f][0];b=e[f][c.status];
  try{G(b)?d.resolve(b(c.value)):1===c.status?d.resolve(c.value):d.reject(c.value)}catch(h){d.reject(h),a(h)}}}))}function g(){this.promise=new d;this.resolve=e(this,this.resolve);this.reject=e(this,this.reject);this.notify=e(this,this.notify)}var h=S("$q",TypeError);d.prototype={then:function(a,b,c){var d=new g;this.$$state.pending=this.$$state.pending||[];this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&f(this.$$state);return d.promise},"catch":function(a){return this.then(null,a)},"finally":function(a,
  b){return this.then(function(b){return k(b,!0,a)},function(b){return k(b,!1,a)},b)}};g.prototype={resolve:function(a){this.promise.$$state.status||(a===this.promise?this.$$reject(h("qcycle",a)):this.$$resolve(a))},$$resolve:function(b){var d,e;e=c(this,this.$$resolve,this.$$reject);try{if(I(b)||G(b))d=b&&b.then;G(d)?(this.promise.$$state.status=-1,d.call(b,e[0],e[1],this.notify)):(this.promise.$$state.value=b,this.promise.$$state.status=1,f(this.promise.$$state))}catch(g){e[1](g),a(g)}},reject:function(a){this.promise.$$state.status||
  this.$$reject(a)},$$reject:function(a){this.promise.$$state.value=a;this.promise.$$state.status=2;f(this.promise.$$state)},notify:function(c){var d=this.promise.$$state.pending;0>=this.promise.$$state.status&&d&&d.length&&b(function(){for(var b,e,f=0,g=d.length;f<g;f++){e=d[f][0];b=d[f][3];try{e.notify(G(b)?b(c):c)}catch(h){a(h)}}})}};var l=function(a,b){var c=new g;b?c.resolve(a):c.reject(a);return c.promise},k=function(a,b,c){var d=null;try{G(c)&&(d=c())}catch(e){return l(e,!1)}return d&&G(d.then)?
  d.then(function(){return l(a,b)},function(a){return l(a,!1)}):l(a,b)},m=function(a,b,c,d){var e=new g;e.resolve(a);return e.promise.then(b,c,d)},n=function u(a){if(!G(a))throw h("norslvr",a);if(!(this instanceof u))return new u(a);var b=new g;a(function(a){b.resolve(a)},function(a){b.reject(a)});return b.promise};n.defer=function(){return new g};n.reject=function(a){var b=new g;b.reject(a);return b.promise};n.when=m;n.all=function(a){var b=new g,c=0,d=H(a)?[]:{};s(a,function(a,e){c++;m(a).then(function(a){d.hasOwnProperty(e)||
  (d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)||b.reject(a)})});0===c&&b.resolve(d);return b.promise};return n}function $e(){this.$get=["$window","$timeout",function(b,a){var c=b.requestAnimationFrame||b.webkitRequestAnimationFrame,d=b.cancelAnimationFrame||b.webkitCancelAnimationFrame||b.webkitCancelRequestAnimationFrame,e=!!c,f=e?function(a){var b=c(a);return function(){d(b)}}:function(b){var c=a(b,16.66,!1);return function(){a.cancel(c)}};f.supported=e;return f}]}function Pe(){var b=
  10,a=S("$rootScope"),c=null,d=null;this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=["$injector","$exceptionHandler","$parse","$browser",function(e,f,g,h){function l(){this.$id=++ob;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this.$root=this;this.$$destroyed=!1;this.$$listeners={};this.$$listenerCount={};this.$$isolateBindings=null}function k(b){if(r.$$phase)throw a("inprog",r.$$phase);r.$$phase=b}function m(a,
  b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function n(){}function q(){for(;v.length;)try{v.shift()()}catch(a){f(a)}d=null}function u(){null===d&&(d=h.defer(function(){r.$apply(q)}))}l.prototype={constructor:l,$new:function(a,b){function c(){d.$$destroyed=!0}var d;b=b||this;a?(d=new l,d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=function(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners=
  {};this.$$listenerCount={};this.$id=++ob;this.$$ChildScope=null},this.$$ChildScope.prototype=this),d=new this.$$ChildScope);d.$parent=b;d.$$prevSibling=b.$$childTail;b.$$childHead?(b.$$childTail.$$nextSibling=d,b.$$childTail=d):b.$$childHead=b.$$childTail=d;(a||b!=this)&&d.$on("$destroy",c);return d},$watch:function(a,b,d){var e=g(a);if(e.$$watchDelegate)return e.$$watchDelegate(this,b,d,e);var f=this.$$watchers,h={fn:b,last:n,get:e,exp:a,eq:!!d};c=null;G(b)||(h.fn=z);f||(f=this.$$watchers=[]);f.unshift(h);
  return function(){Xa(f,h);c=null}},$watchGroup:function(a,b){function c(){h=!1;l?(l=!1,b(e,e,g)):b(e,d,g)}var d=Array(a.length),e=Array(a.length),f=[],g=this,h=!1,l=!0;if(!a.length){var k=!0;g.$evalAsync(function(){k&&b(e,e,g)});return function(){k=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=a;d[0]=c;b(e,a===c?e:d,f)});s(a,function(a,b){var l=g.$watch(a,function(a,f){e[b]=a;d[b]=f;h||(h=!0,g.$evalAsync(c))});f.push(l)});return function(){for(;f.length;)f.shift()()}},$watchCollection:function(a,
  b){function c(a){e=a;var b,d,g,h;if(!B(e)){if(I(e))if(Ta(e))for(f!==q&&(f=q,u=f.length=0,k++),a=e.length,u!==a&&(k++,f.length=u=a),b=0;b<a;b++)h=f[b],g=e[b],d=h!==h&&g!==g,d||h===g||(k++,f[b]=g);else{f!==m&&(f=m={},u=0,k++);a=0;for(b in e)e.hasOwnProperty(b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(k++,f[b]=g)):(u++,f[b]=g,k++));if(u>a)for(b in k++,f)e.hasOwnProperty(b)||(u--,delete f[b])}else f!==e&&(f=e,k++);return k}}c.$stateful=!0;var d=this,e,f,h,l=1<b.length,k=0,n=g(a,c),q=[],m=
  {},p=!0,u=0;return this.$watch(n,function(){p?(p=!1,b(e,e,d)):b(e,h,d);if(l)if(I(e))if(Ta(e)){h=Array(e.length);for(var a=0;a<e.length;a++)h[a]=e[a]}else for(a in h={},e)sc.call(e,a)&&(h[a]=e[a]);else h=e})},$digest:function(){var e,g,l,m,u,v,s=b,t,W=[],y,J;k("$digest");h.$$checkUrlChange();this===r&&null!==d&&(h.defer.cancel(d),q());c=null;do{v=!1;for(t=this;P.length;){try{J=P.shift(),J.scope.$eval(J.expression,J.locals)}catch(D){f(D)}c=null}a:do{if(m=t.$$watchers)for(u=m.length;u--;)try{if(e=m[u])if((g=
  e.get(t))!==(l=e.last)&&!(e.eq?ga(g,l):"number"===typeof g&&"number"===typeof l&&isNaN(g)&&isNaN(l)))v=!0,c=e,e.last=e.eq?Ea(g,null):g,e.fn(g,l===n?g:l,t),5>s&&(y=4-s,W[y]||(W[y]=[]),W[y].push({msg:G(e.exp)?"fn: "+(e.exp.name||e.exp.toString()):e.exp,newVal:g,oldVal:l}));else if(e===c){v=!1;break a}}catch(B){f(B)}if(!(m=t.$$childHead||t!==this&&t.$$nextSibling))for(;t!==this&&!(m=t.$$nextSibling);)t=t.$parent}while(t=m);if((v||P.length)&&!s--)throw r.$$phase=null,a("infdig",b,W);}while(v||P.length);
  for(r.$$phase=null;p.length;)try{p.shift()()}catch(da){f(da)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;if(this!==r){for(var b in this.$$listenerCount)m(this,this.$$listenerCount[b],b);a.$$childHead==this&&(a.$$childHead=this.$$nextSibling);a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);
  this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync=z;this.$on=this.$watch=this.$watchGroup=function(){return z};this.$$listeners={};this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=this.$root=this.$$watchers=null}}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a,b){r.$$phase||P.length||h.defer(function(){P.length&&r.$digest()});P.push({scope:this,expression:a,locals:b})},$$postDigest:function(a){p.push(a)},$apply:function(a){try{return k("$apply"),
  this.$eval(a)}catch(b){f(b)}finally{r.$$phase=null;try{r.$digest()}catch(c){throw f(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&v.push(b);u()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,m(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,g=!1,h={name:a,targetScope:e,
  stopPropagation:function(){g=!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},l=Ya([h],arguments,1),k,m;do{d=e.$$listeners[a]||c;h.currentScope=e;k=0;for(m=d.length;k<m;k++)if(d[k])try{d[k].apply(null,l)}catch(n){f(n)}else d.splice(k,1),k--,m--;if(g)return h.currentScope=null,h;e=e.$parent}while(e);h.currentScope=null;return h},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;
  for(var g=Ya([e],arguments,1),h,l;c=d;){e.currentScope=c;d=c.$$listeners[a]||[];h=0;for(l=d.length;h<l;h++)if(d[h])try{d[h].apply(null,g)}catch(k){f(k)}else d.splice(h,1),h--,l--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var r=new l,P=r.$$asyncQueue=[],p=r.$$postDigestQueue=[],v=r.$$applyAsyncQueue=[];return r}]}function Sd(){var b=/^\s*(https?|ftp|mailto|tel|file):/,a=/^\s*((https?|ftp|file|blob):|data:image\/)/;
  this.aHrefSanitizationWhitelist=function(a){return y(a)?(b=a,this):b};this.imgSrcSanitizationWhitelist=function(b){return y(b)?(a=b,this):a};this.$get=function(){return function(c,d){var e=d?a:b,f;f=Ba(c).href;return""===f||f.match(e)?c:"unsafe:"+f}}}function Cf(b){if("self"===b)return b;if(F(b)){if(-1<b.indexOf("***"))throw Ca("iwcard",b);b=gd(b).replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return new RegExp("^"+b+"$")}if(pb(b))return new RegExp("^"+b.source+"$");throw Ca("imatcher");}function hd(b){var a=
  [];y(b)&&s(b,function(b){a.push(Cf(b))});return a}function Te(){this.SCE_CONTEXTS=na;var b=["self"],a=[];this.resourceUrlWhitelist=function(a){arguments.length&&(b=hd(a));return b};this.resourceUrlBlacklist=function(b){arguments.length&&(a=hd(b));return a};this.$get=["$injector",function(c){function d(a,b){return"self"===a?$c(b):!!a.exec(b.href)}function e(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};
  b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b}var f=function(a){throw Ca("unsafe");};c.has("$sanitize")&&(f=c.get("$sanitize"));var g=e(),h={};h[na.HTML]=e(g);h[na.CSS]=e(g);h[na.URL]=e(g);h[na.JS]=e(g);h[na.RESOURCE_URL]=e(h[na.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw Ca("icontext",a,b);if(null===b||b===t||""===b)return b;if("string"!==typeof b)throw Ca("itype",a);return new c(b)},getTrusted:function(c,e){if(null===
  e||e===t||""===e)return e;var g=h.hasOwnProperty(c)?h[c]:null;if(g&&e instanceof g)return e.$$unwrapTrustedValue();if(c===na.RESOURCE_URL){var g=Ba(e.toString()),n,q,u=!1;n=0;for(q=b.length;n<q;n++)if(d(b[n],g)){u=!0;break}if(u)for(n=0,q=a.length;n<q;n++)if(d(a[n],g)){u=!1;break}if(u)return e;throw Ca("insecurl",e.toString());}if(c===na.HTML)return f(e);throw Ca("unsafe");},valueOf:function(a){return a instanceof g?a.$$unwrapTrustedValue():a}}}]}function Se(){var b=!0;this.enabled=function(a){arguments.length&&
  (b=!!a);return b};this.$get=["$parse","$sceDelegate",function(a,c){if(b&&8>Ra)throw Ca("iequirks");var d=ra(na);d.isEnabled=function(){return b};d.trustAs=c.trustAs;d.getTrusted=c.getTrusted;d.valueOf=c.valueOf;b||(d.trustAs=d.getTrusted=function(a,b){return b},d.valueOf=pa);d.parseAs=function(b,c){var e=a(c);return e.literal&&e.constant?e:a(c,function(a){return d.getTrusted(b,a)})};var e=d.parseAs,f=d.getTrusted,g=d.trustAs;s(na,function(a,b){var c=Q(b);d[db("parse_as_"+c)]=function(b){return e(a,
  b)};d[db("get_trusted_"+c)]=function(b){return f(a,b)};d[db("trust_as_"+c)]=function(b){return g(a,b)}});return d}]}function Ue(){this.$get=["$window","$document",function(b,a){var c={},d=ba((/android (\d+)/.exec(Q((b.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),f=a[0]||{},g,h=/^(Moz|webkit|ms)(?=[A-Z])/,l=f.body&&f.body.style,k=!1,m=!1;if(l){for(var n in l)if(k=h.exec(n)){g=k[0];g=g.substr(0,1).toUpperCase()+g.substr(1);break}g||(g="WebkitOpacity"in l&&"webkit");
  k=!!("transition"in l||g+"Transition"in l);m=!!("animation"in l||g+"Animation"in l);!d||k&&m||(k=F(f.body.style.webkitTransition),m=F(f.body.style.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hasEvent:function(a){if("input"===a&&11>=Ra)return!1;if(B(c[a])){var b=f.createElement("div");c[a]="on"+a in b}return c[a]},csp:bb(),vendorPrefix:g,transitions:k,animations:m,android:d}}]}function We(){this.$get=["$templateCache","$http","$q",function(b,a,c){function d(e,f){d.totalPendingRequests++;
  var g=a.defaults&&a.defaults.transformResponse;H(g)?g=g.filter(function(a){return a!==Zb}):g===Zb&&(g=null);return a.get(e,{cache:b,transformResponse:g}).finally(function(){d.totalPendingRequests--}).then(function(a){return a.data},function(a){if(!f)throw ja("tpload",e);return c.reject(a)})}d.totalPendingRequests=0;return d}]}function Xe(){this.$get=["$rootScope","$browser","$location",function(b,a,c){return{findBindings:function(a,b,c){a=a.getElementsByClassName("ng-binding");var g=[];s(a,function(a){var d=
  ca.element(a).data("$binding");d&&s(d,function(d){c?(new RegExp("(^|\\s)"+gd(b)+"(\\s|\\||$)")).test(d)&&g.push(a):-1!=d.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,c){for(var g=["ng-","data-ng-","ng\\:"],h=0;h<g.length;++h){var l=a.querySelectorAll("["+g[h]+"model"+(c?"=":"*=")+'"'+b+'"]');if(l.length)return l}},getLocation:function(){return c.url()},setLocation:function(a){a!==c.url()&&(c.url(a),b.$digest())},whenStable:function(b){a.notifyWhenNoOutstandingRequests(b)}}}]}function Ye(){this.$get=
  ["$rootScope","$browser","$q","$$q","$exceptionHandler",function(b,a,c,d,e){function f(f,l,k){var m=y(k)&&!k,n=(m?d:c).defer(),q=n.promise;l=a.defer(function(){try{n.resolve(f())}catch(a){n.reject(a),e(a)}finally{delete g[q.$$timeoutId]}m||b.$apply()},l);q.$$timeoutId=l;g[l]=n;return q}var g={};f.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),delete g[b.$$timeoutId],a.defer.cancel(b.$$timeoutId)):!1};return f}]}function Ba(b){Ra&&(Z.setAttribute("href",b),b=Z.href);
  Z.setAttribute("href",b);return{href:Z.href,protocol:Z.protocol?Z.protocol.replace(/:$/,""):"",host:Z.host,search:Z.search?Z.search.replace(/^\?/,""):"",hash:Z.hash?Z.hash.replace(/^#/,""):"",hostname:Z.hostname,port:Z.port,pathname:"/"===Z.pathname.charAt(0)?Z.pathname:"/"+Z.pathname}}function $c(b){b=F(b)?Ba(b):b;return b.protocol===id.protocol&&b.host===id.host}function Ze(){this.$get=ea(M)}function Ec(b){function a(c,d){if(I(c)){var e={};s(c,function(b,c){e[c]=a(c,b)});return e}return b.factory(c+
  "Filter",d)}this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+"Filter")}}];a("currency",jd);a("date",kd);a("filter",Df);a("json",Ef);a("limitTo",Ff);a("lowercase",Gf);a("number",ld);a("orderBy",md);a("uppercase",Hf)}function Df(){return function(b,a,c){if(!H(b))return b;var d;switch(typeof a){case "function":break;case "boolean":case "number":case "string":d=!0;case "object":a=If(a,c,d);break;default:return b}return b.filter(a)}}function If(b,a,c){var d=I(b)&&"$"in
  b;!0===a?a=ga:G(a)||(a=function(a,b){if(I(a)||I(b))return!1;a=Q(""+a);b=Q(""+b);return-1!==a.indexOf(b)});return function(e){return d&&!I(e)?Ia(e,b.$,a,!1):Ia(e,b,a,c)}}function Ia(b,a,c,d,e){var f=typeof b,g=typeof a;if("string"===g&&"!"===a.charAt(0))return!Ia(b,a.substring(1),c,d);if(H(b))return b.some(function(b){return Ia(b,a,c,d)});switch(f){case "object":var h;if(d){for(h in b)if("$"!==h.charAt(0)&&Ia(b[h],a,c,!0))return!0;return e?!1:Ia(b,a,c,!1)}if("object"===g){for(h in a)if(e=a[h],!G(e)&&
  (f="$"===h,!Ia(f?b:b[h],e,c,f,f)))return!1;return!0}return c(b,a);case "function":return!1;default:return c(b,a)}}function jd(b){var a=b.NUMBER_FORMATS;return function(b,d,e){B(d)&&(d=a.CURRENCY_SYM);B(e)&&(e=a.PATTERNS[1].maxFrac);return null==b?b:nd(b,a.PATTERNS[1],a.GROUP_SEP,a.DECIMAL_SEP,e).replace(/\u00A4/g,d)}}function ld(b){var a=b.NUMBER_FORMATS;return function(b,d){return null==b?b:nd(b,a.PATTERNS[0],a.GROUP_SEP,a.DECIMAL_SEP,d)}}function nd(b,a,c,d,e){if(!isFinite(b)||I(b))return"";var f=
  0>b;b=Math.abs(b);var g=b+"",h="",l=[],k=!1;if(-1!==g.indexOf("e")){var m=g.match(/([\d\.]+)e(-?)(\d+)/);m&&"-"==m[2]&&m[3]>e+1?b=0:(h=g,k=!0)}if(k)0<e&&1>b&&(h=b.toFixed(e),b=parseFloat(h));else{g=(g.split(od)[1]||"").length;B(e)&&(e=Math.min(Math.max(a.minFrac,g),a.maxFrac));b=+(Math.round(+(b.toString()+"e"+e)).toString()+"e"+-e);var g=(""+b).split(od),k=g[0],g=g[1]||"",n=0,q=a.lgSize,u=a.gSize;if(k.length>=q+u)for(n=k.length-q,m=0;m<n;m++)0===(n-m)%u&&0!==m&&(h+=c),h+=k.charAt(m);for(m=n;m<k.length;m++)0===
  (k.length-m)%q&&0!==m&&(h+=c),h+=k.charAt(m);for(;g.length<e;)g+="0";e&&"0"!==e&&(h+=d+g.substr(0,e))}0===b&&(f=!1);l.push(f?a.negPre:a.posPre,h,f?a.negSuf:a.posSuf);return l.join("")}function Jb(b,a,c){var d="";0>b&&(d="-",b=-b);for(b=""+b;b.length<a;)b="0"+b;c&&(b=b.substr(b.length-a));return d+b}function $(b,a,c,d){c=c||0;return function(e){e=e["get"+b]();if(0<c||e>-c)e+=c;0===e&&-12==c&&(e=12);return Jb(e,a,d)}}function Kb(b,a){return function(c,d){var e=c["get"+b](),f=vb(a?"SHORT"+b:b);return d[f][e]}}
  function pd(b){var a=(new Date(b,0,1)).getDay();return new Date(b,0,(4>=a?5:12)-a)}function qd(b){return function(a){var c=pd(a.getFullYear());a=+new Date(a.getFullYear(),a.getMonth(),a.getDate()+(4-a.getDay()))-+c;a=1+Math.round(a/6048E5);return Jb(a,b)}}function kd(b){function a(a){var b;if(b=a.match(c)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,l=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=ba(b[9]+b[10]),g=ba(b[9]+b[11]));h.call(a,ba(b[1]),ba(b[2])-1,ba(b[3]));f=ba(b[4]||0)-f;
  g=ba(b[5]||0)-g;h=ba(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));l.call(a,f,g,h,b)}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,e,f){var g="",h=[],l,k;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;F(c)&&(c=Jf.test(c)?ba(c):a(c));V(c)&&(c=new Date(c));if(!qa(c))return c;for(;e;)(k=Kf.exec(e))?(h=Ya(h,k,1),e=h.pop()):(h.push(e),e=null);f&&"UTC"===f&&(c=new Date(c.getTime()),c.setMinutes(c.getMinutes()+
  c.getTimezoneOffset()));s(h,function(a){l=Lf[a];g+=l?l(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Ef(){return function(b,a){B(a)&&(a=2);return $a(b,a)}}function Ff(){return function(b,a){V(b)&&(b=b.toString());return H(b)||F(b)?(a=Infinity===Math.abs(Number(a))?Number(a):ba(a))?0<a?b.slice(0,a):b.slice(a):F(b)?"":[]:b}}function md(b){return function(a,c,d){function e(a,b){return b?function(b,c){return a(c,b)}:a}function f(a){switch(typeof a){case "number":case "boolean":case "string":return!0;
  default:return!1}}function g(a){return null===a?"null":"function"===typeof a.valueOf&&(a=a.valueOf(),f(a))||"function"===typeof a.toString&&(a=a.toString(),f(a))?a:""}function h(a,b){var c=typeof a,d=typeof b;c===d&&"object"===c&&(a=g(a),b=g(b));return c===d?("string"===c&&(a=a.toLowerCase(),b=b.toLowerCase()),a===b?0:a<b?-1:1):c<d?-1:1}if(!Ta(a))return a;c=H(c)?c:[c];0===c.length&&(c=["+"]);c=c.map(function(a){var c=!1,d=a||pa;if(F(a)){if("+"==a.charAt(0)||"-"==a.charAt(0))c="-"==a.charAt(0),a=a.substring(1);
  if(""===a)return e(h,c);d=b(a);if(d.constant){var f=d();return e(function(a,b){return h(a[f],b[f])},c)}}return e(function(a,b){return h(d(a),d(b))},c)});return Za.call(a).sort(e(function(a,b){for(var d=0;d<c.length;d++){var e=c[d](a,b);if(0!==e)return e}return 0},d))}}function Ja(b){G(b)&&(b={link:b});b.restrict=b.restrict||"AC";return ea(b)}function rd(b,a,c,d,e){var f=this,g=[],h=f.$$parentForm=b.parent().controller("form")||Lb;f.$error={};f.$$success={};f.$pending=t;f.$name=e(a.name||a.ngForm||
  "")(c);f.$dirty=!1;f.$pristine=!0;f.$valid=!0;f.$invalid=!1;f.$submitted=!1;h.$addControl(f);f.$rollbackViewValue=function(){s(g,function(a){a.$rollbackViewValue()})};f.$commitViewValue=function(){s(g,function(a){a.$commitViewValue()})};f.$addControl=function(a){Ma(a.$name,"input");g.push(a);a.$name&&(f[a.$name]=a)};f.$$renameControl=function(a,b){var c=a.$name;f[c]===a&&delete f[c];f[b]=a;a.$name=b};f.$removeControl=function(a){a.$name&&f[a.$name]===a&&delete f[a.$name];s(f.$pending,function(b,c){f.$setValidity(c,
  null,a)});s(f.$error,function(b,c){f.$setValidity(c,null,a)});s(f.$$success,function(b,c){f.$setValidity(c,null,a)});Xa(g,a)};sd({ctrl:this,$element:b,set:function(a,b,c){var d=a[b];d?-1===d.indexOf(c)&&d.push(c):a[b]=[c]},unset:function(a,b,c){var d=a[b];d&&(Xa(d,c),0===d.length&&delete a[b])},parentForm:h,$animate:d});f.$setDirty=function(){d.removeClass(b,Sa);d.addClass(b,Mb);f.$dirty=!0;f.$pristine=!1;h.$setDirty()};f.$setPristine=function(){d.setClass(b,Sa,Mb+" ng-submitted");f.$dirty=!1;f.$pristine=
  !0;f.$submitted=!1;s(g,function(a){a.$setPristine()})};f.$setUntouched=function(){s(g,function(a){a.$setUntouched()})};f.$setSubmitted=function(){d.addClass(b,"ng-submitted");f.$submitted=!0;h.$setSubmitted()}}function ic(b){b.$formatters.push(function(a){return b.$isEmpty(a)?a:a.toString()})}function jb(b,a,c,d,e,f){var g=Q(a[0].type);if(!e.android){var h=!1;a.on("compositionstart",function(a){h=!0});a.on("compositionend",function(){h=!1;l()})}var l=function(b){k&&(f.defer.cancel(k),k=null);if(!h){var e=
  a.val();b=b&&b.type;"password"===g||c.ngTrim&&"false"===c.ngTrim||(e=U(e));(d.$viewValue!==e||""===e&&d.$$hasNativeValidators)&&d.$setViewValue(e,b)}};if(e.hasEvent("input"))a.on("input",l);else{var k,m=function(a,b,c){k||(k=f.defer(function(){k=null;b&&b.value===c||l(a)}))};a.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))a.on("paste cut",m)}a.on("change",l);d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)}}
  function Nb(b,a){return function(c,d){var e,f;if(qa(c))return c;if(F(c)){'"'==c.charAt(0)&&'"'==c.charAt(c.length-1)&&(c=c.substring(1,c.length-1));if(Mf.test(c))return new Date(c);b.lastIndex=0;if(e=b.exec(c))return e.shift(),f=d?{yyyy:d.getFullYear(),MM:d.getMonth()+1,dd:d.getDate(),HH:d.getHours(),mm:d.getMinutes(),ss:d.getSeconds(),sss:d.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},s(e,function(b,c){c<a.length&&(f[a[c]]=+b)}),new Date(f.yyyy,f.MM-1,f.dd,f.HH,f.mm,f.ss||0,
  1E3*f.sss||0)}return NaN}}function kb(b,a,c,d){return function(e,f,g,h,l,k,m){function n(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function q(a){return y(a)?qa(a)?a:c(a):t}td(e,f,g,h);jb(e,f,g,h,l,k);var u=h&&h.$options&&h.$options.timezone,r;h.$$parserName=b;h.$parsers.push(function(b){return h.$isEmpty(b)?null:a.test(b)?(b=c(b,r),"UTC"===u&&b.setMinutes(b.getMinutes()-b.getTimezoneOffset()),b):t});h.$formatters.push(function(a){if(a&&!qa(a))throw Ob("datefmt",a);if(n(a)){if((r=a)&&"UTC"===
  u){var b=6E4*r.getTimezoneOffset();r=new Date(r.getTime()+b)}return m("date")(a,d,u)}r=null;return""});if(y(g.min)||g.ngMin){var s;h.$validators.min=function(a){return!n(a)||B(s)||c(a)>=s};g.$observe("min",function(a){s=q(a);h.$validate()})}if(y(g.max)||g.ngMax){var p;h.$validators.max=function(a){return!n(a)||B(p)||c(a)<=p};g.$observe("max",function(a){p=q(a);h.$validate()})}}}function td(b,a,c,d){(d.$$hasNativeValidators=I(a[0].validity))&&d.$parsers.push(function(b){var c=a.prop("validity")||{};
  return c.badInput&&!c.typeMismatch?t:b})}function ud(b,a,c,d,e){if(y(d)){b=b(d);if(!b.constant)throw S("ngModel")("constexpr",c,d);return b(a)}return e}function jc(b,a){b="ngClass"+b;return["$animate",function(c){function d(a,b){var c=[],d=0;a:for(;d<a.length;d++){for(var e=a[d],m=0;m<b.length;m++)if(e==b[m])continue a;c.push(e)}return c}function e(a){if(!H(a)){if(F(a))return a.split(" ");if(I(a)){var b=[];s(a,function(a,c){a&&(b=b.concat(c.split(" ")))});return b}}return a}return{restrict:"AC",link:function(f,
  g,h){function l(a,b){var c=g.data("$classCounts")||{},d=[];s(a,function(a){if(0<b||c[a])c[a]=(c[a]||0)+b,c[a]===+(0<b)&&d.push(a)});g.data("$classCounts",c);return d.join(" ")}function k(b){if(!0===a||f.$index%2===a){var k=e(b||[]);if(!m){var u=l(k,1);h.$addClass(u)}else if(!ga(b,m)){var r=e(m),u=d(k,r),k=d(r,k),u=l(u,1),k=l(k,-1);u&&u.length&&c.addClass(g,u);k&&k.length&&c.removeClass(g,k)}}m=ra(b)}var m;f.$watch(h[b],k,!0);h.$observe("class",function(a){k(f.$eval(h[b]))});"ngClass"!==b&&f.$watch("$index",
  function(c,d){var g=c&1;if(g!==(d&1)){var k=e(f.$eval(h[b]));g===a?(g=l(k,1),h.$addClass(g)):(g=l(k,-1),h.$removeClass(g))}})}}}]}function sd(b){function a(a,b){b&&!f[a]?(k.addClass(e,a),f[a]=!0):!b&&f[a]&&(k.removeClass(e,a),f[a]=!1)}function c(b,c){b=b?"-"+uc(b,"-"):"";a(lb+b,!0===c);a(vd+b,!1===c)}var d=b.ctrl,e=b.$element,f={},g=b.set,h=b.unset,l=b.parentForm,k=b.$animate;f[vd]=!(f[lb]=e.hasClass(lb));d.$setValidity=function(b,e,f){e===t?(d.$pending||(d.$pending={}),g(d.$pending,b,f)):(d.$pending&&
  h(d.$pending,b,f),wd(d.$pending)&&(d.$pending=t));Wa(e)?e?(h(d.$error,b,f),g(d.$$success,b,f)):(g(d.$error,b,f),h(d.$$success,b,f)):(h(d.$error,b,f),h(d.$$success,b,f));d.$pending?(a(xd,!0),d.$valid=d.$invalid=t,c("",null)):(a(xd,!1),d.$valid=wd(d.$error),d.$invalid=!d.$valid,c("",d.$valid));e=d.$pending&&d.$pending[b]?t:d.$error[b]?!1:d.$$success[b]?!0:null;c(b,e);l.$setValidity(b,e,d)}}function wd(b){if(b)for(var a in b)return!1;return!0}var Nf=/^\/(.+)\/([a-z]*)$/,Q=function(b){return F(b)?b.toLowerCase():
  b},sc=Object.prototype.hasOwnProperty,vb=function(b){return F(b)?b.toUpperCase():b},Ra,D,sa,Za=[].slice,pf=[].splice,Of=[].push,Da=Object.prototype.toString,Ka=S("ng"),ca=M.angular||(M.angular={}),cb,ob=0;Ra=Y.documentMode;z.$inject=[];pa.$inject=[];var H=Array.isArray,U=function(b){return F(b)?b.trim():b},gd=function(b){return b.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08")},bb=function(){if(y(bb.isActive_))return bb.isActive_;var b=!(!Y.querySelector("[ng-csp]")&&!Y.querySelector("[data-ng-csp]"));
  if(!b)try{new Function("")}catch(a){b=!0}return bb.isActive_=b},sb=["ng-","data-ng-","ng:","x-ng-"],Md=/[A-Z]/g,vc=!1,Rb,oa=1,qb=3,Qd={full:"1.3.13",major:1,minor:3,dot:13,codeName:"meticulous-riffleshuffle"};R.expando="ng339";var Ab=R.cache={},hf=1;R._data=function(b){return this.cache[b[this.expando]]||{}};var cf=/([\:\-\_]+(.))/g,df=/^moz([A-Z])/,Pf={mouseleave:"mouseout",mouseenter:"mouseover"},Ub=S("jqLite"),gf=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,Tb=/<|&#?\w+;/,ef=/<([\w:]+)/,ff=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
  ia={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ia.optgroup=ia.option;ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead;ia.th=ia.td;var La=R.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===Y.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),R(M).on("load",a))},
  toString:function(){var b=[];s(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?D(this[b]):D(this[this.length+b])},length:0,push:Of,sort:[].sort,splice:[].splice},Fb={};s("multiple selected checked disabled readOnly required open".split(" "),function(b){Fb[Q(b)]=b});var Nc={};s("input select option textarea button form details".split(" "),function(b){Nc[b]=!0});var Oc={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"};
  s({data:Wb,removeData:yb},function(b,a){R[a]=b});s({data:Wb,inheritedData:Eb,scope:function(b){return D.data(b,"$scope")||Eb(b.parentNode||b,["$isolateScope","$scope"])},isolateScope:function(b){return D.data(b,"$isolateScope")||D.data(b,"$isolateScopeNoTemplate")},controller:Jc,injector:function(b){return Eb(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Bb,css:function(b,a,c){a=db(a);if(y(c))b.style[a]=c;else return b.style[a]},attr:function(b,a,c){var d=Q(a);if(Fb[d])if(y(c))c?
  (b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||z).specified?d:t;else if(y(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?t:b},prop:function(b,a,c){if(y(c))b[a]=c;else return b[a]},text:function(){function b(a,b){if(B(b)){var d=a.nodeType;return d===oa||d===qb?a.textContent:""}a.textContent=b}b.$dv="";return b}(),val:function(b,a){if(B(a)){if(b.multiple&&"select"===ua(b)){var c=[];s(b.options,function(a){a.selected&&
  c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(B(a))return b.innerHTML;xb(b,!0);b.innerHTML=a},empty:Kc},function(b,a){R.prototype[a]=function(a,d){var e,f,g=this.length;if(b!==Kc&&(2==b.length&&b!==Bb&&b!==Jc?a:d)===t){if(I(a)){for(e=0;e<g;e++)if(b===Wb)b(this[e],a);else for(f in a)b(this[e],f,a[f]);return this}e=b.$dv;g=e===t?Math.min(g,1):g;for(f=0;f<g;f++){var h=b(this[f],a,d);e=e?e+h:h}return e}for(e=0;e<g;e++)b(this[e],a,d);return this}});
  s({removeData:yb,on:function a(c,d,e,f){if(y(f))throw Ub("onargs");if(Fc(c)){var g=zb(c,!0);f=g.events;var h=g.handle;h||(h=g.handle=lf(c,f));for(var g=0<=d.indexOf(" ")?d.split(" "):[d],l=g.length;l--;){d=g[l];var k=f[d];k||(f[d]=[],"mouseenter"===d||"mouseleave"===d?a(c,Pf[d],function(a){var c=a.relatedTarget;c&&(c===this||this.contains(c))||h(a,d)}):"$destroy"!==d&&c.addEventListener(d,h,!1),k=f[d]);k.push(e)}}},off:Ic,one:function(a,c,d){a=D(a);a.on(c,function f(){a.off(c,d);a.off(c,f)});a.on(c,
  d)},replaceWith:function(a,c){var d,e=a.parentNode;xb(a);s(new R(c),function(c){d?e.insertBefore(c,d.nextSibling):e.replaceChild(c,a);d=c})},children:function(a){var c=[];s(a.childNodes,function(a){a.nodeType===oa&&c.push(a)});return c},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,c){var d=a.nodeType;if(d===oa||11===d){c=new R(c);for(var d=0,e=c.length;d<e;d++)a.appendChild(c[d])}},prepend:function(a,c){if(a.nodeType===oa){var d=a.firstChild;s(new R(c),function(c){a.insertBefore(c,
  d)})}},wrap:function(a,c){c=D(c).eq(0).clone()[0];var d=a.parentNode;d&&d.replaceChild(c,a);c.appendChild(a)},remove:Lc,detach:function(a){Lc(a,!0)},after:function(a,c){var d=a,e=a.parentNode;c=new R(c);for(var f=0,g=c.length;f<g;f++){var h=c[f];e.insertBefore(h,d.nextSibling);d=h}},addClass:Db,removeClass:Cb,toggleClass:function(a,c,d){c&&s(c.split(" "),function(c){var f=d;B(f)&&(f=!Bb(a,c));(f?Db:Cb)(a,c)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},
  find:function(a,c){return a.getElementsByTagName?a.getElementsByTagName(c):[]},clone:Vb,triggerHandler:function(a,c,d){var e,f,g=c.type||c,h=zb(a);if(h=(h=h&&h.events)&&h[g])e={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopImmediatePropagation:function(){this.immediatePropagationStopped=!0},isImmediatePropagationStopped:function(){return!0===this.immediatePropagationStopped},stopPropagation:z,type:g,target:a},c.type&&(e=x(e,
  c)),c=ra(h),f=d?[e].concat(d):[e],s(c,function(c){e.isImmediatePropagationStopped()||c.apply(a,f)})}},function(a,c){R.prototype[c]=function(c,e,f){for(var g,h=0,l=this.length;h<l;h++)B(g)?(g=a(this[h],c,e,f),y(g)&&(g=D(g))):Hc(g,a(this[h],c,e,f));return y(g)?g:this};R.prototype.bind=R.prototype.on;R.prototype.unbind=R.prototype.off});eb.prototype={put:function(a,c){this[Na(a,this.nextUid)]=c},get:function(a){return this[Na(a,this.nextUid)]},remove:function(a){var c=this[a=Na(a,this.nextUid)];delete this[a];
  return c}};var Qc=/^function\s*[^\(]*\(\s*([^\)]*)\)/m,Qf=/,/,Rf=/^\s*(_?)(\S+?)\1\s*$/,Pc=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ga=S("$injector");ab.$$annotate=function(a,c,d){var e;if("function"===typeof a){if(!(e=a.$inject)){e=[];if(a.length){if(c)throw F(d)&&d||(d=a.name||mf(a)),Ga("strictdi",d);c=a.toString().replace(Pc,"");c=c.match(Qc);s(c[1].split(Qf),function(a){a.replace(Rf,function(a,c,d){e.push(d)})})}a.$inject=e}}else H(a)?(c=a.length-1,tb(a[c],"fn"),e=a.slice(0,c)):tb(a,"fn",!0);return e};
  var Sf=S("$animate"),Ce=["$provide",function(a){this.$$selectors={};this.register=function(c,d){var e=c+"-animation";if(c&&"."!=c.charAt(0))throw Sf("notcsel",c);this.$$selectors[c.substr(1)]=e;a.factory(e,d)};this.classNameFilter=function(a){1===arguments.length&&(this.$$classNameFilter=a instanceof RegExp?a:null);return this.$$classNameFilter};this.$get=["$$q","$$asyncCallback","$rootScope",function(a,d,e){function f(d){var f,g=a.defer();g.promise.$$cancelFn=function(){f&&f()};e.$$postDigest(function(){f=
  d(function(){g.resolve()})});return g.promise}function g(a,c){var d=[],e=[],f=ha();s((a.attr("class")||"").split(/\s+/),function(a){f[a]=!0});s(c,function(a,c){var g=f[c];!1===a&&g?e.push(c):!0!==a||g||d.push(c)});return 0<d.length+e.length&&[d.length?d:null,e.length?e:null]}function h(a,c,d){for(var e=0,f=c.length;e<f;++e)a[c[e]]=d}function l(){m||(m=a.defer(),d(function(){m.resolve();m=null}));return m.promise}function k(a,c){if(ca.isObject(c)){var d=x(c.from||{},c.to||{});a.css(d)}}var m;return{animate:function(a,
  c,d){k(a,{from:c,to:d});return l()},enter:function(a,c,d,e){k(a,e);d?d.after(a):c.prepend(a);return l()},leave:function(a,c){a.remove();return l()},move:function(a,c,d,e){return this.enter(a,c,d,e)},addClass:function(a,c,d){return this.setClass(a,c,[],d)},$$addClassImmediately:function(a,c,d){a=D(a);c=F(c)?c:H(c)?c.join(" "):"";s(a,function(a){Db(a,c)});k(a,d);return l()},removeClass:function(a,c,d){return this.setClass(a,[],c,d)},$$removeClassImmediately:function(a,c,d){a=D(a);c=F(c)?c:H(c)?c.join(" "):
  "";s(a,function(a){Cb(a,c)});k(a,d);return l()},setClass:function(a,c,d,e){var k=this,l=!1;a=D(a);var m=a.data("$$animateClasses");m?e&&m.options&&(m.options=ca.extend(m.options||{},e)):(m={classes:{},options:e},l=!0);e=m.classes;c=H(c)?c:c.split(" ");d=H(d)?d:d.split(" ");h(e,c,!0);h(e,d,!1);l&&(m.promise=f(function(c){var d=a.data("$$animateClasses");a.removeData("$$animateClasses");if(d){var e=g(a,d.classes);e&&k.$$setClassImmediately(a,e[0],e[1],d.options)}c()}),a.data("$$animateClasses",m));
  return m.promise},$$setClassImmediately:function(a,c,d,e){c&&this.$$addClassImmediately(a,c);d&&this.$$removeClassImmediately(a,d);k(a,e);return l()},enabled:z,cancel:z}}]}],ja=S("$compile");xc.$inject=["$provide","$$sanitizeUriProvider"];var Sc=/^((?:x|data)[\:\-_])/i,qf=S("$controller"),Wc="application/json",$b={"Content-Type":Wc+";charset=utf-8"},sf=/^\[|^\{(?!\{)/,tf={"[":/]$/,"{":/}$/},rf=/^\)\]\}',?\n/,ac=S("$interpolate"),Tf=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,wf={http:80,https:443,ftp:21},Hb=
  S("$location"),Uf={$$html5:!1,$$replace:!1,absUrl:Ib("$$absUrl"),url:function(a){if(B(a))return this.$$url;var c=Tf.exec(a);(c[1]||""===a)&&this.path(decodeURIComponent(c[1]));(c[2]||c[1]||""===a)&&this.search(c[3]||"");this.hash(c[5]||"");return this},protocol:Ib("$$protocol"),host:Ib("$$host"),port:Ib("$$port"),path:dd("$$path",function(a){a=null!==a?a.toString():"";return"/"==a.charAt(0)?a:"/"+a}),search:function(a,c){switch(arguments.length){case 0:return this.$$search;case 1:if(F(a)||V(a))a=
  a.toString(),this.$$search=rc(a);else if(I(a))a=Ea(a,{}),s(a,function(c,e){null==c&&delete a[e]}),this.$$search=a;else throw Hb("isrcharg");break;default:B(c)||null===c?delete this.$$search[a]:this.$$search[a]=c}this.$$compose();return this},hash:dd("$$hash",function(a){return null!==a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};s([cd,ec,dc],function(a){a.prototype=Object.create(Uf);a.prototype.state=function(c){if(!arguments.length)return this.$$state;if(a!==dc||!this.$$html5)throw Hb("nostate");
  this.$$state=B(c)?null:c;return this}});var la=S("$parse"),Vf=Function.prototype.call,Wf=Function.prototype.apply,Xf=Function.prototype.bind,mb=ha();s({"null":function(){return null},"true":function(){return!0},"false":function(){return!1},undefined:function(){}},function(a,c){a.constant=a.literal=a.sharedGetter=!0;mb[c]=a});mb["this"]=function(a){return a};mb["this"].sharedGetter=!0;var nb=x(ha(),{"+":function(a,c,d,e){d=d(a,c);e=e(a,c);return y(d)?y(e)?d+e:d:y(e)?e:t},"-":function(a,c,d,e){d=d(a,
  c);e=e(a,c);return(y(d)?d:0)-(y(e)?e:0)},"*":function(a,c,d,e){return d(a,c)*e(a,c)},"/":function(a,c,d,e){return d(a,c)/e(a,c)},"%":function(a,c,d,e){return d(a,c)%e(a,c)},"===":function(a,c,d,e){return d(a,c)===e(a,c)},"!==":function(a,c,d,e){return d(a,c)!==e(a,c)},"==":function(a,c,d,e){return d(a,c)==e(a,c)},"!=":function(a,c,d,e){return d(a,c)!=e(a,c)},"<":function(a,c,d,e){return d(a,c)<e(a,c)},">":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,
  c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"!":function(a,c,d){return!d(a,c)},"=":!0,"|":!0}),Yf={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},hc=function(a){this.options=a};hc.prototype={constructor:hc,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();
  else if(this.isIdent(a))this.readIdent();else if(this.is(a,"(){}[].,;:?"))this.tokens.push({index:this.index,text:a}),this.index++;else if(this.isWhitespace(a))this.index++;else{var c=a+this.peek(),d=c+this.peek(2),e=nb[c],f=nb[d];nb[a]||e||f?(a=f?d:e?c:a,this.tokens.push({index:this.index,text:a,operator:!0}),this.index+=a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,c){return-1!==c.indexOf(a)},peek:function(a){a=a||1;return this.index+
  a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,c,d){d=d||this.index;c=y(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw la("lexerr",a,c,this.text);
  },readNumber:function(){for(var a="",c=this.index;this.index<this.text.length;){var d=Q(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var e=this.peek();if("e"==d&&this.isExpOperator(e))a+=d;else if(this.isExpOperator(d)&&e&&this.isNumber(e)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||e&&this.isNumber(e)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:c,text:a,constant:!0,value:Number(a)})},readIdent:function(){for(var a=
  this.index;this.index<this.text.length;){var c=this.text.charAt(this.index);if(!this.isIdent(c)&&!this.isNumber(c))break;this.index++}this.tokens.push({index:a,text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var c=this.index;this.index++;for(var d="",e=a,f=!1;this.index<this.text.length;){var g=this.text.charAt(this.index),e=e+g;if(f)"u"===g?(f=this.text.substring(this.index+1,this.index+5),f.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+f+"]"),this.index+=
  4,d+=String.fromCharCode(parseInt(f,16))):d+=Yf[g]||g,f=!1;else if("\\"===g)f=!0;else{if(g===a){this.index++;this.tokens.push({index:c,text:e,constant:!0,value:d});return}d+=g}this.index++}this.throwError("Unterminated quote",c)}};var ib=function(a,c,d){this.lexer=a;this.$filter=c;this.options=d};ib.ZERO=x(function(){return 0},{sharedGetter:!0,constant:!0});ib.prototype={constructor:ib,parse:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.statements();0!==this.tokens.length&&this.throwError("is an unexpected token",
  this.tokens[0]);a.literal=!!a.literal;a.constant=!!a.constant;return a},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.peek().identifier&&this.peek().text in mb?a=mb[this.consume().text]:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var c,d;c=this.expect("(","[",".");)"("===c.text?(a=this.functionCall(a,
  d),d=null):"["===c.text?(d=a,a=this.objectIndex(a)):"."===c.text?(d=a,a=this.fieldAccess(a)):this.throwError("IMPOSSIBLE");return a},throwError:function(a,c){throw la("syntax",c.text,a,c.index+1,this.text,this.text.substring(c.index));},peekToken:function(){if(0===this.tokens.length)throw la("ueoe",this.text);return this.tokens[0]},peek:function(a,c,d,e){return this.peekAhead(0,a,c,d,e)},peekAhead:function(a,c,d,e,f){if(this.tokens.length>a){a=this.tokens[a];var g=a.text;if(g===c||g===d||g===e||g===
  f||!(c||d||e||f))return a}return!1},expect:function(a,c,d,e){return(a=this.peek(a,c,d,e))?(this.tokens.shift(),a):!1},consume:function(a){if(0===this.tokens.length)throw la("ueoe",this.text);var c=this.expect(a);c||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return c},unaryFn:function(a,c){var d=nb[a];return x(function(a,f){return d(a,f,c)},{constant:c.constant,inputs:[c]})},binaryFn:function(a,c,d,e){var f=nb[c];return x(function(c,e){return f(c,e,a,d)},{constant:a.constant&&
  d.constant,inputs:!e&&[a,d]})},identifier:function(){for(var a=this.consume().text;this.peek(".")&&this.peekAhead(1).identifier&&!this.peekAhead(2,"(");)a+=this.consume().text+this.consume().text;return yf(a,this.options,this.text)},constant:function(){var a=this.consume().value;return x(function(){return a},{constant:!0,literal:!0})},statements:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.filterChain()),!this.expect(";"))return 1===a.length?a[0]:function(c,
  d){for(var e,f=0,g=a.length;f<g;f++)e=a[f](c,d);return e}},filterChain:function(){for(var a=this.expression();this.expect("|");)a=this.filter(a);return a},filter:function(a){var c=this.$filter(this.consume().text),d,e;if(this.peek(":"))for(d=[],e=[];this.expect(":");)d.push(this.expression());var f=[a].concat(d||[]);return x(function(f,h){var l=a(f,h);if(e){e[0]=l;for(l=d.length;l--;)e[l+1]=d[l](f,h);return c.apply(t,e)}return c(l)},{constant:!c.$stateful&&f.every(fc),inputs:!c.$stateful&&f})},expression:function(){return this.assignment()},
  assignment:function(){var a=this.ternary(),c,d;return(d=this.expect("="))?(a.assign||this.throwError("implies assignment but ["+this.text.substring(0,d.index)+"] can not be assigned to",d),c=this.ternary(),x(function(d,f){return a.assign(d,c(d,f),f)},{inputs:[a,c]})):a},ternary:function(){var a=this.logicalOR(),c;if(this.expect("?")&&(c=this.assignment(),this.consume(":"))){var d=this.assignment();return x(function(e,f){return a(e,f)?c(e,f):d(e,f)},{constant:a.constant&&c.constant&&d.constant})}return a},
  logicalOR:function(){for(var a=this.logicalAND(),c;c=this.expect("||");)a=this.binaryFn(a,c.text,this.logicalAND(),!0);return a},logicalAND:function(){for(var a=this.equality(),c;c=this.expect("&&");)a=this.binaryFn(a,c.text,this.equality(),!0);return a},equality:function(){for(var a=this.relational(),c;c=this.expect("==","!=","===","!==");)a=this.binaryFn(a,c.text,this.relational());return a},relational:function(){for(var a=this.additive(),c;c=this.expect("<",">","<=",">=");)a=this.binaryFn(a,c.text,
  this.additive());return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a=this.binaryFn(a,c.text,this.multiplicative());return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a=this.binaryFn(a,c.text,this.unary());return a},unary:function(){var a;return this.expect("+")?this.primary():(a=this.expect("-"))?this.binaryFn(ib.ZERO,a.text,this.unary()):(a=this.expect("!"))?this.unaryFn(a.text,this.unary()):this.primary()},fieldAccess:function(a){var c=
  this.identifier();return x(function(d,e,f){d=f||a(d,e);return null==d?t:c(d)},{assign:function(d,e,f){var g=a(d,f);g||a.assign(d,g={},f);return c.assign(g,e)}})},objectIndex:function(a){var c=this.text,d=this.expression();this.consume("]");return x(function(e,f){var g=a(e,f),h=d(e,f);ta(h,c);return g?ma(g[h],c):t},{assign:function(e,f,g){var h=ta(d(e,g),c),l=ma(a(e,g),c);l||a.assign(e,l={},g);return l[h]=f}})},functionCall:function(a,c){var d=[];if(")"!==this.peekToken().text){do d.push(this.expression());
  while(this.expect(","))}this.consume(")");var e=this.text,f=d.length?[]:null;return function(g,h){var l=c?c(g,h):y(c)?t:g,k=a(g,h,l)||z;if(f)for(var m=d.length;m--;)f[m]=ma(d[m](g,h),e);ma(l,e);if(k){if(k.constructor===k)throw la("isecfn",e);if(k===Vf||k===Wf||k===Xf)throw la("isecff",e);}l=k.apply?k.apply(l,f):k(f[0],f[1],f[2],f[3],f[4]);f&&(f.length=0);return ma(l,e)}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))
  }this.consume("]");return x(function(c,d){for(var e=[],f=0,g=a.length;f<g;f++)e.push(a[f](c,d));return e},{literal:!0,constant:a.every(fc),inputs:a})},object:function(){var a=[],c=[];if("}"!==this.peekToken().text){do{if(this.peek("}"))break;var d=this.consume();d.constant?a.push(d.value):d.identifier?a.push(d.text):this.throwError("invalid key",d);this.consume(":");c.push(this.expression())}while(this.expect(","))}this.consume("}");return x(function(d,f){for(var g={},h=0,l=c.length;h<l;h++)g[a[h]]=
  c[h](d,f);return g},{literal:!0,constant:c.every(fc),inputs:c})}};var Af=ha(),zf=ha(),Bf=Object.prototype.valueOf,Ca=S("$sce"),na={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},ja=S("$compile"),Z=Y.createElement("a"),id=Ba(M.location.href);Ec.$inject=["$provide"];jd.$inject=["$locale"];ld.$inject=["$locale"];var od=".",Lf={yyyy:$("FullYear",4),yy:$("FullYear",2,0,!0),y:$("FullYear",1),MMMM:Kb("Month"),MMM:Kb("Month",!0),MM:$("Month",2,1),M:$("Month",1,1),dd:$("Date",2),d:$("Date",
  1),HH:$("Hours",2),H:$("Hours",1),hh:$("Hours",2,-12),h:$("Hours",1,-12),mm:$("Minutes",2),m:$("Minutes",1),ss:$("Seconds",2),s:$("Seconds",1),sss:$("Milliseconds",3),EEEE:Kb("Day"),EEE:Kb("Day",!0),a:function(a,c){return 12>a.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<=a?"+":"")+(Jb(Math[0<a?"floor":"ceil"](a/60),2)+Jb(Math.abs(a%60),2))},ww:qd(2),w:qd(1)},Kf=/((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/,Jf=/^\-?\d+$/;
  kd.$inject=["$locale"];var Gf=ea(Q),Hf=ea(vb);md.$inject=["$parse"];var Td=ea({restrict:"E",compile:function(a,c){if(!c.href&&!c.xlinkHref&&!c.name)return function(a,c){if("a"===c[0].nodeName.toLowerCase()){var f="[object SVGAnimatedString]"===Da.call(c.prop("href"))?"xlink:href":"href";c.on("click",function(a){c.attr(f)||a.preventDefault()})}}}}),wb={};s(Fb,function(a,c){if("multiple"!=a){var d=ya("ng-"+c);wb[d]=function(){return{restrict:"A",priority:100,link:function(a,f,g){a.$watch(g[d],function(a){g.$set(c,
  !!a)})}}}}});s(Oc,function(a,c){wb[c]=function(){return{priority:100,link:function(a,e,f){if("ngPattern"===c&&"/"==f.ngPattern.charAt(0)&&(e=f.ngPattern.match(Nf))){f.$set("ngPattern",new RegExp(e[1],e[2]));return}a.$watch(f[c],function(a){f.$set(c,a)})}}}});s(["src","srcset","href"],function(a){var c=ya("ng-"+a);wb[c]=function(){return{priority:99,link:function(d,e,f){var g=a,h=a;"href"===a&&"[object SVGAnimatedString]"===Da.call(e.prop("href"))&&(h="xlinkHref",f.$attr[h]="xlink:href",g=null);f.$observe(c,
  function(c){c?(f.$set(h,c),Ra&&g&&e.prop(g,f[h])):"href"===a&&f.$set(h,null)})}}}});var Lb={$addControl:z,$$renameControl:function(a,c){a.$name=c},$removeControl:z,$setValidity:z,$setDirty:z,$setPristine:z,$setSubmitted:z};rd.$inject=["$element","$attrs","$scope","$animate","$interpolate"];var yd=function(a){return["$timeout",function(c){return{name:"form",restrict:a?"EAC":"E",controller:rd,compile:function(a){a.addClass(Sa).addClass(lb);return{pre:function(a,d,g,h){if(!("action"in g)){var l=function(c){a.$apply(function(){h.$commitViewValue();
  h.$setSubmitted()});c.preventDefault()};d[0].addEventListener("submit",l,!1);d.on("$destroy",function(){c(function(){d[0].removeEventListener("submit",l,!1)},0,!1)})}var k=h.$$parentForm,m=h.$name;m&&(hb(a,null,m,h,m),g.$observe(g.name?"name":"ngForm",function(c){m!==c&&(hb(a,null,m,t,m),m=c,hb(a,null,m,h,m),k.$$renameControl(h,m))}));d.on("$destroy",function(){k.$removeControl(h);m&&hb(a,null,m,t,m);x(h,Lb)})}}}}}]},Ud=yd(),ge=yd(!0),Mf=/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/,
  Zf=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,$f=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,ag=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,zd=/^(\d{4})-(\d{2})-(\d{2})$/,Ad=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,kc=/^(\d{4})-W(\d\d)$/,Bd=/^(\d{4})-(\d\d)$/,Cd=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Dd={text:function(a,c,d,e,f,g){jb(a,c,d,e,f,g);ic(e)},date:kb("date",zd,Nb(zd,["yyyy",
  "MM","dd"]),"yyyy-MM-dd"),"datetime-local":kb("datetimelocal",Ad,Nb(Ad,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:kb("time",Cd,Nb(Cd,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:kb("week",kc,function(a,c){if(qa(a))return a;if(F(a)){kc.lastIndex=0;var d=kc.exec(a);if(d){var e=+d[1],f=+d[2],g=d=0,h=0,l=0,k=pd(e),f=7*(f-1);c&&(d=c.getHours(),g=c.getMinutes(),h=c.getSeconds(),l=c.getMilliseconds());return new Date(e,0,k.getDate()+f,d,g,h,l)}}return NaN},"yyyy-Www"),month:kb("month",
  Bd,Nb(Bd,["yyyy","MM"]),"yyyy-MM"),number:function(a,c,d,e,f,g){td(a,c,d,e);jb(a,c,d,e,f,g);e.$$parserName="number";e.$parsers.push(function(a){return e.$isEmpty(a)?null:ag.test(a)?parseFloat(a):t});e.$formatters.push(function(a){if(!e.$isEmpty(a)){if(!V(a))throw Ob("numfmt",a);a=a.toString()}return a});if(d.min||d.ngMin){var h;e.$validators.min=function(a){return e.$isEmpty(a)||B(h)||a>=h};d.$observe("min",function(a){y(a)&&!V(a)&&(a=parseFloat(a,10));h=V(a)&&!isNaN(a)?a:t;e.$validate()})}if(d.max||
  d.ngMax){var l;e.$validators.max=function(a){return e.$isEmpty(a)||B(l)||a<=l};d.$observe("max",function(a){y(a)&&!V(a)&&(a=parseFloat(a,10));l=V(a)&&!isNaN(a)?a:t;e.$validate()})}},url:function(a,c,d,e,f,g){jb(a,c,d,e,f,g);ic(e);e.$$parserName="url";e.$validators.url=function(a,c){var d=a||c;return e.$isEmpty(d)||Zf.test(d)}},email:function(a,c,d,e,f,g){jb(a,c,d,e,f,g);ic(e);e.$$parserName="email";e.$validators.email=function(a,c){var d=a||c;return e.$isEmpty(d)||$f.test(d)}},radio:function(a,c,
  d,e){B(d.name)&&c.attr("name",++ob);c.on("click",function(a){c[0].checked&&e.$setViewValue(d.value,a&&a.type)});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e,f,g,h,l){var k=ud(l,a,"ngTrueValue",d.ngTrueValue,!0),m=ud(l,a,"ngFalseValue",d.ngFalseValue,!1);c.on("click",function(a){e.$setViewValue(c[0].checked,a&&a.type)});e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return!1===a};e.$formatters.push(function(a){return ga(a,
  k)});e.$parsers.push(function(a){return a?k:m})},hidden:z,button:z,submit:z,reset:z,file:z},yc=["$browser","$sniffer","$filter","$parse",function(a,c,d,e){return{restrict:"E",require:["?ngModel"],link:{pre:function(f,g,h,l){l[0]&&(Dd[Q(h.type)]||Dd.text)(f,g,h,l[0],c,a,d,e)}}}}],bg=/^(true|false|\d+)$/,ye=function(){return{restrict:"A",priority:100,compile:function(a,c){return bg.test(c.ngValue)?function(a,c,f){f.$set("value",a.$eval(f.ngValue))}:function(a,c,f){a.$watch(f.ngValue,function(a){f.$set("value",
  a)})}}}},Zd=["$compile",function(a){return{restrict:"AC",compile:function(c){a.$$addBindingClass(c);return function(c,e,f){a.$$addBindingInfo(e,f.ngBind);e=e[0];c.$watch(f.ngBind,function(a){e.textContent=a===t?"":a})}}}}],ae=["$interpolate","$compile",function(a,c){return{compile:function(d){c.$$addBindingClass(d);return function(d,f,g){d=a(f.attr(g.$attr.ngBindTemplate));c.$$addBindingInfo(f,d.expressions);f=f[0];g.$observe("ngBindTemplate",function(a){f.textContent=a===t?"":a})}}}}],$d=["$sce",
  "$parse","$compile",function(a,c,d){return{restrict:"A",compile:function(e,f){var g=c(f.ngBindHtml),h=c(f.ngBindHtml,function(a){return(a||"").toString()});d.$$addBindingClass(e);return function(c,e,f){d.$$addBindingInfo(e,f.ngBindHtml);c.$watch(h,function(){e.html(a.getTrustedHtml(g(c))||"")})}}}}],xe=ea({restrict:"A",require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),be=jc("",!0),de=jc("Odd",0),ce=jc("Even",1),ee=Ja({compile:function(a,c){c.$set("ngCloak",
  t);a.removeClass("ng-cloak")}}),fe=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],Dc={},cg={blur:!0,focus:!0};s("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var c=ya("ng-"+a);Dc[c]=["$parse","$rootScope",function(d,e){return{restrict:"A",compile:function(f,g){var h=d(g[c],null,!0);return function(c,d){d.on(a,function(d){var f=function(){h(c,{$event:d})};
  cg[a]&&e.$$phase?c.$evalAsync(f):c.$apply(f)})}}}}]});var ie=["$animate",function(a){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c,d,e,f,g){var h,l,k;c.$watch(e.ngIf,function(c){c?l||g(function(c,f){l=f;c[c.length++]=Y.createComment(" end ngIf: "+e.ngIf+" ");h={clone:c};a.enter(c,d.parent(),d)}):(k&&(k.remove(),k=null),l&&(l.$destroy(),l=null),h&&(k=ub(h.clone),a.leave(k).then(function(){k=null}),h=null))})}}}],je=["$templateRequest","$anchorScroll",
  "$animate","$sce",function(a,c,d,e){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ca.noop,compile:function(f,g){var h=g.ngInclude||g.src,l=g.onload||"",k=g.autoscroll;return function(f,g,q,s,r){var t=0,p,v,w,L=function(){v&&(v.remove(),v=null);p&&(p.$destroy(),p=null);w&&(d.leave(w).then(function(){v=null}),v=w,w=null)};f.$watch(e.parseAsResourceUrl(h),function(e){var h=function(){!y(k)||k&&!f.$eval(k)||c()},q=++t;e?(a(e,!0).then(function(a){if(q===t){var c=f.$new();
  s.template=a;a=r(c,function(a){L();d.enter(a,null,g).then(h)});p=c;w=a;p.$emit("$includeContentLoaded",e);f.$eval(l)}},function(){q===t&&(L(),f.$emit("$includeContentError",e))}),f.$emit("$includeContentRequested",e)):(L(),s.template=null)})}}}}],Ae=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(c,d,e,f){/SVG/.test(d[0].toString())?(d.empty(),a(Gc(f.template,Y).childNodes)(c,function(a){d.append(a)},{futureParentElement:d})):(d.html(f.template),a(d.contents())(c))}}}],
  ke=Ja({priority:450,compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),we=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,c,d,e){var f=c.attr(d.$attr.ngList)||", ",g="false"!==d.ngTrim,h=g?U(f):f;e.$parsers.push(function(a){if(!B(a)){var c=[];a&&s(a.split(h),function(a){a&&c.push(g?U(a):a)});return c}});e.$formatters.push(function(a){return H(a)?a.join(f):t});e.$isEmpty=function(a){return!a||!a.length}}}},lb="ng-valid",vd="ng-invalid",Sa="ng-pristine",
  Mb="ng-dirty",xd="ng-pending",Ob=new S("ngModel"),dg=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q","$interpolate",function(a,c,d,e,f,g,h,l,k,m){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=t;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success=
  {};this.$pending=t;this.$name=m(d.name||"",!1)(a);var n=f(d.ngModel),q=n.assign,u=n,r=q,P=null,p=this;this.$$setOptions=function(a){if((p.$options=a)&&a.getterSetter){var c=f(d.ngModel+"()"),g=f(d.ngModel+"($$$p)");u=function(a){var d=n(a);G(d)&&(d=c(a));return d};r=function(a,c){G(n(a))?g(a,{$$$p:p.$modelValue}):q(a,p.$modelValue)}}else if(!n.assign)throw Ob("nonassign",d.ngModel,va(e));};this.$render=z;this.$isEmpty=function(a){return B(a)||""===a||null===a||a!==a};var v=e.inheritedData("$formController")||
  Lb,w=0;sd({ctrl:this,$element:e,set:function(a,c){a[c]=!0},unset:function(a,c){delete a[c]},parentForm:v,$animate:g});this.$setPristine=function(){p.$dirty=!1;p.$pristine=!0;g.removeClass(e,Mb);g.addClass(e,Sa)};this.$setDirty=function(){p.$dirty=!0;p.$pristine=!1;g.removeClass(e,Sa);g.addClass(e,Mb);v.$setDirty()};this.$setUntouched=function(){p.$touched=!1;p.$untouched=!0;g.setClass(e,"ng-untouched","ng-touched")};this.$setTouched=function(){p.$touched=!0;p.$untouched=!1;g.setClass(e,"ng-touched",
  "ng-untouched")};this.$rollbackViewValue=function(){h.cancel(P);p.$viewValue=p.$$lastCommittedViewValue;p.$render()};this.$validate=function(){if(!V(p.$modelValue)||!isNaN(p.$modelValue)){var a=p.$$rawModelValue,c=p.$valid,d=p.$modelValue,e=p.$options&&p.$options.allowInvalid;p.$$runValidators(p.$error[p.$$parserName||"parse"]?!1:t,a,p.$$lastCommittedViewValue,function(f){e||c===f||(p.$modelValue=f?a:t,p.$modelValue!==d&&p.$$writeModelToScope())})}};this.$$runValidators=function(a,c,d,e){function f(){var a=
  !0;s(p.$validators,function(e,f){var g=e(c,d);a=a&&g;h(f,g)});return a?!0:(s(p.$asyncValidators,function(a,c){h(c,null)}),!1)}function g(){var a=[],e=!0;s(p.$asyncValidators,function(f,g){var l=f(c,d);if(!l||!G(l.then))throw Ob("$asyncValidators",l);h(g,t);a.push(l.then(function(){h(g,!0)},function(a){e=!1;h(g,!1)}))});a.length?k.all(a).then(function(){l(e)},z):l(!0)}function h(a,c){m===w&&p.$setValidity(a,c)}function l(a){m===w&&e(a)}w++;var m=w;(function(a){var c=p.$$parserName||"parse";if(a===
  t)h(c,null);else if(h(c,a),!a)return s(p.$validators,function(a,c){h(c,null)}),s(p.$asyncValidators,function(a,c){h(c,null)}),!1;return!0})(a)?f()?g():l(!1):l(!1)};this.$commitViewValue=function(){var a=p.$viewValue;h.cancel(P);if(p.$$lastCommittedViewValue!==a||""===a&&p.$$hasNativeValidators)p.$$lastCommittedViewValue=a,p.$pristine&&this.$setDirty(),this.$$parseAndValidate()};this.$$parseAndValidate=function(){var c=p.$$lastCommittedViewValue,d=B(c)?t:!0;if(d)for(var e=0;e<p.$parsers.length;e++)if(c=
  p.$parsers[e](c),B(c)){d=!1;break}V(p.$modelValue)&&isNaN(p.$modelValue)&&(p.$modelValue=u(a));var f=p.$modelValue,g=p.$options&&p.$options.allowInvalid;p.$$rawModelValue=c;g&&(p.$modelValue=c,p.$modelValue!==f&&p.$$writeModelToScope());p.$$runValidators(d,c,p.$$lastCommittedViewValue,function(a){g||(p.$modelValue=a?c:t,p.$modelValue!==f&&p.$$writeModelToScope())})};this.$$writeModelToScope=function(){r(a,p.$modelValue);s(p.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};this.$setViewValue=
  function(a,c){p.$viewValue=a;p.$options&&!p.$options.updateOnDefault||p.$$debounceViewValueCommit(c)};this.$$debounceViewValueCommit=function(c){var d=0,e=p.$options;e&&y(e.debounce)&&(e=e.debounce,V(e)?d=e:V(e[c])?d=e[c]:V(e["default"])&&(d=e["default"]));h.cancel(P);d?P=h(function(){p.$commitViewValue()},d):l.$$phase?p.$commitViewValue():a.$apply(function(){p.$commitViewValue()})};a.$watch(function(){var c=u(a);if(c!==p.$modelValue){p.$modelValue=p.$$rawModelValue=c;for(var d=p.$formatters,e=d.length,
  f=c;e--;)f=d[e](f);p.$viewValue!==f&&(p.$viewValue=p.$$lastCommittedViewValue=f,p.$render(),p.$$runValidators(t,c,f,z))}return c})}],ve=["$rootScope",function(a){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:dg,priority:1,compile:function(c){c.addClass(Sa).addClass("ng-untouched").addClass(lb);return{pre:function(a,c,f,g){var h=g[0],l=g[1]||Lb;h.$$setOptions(g[2]&&g[2].$options);l.$addControl(h);f.$observe("name",function(a){h.$name!==a&&l.$$renameControl(h,a)});a.$on("$destroy",
  function(){l.$removeControl(h)})},post:function(c,e,f,g){var h=g[0];if(h.$options&&h.$options.updateOn)e.on(h.$options.updateOn,function(a){h.$$debounceViewValueCommit(a&&a.type)});e.on("blur",function(e){h.$touched||(a.$$phase?c.$evalAsync(h.$setTouched):c.$apply(h.$setTouched))})}}}}}],eg=/(\s+|^)default(\s+|$)/,ze=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,c){var d=this;this.$options=a.$eval(c.ngModelOptions);this.$options.updateOn!==t?(this.$options.updateOnDefault=
  !1,this.$options.updateOn=U(this.$options.updateOn.replace(eg,function(){d.$options.updateOnDefault=!0;return" "}))):this.$options.updateOnDefault=!0}]}},le=Ja({terminal:!0,priority:1E3}),me=["$locale","$interpolate",function(a,c){var d=/{}/g,e=/^when(Minus)?(.+)$/;return{restrict:"EA",link:function(f,g,h){function l(a){g.text(a||"")}var k=h.count,m=h.$attr.when&&g.attr(h.$attr.when),n=h.offset||0,q=f.$eval(m)||{},u={},m=c.startSymbol(),r=c.endSymbol(),t=m+k+"-"+n+r,p=ca.noop,v;s(h,function(a,c){var d=
  e.exec(c);d&&(d=(d[1]?"-":"")+Q(d[2]),q[d]=g.attr(h.$attr[c]))});s(q,function(a,e){u[e]=c(a.replace(d,t))});f.$watch(k,function(c){c=parseFloat(c);var d=isNaN(c);d||c in q||(c=a.pluralCat(c-n));c===v||d&&isNaN(v)||(p(),p=f.$watch(u[c],l),v=c)})}}}],ne=["$parse","$animate",function(a,c){var d=S("ngRepeat"),e=function(a,c,d,e,k,m,n){a[d]=e;k&&(a[k]=m);a.$index=c;a.$first=0===c;a.$last=c===n-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(c&1))};return{restrict:"A",multiElement:!0,transclude:"element",
  priority:1E3,terminal:!0,$$tlb:!0,compile:function(f,g){var h=g.ngRepeat,l=Y.createComment(" end ngRepeat: "+h+" "),k=h.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);if(!k)throw d("iexp",h);var m=k[1],n=k[2],q=k[3],u=k[4],k=m.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);if(!k)throw d("iidexp",m);var r=k[3]||k[1],y=k[2];if(q&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(q)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(q)))throw d("badident",
  q);var p,v,w,B,z={$id:Na};u?p=a(u):(w=function(a,c){return Na(c)},B=function(a){return a});return function(a,f,g,k,m){p&&(v=function(c,d,e){y&&(z[y]=c);z[r]=d;z.$index=e;return p(a,z)});var u=ha();a.$watchCollection(n,function(g){var k,p,n=f[0],E,z=ha(),x,T,N,G,H,C,I;q&&(a[q]=g);if(Ta(g))H=g,p=v||w;else{p=v||B;H=[];for(I in g)g.hasOwnProperty(I)&&"$"!=I.charAt(0)&&H.push(I);H.sort()}x=H.length;I=Array(x);for(k=0;k<x;k++)if(T=g===H?k:H[k],N=g[T],G=p(T,N,k),u[G])C=u[G],delete u[G],z[G]=C,I[k]=C;else{if(z[G])throw s(I,
  function(a){a&&a.scope&&(u[a.id]=a)}),d("dupes",h,G,N);I[k]={id:G,scope:t,clone:t};z[G]=!0}for(E in u){C=u[E];G=ub(C.clone);c.leave(G);if(G[0].parentNode)for(k=0,p=G.length;k<p;k++)G[k].$$NG_REMOVED=!0;C.scope.$destroy()}for(k=0;k<x;k++)if(T=g===H?k:H[k],N=g[T],C=I[k],C.scope){E=n;do E=E.nextSibling;while(E&&E.$$NG_REMOVED);C.clone[0]!=E&&c.move(ub(C.clone),null,D(n));n=C.clone[C.clone.length-1];e(C.scope,k,r,N,y,T,x)}else m(function(a,d){C.scope=d;var f=l.cloneNode(!1);a[a.length++]=f;c.enter(a,
  null,D(n));n=f;C.clone=a;z[C.id]=C;e(C.scope,k,r,N,y,T,x)});u=z})}}}}],oe=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(c,d,e){c.$watch(e.ngShow,function(c){a[c?"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],he=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(c,d,e){c.$watch(e.ngHide,function(c){a[c?"addClass":"removeClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],pe=Ja(function(a,c,d){a.$watchCollection(d.ngStyle,
  function(a,d){d&&a!==d&&s(d,function(a,d){c.css(d,"")});a&&c.css(a)})}),qe=["$animate",function(a){return{restrict:"EA",require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(c,d,e,f){var g=[],h=[],l=[],k=[],m=function(a,c){return function(){a.splice(c,1)}};c.$watch(e.ngSwitch||e.on,function(c){var d,e;d=0;for(e=l.length;d<e;++d)a.cancel(l[d]);d=l.length=0;for(e=k.length;d<e;++d){var r=ub(h[d].clone);k[d].$destroy();(l[d]=a.leave(r)).then(m(l,d))}h.length=0;k.length=0;(g=
  f.cases["!"+c]||f.cases["?"])&&s(g,function(c){c.transclude(function(d,e){k.push(e);var f=c.element;d[d.length++]=Y.createComment(" end ngSwitchWhen: ");h.push({clone:d});a.enter(d,f.parent(),f)})})})}}}],re=Ja({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,c,d,e,f){e.cases["!"+d.ngSwitchWhen]=e.cases["!"+d.ngSwitchWhen]||[];e.cases["!"+d.ngSwitchWhen].push({transclude:f,element:c})}}),se=Ja({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,
  link:function(a,c,d,e,f){e.cases["?"]=e.cases["?"]||[];e.cases["?"].push({transclude:f,element:c})}}),ue=Ja({restrict:"EAC",link:function(a,c,d,e,f){if(!f)throw S("ngTransclude")("orphan",va(c));f(function(a){c.empty();c.append(a)})}}),Vd=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(c,d){"text/ng-template"==d.type&&a.put(d.id,c[0].text)}}}],fg=S("ngOptions"),te=ea({restrict:"A",terminal:!0}),Wd=["$compile","$parse",function(a,c){var d=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
  e={$setViewValue:z};return{restrict:"E",require:["select","?ngModel"],controller:["$element","$scope","$attrs",function(a,c,d){var l=this,k={},m=e,n;l.databound=d.ngModel;l.init=function(a,c,d){m=a;n=d};l.addOption=function(c,d){Ma(c,'"option value"');k[c]=!0;m.$viewValue==c&&(a.val(c),n.parent()&&n.remove());d&&d[0].hasAttribute("selected")&&(d[0].selected=!0)};l.removeOption=function(a){this.hasOption(a)&&(delete k[a],m.$viewValue===a&&this.renderUnknownOption(a))};l.renderUnknownOption=function(c){c=
  "? "+Na(c)+" ?";n.val(c);a.prepend(n);a.val(c);n.prop("selected",!0)};l.hasOption=function(a){return k.hasOwnProperty(a)};c.$on("$destroy",function(){l.renderUnknownOption=z})}],link:function(e,g,h,l){function k(a,c,d,e){d.$render=function(){var a=d.$viewValue;e.hasOption(a)?(C.parent()&&C.remove(),c.val(a),""===a&&p.prop("selected",!0)):B(a)&&p?c.val(""):e.renderUnknownOption(a)};c.on("change",function(){a.$apply(function(){C.parent()&&C.remove();d.$setViewValue(c.val())})})}function m(a,c,d){var e;
  d.$render=function(){var a=new eb(d.$viewValue);s(c.find("option"),function(c){c.selected=y(a.get(c.value))})};a.$watch(function(){ga(e,d.$viewValue)||(e=ra(d.$viewValue),d.$render())});c.on("change",function(){a.$apply(function(){var a=[];s(c.find("option"),function(c){c.selected&&a.push(c.value)});d.$setViewValue(a)})})}function n(e,f,g){function h(a,c,d){S[x]=d;D&&(S[D]=c);return a(e,S)}function k(a){var c;if(u)if(M&&H(a)){c=new eb([]);for(var d=0;d<a.length;d++)c.put(h(M,null,a[d]),!0)}else c=
  new eb(a);else M&&(a=h(M,null,a));return function(d,e){var f;f=M?M:B?B:F;return u?y(c.remove(h(f,d,e))):a===h(f,d,e)}}function l(){v||(e.$$postDigest(p),v=!0)}function m(a,c,d){a[c]=a[c]||0;a[c]+=d?1:-1}function p(){v=!1;var a={"":[]},c=[""],d,l,n,r,t;n=g.$viewValue;r=O(e)||[];var B=D?Object.keys(r).sort():r,x,A,H,F,N={};t=k(n);var J=!1,U,V;Q={};for(F=0;H=B.length,F<H;F++){x=F;if(D&&(x=B[F],"$"===x.charAt(0)))continue;A=r[x];d=h(I,x,A)||"";(l=a[d])||(l=a[d]=[],c.push(d));d=t(x,A);J=J||d;A=h(C,x,A);
  A=y(A)?A:"";V=M?M(e,S):D?B[F]:F;M&&(Q[V]=x);l.push({id:V,label:A,selected:d})}u||(z||null===n?a[""].unshift({id:"",label:"",selected:!J}):J||a[""].unshift({id:"?",label:"",selected:!0}));x=0;for(B=c.length;x<B;x++){d=c[x];l=a[d];R.length<=x?(n={element:G.clone().attr("label",d),label:l.label},r=[n],R.push(r),f.append(n.element)):(r=R[x],n=r[0],n.label!=d&&n.element.attr("label",n.label=d));J=null;F=0;for(H=l.length;F<H;F++)d=l[F],(t=r[F+1])?(J=t.element,t.label!==d.label&&(m(N,t.label,!1),m(N,d.label,
  !0),J.text(t.label=d.label),J.prop("label",t.label)),t.id!==d.id&&J.val(t.id=d.id),J[0].selected!==d.selected&&(J.prop("selected",t.selected=d.selected),Ra&&J.prop("selected",t.selected))):(""===d.id&&z?U=z:(U=w.clone()).val(d.id).prop("selected",d.selected).attr("selected",d.selected).prop("label",d.label).text(d.label),r.push(t={element:U,label:d.label,id:d.id,selected:d.selected}),m(N,d.label,!0),J?J.after(U):n.element.append(U),J=U);for(F++;r.length>F;)d=r.pop(),m(N,d.label,!1),d.element.remove()}for(;R.length>
  x;){l=R.pop();for(F=1;F<l.length;++F)m(N,l[F].label,!1);l[0].element.remove()}s(N,function(a,c){0<a?q.addOption(c):0>a&&q.removeOption(c)})}var n;if(!(n=r.match(d)))throw fg("iexp",r,va(f));var C=c(n[2]||n[1]),x=n[4]||n[6],A=/ as /.test(n[0])&&n[1],B=A?c(A):null,D=n[5],I=c(n[3]||""),F=c(n[2]?n[1]:x),O=c(n[7]),M=n[8]?c(n[8]):null,Q={},R=[[{element:f,label:""}]],S={};z&&(a(z)(e),z.removeClass("ng-scope"),z.remove());f.empty();f.on("change",function(){e.$apply(function(){var a=O(e)||[],c;if(u)c=[],s(f.val(),
  function(d){d=M?Q[d]:d;c.push("?"===d?t:""===d?null:h(B?B:F,d,a[d]))});else{var d=M?Q[f.val()]:f.val();c="?"===d?t:""===d?null:h(B?B:F,d,a[d])}g.$setViewValue(c);p()})});g.$render=p;e.$watchCollection(O,l);e.$watchCollection(function(){var a=O(e),c;if(a&&H(a)){c=Array(a.length);for(var d=0,f=a.length;d<f;d++)c[d]=h(C,d,a[d])}else if(a)for(d in c={},a)a.hasOwnProperty(d)&&(c[d]=h(C,d,a[d]));return c},l);u&&e.$watchCollection(function(){return g.$modelValue},l)}if(l[1]){var q=l[0];l=l[1];var u=h.multiple,
  r=h.ngOptions,z=!1,p,v=!1,w=D(Y.createElement("option")),G=D(Y.createElement("optgroup")),C=w.clone();h=0;for(var A=g.children(),x=A.length;h<x;h++)if(""===A[h].value){p=z=A.eq(h);break}q.init(l,z,C);u&&(l.$isEmpty=function(a){return!a||0===a.length});r?n(e,g,l):u?m(e,g,l):k(e,g,l,q)}}}}],Yd=["$interpolate",function(a){var c={addOption:z,removeOption:z};return{restrict:"E",priority:100,compile:function(d,e){if(B(e.value)){var f=a(d.text(),!0);f||e.$set("value",d.text())}return function(a,d,e){var k=
  d.parent(),m=k.data("$selectController")||k.parent().data("$selectController");m&&m.databound||(m=c);f?a.$watch(f,function(a,c){e.$set("value",a);c!==a&&m.removeOption(c);m.addOption(a,d)}):m.addOption(e.value,d);d.on("$destroy",function(){m.removeOption(e.value)})}}}}],Xd=ea({restrict:"E",terminal:!1}),Ac=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){e&&(d.required=!0,e.$validators.required=function(a,c){return!d.required||!e.$isEmpty(c)},d.$observe("required",function(){e.$validate()}))}}},
  zc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f,g=d.ngPattern||d.pattern;d.$observe("pattern",function(a){F(a)&&0<a.length&&(a=new RegExp("^"+a+"$"));if(a&&!a.test)throw S("ngPattern")("noregexp",g,a,va(c));f=a||t;e.$validate()});e.$validators.pattern=function(a){return e.$isEmpty(a)||B(f)||f.test(a)}}}}},Cc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=-1;d.$observe("maxlength",function(a){a=ba(a);f=isNaN(a)?-1:a;e.$validate()});
  e.$validators.maxlength=function(a,c){return 0>f||e.$isEmpty(c)||c.length<=f}}}}},Bc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=0;d.$observe("minlength",function(a){f=ba(a)||0;e.$validate()});e.$validators.minlength=function(a,c){return e.$isEmpty(c)||c.length>=f}}}}};M.angular.bootstrap?console.log("WARNING: Tried to load angular more than once."):(Nd(),Pd(ca),D(Y).ready(function(){Jd(Y,tc)}))})(window,document);!window.angular.$$csp()&&window.angular.element(document).find("head").prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}</style>');
  //# sourceMappingURL=angular.min.js.map
  
  /*!
   * ionic.bundle.js is a concatenation of:
   * ionic.js, angular.js, angular-animate.js,
   * angular-sanitize.js, angular-ui-router.js,
   * and ionic-angular.js
   */
  
  /*
   AngularJS v1.3.13
   (c) 2010-2014 Google, Inc. http://angularjs.org
   License: MIT
  */
  (function(N,f,W){'use strict';f.module("ngAnimate",["ng"]).directive("ngAnimateChildren",function(){return function(X,C,g){g=g.ngAnimateChildren;f.isString(g)&&0===g.length?C.data("$$ngAnimateChildren",!0):X.$watch(g,function(f){C.data("$$ngAnimateChildren",!!f)})}}).factory("$$animateReflow",["$$rAF","$document",function(f,C){return function(g){return f(function(){g()})}}]).config(["$provide","$animateProvider",function(X,C){function g(f){for(var n=0;n<f.length;n++){var g=f[n];if(1==g.nodeType)return g}}
  function ba(f,n){return g(f)==g(n)}var t=f.noop,n=f.forEach,da=C.$$selectors,aa=f.isArray,ea=f.isString,ga=f.isObject,r={running:!0},u;X.decorator("$animate",["$delegate","$$q","$injector","$sniffer","$rootElement","$$asyncCallback","$rootScope","$document","$templateRequest","$$jqLite",function(O,N,M,Y,y,H,P,W,Z,Q){function R(a,c){var b=a.data("$$ngAnimateState")||{};c&&(b.running=!0,b.structural=!0,a.data("$$ngAnimateState",b));return b.disabled||b.running&&b.structural}function D(a){var c,b=N.defer();
  b.promise.$$cancelFn=function(){c&&c()};P.$$postDigest(function(){c=a(function(){b.resolve()})});return b.promise}function I(a){if(ga(a))return a.tempClasses&&ea(a.tempClasses)&&(a.tempClasses=a.tempClasses.split(/\s+/)),a}function S(a,c,b){b=b||{};var d={};n(b,function(e,a){n(a.split(" "),function(a){d[a]=e})});var h=Object.create(null);n((a.attr("class")||"").split(/\s+/),function(e){h[e]=!0});var f=[],l=[];n(c&&c.classes||[],function(e,a){var b=h[a],c=d[a]||{};!1===e?(b||"addClass"==c.event)&&
  l.push(a):!0===e&&(b&&"removeClass"!=c.event||f.push(a))});return 0<f.length+l.length&&[f.join(" "),l.join(" ")]}function T(a){if(a){var c=[],b={};a=a.substr(1).split(".");(Y.transitions||Y.animations)&&c.push(M.get(da[""]));for(var d=0;d<a.length;d++){var f=a[d],k=da[f];k&&!b[f]&&(c.push(M.get(k)),b[f]=!0)}return c}}function U(a,c,b,d){function h(e,a){var b=e[a],c=e["before"+a.charAt(0).toUpperCase()+a.substr(1)];if(b||c)return"leave"==a&&(c=b,b=null),u.push({event:a,fn:b}),J.push({event:a,fn:c}),
  !0}function k(c,l,w){var E=[];n(c,function(a){a.fn&&E.push(a)});var m=0;n(E,function(c,f){var p=function(){a:{if(l){(l[f]||t)();if(++m<E.length)break a;l=null}w()}};switch(c.event){case "setClass":l.push(c.fn(a,e,A,p,d));break;case "animate":l.push(c.fn(a,b,d.from,d.to,p));break;case "addClass":l.push(c.fn(a,e||b,p,d));break;case "removeClass":l.push(c.fn(a,A||b,p,d));break;default:l.push(c.fn(a,p,d))}});l&&0===l.length&&w()}var l=a[0];if(l){d&&(d.to=d.to||{},d.from=d.from||{});var e,A;aa(b)&&(e=
  b[0],A=b[1],e?A?b=e+" "+A:(b=e,c="addClass"):(b=A,c="removeClass"));var w="setClass"==c,E=w||"addClass"==c||"removeClass"==c||"animate"==c,p=a.attr("class")+" "+b;if(x(p)){var ca=t,m=[],J=[],g=t,s=[],u=[],p=(" "+p).replace(/\s+/g,".");n(T(p),function(a){!h(a,c)&&w&&(h(a,"addClass"),h(a,"removeClass"))});return{node:l,event:c,className:b,isClassBased:E,isSetClassOperation:w,applyStyles:function(){d&&a.css(f.extend(d.from||{},d.to||{}))},before:function(a){ca=a;k(J,m,function(){ca=t;a()})},after:function(a){g=
  a;k(u,s,function(){g=t;a()})},cancel:function(){m&&(n(m,function(a){(a||t)(!0)}),ca(!0));s&&(n(s,function(a){(a||t)(!0)}),g(!0))}}}}}function G(a,c,b,d,h,k,l,e){function A(e){var l="$animate:"+e;J&&J[l]&&0<J[l].length&&H(function(){b.triggerHandler(l,{event:a,className:c})})}function w(){A("before")}function E(){A("after")}function p(){p.hasBeenRun||(p.hasBeenRun=!0,k())}function g(){if(!g.hasBeenRun){m&&m.applyStyles();g.hasBeenRun=!0;l&&l.tempClasses&&n(l.tempClasses,function(a){u.removeClass(b,
  a)});var w=b.data("$$ngAnimateState");w&&(m&&m.isClassBased?B(b,c):(H(function(){var e=b.data("$$ngAnimateState")||{};fa==e.index&&B(b,c,a)}),b.data("$$ngAnimateState",w)));A("close");e()}}var m=U(b,a,c,l);if(!m)return p(),w(),E(),g(),t;a=m.event;c=m.className;var J=f.element._data(m.node),J=J&&J.events;d||(d=h?h.parent():b.parent());if(z(b,d))return p(),w(),E(),g(),t;d=b.data("$$ngAnimateState")||{};var L=d.active||{},s=d.totalActive||0,q=d.last;h=!1;if(0<s){s=[];if(m.isClassBased)"setClass"==q.event?
  (s.push(q),B(b,c)):L[c]&&(v=L[c],v.event==a?h=!0:(s.push(v),B(b,c)));else if("leave"==a&&L["ng-leave"])h=!0;else{for(var v in L)s.push(L[v]);d={};B(b,!0)}0<s.length&&n(s,function(a){a.cancel()})}!m.isClassBased||m.isSetClassOperation||"animate"==a||h||(h="addClass"==a==b.hasClass(c));if(h)return p(),w(),E(),A("close"),e(),t;L=d.active||{};s=d.totalActive||0;if("leave"==a)b.one("$destroy",function(a){a=f.element(this);var e=a.data("$$ngAnimateState");e&&(e=e.active["ng-leave"])&&(e.cancel(),B(a,"ng-leave"))});
  u.addClass(b,"ng-animate");l&&l.tempClasses&&n(l.tempClasses,function(a){u.addClass(b,a)});var fa=K++;s++;L[c]=m;b.data("$$ngAnimateState",{last:m,active:L,index:fa,totalActive:s});w();m.before(function(e){var l=b.data("$$ngAnimateState");e=e||!l||!l.active[c]||m.isClassBased&&l.active[c].event!=a;p();!0===e?g():(E(),m.after(g))});return m.cancel}function q(a){if(a=g(a))a=f.isFunction(a.getElementsByClassName)?a.getElementsByClassName("ng-animate"):a.querySelectorAll(".ng-animate"),n(a,function(a){a=
  f.element(a);(a=a.data("$$ngAnimateState"))&&a.active&&n(a.active,function(a){a.cancel()})})}function B(a,c){if(ba(a,y))r.disabled||(r.running=!1,r.structural=!1);else if(c){var b=a.data("$$ngAnimateState")||{},d=!0===c;!d&&b.active&&b.active[c]&&(b.totalActive--,delete b.active[c]);if(d||!b.totalActive)u.removeClass(a,"ng-animate"),a.removeData("$$ngAnimateState")}}function z(a,c){if(r.disabled)return!0;if(ba(a,y))return r.running;var b,d,g;do{if(0===c.length)break;var k=ba(c,y),l=k?r:c.data("$$ngAnimateState")||
  {};if(l.disabled)return!0;k&&(g=!0);!1!==b&&(k=c.data("$$ngAnimateChildren"),f.isDefined(k)&&(b=k));d=d||l.running||l.last&&!l.last.isClassBased}while(c=c.parent());return!g||!b&&d}u=Q;y.data("$$ngAnimateState",r);var $=P.$watch(function(){return Z.totalPendingRequests},function(a,c){0===a&&($(),P.$$postDigest(function(){P.$$postDigest(function(){r.running=!1})}))}),K=0,V=C.classNameFilter(),x=V?function(a){return V.test(a)}:function(){return!0};return{animate:function(a,c,b,d,h){d=d||"ng-inline-animate";
  h=I(h)||{};h.from=b?c:null;h.to=b?b:c;return D(function(b){return G("animate",d,f.element(g(a)),null,null,t,h,b)})},enter:function(a,c,b,d){d=I(d);a=f.element(a);c=c&&f.element(c);b=b&&f.element(b);R(a,!0);O.enter(a,c,b);return D(function(h){return G("enter","ng-enter",f.element(g(a)),c,b,t,d,h)})},leave:function(a,c){c=I(c);a=f.element(a);q(a);R(a,!0);return D(function(b){return G("leave","ng-leave",f.element(g(a)),null,null,function(){O.leave(a)},c,b)})},move:function(a,c,b,d){d=I(d);a=f.element(a);
  c=c&&f.element(c);b=b&&f.element(b);q(a);R(a,!0);O.move(a,c,b);return D(function(h){return G("move","ng-move",f.element(g(a)),c,b,t,d,h)})},addClass:function(a,c,b){return this.setClass(a,c,[],b)},removeClass:function(a,c,b){return this.setClass(a,[],c,b)},setClass:function(a,c,b,d){d=I(d);a=f.element(a);a=f.element(g(a));if(R(a))return O.$$setClassImmediately(a,c,b,d);var h,k=a.data("$$animateClasses"),l=!!k;k||(k={classes:{}});h=k.classes;c=aa(c)?c:c.split(" ");n(c,function(a){a&&a.length&&(h[a]=
  !0)});b=aa(b)?b:b.split(" ");n(b,function(a){a&&a.length&&(h[a]=!1)});if(l)return d&&k.options&&(k.options=f.extend(k.options||{},d)),k.promise;a.data("$$animateClasses",k={classes:h,options:d});return k.promise=D(function(e){var l=a.parent(),b=g(a),c=b.parentNode;if(!c||c.$$NG_REMOVED||b.$$NG_REMOVED)e();else{b=a.data("$$animateClasses");a.removeData("$$animateClasses");var c=a.data("$$ngAnimateState")||{},d=S(a,b,c.active);return d?G("setClass",d,a,l,null,function(){d[0]&&O.$$addClassImmediately(a,
  d[0]);d[1]&&O.$$removeClassImmediately(a,d[1])},b.options,e):e()}})},cancel:function(a){a.$$cancelFn()},enabled:function(a,c){switch(arguments.length){case 2:if(a)B(c);else{var b=c.data("$$ngAnimateState")||{};b.disabled=!0;c.data("$$ngAnimateState",b)}break;case 1:r.disabled=!a;break;default:a=!r.disabled}return!!a}}}]);C.register("",["$window","$sniffer","$timeout","$$animateReflow",function(r,C,M,Y){function y(){b||(b=Y(function(){c=[];b=null;x={}}))}function H(a,e){b&&b();c.push(e);b=Y(function(){n(c,
  function(a){a()});c=[];b=null;x={}})}function P(a,e){var b=g(a);a=f.element(b);k.push(a);b=Date.now()+e;b<=h||(M.cancel(d),h=b,d=M(function(){X(k);k=[]},e,!1))}function X(a){n(a,function(a){(a=a.data("$$ngAnimateCSS3Data"))&&n(a.closeAnimationFns,function(a){a()})})}function Z(a,e){var b=e?x[e]:null;if(!b){var c=0,d=0,f=0,g=0;n(a,function(a){if(1==a.nodeType){a=r.getComputedStyle(a)||{};c=Math.max(Q(a[z+"Duration"]),c);d=Math.max(Q(a[z+"Delay"]),d);g=Math.max(Q(a[K+"Delay"]),g);var e=Q(a[K+"Duration"]);
  0<e&&(e*=parseInt(a[K+"IterationCount"],10)||1);f=Math.max(e,f)}});b={total:0,transitionDelay:d,transitionDuration:c,animationDelay:g,animationDuration:f};e&&(x[e]=b)}return b}function Q(a){var e=0;a=ea(a)?a.split(/\s*,\s*/):[];n(a,function(a){e=Math.max(parseFloat(a)||0,e)});return e}function R(b,e,c,d){b=0<=["ng-enter","ng-leave","ng-move"].indexOf(c);var f,p=e.parent(),h=p.data("$$ngAnimateKey");h||(p.data("$$ngAnimateKey",++a),h=a);f=h+"-"+g(e).getAttribute("class");var p=f+" "+c,h=x[p]?++x[p].total:
  0,m={};if(0<h){var n=c+"-stagger",m=f+" "+n;(f=!x[m])&&u.addClass(e,n);m=Z(e,m);f&&u.removeClass(e,n)}u.addClass(e,c);var n=e.data("$$ngAnimateCSS3Data")||{},k=Z(e,p);f=k.transitionDuration;k=k.animationDuration;if(b&&0===f&&0===k)return u.removeClass(e,c),!1;c=d||b&&0<f;b=0<k&&0<m.animationDelay&&0===m.animationDuration;e.data("$$ngAnimateCSS3Data",{stagger:m,cacheKey:p,running:n.running||0,itemIndex:h,blockTransition:c,closeAnimationFns:n.closeAnimationFns||[]});p=g(e);c&&(I(p,!0),d&&e.css(d));
  b&&(p.style[K+"PlayState"]="paused");return!0}function D(a,e,b,c,d){function f(){e.off(D,h);u.removeClass(e,k);u.removeClass(e,t);z&&M.cancel(z);G(e,b);var a=g(e),c;for(c in s)a.style.removeProperty(s[c])}function h(a){a.stopPropagation();var b=a.originalEvent||a;a=b.$manualTimeStamp||b.timeStamp||Date.now();b=parseFloat(b.elapsedTime.toFixed(3));Math.max(a-H,0)>=C&&b>=x&&c()}var m=g(e);a=e.data("$$ngAnimateCSS3Data");if(-1!=m.getAttribute("class").indexOf(b)&&a){var k="",t="";n(b.split(" "),function(a,
  b){var e=(0<b?" ":"")+a;k+=e+"-active";t+=e+"-pending"});var s=[],q=a.itemIndex,v=a.stagger,r=0;if(0<q){r=0;0<v.transitionDelay&&0===v.transitionDuration&&(r=v.transitionDelay*q);var y=0;0<v.animationDelay&&0===v.animationDuration&&(y=v.animationDelay*q,s.push(B+"animation-play-state"));r=Math.round(100*Math.max(r,y))/100}r||(u.addClass(e,k),a.blockTransition&&I(m,!1));var F=Z(e,a.cacheKey+" "+k),x=Math.max(F.transitionDuration,F.animationDuration);if(0===x)u.removeClass(e,k),G(e,b),c();else{!r&&
  d&&0<Object.keys(d).length&&(F.transitionDuration||(e.css("transition",F.animationDuration+"s linear all"),s.push("transition")),e.css(d));var q=Math.max(F.transitionDelay,F.animationDelay),C=1E3*q;0<s.length&&(v=m.getAttribute("style")||"",";"!==v.charAt(v.length-1)&&(v+=";"),m.setAttribute("style",v+" "));var H=Date.now(),D=V+" "+$,q=1E3*(r+1.5*(q+x)),z;0<r&&(u.addClass(e,t),z=M(function(){z=null;0<F.transitionDuration&&I(m,!1);0<F.animationDuration&&(m.style[K+"PlayState"]="");u.addClass(e,k);
  u.removeClass(e,t);d&&(0===F.transitionDuration&&e.css("transition",F.animationDuration+"s linear all"),e.css(d),s.push("transition"))},1E3*r,!1));e.on(D,h);a.closeAnimationFns.push(function(){f();c()});a.running++;P(e,q);return f}}else c()}function I(a,b){a.style[z+"Property"]=b?"none":""}function S(a,b,c,d){if(R(a,b,c,d))return function(a){a&&G(b,c)}}function T(a,b,c,d,f){if(b.data("$$ngAnimateCSS3Data"))return D(a,b,c,d,f);G(b,c);d()}function U(a,b,c,d,f){var g=S(a,b,c,f.from);if(g){var h=g;H(b,
  function(){h=T(a,b,c,d,f.to)});return function(a){(h||t)(a)}}y();d()}function G(a,b){u.removeClass(a,b);var c=a.data("$$ngAnimateCSS3Data");c&&(c.running&&c.running--,c.running&&0!==c.running||a.removeData("$$ngAnimateCSS3Data"))}function q(a,b){var c="";a=aa(a)?a:a.split(/\s+/);n(a,function(a,d){a&&0<a.length&&(c+=(0<d?" ":"")+a+b)});return c}var B="",z,$,K,V;N.ontransitionend===W&&N.onwebkittransitionend!==W?(B="-webkit-",z="WebkitTransition",$="webkitTransitionEnd transitionend"):(z="transition",
  $="transitionend");N.onanimationend===W&&N.onwebkitanimationend!==W?(B="-webkit-",K="WebkitAnimation",V="webkitAnimationEnd animationend"):(K="animation",V="animationend");var x={},a=0,c=[],b,d=null,h=0,k=[];return{animate:function(a,b,c,d,f,g){g=g||{};g.from=c;g.to=d;return U("animate",a,b,f,g)},enter:function(a,b,c){c=c||{};return U("enter",a,"ng-enter",b,c)},leave:function(a,b,c){c=c||{};return U("leave",a,"ng-leave",b,c)},move:function(a,b,c){c=c||{};return U("move",a,"ng-move",b,c)},beforeSetClass:function(a,
  b,c,d,f){f=f||{};b=q(c,"-remove")+" "+q(b,"-add");if(f=S("setClass",a,b,f.from))return H(a,d),f;y();d()},beforeAddClass:function(a,b,c,d){d=d||{};if(b=S("addClass",a,q(b,"-add"),d.from))return H(a,c),b;y();c()},beforeRemoveClass:function(a,b,c,d){d=d||{};if(b=S("removeClass",a,q(b,"-remove"),d.from))return H(a,c),b;y();c()},setClass:function(a,b,c,d,f){f=f||{};c=q(c,"-remove");b=q(b,"-add");return T("setClass",a,c+" "+b,d,f.to)},addClass:function(a,b,c,d){d=d||{};return T("addClass",a,q(b,"-add"),
  c,d.to)},removeClass:function(a,b,c,d){d=d||{};return T("removeClass",a,q(b,"-remove"),c,d.to)}}}])}])})(window,window.angular);
  //# sourceMappingURL=angular-animate.min.js.map
  
  /*!
   * ionic.bundle.js is a concatenation of:
   * ionic.js, angular.js, angular-animate.js,
   * angular-sanitize.js, angular-ui-router.js,
   * and ionic-angular.js
   */
  
  /*
   AngularJS v1.3.13
   (c) 2010-2014 Google, Inc. http://angularjs.org
   License: MIT
  */
  (function(n,h,p){'use strict';function E(a){var d=[];s(d,h.noop).chars(a);return d.join("")}function g(a){var d={};a=a.split(",");var c;for(c=0;c<a.length;c++)d[a[c]]=!0;return d}function F(a,d){function c(a,b,c,l){b=h.lowercase(b);if(t[b])for(;f.last()&&u[f.last()];)e("",f.last());v[b]&&f.last()==b&&e("",b);(l=w[b]||!!l)||f.push(b);var m={};c.replace(G,function(a,b,d,c,e){m[b]=r(d||c||e||"")});d.start&&d.start(b,m,l)}function e(a,b){var c=0,e;if(b=h.lowercase(b))for(c=f.length-1;0<=c&&f[c]!=b;c--);
  if(0<=c){for(e=f.length-1;e>=c;e--)d.end&&d.end(f[e]);f.length=c}}"string"!==typeof a&&(a=null===a||"undefined"===typeof a?"":""+a);var b,k,f=[],m=a,l;for(f.last=function(){return f[f.length-1]};a;){l="";k=!0;if(f.last()&&x[f.last()])a=a.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(a,b){b=b.replace(H,"$1").replace(I,"$1");d.chars&&d.chars(r(b));return""}),e("",f.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(d.comment&&
  d.comment(a.substring(4,b)),a=a.substring(b+3),k=!1);else if(y.test(a)){if(b=a.match(y))a=a.replace(b[0],""),k=!1}else if(J.test(a)){if(b=a.match(z))a=a.substring(b[0].length),b[0].replace(z,e),k=!1}else K.test(a)&&((b=a.match(A))?(b[4]&&(a=a.substring(b[0].length),b[0].replace(A,c)),k=!1):(l+="<",a=a.substring(1)));k&&(b=a.indexOf("<"),l+=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),d.chars&&d.chars(r(l)))}if(a==m)throw L("badparse",a);m=a}e()}function r(a){if(!a)return"";var d=M.exec(a);a=d[1];
  var c=d[3];if(d=d[2])q.innerHTML=d.replace(/</g,"&lt;"),d="textContent"in q?q.textContent:q.innerText;return a+d+c}function B(a){return a.replace(/&/g,"&amp;").replace(N,function(a){var c=a.charCodeAt(0);a=a.charCodeAt(1);return"&#"+(1024*(c-55296)+(a-56320)+65536)+";"}).replace(O,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"&lt;").replace(/>/g,"&gt;")}function s(a,d){var c=!1,e=h.bind(a,a.push);return{start:function(a,k,f){a=h.lowercase(a);!c&&x[a]&&(c=a);c||!0!==C[a]||(e("<"),e(a),
  h.forEach(k,function(c,f){var k=h.lowercase(f),g="img"===a&&"src"===k||"background"===k;!0!==P[k]||!0===D[k]&&!d(c,g)||(e(" "),e(f),e('="'),e(B(c)),e('"'))}),e(f?"/>":">"))},end:function(a){a=h.lowercase(a);c||!0!==C[a]||(e("</"),e(a),e(">"));a==c&&(c=!1)},chars:function(a){c||e(B(a))}}}var L=h.$$minErr("$sanitize"),A=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,z=/^<\/\s*([\w:-]+)[^>]*>/,G=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
  K=/^</,J=/^<\//,H=/\x3c!--(.*?)--\x3e/g,y=/<!DOCTYPE([^>]*?)>/i,I=/<!\[CDATA\[(.*?)]]\x3e/g,N=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,O=/([^\#-~| |!])/g,w=g("area,br,col,hr,img,wbr");n=g("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr");p=g("rp,rt");var v=h.extend({},p,n),t=h.extend({},n,g("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),u=h.extend({},p,g("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
  n=g("animate,animateColor,animateMotion,animateTransform,circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set,stop,svg,switch,text,title,tspan,use");var x=g("script,style"),C=h.extend({},w,t,u,v,n),D=g("background,cite,href,longdesc,src,usemap,xlink:href");n=g("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,target,title,type,valign,value,vspace,width");
  p=g("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan");
  var P=h.extend({},D,p,n),q=document.createElement("pre"),M=/^(\s*)([\s\S]*?)(\s*)$/;h.module("ngSanitize",[]).provider("$sanitize",function(){this.$get=["$$sanitizeUri",function(a){return function(d){var c=[];F(d,s(c,function(c,b){return!/^unsafe/.test(a(c,b))}));return c.join("")}}]});h.module("ngSanitize").filter("linky",["$sanitize",function(a){var d=/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/,c=/^mailto:/;return function(e,b){function k(a){a&&g.push(E(a))}
  function f(a,c){g.push("<a ");h.isDefined(b)&&g.push('target="',b,'" ');g.push('href="',a.replace(/"/g,"&quot;"),'">');k(c);g.push("</a>")}if(!e)return e;for(var m,l=e,g=[],n,p;m=l.match(d);)n=m[0],m[2]||m[4]||(n=(m[3]?"http://":"mailto:")+n),p=m.index,k(l.substr(0,p)),f(n,m[0].replace(c,"")),l=l.substring(p+m[0].length);k(l);return a(g.join(""))}}])})(window,window.angular);
  //# sourceMappingURL=angular-sanitize.min.js.map
  
  /*!
   * ionic.bundle.js is a concatenation of:
   * ionic.js, angular.js, angular-animate.js,
   * angular-sanitize.js, angular-ui-router.js,
   * and ionic-angular.js
   */
  
  /**
   * State-based routing for AngularJS
   * @version v0.2.13
   * @link http://angular-ui.github.com/
   * @license MIT License, http://www.opensource.org/licenses/MIT
   */
  "undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports="ui.router"),function(a,b,c){"use strict";function d(a,b){return M(new(M(function(){},{prototype:a})),b)}function e(a){return L(arguments,function(b){b!==a&&L(b,function(b,c){a.hasOwnProperty(c)||(a[c]=b)})}),a}function f(a,b){var c=[];for(var d in a.path){if(a.path[d]!==b.path[d])break;c.push(a.path[d])}return c}function g(a){if(Object.keys)return Object.keys(a);var c=[];return b.forEach(a,function(a,b){c.push(b)}),c}function h(a,b){if(Array.prototype.indexOf)return a.indexOf(b,Number(arguments[2])||0);var c=a.length>>>0,d=Number(arguments[2])||0;for(d=0>d?Math.ceil(d):Math.floor(d),0>d&&(d+=c);c>d;d++)if(d in a&&a[d]===b)return d;return-1}function i(a,b,c,d){var e,i=f(c,d),j={},k=[];for(var l in i)if(i[l].params&&(e=g(i[l].params),e.length))for(var m in e)h(k,e[m])>=0||(k.push(e[m]),j[e[m]]=a[e[m]]);return M({},j,b)}function j(a,b,c){if(!c){c=[];for(var d in a)c.push(d)}for(var e=0;e<c.length;e++){var f=c[e];if(a[f]!=b[f])return!1}return!0}function k(a,b){var c={};return L(a,function(a){c[a]=b[a]}),c}function l(a){var b={},c=Array.prototype.concat.apply(Array.prototype,Array.prototype.slice.call(arguments,1));for(var d in a)-1==h(c,d)&&(b[d]=a[d]);return b}function m(a,b){var c=K(a),d=c?[]:{};return L(a,function(a,e){b(a,e)&&(d[c?d.length:e]=a)}),d}function n(a,b){var c=K(a)?[]:{};return L(a,function(a,d){c[d]=b(a,d)}),c}function o(a,b){var d=1,f=2,i={},j=[],k=i,m=M(a.when(i),{$$promises:i,$$values:i});this.study=function(i){function n(a,c){if(s[c]!==f){if(r.push(c),s[c]===d)throw r.splice(0,h(r,c)),new Error("Cyclic dependency: "+r.join(" -> "));if(s[c]=d,I(a))q.push(c,[function(){return b.get(a)}],j);else{var e=b.annotate(a);L(e,function(a){a!==c&&i.hasOwnProperty(a)&&n(i[a],a)}),q.push(c,a,e)}r.pop(),s[c]=f}}function o(a){return J(a)&&a.then&&a.$$promises}if(!J(i))throw new Error("'invocables' must be an object");var p=g(i||{}),q=[],r=[],s={};return L(i,n),i=r=s=null,function(d,f,g){function h(){--u||(v||e(t,f.$$values),r.$$values=t,r.$$promises=r.$$promises||!0,delete r.$$inheritedValues,n.resolve(t))}function i(a){r.$$failure=a,n.reject(a)}function j(c,e,f){function j(a){l.reject(a),i(a)}function k(){if(!G(r.$$failure))try{l.resolve(b.invoke(e,g,t)),l.promise.then(function(a){t[c]=a,h()},j)}catch(a){j(a)}}var l=a.defer(),m=0;L(f,function(a){s.hasOwnProperty(a)&&!d.hasOwnProperty(a)&&(m++,s[a].then(function(b){t[a]=b,--m||k()},j))}),m||k(),s[c]=l.promise}if(o(d)&&g===c&&(g=f,f=d,d=null),d){if(!J(d))throw new Error("'locals' must be an object")}else d=k;if(f){if(!o(f))throw new Error("'parent' must be a promise returned by $resolve.resolve()")}else f=m;var n=a.defer(),r=n.promise,s=r.$$promises={},t=M({},d),u=1+q.length/3,v=!1;if(G(f.$$failure))return i(f.$$failure),r;f.$$inheritedValues&&e(t,l(f.$$inheritedValues,p)),M(s,f.$$promises),f.$$values?(v=e(t,l(f.$$values,p)),r.$$inheritedValues=l(f.$$values,p),h()):(f.$$inheritedValues&&(r.$$inheritedValues=l(f.$$inheritedValues,p)),f.then(h,i));for(var w=0,x=q.length;x>w;w+=3)d.hasOwnProperty(q[w])?h():j(q[w],q[w+1],q[w+2]);return r}},this.resolve=function(a,b,c,d){return this.study(a)(b,c,d)}}function p(a,b,c){this.fromConfig=function(a,b,c){return G(a.template)?this.fromString(a.template,b):G(a.templateUrl)?this.fromUrl(a.templateUrl,b):G(a.templateProvider)?this.fromProvider(a.templateProvider,b,c):null},this.fromString=function(a,b){return H(a)?a(b):a},this.fromUrl=function(c,d){return H(c)&&(c=c(d)),null==c?null:a.get(c,{cache:b,headers:{Accept:"text/html"}}).then(function(a){return a.data})},this.fromProvider=function(a,b,d){return c.invoke(a,null,d||{params:b})}}function q(a,b,e){function f(b,c,d,e){if(q.push(b),o[b])return o[b];if(!/^\w+(-+\w+)*(?:\[\])?$/.test(b))throw new Error("Invalid parameter name '"+b+"' in pattern '"+a+"'");if(p[b])throw new Error("Duplicate parameter name '"+b+"' in pattern '"+a+"'");return p[b]=new O.Param(b,c,d,e),p[b]}function g(a,b,c){var d=["",""],e=a.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!b)return e;switch(c){case!1:d=["(",")"];break;case!0:d=["?(",")?"];break;default:d=["("+c+"|",")?"]}return e+d[0]+b+d[1]}function h(c,e){var f,g,h,i,j;return f=c[2]||c[3],j=b.params[f],h=a.substring(m,c.index),g=e?c[4]:c[4]||("*"==c[1]?".*":null),i=O.type(g||"string")||d(O.type("string"),{pattern:new RegExp(g)}),{id:f,regexp:g,segment:h,type:i,cfg:j}}b=M({params:{}},J(b)?b:{});var i,j=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,k=/([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,l="^",m=0,n=this.segments=[],o=e?e.params:{},p=this.params=e?e.params.$$new():new O.ParamSet,q=[];this.source=a;for(var r,s,t;(i=j.exec(a))&&(r=h(i,!1),!(r.segment.indexOf("?")>=0));)s=f(r.id,r.type,r.cfg,"path"),l+=g(r.segment,s.type.pattern.source,s.squash),n.push(r.segment),m=j.lastIndex;t=a.substring(m);var u=t.indexOf("?");if(u>=0){var v=this.sourceSearch=t.substring(u);if(t=t.substring(0,u),this.sourcePath=a.substring(0,m+u),v.length>0)for(m=0;i=k.exec(v);)r=h(i,!0),s=f(r.id,r.type,r.cfg,"search"),m=j.lastIndex}else this.sourcePath=a,this.sourceSearch="";l+=g(t)+(b.strict===!1?"/?":"")+"$",n.push(t),this.regexp=new RegExp(l,b.caseInsensitive?"i":c),this.prefix=n[0],this.$$paramNames=q}function r(a){M(this,a)}function s(){function a(a){return null!=a?a.toString().replace(/\//g,"%2F"):a}function e(a){return null!=a?a.toString().replace(/%2F/g,"/"):a}function f(a){return this.pattern.test(a)}function i(){return{strict:t,caseInsensitive:p}}function j(a){return H(a)||K(a)&&H(a[a.length-1])}function k(){for(;x.length;){var a=x.shift();if(a.pattern)throw new Error("You cannot override a type's .pattern at runtime.");b.extend(v[a.name],o.invoke(a.def))}}function l(a){M(this,a||{})}O=this;var o,p=!1,t=!0,u=!1,v={},w=!0,x=[],y={string:{encode:a,decode:e,is:f,pattern:/[^/]*/},"int":{encode:a,decode:function(a){return parseInt(a,10)},is:function(a){return G(a)&&this.decode(a.toString())===a},pattern:/\d+/},bool:{encode:function(a){return a?1:0},decode:function(a){return 0!==parseInt(a,10)},is:function(a){return a===!0||a===!1},pattern:/0|1/},date:{encode:function(a){return this.is(a)?[a.getFullYear(),("0"+(a.getMonth()+1)).slice(-2),("0"+a.getDate()).slice(-2)].join("-"):c},decode:function(a){if(this.is(a))return a;var b=this.capture.exec(a);return b?new Date(b[1],b[2]-1,b[3]):c},is:function(a){return a instanceof Date&&!isNaN(a.valueOf())},equals:function(a,b){return this.is(a)&&this.is(b)&&a.toISOString()===b.toISOString()},pattern:/[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,capture:/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/},json:{encode:b.toJson,decode:b.fromJson,is:b.isObject,equals:b.equals,pattern:/[^/]*/},any:{encode:b.identity,decode:b.identity,is:b.identity,equals:b.equals,pattern:/.*/}};s.$$getDefaultValue=function(a){if(!j(a.value))return a.value;if(!o)throw new Error("Injectable functions cannot be called at configuration time");return o.invoke(a.value)},this.caseInsensitive=function(a){return G(a)&&(p=a),p},this.strictMode=function(a){return G(a)&&(t=a),t},this.defaultSquashPolicy=function(a){if(!G(a))return u;if(a!==!0&&a!==!1&&!I(a))throw new Error("Invalid squash policy: "+a+". Valid policies: false, true, arbitrary-string");return u=a,a},this.compile=function(a,b){return new q(a,M(i(),b))},this.isMatcher=function(a){if(!J(a))return!1;var b=!0;return L(q.prototype,function(c,d){H(c)&&(b=b&&G(a[d])&&H(a[d]))}),b},this.type=function(a,b,c){if(!G(b))return v[a];if(v.hasOwnProperty(a))throw new Error("A type named '"+a+"' has already been defined.");return v[a]=new r(M({name:a},b)),c&&(x.push({name:a,def:c}),w||k()),this},L(y,function(a,b){v[b]=new r(M({name:b},a))}),v=d(v,{}),this.$get=["$injector",function(a){return o=a,w=!1,k(),L(y,function(a,b){v[b]||(v[b]=new r(a))}),this}],this.Param=function(a,b,d,e){function f(a){var b=J(a)?g(a):[],c=-1===h(b,"value")&&-1===h(b,"type")&&-1===h(b,"squash")&&-1===h(b,"array");return c&&(a={value:a}),a.$$fn=j(a.value)?a.value:function(){return a.value},a}function i(b,c,d){if(b.type&&c)throw new Error("Param '"+a+"' has two type configurations.");return c?c:b.type?b.type instanceof r?b.type:new r(b.type):"config"===d?v.any:v.string}function k(){var b={array:"search"===e?"auto":!1},c=a.match(/\[\]$/)?{array:!0}:{};return M(b,c,d).array}function l(a,b){var c=a.squash;if(!b||c===!1)return!1;if(!G(c)||null==c)return u;if(c===!0||I(c))return c;throw new Error("Invalid squash policy: '"+c+"'. Valid policies: false, true, or arbitrary string")}function p(a,b,d,e){var f,g,i=[{from:"",to:d||b?c:""},{from:null,to:d||b?c:""}];return f=K(a.replace)?a.replace:[],I(e)&&f.push({from:e,to:c}),g=n(f,function(a){return a.from}),m(i,function(a){return-1===h(g,a.from)}).concat(f)}function q(){if(!o)throw new Error("Injectable functions cannot be called at configuration time");return o.invoke(d.$$fn)}function s(a){function b(a){return function(b){return b.from===a}}function c(a){var c=n(m(w.replace,b(a)),function(a){return a.to});return c.length?c[0]:a}return a=c(a),G(a)?w.type.decode(a):q()}function t(){return"{Param:"+a+" "+b+" squash: '"+z+"' optional: "+y+"}"}var w=this;d=f(d),b=i(d,b,e);var x=k();b=x?b.$asArray(x,"search"===e):b,"string"!==b.name||x||"path"!==e||d.value!==c||(d.value="");var y=d.value!==c,z=l(d,y),A=p(d,x,y,z);M(this,{id:a,type:b,location:e,array:x,squash:z,replace:A,isOptional:y,value:s,dynamic:c,config:d,toString:t})},l.prototype={$$new:function(){return d(this,M(new l,{$$parent:this}))},$$keys:function(){for(var a=[],b=[],c=this,d=g(l.prototype);c;)b.push(c),c=c.$$parent;return b.reverse(),L(b,function(b){L(g(b),function(b){-1===h(a,b)&&-1===h(d,b)&&a.push(b)})}),a},$$values:function(a){var b={},c=this;return L(c.$$keys(),function(d){b[d]=c[d].value(a&&a[d])}),b},$$equals:function(a,b){var c=!0,d=this;return L(d.$$keys(),function(e){var f=a&&a[e],g=b&&b[e];d[e].type.equals(f,g)||(c=!1)}),c},$$validates:function(a){var b,c,d,e=!0,f=this;return L(this.$$keys(),function(g){d=f[g],c=a[g],b=!c&&d.isOptional,e=e&&(b||!!d.type.is(c))}),e},$$parent:c},this.ParamSet=l}function t(a,d){function e(a){var b=/^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(a.source);return null!=b?b[1].replace(/\\(.)/g,"$1"):""}function f(a,b){return a.replace(/\$(\$|\d{1,2})/,function(a,c){return b["$"===c?0:Number(c)]})}function g(a,b,c){if(!c)return!1;var d=a.invoke(b,b,{$match:c});return G(d)?d:!0}function h(d,e,f,g){function h(a,b,c){return"/"===p?a:b?p.slice(0,-1)+a:c?p.slice(1)+a:a}function m(a){function b(a){var b=a(f,d);return b?(I(b)&&d.replace().url(b),!0):!1}if(!a||!a.defaultPrevented){var e=o&&d.url()===o;if(o=c,e)return!0;var g,h=j.length;for(g=0;h>g;g++)if(b(j[g]))return;k&&b(k)}}function n(){return i=i||e.$on("$locationChangeSuccess",m)}var o,p=g.baseHref(),q=d.url();return l||n(),{sync:function(){m()},listen:function(){return n()},update:function(a){return a?void(q=d.url()):void(d.url()!==q&&(d.url(q),d.replace()))},push:function(a,b,e){d.url(a.format(b||{})),o=e&&e.$$avoidResync?d.url():c,e&&e.replace&&d.replace()},href:function(c,e,f){if(!c.validates(e))return null;var g=a.html5Mode();b.isObject(g)&&(g=g.enabled);var i=c.format(e);if(f=f||{},g||null===i||(i="#"+a.hashPrefix()+i),i=h(i,g,f.absolute),!f.absolute||!i)return i;var j=!g&&i?"/":"",k=d.port();return k=80===k||443===k?"":":"+k,[d.protocol(),"://",d.host(),k,j,i].join("")}}}var i,j=[],k=null,l=!1;this.rule=function(a){if(!H(a))throw new Error("'rule' must be a function");return j.push(a),this},this.otherwise=function(a){if(I(a)){var b=a;a=function(){return b}}else if(!H(a))throw new Error("'rule' must be a function");return k=a,this},this.when=function(a,b){var c,h=I(b);if(I(a)&&(a=d.compile(a)),!h&&!H(b)&&!K(b))throw new Error("invalid 'handler' in when()");var i={matcher:function(a,b){return h&&(c=d.compile(b),b=["$match",function(a){return c.format(a)}]),M(function(c,d){return g(c,b,a.exec(d.path(),d.search()))},{prefix:I(a.prefix)?a.prefix:""})},regex:function(a,b){if(a.global||a.sticky)throw new Error("when() RegExp must not be global or sticky");return h&&(c=b,b=["$match",function(a){return f(c,a)}]),M(function(c,d){return g(c,b,a.exec(d.path()))},{prefix:e(a)})}},j={matcher:d.isMatcher(a),regex:a instanceof RegExp};for(var k in j)if(j[k])return this.rule(i[k](a,b));throw new Error("invalid 'what' in when()")},this.deferIntercept=function(a){a===c&&(a=!0),l=a},this.$get=h,h.$inject=["$location","$rootScope","$injector","$browser"]}function u(a,e){function f(a){return 0===a.indexOf(".")||0===a.indexOf("^")}function l(a,b){if(!a)return c;var d=I(a),e=d?a:a.name,g=f(e);if(g){if(!b)throw new Error("No reference point given for path '"+e+"'");b=l(b);for(var h=e.split("."),i=0,j=h.length,k=b;j>i;i++)if(""!==h[i]||0!==i){if("^"!==h[i])break;if(!k.parent)throw new Error("Path '"+e+"' not valid for state '"+b.name+"'");k=k.parent}else k=b;h=h.slice(i).join("."),e=k.name+(k.name&&h?".":"")+h}var m=y[e];return!m||!d&&(d||m!==a&&m.self!==a)?c:m}function m(a,b){z[a]||(z[a]=[]),z[a].push(b)}function o(a){for(var b=z[a]||[];b.length;)p(b.shift())}function p(b){b=d(b,{self:b,resolve:b.resolve||{},toString:function(){return this.name}});var c=b.name;if(!I(c)||c.indexOf("@")>=0)throw new Error("State must have a valid name");if(y.hasOwnProperty(c))throw new Error("State '"+c+"'' is already defined");var e=-1!==c.indexOf(".")?c.substring(0,c.lastIndexOf(".")):I(b.parent)?b.parent:J(b.parent)&&I(b.parent.name)?b.parent.name:"";if(e&&!y[e])return m(e,b.self);for(var f in B)H(B[f])&&(b[f]=B[f](b,B.$delegates[f]));return y[c]=b,!b[A]&&b.url&&a.when(b.url,["$match","$stateParams",function(a,c){x.$current.navigable==b&&j(a,c)||x.transitionTo(b,a,{inherit:!0,location:!1})}]),o(c),b}function q(a){return a.indexOf("*")>-1}function r(a){var b=a.split("."),c=x.$current.name.split(".");if("**"===b[0]&&(c=c.slice(h(c,b[1])),c.unshift("**")),"**"===b[b.length-1]&&(c.splice(h(c,b[b.length-2])+1,Number.MAX_VALUE),c.push("**")),b.length!=c.length)return!1;for(var d=0,e=b.length;e>d;d++)"*"===b[d]&&(c[d]="*");return c.join("")===b.join("")}function s(a,b){return I(a)&&!G(b)?B[a]:H(b)&&I(a)?(B[a]&&!B.$delegates[a]&&(B.$delegates[a]=B[a]),B[a]=b,this):this}function t(a,b){return J(a)?b=a:b.name=a,p(b),this}function u(a,e,f,h,m,o,p){function s(b,c,d,f){var g=a.$broadcast("$stateNotFound",b,c,d);if(g.defaultPrevented)return p.update(),B;if(!g.retry)return null;if(f.$retry)return p.update(),C;var h=x.transition=e.when(g.retry);return h.then(function(){return h!==x.transition?u:(b.options.$retry=!0,x.transitionTo(b.to,b.toParams,b.options))},function(){return B}),p.update(),h}function t(a,c,d,g,i,j){var l=d?c:k(a.params.$$keys(),c),n={$stateParams:l};i.resolve=m.resolve(a.resolve,n,i.resolve,a);var o=[i.resolve.then(function(a){i.globals=a})];return g&&o.push(g),L(a.views,function(c,d){var e=c.resolve&&c.resolve!==a.resolve?c.resolve:{};e.$template=[function(){return f.load(d,{view:c,locals:n,params:l,notify:j.notify})||""}],o.push(m.resolve(e,n,i.resolve,a).then(function(f){if(H(c.controllerProvider)||K(c.controllerProvider)){var g=b.extend({},e,n);f.$$controller=h.invoke(c.controllerProvider,null,g)}else f.$$controller=c.controller;f.$$state=a,f.$$controllerAs=c.controllerAs,i[d]=f}))}),e.all(o).then(function(){return i})}var u=e.reject(new Error("transition superseded")),z=e.reject(new Error("transition prevented")),B=e.reject(new Error("transition aborted")),C=e.reject(new Error("transition failed"));return w.locals={resolve:null,globals:{$stateParams:{}}},x={params:{},current:w.self,$current:w,transition:null},x.reload=function(){return x.transitionTo(x.current,o,{reload:!0,inherit:!1,notify:!0})},x.go=function(a,b,c){return x.transitionTo(a,b,M({inherit:!0,relative:x.$current},c))},x.transitionTo=function(b,c,f){c=c||{},f=M({location:!0,inherit:!1,relative:null,notify:!0,reload:!1,$retry:!1},f||{});var g,j=x.$current,m=x.params,n=j.path,q=l(b,f.relative);if(!G(q)){var r={to:b,toParams:c,options:f},y=s(r,j.self,m,f);if(y)return y;if(b=r.to,c=r.toParams,f=r.options,q=l(b,f.relative),!G(q)){if(!f.relative)throw new Error("No such state '"+b+"'");throw new Error("Could not resolve '"+b+"' from state '"+f.relative+"'")}}if(q[A])throw new Error("Cannot transition to abstract state '"+b+"'");if(f.inherit&&(c=i(o,c||{},x.$current,q)),!q.params.$$validates(c))return C;c=q.params.$$values(c),b=q;var B=b.path,D=0,E=B[D],F=w.locals,H=[];if(!f.reload)for(;E&&E===n[D]&&E.ownParams.$$equals(c,m);)F=H[D]=E.locals,D++,E=B[D];if(v(b,j,F,f))return b.self.reloadOnSearch!==!1&&p.update(),x.transition=null,e.when(x.current);if(c=k(b.params.$$keys(),c||{}),f.notify&&a.$broadcast("$stateChangeStart",b.self,c,j.self,m).defaultPrevented)return p.update(),z;for(var I=e.when(F),J=D;J<B.length;J++,E=B[J])F=H[J]=d(F),I=t(E,c,E===b,I,F,f);var K=x.transition=I.then(function(){var d,e,g;if(x.transition!==K)return u;for(d=n.length-1;d>=D;d--)g=n[d],g.self.onExit&&h.invoke(g.self.onExit,g.self,g.locals.globals),g.locals=null;for(d=D;d<B.length;d++)e=B[d],e.locals=H[d],e.self.onEnter&&h.invoke(e.self.onEnter,e.self,e.locals.globals);return x.transition!==K?u:(x.$current=b,x.current=b.self,x.params=c,N(x.params,o),x.transition=null,f.location&&b.navigable&&p.push(b.navigable.url,b.navigable.locals.globals.$stateParams,{$$avoidResync:!0,replace:"replace"===f.location}),f.notify&&a.$broadcast("$stateChangeSuccess",b.self,c,j.self,m),p.update(!0),x.current)},function(d){return x.transition!==K?u:(x.transition=null,g=a.$broadcast("$stateChangeError",b.self,c,j.self,m,d),g.defaultPrevented||p.update(),e.reject(d))});return K},x.is=function(a,b,d){d=M({relative:x.$current},d||{});var e=l(a,d.relative);return G(e)?x.$current!==e?!1:b?j(e.params.$$values(b),o):!0:c},x.includes=function(a,b,d){if(d=M({relative:x.$current},d||{}),I(a)&&q(a)){if(!r(a))return!1;a=x.$current.name}var e=l(a,d.relative);return G(e)?G(x.$current.includes[e.name])?b?j(e.params.$$values(b),o,g(b)):!0:!1:c},x.href=function(a,b,d){d=M({lossy:!0,inherit:!0,absolute:!1,relative:x.$current},d||{});var e=l(a,d.relative);if(!G(e))return null;d.inherit&&(b=i(o,b||{},x.$current,e));var f=e&&d.lossy?e.navigable:e;return f&&f.url!==c&&null!==f.url?p.href(f.url,k(e.params.$$keys(),b||{}),{absolute:d.absolute}):null},x.get=function(a,b){if(0===arguments.length)return n(g(y),function(a){return y[a].self});var c=l(a,b||x.$current);return c&&c.self?c.self:null},x}function v(a,b,c,d){return a!==b||(c!==b.locals||d.reload)&&a.self.reloadOnSearch!==!1?void 0:!0}var w,x,y={},z={},A="abstract",B={parent:function(a){if(G(a.parent)&&a.parent)return l(a.parent);var b=/^(.+)\.[^.]+$/.exec(a.name);return b?l(b[1]):w},data:function(a){return a.parent&&a.parent.data&&(a.data=a.self.data=M({},a.parent.data,a.data)),a.data},url:function(a){var b=a.url,c={params:a.params||{}};if(I(b))return"^"==b.charAt(0)?e.compile(b.substring(1),c):(a.parent.navigable||w).url.concat(b,c);if(!b||e.isMatcher(b))return b;throw new Error("Invalid url '"+b+"' in state '"+a+"'")},navigable:function(a){return a.url?a:a.parent?a.parent.navigable:null},ownParams:function(a){var b=a.url&&a.url.params||new O.ParamSet;return L(a.params||{},function(a,c){b[c]||(b[c]=new O.Param(c,null,a,"config"))}),b},params:function(a){return a.parent&&a.parent.params?M(a.parent.params.$$new(),a.ownParams):new O.ParamSet},views:function(a){var b={};return L(G(a.views)?a.views:{"":a},function(c,d){d.indexOf("@")<0&&(d+="@"+a.parent.name),b[d]=c}),b},path:function(a){return a.parent?a.parent.path.concat(a):[]},includes:function(a){var b=a.parent?M({},a.parent.includes):{};return b[a.name]=!0,b},$delegates:{}};w=p({name:"",url:"^",views:null,"abstract":!0}),w.navigable=null,this.decorator=s,this.state=t,this.$get=u,u.$inject=["$rootScope","$q","$view","$injector","$resolve","$stateParams","$urlRouter","$location","$urlMatcherFactory"]}function v(){function a(a,b){return{load:function(c,d){var e,f={template:null,controller:null,view:null,locals:null,notify:!0,async:!0,params:{}};return d=M(f,d),d.view&&(e=b.fromConfig(d.view,d.params,d.locals)),e&&d.notify&&a.$broadcast("$viewContentLoading",d),e}}}this.$get=a,a.$inject=["$rootScope","$templateFactory"]}function w(){var a=!1;this.useAnchorScroll=function(){a=!0},this.$get=["$anchorScroll","$timeout",function(b,c){return a?b:function(a){c(function(){a[0].scrollIntoView()},0,!1)}}]}function x(a,c,d,e){function f(){return c.has?function(a){return c.has(a)?c.get(a):null}:function(a){try{return c.get(a)}catch(b){return null}}}function g(a,b){var c=function(){return{enter:function(a,b,c){b.after(a),c()},leave:function(a,b){a.remove(),b()}}};if(j)return{enter:function(a,b,c){var d=j.enter(a,null,b,c);d&&d.then&&d.then(c)},leave:function(a,b){var c=j.leave(a,b);c&&c.then&&c.then(b)}};if(i){var d=i&&i(b,a);return{enter:function(a,b,c){d.enter(a,null,b),c()},leave:function(a,b){d.leave(a),b()}}}return c()}var h=f(),i=h("$animator"),j=h("$animate"),k={restrict:"ECA",terminal:!0,priority:400,transclude:"element",compile:function(c,f,h){return function(c,f,i){function j(){l&&(l.remove(),l=null),n&&(n.$destroy(),n=null),m&&(r.leave(m,function(){l=null}),l=m,m=null)}function k(g){var k,l=z(c,i,f,e),s=l&&a.$current&&a.$current.locals[l];if(g||s!==o){k=c.$new(),o=a.$current.locals[l];var t=h(k,function(a){r.enter(a,f,function(){n&&n.$emit("$viewContentAnimationEnded"),(b.isDefined(q)&&!q||c.$eval(q))&&d(a)}),j()});m=t,n=k,n.$emit("$viewContentLoaded"),n.$eval(p)}}var l,m,n,o,p=i.onload||"",q=i.autoscroll,r=g(i,c);c.$on("$stateChangeSuccess",function(){k(!1)}),c.$on("$viewContentLoading",function(){k(!1)}),k(!0)}}};return k}function y(a,b,c,d){return{restrict:"ECA",priority:-400,compile:function(e){var f=e.html();return function(e,g,h){var i=c.$current,j=z(e,h,g,d),k=i&&i.locals[j];if(k){g.data("$uiView",{name:j,state:k.$$state}),g.html(k.$template?k.$template:f);var l=a(g.contents());if(k.$$controller){k.$scope=e;var m=b(k.$$controller,k);k.$$controllerAs&&(e[k.$$controllerAs]=m),g.data("$ngControllerController",m),g.children().data("$ngControllerController",m)}l(e)}}}}}function z(a,b,c,d){var e=d(b.uiView||b.name||"")(a),f=c.inheritedData("$uiView");return e.indexOf("@")>=0?e:e+"@"+(f?f.state.name:"")}function A(a,b){var c,d=a.match(/^\s*({[^}]*})\s*$/);if(d&&(a=b+"("+d[1]+")"),c=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/),!c||4!==c.length)throw new Error("Invalid state ref '"+a+"'");return{state:c[1],paramExpr:c[3]||null}}function B(a){var b=a.parent().inheritedData("$uiView");return b&&b.state&&b.state.name?b.state:void 0}function C(a,c){var d=["location","inherit","reload"];return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(e,f,g,h){var i=A(g.uiSref,a.current.name),j=null,k=B(f)||a.$current,l=null,m="A"===f.prop("tagName"),n="FORM"===f[0].nodeName,o=n?"action":"href",p=!0,q={relative:k,inherit:!0},r=e.$eval(g.uiSrefOpts)||{};b.forEach(d,function(a){a in r&&(q[a]=r[a])});var s=function(c){if(c&&(j=b.copy(c)),p){l=a.href(i.state,j,q);var d=h[1]||h[0];return d&&d.$$setStateInfo(i.state,j),null===l?(p=!1,!1):void g.$set(o,l)}};i.paramExpr&&(e.$watch(i.paramExpr,function(a){a!==j&&s(a)},!0),j=b.copy(e.$eval(i.paramExpr))),s(),n||f.bind("click",function(b){var d=b.which||b.button;if(!(d>1||b.ctrlKey||b.metaKey||b.shiftKey||f.attr("target"))){var e=c(function(){a.go(i.state,j,q)});b.preventDefault();var g=m&&!l?1:0;b.preventDefault=function(){g--<=0&&c.cancel(e)}}})}}}function D(a,b,c){return{restrict:"A",controller:["$scope","$element","$attrs",function(b,d,e){function f(){g()?d.addClass(j):d.removeClass(j)}function g(){return"undefined"!=typeof e.uiSrefActiveEq?h&&a.is(h.name,i):h&&a.includes(h.name,i)}var h,i,j;j=c(e.uiSrefActiveEq||e.uiSrefActive||"",!1)(b),this.$$setStateInfo=function(b,c){h=a.get(b,B(d)),i=c,f()},b.$on("$stateChangeSuccess",f)}]}}function E(a){var b=function(b){return a.is(b)};return b.$stateful=!0,b}function F(a){var b=function(b){return a.includes(b)};return b.$stateful=!0,b}var G=b.isDefined,H=b.isFunction,I=b.isString,J=b.isObject,K=b.isArray,L=b.forEach,M=b.extend,N=b.copy;b.module("ui.router.util",["ng"]),b.module("ui.router.router",["ui.router.util"]),b.module("ui.router.state",["ui.router.router","ui.router.util"]),b.module("ui.router",["ui.router.state"]),b.module("ui.router.compat",["ui.router"]),o.$inject=["$q","$injector"],b.module("ui.router.util").service("$resolve",o),p.$inject=["$http","$templateCache","$injector"],b.module("ui.router.util").service("$templateFactory",p);var O;q.prototype.concat=function(a,b){var c={caseInsensitive:O.caseInsensitive(),strict:O.strictMode(),squash:O.defaultSquashPolicy()};return new q(this.sourcePath+a+this.sourceSearch,M(c,b),this)},q.prototype.toString=function(){return this.source},q.prototype.exec=function(a,b){function c(a){function b(a){return a.split("").reverse().join("")}function c(a){return a.replace(/\\-/,"-")}var d=b(a).split(/-(?!\\)/),e=n(d,b);return n(e,c).reverse()}var d=this.regexp.exec(a);if(!d)return null;b=b||{};var e,f,g,h=this.parameters(),i=h.length,j=this.segments.length-1,k={};if(j!==d.length-1)throw new Error("Unbalanced capture group in route '"+this.source+"'");for(e=0;j>e;e++){g=h[e];var l=this.params[g],m=d[e+1];for(f=0;f<l.replace;f++)l.replace[f].from===m&&(m=l.replace[f].to);m&&l.array===!0&&(m=c(m)),k[g]=l.value(m)}for(;i>e;e++)g=h[e],k[g]=this.params[g].value(b[g]);return k},q.prototype.parameters=function(a){return G(a)?this.params[a]||null:this.$$paramNames},q.prototype.validates=function(a){return this.params.$$validates(a)},q.prototype.format=function(a){function b(a){return encodeURIComponent(a).replace(/-/g,function(a){return"%5C%"+a.charCodeAt(0).toString(16).toUpperCase()})}a=a||{};var c=this.segments,d=this.parameters(),e=this.params;if(!this.validates(a))return null;var f,g=!1,h=c.length-1,i=d.length,j=c[0];for(f=0;i>f;f++){var k=h>f,l=d[f],m=e[l],o=m.value(a[l]),p=m.isOptional&&m.type.equals(m.value(),o),q=p?m.squash:!1,r=m.type.encode(o);if(k){var s=c[f+1];if(q===!1)null!=r&&(j+=K(r)?n(r,b).join("-"):encodeURIComponent(r)),j+=s;else if(q===!0){var t=j.match(/\/$/)?/\/?(.*)/:/(.*)/;j+=s.match(t)[1]}else I(q)&&(j+=q+s)}else{if(null==r||p&&q!==!1)continue;K(r)||(r=[r]),r=n(r,encodeURIComponent).join("&"+l+"="),j+=(g?"&":"?")+(l+"="+r),g=!0}}return j},r.prototype.is=function(){return!0},r.prototype.encode=function(a){return a},r.prototype.decode=function(a){return a},r.prototype.equals=function(a,b){return a==b},r.prototype.$subPattern=function(){var a=this.pattern.toString();return a.substr(1,a.length-2)},r.prototype.pattern=/.*/,r.prototype.toString=function(){return"{Type:"+this.name+"}"},r.prototype.$asArray=function(a,b){function d(a,b){function d(a,b){return function(){return a[b].apply(a,arguments)}}function e(a){return K(a)?a:G(a)?[a]:[]}function f(a){switch(a.length){case 0:return c;case 1:return"auto"===b?a[0]:a;default:return a}}function g(a){return!a}function h(a,b){return function(c){c=e(c);var d=n(c,a);return b===!0?0===m(d,g).length:f(d)}}function i(a){return function(b,c){var d=e(b),f=e(c);if(d.length!==f.length)return!1;for(var g=0;g<d.length;g++)if(!a(d[g],f[g]))return!1;return!0}}this.encode=h(d(a,"encode")),this.decode=h(d(a,"decode")),this.is=h(d(a,"is"),!0),this.equals=i(d(a,"equals")),this.pattern=a.pattern,this.$arrayMode=b}if(!a)return this;if("auto"===a&&!b)throw new Error("'auto' array mode is for query parameters only");return new d(this,a)},b.module("ui.router.util").provider("$urlMatcherFactory",s),b.module("ui.router.util").run(["$urlMatcherFactory",function(){}]),t.$inject=["$locationProvider","$urlMatcherFactoryProvider"],b.module("ui.router.router").provider("$urlRouter",t),u.$inject=["$urlRouterProvider","$urlMatcherFactoryProvider"],b.module("ui.router.state").value("$stateParams",{}).provider("$state",u),v.$inject=[],b.module("ui.router.state").provider("$view",v),b.module("ui.router.state").provider("$uiViewScroll",w),x.$inject=["$state","$injector","$uiViewScroll","$interpolate"],y.$inject=["$compile","$controller","$state","$interpolate"],b.module("ui.router.state").directive("uiView",x),b.module("ui.router.state").directive("uiView",y),C.$inject=["$state","$timeout"],D.$inject=["$state","$stateParams","$interpolate"],b.module("ui.router.state").directive("uiSref",C).directive("uiSrefActive",D).directive("uiSrefActiveEq",D),E.$inject=["$state"],F.$inject=["$state"],b.module("ui.router.state").filter("isState",E).filter("includedByState",F)}(window,window.angular);
  /*!
   * ionic.bundle.js is a concatenation of:
   * ionic.js, angular.js, angular-animate.js,
   * angular-sanitize.js, angular-ui-router.js,
   * and ionic-angular.js
   */
  
  /*!
   * Copyright 2014 Drifty Co.
   * http://drifty.com/
   *
   * Ionic, v1.0.0-beta.14
   * A powerful HTML5 mobile app framework.
   * http://ionicframework.com/
   *
   * By @maxlynch, @benjsperry, @adamdbradley <3
   *
   * Licensed under the MIT license. Please see LICENSE for more information.
   *
   */
  
  /*!
   * Copyright 2014 Drifty Co.
   * http://drifty.com/
   *
   * Ionic, v1.0.0-beta.14
   * A powerful HTML5 mobile app framework.
   * http://ionicframework.com/
   *
   * By @maxlynch, @benjsperry, @adamdbradley <3
   *
   * Licensed under the MIT license. Please see LICENSE for more information.
   *
   */
  
  (function() {
  /*
   * deprecated.js
   * https://github.com/wearefractal/deprecated/
   * Copyright (c) 2014 Fractal <contact@wearefractal.com>
   * License MIT
   */
  //Interval object
  var deprecated = {
    method: function(msg, log, fn) {
      var called = false;
      return function deprecatedMethod() {
        if (!called) {
          called = true;
          log(msg);
        }
        return fn.apply(this, arguments);
      };
    },
  
    field: function(msg, log, parent, field, val) {
      var called = false;
      var getter = function() {
        if (!called) {
          called = true;
          log(msg);
        }
        return val;
      };
      var setter = function(v) {
        if (!called) {
          called = true;
          log(msg);
        }
        val = v;
        return v;
      };
      Object.defineProperty(parent, field, {
        get: getter,
        set: setter,
        enumerable: true
      });
      return;
    }
  };
  
  var IonicModule = angular.module('ionic', ['ngAnimate', 'ngSanitize', 'ui.router']),
    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',
  function($rootScope, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform, $ionicBody) {
  
    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)();
        });
      };
  
      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);
        },
        PLATFORM_BACK_BUTTON_PRIORITY_ACTION_SHEET
      );
  
      // 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.
   *
   * @usage
   *
   * ```js
   * function MyController($scope, $ionicBackdrop, $timeout) {
   *   //Show a backdrop for one second
   *   $scope.action = function() {
   *     $ionicBackdrop.retain();
   *     $timeout(function() {
   *       $ionicBackdrop.release();
   *     }, 1000);
   *   };
   * }
   * ```
   */
  IonicModule
  .factory('$ionicBackdrop', [
    '$document', '$timeout',
  function($document, $timeout) {
  
    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() {
      if ((++backdropHolds) === 1) {
        el.addClass('visible');
        ionic.requestAnimationFrame(function() {
          backdropHolds && el.addClass('active');
        });
      }
    }
    function release() {
      if ((--backdropHolds) === 0) {
        el.removeClass('active');
        $timeout(function() {
          !backdropHolds && el.removeClass('visible');
        }, 400, false);
      }
    }
  
    function getElement() {
      return el;
    }
  
  }]);
  
  /**
   * @private
   */
  IonicModule
  .factory('$ionicBind', ['$parse', '$interpolate', function($parse, $interpolate) {
    var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
    return function(scope, attrs, bindDefinition) {
      forEach(bindDefinition || {}, function (definition, scopeName) {
        //Adapted from angular.js $compile
        var match = definition.match(LOCAL_REGEXP) || [],
          attrName = match[3] || scopeName,
          mode = match[1], // @, =, or &
          parentGet,
          unwatch;
  
        switch (mode) {
          case '@':
            if (!attrs[attrName]) {
              return;
            }
            attrs.$observe(attrName, function(value) {
              scope[scopeName] = value;
            });
            // we trigger an interpolation to ensure
            // the value is there for use immediately
            if (attrs[attrName]) {
              scope[scopeName] = $interpolate(attrs[attrName])(scope);
            }
            break;
  
          case '=':
            if (!attrs[attrName]) {
              return;
            }
            unwatch = scope.$watch(attrs[attrName], function(value) {
              scope[scopeName] = value;
            });
            //Destroy parent scope watcher when this scope is destroyed
            scope.$on('$destroy', unwatch);
            break;
  
          case '&':
            /* jshint -W044 */
            if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) {
              throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' +
                            attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.');
            }
            parentGet = $parse(attrs[attrName]);
            scope[scopeName] = function(locals) {
              return parentGet(scope, locals);
            };
            break;
        }
      });
    };
  }]);
  
  /**
   * @ngdoc service
   * @name $ionicBody
   * @module ionic
   * @description An angular utility service to easily and efficiently
   * add and remove CSS classes from the document's body element.
   */
  IonicModule
  .factory('$ionicBody', ['$document', function($document) {
    return {
      /**
       * @ngdoc method
       * @name $ionicBody#add
       * @description Add a class to the document's body element.
       * @param {string} class Each argument will be added to the body element.
       * @returns {$ionicBody} The $ionicBody service so methods can be chained.
       */
      addClass: function() {
        for (var x = 0; x < arguments.length; x++) {
          $document[0].body.classList.add(arguments[x]);
        }
        return this;
      },
      /**
       * @ngdoc method
       * @name $ionicBody#removeClass
       * @description Remove a class from the document's body element.
       * @param {string} class Each argument will be removed from the body element.
       * @returns {$ionicBody} The $ionicBody service so methods can be chained.
       */
      removeClass: function() {
        for (var x = 0; x < arguments.length; x++) {
          $document[0].body.classList.remove(arguments[x]);
        }
        return this;
      },
      /**
       * @ngdoc method
       * @name $ionicBody#enableClass
       * @description Similar to the `add` method, except the first parameter accepts a boolean
       * value determining if the class should be added or removed. Rather than writing user code,
       * such as "if true then add the class, else then remove the class", this method can be
       * given a true or false value which reduces redundant code.
       * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed.
       * @param {string} class Each remaining argument would be added or removed depending on
       * the first argument.
       * @returns {$ionicBody} The $ionicBody service so methods can be chained.
       */
      enableClass: function(shouldEnableClass) {
        var args = Array.prototype.slice.call(arguments).slice(1);
        if (shouldEnableClass) {
          this.addClass.apply(this, args);
        } else {
          this.removeClass.apply(this, args);
        }
        return this;
      },
      /**
       * @ngdoc method
       * @name $ionicBody#append
       * @description Append a child to the document's body.
       * @param {element} element The element to be appended to the body. The passed in element
       * can be either a jqLite element, or a DOM element.
       * @returns {$ionicBody} The $ionicBody service so methods can be chained.
       */
      append: function(ele) {
        $document[0].body.appendChild(ele.length ? ele[0] : ele);
        return this;
      },
      /**
       * @ngdoc method
       * @name $ionicBody#get
       * @description Get the document's body element.
       * @returns {element} Returns the document's body element.
       */
      get: function() {
        return $document[0].body;
      }
    };
  }]);
  
  IonicModule
  .factory('$ionicClickBlock', [
    '$document',
    '$ionicBody',
    '$timeout',
  function($document, $ionicBody, $timeout) {
    var CSS_HIDE = 'click-block-hide';
    var cbEle, fallbackTimer, pendingShow;
  
    function 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);
        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, nextViewExpireTimer, forcedNav;
  
    var viewHistory = {
      histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1 } },
      views: {},
      backView: null,
      forwardView: null,
      currentView: null
    };
  
    var View = function() {};
    View.prototype.initialize = function(data) {
      if (data) {
        for (var name in data) this[name] = data[name];
        return this;
      }
      return null;
    };
    View.prototype.go = function() {
  
      if (this.stateName) {
        return $state.go(this.stateName, this.stateParams);
      }
  
      if (this.url && this.url !== $location.url()) {
  
        if (viewHistory.backView === this) {
          return $window.history.go(-1);
        } else if (viewHistory.forwardView === this) {
          return $window.history.go(1);
        }
  
        $location.url(this.url);
        return;
      }
  
      return null;
    };
    View.prototype.destroy = function() {
      if (this.scope) {
        this.scope.$destroy && this.scope.$destroy();
        this.scope = null;
      }
    };
  
  
    function getViewById(viewId) {
      return (viewId ? viewHistory.views[ viewId ] : null);
    }
  
    function getBackView(view) {
      return (view ? getViewById(view.backViewId) : null);
    }
  
    function getForwardView(view) {
      return (view ? getViewById(view.forwardViewId) : null);
    }
  
    function getHistoryById(historyId) {
      return (historyId ? viewHistory.histories[ historyId ] : null);
    }
  
    function getHistory(scope) {
      var histObj = getParentHistoryObj(scope);
  
      if (!viewHistory.histories[ histObj.historyId ]) {
        // this history object exists in parent scope, but doesn't
        // exist in the history data yet
        viewHistory.histories[ histObj.historyId ] = {
          historyId: histObj.historyId,
          parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId,
          stack: [],
          cursor: -1
        };
      }
      return getHistoryById(histObj.historyId);
    }
  
    function getParentHistoryObj(scope) {
      var parentScope = scope;
      while (parentScope) {
        if (parentScope.hasOwnProperty('$historyId')) {
          // this parent scope has a historyId
          return { historyId: parentScope.$historyId, scope: parentScope };
        }
        // nothing found keep climbing up
        parentScope = parentScope.$parent;
      }
      // no history for the parent, use the root
      return { historyId: 'root', scope: $rootScope };
    }
  
    function setNavViews(viewId) {
      viewHistory.currentView = getViewById(viewId);
      viewHistory.backView = getBackView(viewHistory.currentView);
      viewHistory.forwardView = getForwardView(viewHistory.currentView);
    }
  
    function getCurrentStateId() {
      var id;
      if ($state && $state.current && $state.current.name) {
        id = $state.current.name;
        if ($state.params) {
          for (var key in $state.params) {
            if ($state.params.hasOwnProperty(key) && $state.params[key]) {
              id += "_" + key + "=" + $state.params[key];
            }
          }
        }
        return id;
      }
      // if something goes wrong make sure its got a unique stateId
      return ionic.Utils.nextUid();
    }
  
    function getCurrentStateParams() {
      var rtn;
      if ($state && $state.params) {
        for (var key in $state.params) {
          if ($state.params.hasOwnProperty(key)) {
            rtn = rtn || {};
            rtn[key] = $state.params[key];
          }
        }
      }
      return rtn;
    }
  
  
    return {
  
      register: function(parentScope, viewLocals) {
  
        var currentStateId = getCurrentStateId(),
            hist = getHistory(parentScope),
            currentView = viewHistory.currentView,
            backView = viewHistory.backView,
            forwardView = viewHistory.forwardView,
            viewId = null,
            action = null,
            direction = DIRECTION_NONE,
            historyId = hist.historyId,
            url = $location.url(),
            tmp, x, ele;
  
        if (lastStateId !== currentStateId) {
          lastStateId = currentStateId;
          stateChangeCounter++;
        }
  
        if (forcedNav) {
          // we've previously set exactly what to do
          viewId = forcedNav.viewId;
          action = forcedNav.action;
          direction = forcedNav.direction;
          forcedNav = null;
  
        } else if (backView && backView.stateId === currentStateId) {
          // they went back one, set the old current view as a forward view
          viewId = backView.viewId;
          historyId = backView.historyId;
          action = ACTION_MOVE_BACK;
          if (backView.historyId === currentView.historyId) {
            // went back in the same history
            direction = DIRECTION_BACK;
  
          } else if (currentView) {
            direction = DIRECTION_EXIT;
  
            tmp = getHistoryById(backView.historyId);
            if (tmp && tmp.parentHistoryId === currentView.historyId) {
              direction = DIRECTION_ENTER;
  
            } else {
              tmp = getHistoryById(currentView.historyId);
              if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
                direction = DIRECTION_SWAP;
              }
            }
          }
  
        } else if (forwardView && forwardView.stateId === currentStateId) {
          // they went to the forward one, set the forward view to no longer a forward view
          viewId = forwardView.viewId;
          historyId = forwardView.historyId;
          action = ACTION_MOVE_FORWARD;
          if (forwardView.historyId === currentView.historyId) {
            direction = DIRECTION_FORWARD;
  
          } else if (currentView) {
            direction = DIRECTION_EXIT;
  
            if (currentView.historyId === hist.parentHistoryId) {
              direction = DIRECTION_ENTER;
  
            } else {
              tmp = getHistoryById(currentView.historyId);
              if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
                direction = DIRECTION_SWAP;
              }
            }
          }
  
          tmp = getParentHistoryObj(parentScope);
          if (forwardView.historyId && tmp.scope) {
            // if a history has already been created by the forward view then make sure it stays the same
            tmp.scope.$historyId = forwardView.historyId;
            historyId = forwardView.historyId;
          }
  
        } else if (currentView && currentView.historyId !== historyId &&
                  hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length &&
                  hist.stack[hist.cursor].stateId === currentStateId) {
          // they just changed to a different history and the history already has views in it
          var switchToView = hist.stack[hist.cursor];
          viewId = switchToView.viewId;
          historyId = switchToView.historyId;
          action = ACTION_MOVE_BACK;
          direction = DIRECTION_SWAP;
  
          tmp = getHistoryById(currentView.historyId);
          if (tmp && tmp.parentHistoryId === historyId) {
            direction = DIRECTION_EXIT;
  
          } else {
            tmp = getHistoryById(historyId);
            if (tmp && tmp.parentHistoryId === currentView.historyId) {
              direction = DIRECTION_ENTER;
            }
          }
  
          // if switching to a different history, and the history of the view we're switching
          // to has an existing back view from a different history than itself, then
          // it's back view would be better represented using the current view as its back view
          tmp = getViewById(switchToView.backViewId);
          if (tmp && switchToView.historyId !== tmp.historyId) {
            hist.stack[hist.cursor].backViewId = currentView.viewId;
          }
  
        } else {
  
          // create an element from the viewLocals template
          ele = $ionicViewSwitcher.createViewEle(viewLocals);
          if (this.isAbstractEle(ele, viewLocals)) {
            console.log('VIEW', 'abstractView', DIRECTION_NONE, viewHistory.currentView);
            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 stack_x = tmp.stack[x];
                  stack_x && stack_x.destroy && stack_x.destroy();
                  tmp.stack.splice(x);
                }
                historyId = forwardView.historyId;
              }
            }
  
            // its only moving forward if its in the same history
            if (hist.historyId === currentView.historyId) {
              direction = DIRECTION_FORWARD;
  
            } else if (currentView.historyId !== hist.historyId) {
              direction = DIRECTION_ENTER;
  
              tmp = getHistoryById(currentView.historyId);
              if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
                direction = DIRECTION_SWAP;
  
              } else {
                tmp = getHistoryById(tmp.parentHistoryId);
                if (tmp && tmp.historyId === hist.historyId) {
                  direction = DIRECTION_EXIT;
                }
              }
            }
  
          } else {
            // there's no current view, so this must be the initial view
            action = ACTION_INITIAL_VIEW;
          }
  
          if (stateChangeCounter < 2) {
            // views that were spun up on the first load should not animate
            direction = DIRECTION_NONE;
          }
  
          // add the new view
          viewHistory.views[viewId] = this.createView({
            viewId: viewId,
            index: hist.stack.length,
            historyId: hist.historyId,
            backViewId: (currentView && currentView.viewId ? currentView.viewId : null),
            forwardViewId: null,
            stateId: currentStateId,
            stateName: this.currentStateName(),
            stateParams: getCurrentStateParams(),
            url: url
          });
  
          // add the new view to this history's stack
          hist.stack.push(viewHistory.views[viewId]);
        }
  
        $timeout.cancel(nextViewExpireTimer);
        if (nextViewOptions) {
          if (nextViewOptions.disableAnimate) direction = DIRECTION_NONE;
          if (nextViewOptions.disableBack) viewHistory.views[viewId].backViewId = null;
          if (nextViewOptions.historyRoot) {
            for (x = 0; x < hist.stack.length; x++) {
              if (hist.stack[x].viewId === viewId) {
                hist.stack[x].index = 0;
                hist.stack[x].backViewId = hist.stack[x].forwardViewId = null;
              } else {
                delete viewHistory.views[hist.stack[x].viewId];
              }
            }
            hist.stack = [viewHistory.views[viewId]];
          }
          nextViewOptions = null;
        }
  
        setNavViews(viewId);
  
        if (viewHistory.backView && historyId == viewHistory.backView.historyId && currentStateId == viewHistory.backView.stateId && url == viewHistory.backView.url) {
          for (x = 0; x < hist.stack.length; x++) {
            if (hist.stack[x].viewId == viewId) {
              action = 'dupNav';
              direction = DIRECTION_NONE;
              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;
            }
          }
        }
  
        console.log('VIEW', action, direction, viewHistory.currentView);
  
        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
       * @description Navigates the app to the back view, if a back view exists.
       */
      goBack: function() {
        viewHistory.backView && viewHistory.backView.go();
      },
  
  
      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
       * @description Removes all cached views within every {@link ionic.directive:ionNavView}.
       * This both removes the view element from the DOM, and destroy it's scope.
       */
      clearCache: function() {
        $ionicNavViewDelegate._instances.forEach(function(instance) {
          instance.clearCache();
        });
      },
  
      /**
       * @ngdoc method
       * @name $ionicHistory#nextViewOptions
       * @description Sets options for the next view. This method can be useful to override
       * certain view/transition defaults right before a view transition happens. For example,
       * the {@link ionic.directive:menuClose} directive uses this method internally to ensure
       * an animated view transition does not happen when a side menu is open, and also sets
       * the next view as the root of its history stack. After the transition these options
       * are set back to null.
       *
       * Available options:
       *
       * * `disableAnimate`: Do not animate the next transition.
       * * `disableBack`: The next view should forget its back view, and set it to null.
       * * `historyRoot`: The next view should become the root view in its history stack.
       *
       * ```js
       * $ionicHistory.nextViewOptions({
       *   disableAnimate: true,
       *   disableBack: true
       * });
       * ```
       */
      nextViewOptions: function(opts) {
        if (arguments.length) {
          $timeout.cancel(nextViewExpireTimer);
          if (opts === null) {
            nextViewOptions = opts;
          } else {
            nextViewOptions = nextViewOptions || {};
            extend(nextViewOptions, opts);
            if (nextViewOptions.expire) {
              nextViewExpireTimer = $timeout(function() {
                nextViewOptions = null;
              }, nextViewOptions.expire);
            }
          }
        }
        return nextViewOptions;
      },
  
      isAbstractEle: function(ele, viewLocals) {
        if (viewLocals && viewLocals.$$state && viewLocals.$$state.self.abstract) {
          return true;
        }
        return !!(ele && (isAbstractTag(ele) || isAbstractTag(ele.children())));
      },
  
      isActiveScope: function(scope) {
        if (!scope) return false;
  
        var climbScope = scope;
        var currentHistoryId = this.currentHistoryId();
        var foundHistoryId;
  
        while (climbScope) {
          if (climbScope.$$disconnected) {
            return false;
          }
  
          if (!foundHistoryId && climbScope.hasOwnProperty('$historyId')) {
            foundHistoryId = true;
          }
  
          if (currentHistoryId) {
            if (climbScope.hasOwnProperty('$historyId') && currentHistoryId == climbScope.$historyId) {
              return true;
            }
            if (climbScope.hasOwnProperty('$activeHistoryId')) {
              if (currentHistoryId == climbScope.$activeHistoryId) {
                if (climbScope.hasOwnProperty('$historyId')) {
                  return true;
                }
                if (!foundHistoryId) {
                  return true;
                }
              }
            }
          }
  
          if (foundHistoryId && climbScope.hasOwnProperty('$activeHistoryId')) {
            foundHistoryId = false;
          }
  
          climbScope = climbScope.$parent;
        }
  
        return currentHistoryId ? currentHistoryId == 'root' : true;
      }
  
    };
  
    function isAbstractTag(ele) {
      return ele && ele.length && /ion-side-menus|ion-tabs/i.test(ele[0].tagName);
    }
  
  }])
  
  .run([
    '$rootScope',
    '$state',
    '$location',
    '$document',
    '$ionicPlatform',
    '$ionicHistory',
  function($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory) {
  
    // always reset the keyboard state when change stage
    $rootScope.$on('$ionicView.beforeEnter', function() {
      ionic.keyboard && ionic.keyboard.hide && ionic.keyboard.hide();
    });
  
    $rootScope.$on('$ionicHistory.change', function(e, data) {
      if (!data) return;
  
      var viewHistory = $ionicHistory.viewHistory();
  
      var hist = (data.historyId ? viewHistory.histories[ data.historyId ] : null);
      if (hist && hist.cursor > -1 && hist.cursor < hist.stack.length) {
        // the history they're going to already exists
        // go to it's last view in its stack
        var view = hist.stack[ hist.cursor ];
        return view.go(data);
      }
  
      // this history does not have a URL, but it does have a uiSref
      // figure out its URL from the uiSref
      if (!data.url && data.uiSref) {
        data.url = $state.href(data.uiSref);
      }
  
      if (data.url) {
        // don't let it start with a #, messes with $location.url()
        if (data.url.indexOf('#') === 0) {
          data.url = data.url.replace('#', '');
        }
        if (data.url !== $location.url()) {
          // we've got a good URL, ready GO!
          $location.url(data.url);
        }
      }
    });
  
    $rootScope.$ionicGoBack = function() {
      $ionicHistory.goBack();
    };
  
    // Set the document title when a new view is shown
    $rootScope.$on('$ionicView.afterEnter', function(ev, data) {
      if (data && data.title) {
        $document[0].title = data.title;
      }
    });
  
    // Triggered when devices with a hardware back button (Android) is clicked by the user
    // This is a Cordova/Phonegap platform specifc method
    function onHardwareBackButton(e) {
      var backView = $ionicHistory.backView();
      if (backView) {
        // there is a back view, go to it
        backView.go();
      } else {
        // there is no back view, so close the app instead
        ionic.Platform.exitApp();
      }
      e.preventDefault();
      return false;
    }
    $ionicPlatform.registerBackButtonAction(
      onHardwareBackButton,
      PLATFORM_BACK_BUTTON_PRIORITY_VIEW
    );
  
  }]);
  
  /**
   * @ngdoc provider
   * @name $ionicConfigProvider
   * @module ionic
   * @description
   * Ionic automatically takes platform configurations into account to adjust things like what
   * transition style to use and whether tab icons should show on the top or bottom. For example,
   * iOS will move forward by transitioning the entering view from right to center and the leaving
   * view from center to left. However, Android will transition with the entering view going from
   * bottom to center, covering the previous view, which remains stationary. It should be noted
   * that when a platform is not iOS or Android, then it'll default to iOS. So if you are
   * developing on a desktop browser, it's going to take on iOS default configs.
   *
   * These configs can be changed using the `$ionicConfigProvider` during the configuration phase
   * of your app. Additionally, `$ionicConfig` can also set and get config values during the run
   * phase and within the app itself.
   *
   * By default, all base config variables are set to `'platform'`, which means it'll take on the
   * default config of the platform on which it's running. Config variables can be set at this
   * level so all platforms follow the same setting, rather than its platform config.
   * The following code would set the same config variable for all platforms:
   *
   * ```js
   * $ionicConfigProvider.views.maxCache(10);
   * ```
   *
   * Additionally, each platform can have it's own config within the `$ionicConfigProvider.platform`
   * property. The config below would only apply to Android devices.
   *
   * ```js
   * $ionicConfigProvider.platform.android.views.maxCache(5);
   * ```
   *
   * @usage
   * ```js
   * var myApp = angular.module('reallyCoolApp', ['ionic']);
   *
   * myApp.config(function($ionicConfigProvider) {
   *   $ionicConfigProvider.views.maxCache(5);
   *
   *   // note that you can also chain configs
   *   $ionicConfigProvider.backButton.text('Go Back').icon('ion-chevron-left');
   * });
   * ```
   */
  
  /**
   * @ngdoc method
   * @name $ionicConfigProvider#views.transition
   * @description Animation style when transitioning between views. Default `platform`.
   *
   * @param {string} transition Which style of view transitioning to use.
   *
   * * `platform`: Dynamically choose the correct transition style depending on the platform
   * the app is running from. If the platform is not `ios` or `android` then it will default
   * to `ios`.
   * * `ios`: iOS style transition.
   * * `android`: Android style transition.
   * * `none`: Do not preform animated transitions.
   *
   * @returns {string} value
   */
  
  /**
   * @ngdoc method
   * @name $ionicConfigProvider#views.maxCache
   * @description  Maximum number of view elements to cache in the DOM. When the max number is
   * exceeded, the view with the longest time period since it was accessed is removed. Views that
   * stay in the DOM cache the view's scope, current state, and scroll position. The scope is
   * disconnected from the `$watch` cycle when it is cached and reconnected when it enters again.
   * When the maximum cache is `0`, the leaving view's element will be removed from the DOM after
   * each view transition, and the next time the same view is shown, it will have to re-compile,
   * attach to the DOM, and link the element again. This disables caching, in effect.
   * @param {number} maxNumber Maximum number of views to retain. Default `10`.
   * @returns {number} How many views Ionic will hold onto until the a view is removed.
   */
  
  /**
   * @ngdoc method
   * @name $ionicConfigProvider#views.forwardCache
   * @description  By default, when navigating, views that were recently visited are cached, and
   * the same instance data and DOM elements are referenced when navigating back. However, when
   * navigating back in the history, the "forward" views are removed from the cache. If you
   * navigate forward to the same view again, it'll create a new DOM element and controller
   * instance. Basically, any forward views are reset each time. Set this config to `true` to have
   * forward views cached and not reset on each load.
   * @param {boolean} value
   * @returns {boolean}
   */
  
  /**
   * @ngdoc method
   * @name $ionicConfigProvider#backButton.icon
   * @description Back button icon.
   * @param {string} value
   * @returns {string}
   */
  
  /**
   * @ngdoc method
   * @name $ionicConfigProvider#backButton.text
   * @description Back button text.
   * @param {string} value Defaults to `Back`.
   * @returns {string}
   */
  
  /**
   * @ngdoc method
   * @name $ionicConfigProvider#backButton.previousTitleText
   * @description If the previous title text should become the back button text. This
   * is the default for iOS.
   * @param {boolean} value
   * @returns {boolean}
   */
  
  /**
   * @ngdoc method
   * @name $ionicConfigProvider#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#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
      },
      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
      },
  
      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'
      },
  
      tabs: {
        style: 'striped',
        position: 'top'
      }
  
    });
  
  
    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 ? '' : 0;
        css.opacity = opacity === 1 ? '' : opacity;
  
        ctrl.setCss('buttons-left', css);
        ctrl.setCss('buttons-right', css);
        ctrl.setCss('back-button', css);
  
        css[ionic.CSS.TRANSFORM] = 'translate3d(' + backTextX + 'px,0,0)';
        ctrl.setCss('back-text', css);
  
        css[ionic.CSS.TRANSFORM] = 'translate3d(' + titleX + 'px,0,0)';
        ctrl.setCss('title', css);
      }
  
      function enter(ctrlA, ctrlB, step) {
        if (!ctrlA || !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) {
        var css = {};
        css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0;
        css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
        ionic.DomUtil.cachedStyles(ele, css);
      }
  
      var d = {
        run: function(step) {
          if (direction == 'forward') {
            setStyles(enteringEle, (1 - step) * 99); // starting at 98% prevents a flicker
            setStyles(leavingEle, step * -100);
  
          } else if (direction == 'back') {
            setStyles(enteringEle, (1 - step) * -100);
            setStyles(leavingEle, step * 100);
  
          } else {
            // swap, enter, exit
            setStyles(enteringEle, 0);
            setStyles(leavingEle, 0);
          }
        },
        shouldAnimate: shouldAnimate
      };
  
      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;
    };
  });
  
  
  var LOADING_TPL =
    '<div class="loading-container">' +
      '<div class="loading">' +
      '</div>' +
    '</div>';
  
  var LOADING_HIDE_DEPRECATED = '$ionicLoading instance.hide() has been deprecated. Use $ionicLoading.hide().';
  var LOADING_SHOW_DEPRECATED = '$ionicLoading instance.show() has been deprecated. Use $ionicLoading.show().';
  var LOADING_SET_DEPRECATED = '$ionicLoading instance.setContent() has been deprecated. Use $ionicLoading.show({ template: \'my content\' }).';
  
  /**
   * @ngdoc service
   * @name $ionicLoading
   * @module ionic
   * @description
   * An overlay that can be used to indicate activity while blocking user
   * interaction.
   *
   * @usage
   * ```js
   * angular.module('LoadingApp', ['ionic'])
   * .controller('LoadingCtrl', function($scope, $ionicLoading) {
   *   $scope.show = function() {
   *     $ionicLoading.show({
   *       template: 'Loading...'
   *     });
   *   };
   *   $scope.hide = function(){
   *     $ionicLoading.hide();
   *   };
   * });
   * ```
   */
  /**
   * @ngdoc object
   * @name $ionicLoadingConfig
   * @module ionic
   * @description
   * Set the default options to be passed to the {@link ionic.service:$ionicLoading} service.
   *
   * @usage
   * ```js
   * var app = angular.module('myApp', ['ionic'])
   * app.constant('$ionicLoadingConfig', {
   *   template: 'Default Loading Template...'
   * });
   * app.controller('AppCtrl', function($scope, $ionicLoading) {
   *   $scope.showLoading = function() {
   *     $ionicLoading.show(); //options default to values in $ionicLoadingConfig
   *   };
   * });
   * ```
   */
  IonicModule
  .constant('$ionicLoadingConfig', {
    template: '<ion-spinner></ion-spinner>'
  })
  .factory('$ionicLoading', [
    '$ionicLoadingConfig',
    '$ionicBody',
    '$ionicTemplateLoader',
    '$ionicBackdrop',
    '$timeout',
    '$q',
    '$log',
    '$compile',
    '$ionicPlatform',
    '$rootScope',
  function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform, $rootScope) {
  
    var loaderInstance;
    //default values
    var deregisterBackAction = 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.
       * @param {object} opts The options for the loading indicator. Available properties:
       *  - `{string=}` `template` The html content of the indicator.
       *  - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator.
       *  - `{object=}` `scope` The scope to be a child of. Default: creates a child of $rootScope.
       *  - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown.
       *  - `{boolean=}` `hideOnStateChange` Whether to hide the loading spinner when navigating
       *    to a new state. Default false.
       *  - `{number=}` `delay` How many milliseconds to delay showing the indicator. By default there is no delay.
       *  - `{number=}` `duration` How many milliseconds to wait until automatically
       *  hiding the indicator. By default, the indicator will be shown until `.hide()` is called.
       */
      show: showLoader,
      /**
       * @ngdoc method
       * @name $ionicLoading#hide
       * @description Hides the loading indicator, if shown.
       */
      hide: hideLoader,
      /**
       * @private for testing
       */
      _getLoader: getLoader
    };
  
    function getLoader() {
      if (!loaderInstance) {
        loaderInstance = $ionicTemplateLoader.compile({
          template: LOADING_TPL,
          appendTo: $ionicBody.get()
        })
        .then(function(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,
              PLATFORM_BACK_BUTTON_PRIORITY_LOADING
            );
  
            templatePromise.then(function(html) {
              if (html) {
                var loading = self.element.children();
                loading.html(html);
                $compile(loading.contents())(self.scope);
              }
  
              //Don't show until template changes
              if (self.isShown) {
                self.element.addClass('visible');
                ionic.requestAnimationFrame(function() {
                  if (self.isShown) {
                    self.element.addClass('active');
                    $ionicBody.addClass('loading-active');
                  }
                });
              }
            });
  
            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');
              setTimeout(function() {
                !self.isShown && self.element.removeClass('visible');
              }, 200);
            }
            $timeout.cancel(self.durationTimeout);
            self.isShown = false;
          };
  
          return self;
        });
      }
      return loaderInstance;
    }
  
    function showLoader(options) {
      options = extend({}, $ionicLoadingConfig || {}, options || {});
      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);
      loadingShowDelay.then(getLoader).then(function(loader) {
        return loader.show(options);
      });
  
      return {
        hide: deprecated.method(LOADING_HIDE_DEPRECATED, $log.error, hideLoader),
        show: deprecated.method(LOADING_SHOW_DEPRECATED, $log.error, function() {
          showLoader(options);
        }),
        setContent: deprecated.method(LOADING_SET_DEPRECATED, $log.error, function(content) {
          getLoader().then(function(loader) {
            loader.show({ template: content });
          });
        })
      };
    }
  
    function hideLoader() {
      deregisterStateListener1();
      deregisterStateListener2();
      $timeout.cancel(loadingShowDelay);
      getLoader().then(function(loader) {
        loader.hide();
      });
    }
  }]);
  
  /**
   * @ngdoc service
   * @name $ionicModal
   * @module ionic
   * @description
   *
   * Related: {@link ionic.controller:ionicModal ionicModal controller}.
   *
   * The Modal is a content pane that can go over the user's main view
   * temporarily.  Usually used for making a choice or editing an item.
   *
   * Put the content of the modal inside of an `<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',
  function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $q, $log) {
  
    /**
     * @ngdoc controller
     * @name ionicModal
     * @module ionic
     * @description
     * Instantiated by the {@link ionic.service:$ionicModal} service.
     *
     * Be sure to call [remove()](#remove) when you are done with each modal
     * to clean it up and avoid memory leaks.
     *
     * Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
     * scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are
     * called when the modal is removed.
     */
    var ModalView = ionic.views.Modal.inherit({
      /**
       * @ngdoc method
       * @name ionicModal#initialize
       * @description Creates a new modal controller instance.
       * @param {object} options An options object with the following properties:
       *  - `{object=}` `scope` The scope to be a child of.
       *    Default: creates a child of $rootScope.
       *  - `{string=}` `animation` The animation to show & hide with.
       *    Default: 'slide-in-up'
       *  - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
       *    the modal when shown.  Default: false.
       *  - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop.
       *    Default: true.
       *  - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware
       *    back button on Android and similar devices.  Default: true.
       */
      initialize: function(opts) {
        ionic.views.Modal.prototype.initialize.call(this, opts);
        this.animation = opts.animation || 'slide-in-up';
      },
  
      /**
       * @ngdoc method
       * @name ionicModal#show
       * @description Show this modal instance.
       * @returns {promise} A promise which is resolved when the modal is finished animating in.
       */
      show: function(target) {
        var self = this;
  
        if (self.scope.$$destroyed) {
          $log.error('Cannot call ' +  self.viewType + '.show() after remove(). Please create a new ' +  self.viewType + ' instance.');
          return;
        }
  
        var modalEl = jqLite(self.modalEl);
  
        self.el.classList.remove('hide');
        $timeout(function() {
          $ionicBody.addClass(self.viewType + '-open');
        }, 400);
  
        if (!self.el.parentElement) {
          modalEl.addClass(self.animation);
          $ionicBody.append(self.el);
        }
  
        if (target && self.positionView) {
          self.positionView(target, modalEl);
          // set up a listener for in case the window size changes
          ionic.on('resize',function() {
            ionic.off('resize',null,window);
            self.positionView(target,modalEl);
          },window);
        }
  
        modalEl.addClass('ng-enter active')
               .removeClass('ng-leave ng-leave-active');
  
        self._isShown = true;
        self._deregisterBackButton = $ionicPlatform.registerBackButtonAction(
          self.hardwareBackButtonClose ? angular.bind(self, self.hide) : noop,
          PLATFORM_BACK_BUTTON_PRIORITY_MODAL
        );
  
        ionic.views.Modal.prototype.show.call(self);
  
        $timeout(function() {
          modalEl.addClass('ng-enter-active');
          ionic.trigger('resize');
          self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self);
          self.el.classList.add('active');
          self.scope.$broadcast('$ionicHeader.align');
        }, 20);
  
        return $timeout(function() {
          //After animating in, allow hide on backdrop click
          self.$el.on('click', function(e) {
            if (self.backdropClickToClose && e.target === self.el) {
              self.hide();
            }
          });
        }, 400);
      },
  
      /**
       * @ngdoc method
       * @name ionicModal#hide
       * @description Hide this modal instance.
       * @returns {promise} A promise which is resolved when the modal is finished animating out.
       */
      hide: function() {
        var self = this;
        var modalEl = jqLite(self.modalEl);
  
        self.el.classList.remove('active');
        modalEl.addClass('ng-leave');
  
        $timeout(function() {
          modalEl.addClass('ng-leave-active')
                 .removeClass('ng-enter ng-enter-active active');
        }, 20);
  
        self.$el.off('click');
        self._isShown = false;
        self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self);
        self._deregisterBackButton && self._deregisterBackButton();
  
        ionic.views.Modal.prototype.hide.call(self);
  
        // clean up event listeners
        if (self.positionView) {
          ionic.off('resize',null,window);
        }
  
        return $timeout(function() {
          $ionicBody.removeClass(self.viewType + '-open');
          self.el.classList.add('hide');
        }, self.hideDelay || 320);
      },
  
      /**
       * @ngdoc method
       * @name ionicModal#remove
       * @description Remove this modal instance from the DOM and clean up.
       * @returns {promise} A promise which is resolved when the modal is finished animating out.
       */
      remove: function() {
        var self = this;
        self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self);
  
        return self.hide().then(function() {
          self.scope.$destroy();
          self.$el.remove();
        });
      },
  
      /**
       * @ngdoc method
       * @name ionicModal#isShown
       * @returns boolean Whether this modal is currently shown.
       */
      isShown: function() {
        return !!this._isShown;
      }
    });
  
    var createModal = function(templateString, options) {
      // Create a new scope for the modal
      var scope = options.scope && options.scope.$new() || $rootScope.$new(true);
  
      options.viewType = options.viewType || 'modal';
  
      extend(scope, {
        $hasHeader: false,
        $hasSubheader: false,
        $hasFooter: false,
        $hasSubfooter: false,
        $hasTabs: false,
        $hasTabsTop: false
      });
  
      // Compile the template
      var element = $compile('<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;
    };
  
    return {
      /**
       * @ngdoc method
       * @name $ionicModal#fromTemplate
       * @param {string} templateString The template string to use as the modal's
       * content.
       * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
       * @returns {object} An instance of an {@link ionic.controller:ionicModal}
       * controller.
       */
      fromTemplate: function(templateString, options) {
        var modal = createModal(templateString, options || {});
        return modal;
      },
      /**
       * @ngdoc method
       * @name $ionicModal#fromTemplateUrl
       * @param {string} templateUrl The url to load the template from.
       * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
       * options object.
       * @returns {promise} A promise that will be resolved with an instance of
       * an {@link ionic.controller:ionicModal} controller.
       */
      fromTemplateUrl: function(url, options, _) {
        var cb;
        //Deprecated: allow a callback as second parameter. Now we return a promise.
        if (angular.isFunction(options)) {
          cb = options;
          options = _;
        }
        return $ionicTemplateLoader.load(url).then(function(templateString) {
          var modal = createModal(templateString, options || {});
          cb && cb(modal);
          return modal;
        });
      }
    };
  }]);
  
  
  /**
   * @ngdoc service
   * @name $ionicNavBarDelegate
   * @module ionic
   * @description
   * Delegate for controlling the {@link ionic.directive:ionNavBar} directive.
   *
   * @usage
   *
   * ```html
   * <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'
  ]));
  
  
  var PLATFORM_BACK_BUTTON_PRIORITY_VIEW = 100;
  var PLATFORM_BACK_BUTTON_PRIORITY_SIDE_MENU = 150;
  var PLATFORM_BACK_BUTTON_PRIORITY_MODAL = 200;
  var PLATFORM_BACK_BUTTON_PRIORITY_ACTION_SHEET = 300;
  var PLATFORM_BACK_BUTTON_PRIORITY_POPUP = 400;
  var PLATFORM_BACK_BUTTON_PRIORITY_LOADING = 500;
  
  /**
   * @ngdoc service
   * @name $ionicPlatform
   * @module ionic
   * @description
   * An angular abstraction of {@link ionic.utility:ionic.Platform}.
   *
   * Used to detect the current platform, as well as do things like override the
   * Android back button in PhoneGap/Cordova.
   */
  IonicModule
  .provider('$ionicPlatform', function() {
    return {
      $get: ['$q', '$rootScope', function($q, $rootScope) {
        var self = {
  
          /**
           * @ngdoc method
           * @name $ionicPlatform#onHardwareBackButton
           * @description
           * Some platforms have a hardware back button, so this is one way to
           * bind to it.
           * @param {function} callback the callback to trigger when this event occurs
           */
          onHardwareBackButton: function(cb) {
            ionic.Platform.ready(function() {
              document.addEventListener('backbutton', cb, false);
            });
          },
  
          /**
           * @ngdoc method
           * @name $ionicPlatform#offHardwareBackButton
           * @description
           * Remove an event listener for the backbutton.
           * @param {function} callback The listener function that was
           * originally bound.
           */
          offHardwareBackButton: function(fn) {
            ionic.Platform.ready(function() {
              document.removeEventListener('backbutton', fn);
            });
          },
  
          /**
           * @ngdoc method
           * @name $ionicPlatform#registerBackButtonAction
           * @description
           * Register a hardware back button action. Only one action will execute
           * when the back button is clicked, so this method decides which of
           * the registered back button actions has the highest priority.
           *
           * For example, if an actionsheet is showing, the back button should
           * close the actionsheet, but it should not also go back a page view
           * or close a modal which may be open.
           *
           * @param {function} callback Called when the back button is pressed,
           * if this listener is the highest priority.
           * @param {number} priority Only the highest priority will execute.
           * @param {*=} actionId The id to assign this action. Default: a
           * random unique id.
           * @returns {function} A function that, when called, will deregister
           * this backButtonAction.
           */
          $backButtonActions: {},
          registerBackButtonAction: function(fn, priority, actionId) {
  
            if (!self._hasBackButtonHandler) {
              // add a back button listener if one hasn't been setup yet
              self.$backButtonActions = {};
              self.onHardwareBackButton(self.hardwareBackButtonClick);
              self._hasBackButtonHandler = true;
            }
  
            var action = {
              id: (actionId ? actionId : ionic.Utils.nextUid()),
              priority: (priority ? priority : 0),
              fn: fn
            };
            self.$backButtonActions[action.id] = action;
  
            // return a function to de-register this back button action
            return function() {
              delete self.$backButtonActions[action.id];
            };
          },
  
          /**
           * @private
           */
          hardwareBackButtonClick: function(e) {
            // loop through all the registered back button actions
            // and only run the last one of the highest priority
            var priorityAction, actionId;
            for (actionId in self.$backButtonActions) {
              if (!priorityAction || self.$backButtonActions[actionId].priority >= priorityAction.priority) {
                priorityAction = self.$backButtonActions[actionId];
              }
            }
            if (priorityAction) {
              priorityAction.fn(e);
              return priorityAction;
            }
          },
  
          is: function(type) {
            return ionic.Platform.is(type);
          },
  
          /**
           * @ngdoc method
           * @name $ionicPlatform#on
           * @description
           * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`,
           * `offline`, etc. More information about available event types can be found in
           * [Cordova's event documentation](https://cordova.apache.org/docs/en/edge/cordova_events_events.md.html#Events).
           * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/edge/cordova_events_events.md.html#Events).
           * @param {function} callback Called when the Cordova event is fired.
           * @returns {function} Returns a deregistration function to remove the event listener.
           */
          on: function(type, cb) {
            ionic.Platform.ready(function() {
              document.addEventListener(type, cb, false);
            });
            return function() {
              ionic.Platform.ready(function() {
                document.removeEventListener(type, cb);
              });
            };
          },
  
          /**
           * @ngdoc method
           * @name $ionicPlatform#ready
           * @description
           * Trigger a callback once the device is ready,
           * or immediately if the device is already ready.
           * @param {function=} callback The function to call.
           * @returns {promise} A promise which is resolved when the device is ready.
           */
          ready: function(cb) {
            var q = $q.defer();
  
            ionic.Platform.ready(function() {
              q.resolve();
              cb && cb();
            });
  
            return q.promise;
          }
        };
        return self;
      }]
    };
  
  });
  
  /**
   * @ngdoc service
   * @name $ionicPopover
   * @module ionic
   * @description
   *
   * Related: {@link ionic.controller:ionicPopover ionicPopover controller}.
   *
   * The Popover is a view that floats above an app’s content. Popovers provide an
   * easy way to present or gather information from the user and are
   * commonly used in the following situations:
   *
   * - Show more info about the current view
   * - Select a commonly used tool or configuration
   * - Present a list of actions to perform inside one of your views
   *
   * Put the content of the popover inside of an `<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 hide popover
   *   $scope.$on('popover.hidden', function() {
   *     // Execute action
   *   });
   *   // Execute action on remove popover
   *   $scope.$on('popover.removed', function() {
   *     // Execute action
   *   });
   * });
   * ```
   */
  
  
  IonicModule
  .factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', '$window',
  function($ionicModal, $ionicPosition, $document, $window) {
  
    var POPOVER_BODY_PADDING = 6;
  
    var POPOVER_OPTIONS = {
      viewType: 'popover',
      hideDelay: 1,
      animation: 'none',
      positionView: positionView
    };
  
    function positionView(target, popoverEle) {
      var targetEle = jqLite(target.target || target);
      var buttonOffset = $ionicPosition.offset(targetEle);
      var popoverWidth = popoverEle.prop('offsetWidth');
      var popoverHeight = popoverEle.prop('offsetHeight');
      var bodyWidth = $document[0].body.clientWidth;
      // clientHeight doesn't work on all platforms for body
      var bodyHeight = $window.innerHeight;
  
      var popoverCSS = {
        left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2
      };
      var arrowEle = jqLite(popoverEle[0].querySelector('.popover-arrow'));
  
      if (popoverCSS.left < POPOVER_BODY_PADDING) {
        popoverCSS.left = POPOVER_BODY_PADDING;
      } else if (popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) {
        popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING;
      }
  
      // If the popover when popped down stretches past bottom of screen,
      // make it pop up if 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',
  function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicBody, $compile, $ionicPlatform) {
    //TODO allow this to be configured
    var config = {
      stackPushDelay: 75
    };
    var popupStack = [];
    var $ionicPopup = {
      /**
       * @ngdoc method
       * @description
       * Show a complex popup. This is the master show function for all popups.
       *
       * A complex popup has a `buttons` array, with each button having a `text` and `type`
       * field, in addition to an `onTap` function.  The `onTap` function, called when
       * the corresponding button on the popup is tapped, will by default close the popup
       * and resolve the popup promise with its return value.  If you wish to prevent the
       * default and keep the popup open on button tap, call `event.preventDefault()` on the
       * passed in tap event.  Details below.
       *
       * @name $ionicPopup#show
       * @param {object} options The options for the new popup, of the form:
       *
       * ```
       * {
       *   title: '', // String. The title of the popup.
       *   cssClass: '', // String, The custom CSS class name
       *   subTitle: '', // String (optional). The sub-title of the popup.
       *   template: '', // String (optional). The html template to place in the popup body.
       *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
       *   scope: null, // Scope (optional). A scope to link to the popup content.
       *   buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer.
       *     text: 'Cancel',
       *     type: 'button-default',
       *     onTap: function(e) {
       *       // e.preventDefault() will stop the popup from closing when tapped.
       *       e.preventDefault();
       *     }
       *   }, {
       *     text: 'OK',
       *     type: 'button-positive',
       *     onTap: function(e) {
       *       // Returning a value will cause the promise to resolve with the given value.
       *       return scope.data.response;
       *     }
       *   }]
       * }
       * ```
       *
       * @returns {object} A promise which is resolved when the popup is closed. Has an additional
       * `close` function, which can be used to programmatically close the popup.
       */
      show: showPopup,
  
      /**
       * @ngdoc method
       * @name $ionicPopup#alert
       * @description Show a simple alert popup with a message and one button that the user can
       * tap to close the popup.
       *
       * @param {object} options The options for showing the alert, of the form:
       *
       * ```
       * {
       *   title: '', // String. The title of the popup.
       *   cssClass: '', // String, The custom CSS class name
       *   subTitle: '', // String (optional). The sub-title of the popup.
       *   template: '', // String (optional). The html template to place in the popup body.
       *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
       *   okText: '', // String (default: 'OK'). The text of the OK button.
       *   okType: '', // String (default: 'button-positive'). The type of the OK button.
       * }
       * ```
       *
       * @returns {object} A promise which is resolved when the popup is closed. Has one additional
       * function `close`, which can be called with any value to programmatically close the popup
       * with the given value.
       */
      alert: showAlert,
  
      /**
       * @ngdoc method
       * @name $ionicPopup#confirm
       * @description
       * Show a simple confirm popup with a Cancel and OK button.
       *
       * Resolves the promise with true if the user presses the OK button, and false if the
       * user presses the Cancel button.
       *
       * @param {object} options The options for showing the confirm popup, of the form:
       *
       * ```
       * {
       *   title: '', // String. The title of the popup.
       *   cssClass: '', // String, The custom CSS class name
       *   subTitle: '', // String (optional). The sub-title of the popup.
       *   template: '', // String (optional). The html template to place in the popup body.
       *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
       *   cancelText: '', // String (default: 'Cancel'). The text of the Cancel button.
       *   cancelType: '', // String (default: 'button-default'). The type of the Cancel button.
       *   okText: '', // String (default: 'OK'). The text of the OK button.
       *   okType: '', // String (default: 'button-positive'). The type of the OK button.
       * }
       * ```
       *
       * @returns {object} A promise which is resolved when the popup is closed. Has one additional
       * function `close`, which can be called with any value to programmatically close the popup
       * with the given value.
       */
      confirm: showConfirm,
  
      /**
       * @ngdoc method
       * @name $ionicPopup#prompt
       * @description Show a simple prompt popup, which has an input, OK button, and Cancel button.
       * Resolves the promise with the value of the input if the user presses OK, and with undefined
       * if the user presses Cancel.
       *
       * ```javascript
       *  $ionicPopup.prompt({
       *    title: 'Password Check',
       *    template: 'Enter your secret password',
       *    inputType: 'password',
       *    inputPlaceholder: 'Your password'
       *  }).then(function(res) {
       *    console.log('Your password is', res);
       *  });
       * ```
       * @param {object} options The options for showing the prompt popup, of the form:
       *
       * ```
       * {
       *   title: '', // String. The title of the popup.
       *   cssClass: '', // String, The custom CSS class name
       *   subTitle: '', // String (optional). The sub-title of the popup.
       *   template: '', // String (optional). The html template to place in the popup body.
       *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
       *   inputType: // String (default: 'text'). The type of input to use
       *   inputPlaceholder: // String (default: ''). A placeholder to use for the input.
       *   cancelText: // String (default: 'Cancel'. The text of the Cancel button.
       *   cancelType: // String (default: 'button-default'). The type of the Cancel button.
       *   okText: // String (default: 'OK'). The text of the OK button.
       *   okType: // String (default: 'button-positive'). The type of the OK button.
       * }
       * ```
       *
       * @returns {object} A promise which is resolved when the popup is closed. Has one additional
       * function `close`, which can be called with any value to programmatically close the popup
       * with the given value.
       */
      prompt: showPrompt,
      /**
       * @private for testing
       */
      _createPopup: createPopup,
      _popupStack: popupStack
    };
  
    return $ionicPopup;
  
    function createPopup(options) {
      options = extend({
        scope: null,
        title: '',
        buttons: []
      }, options || {});
  
      var popupPromise = $ionicTemplateLoader.compile({
        template: POPUP_TPL,
        scope: options.scope && options.scope.$new(),
        appendTo: $ionicBody.get()
      });
      var contentPromise = options.templateUrl ?
        $ionicTemplateLoader.load(options.templateUrl) :
        $q.when(options.template || options.content || '');
  
      return $q.all([popupPromise, contentPromise])
      .then(function(results) {
        var self = results[0];
        var content = results[1];
        var responseDeferred = $q.defer();
  
        self.responseDeferred = responseDeferred;
  
        //Can't ng-bind-html for popup-body because it can be insecure html
        //(eg an input in case of prompt)
        var body = jqLite(self.element[0].querySelector('.popup-body'));
        if (content) {
          body.html(content);
          $compile(body.contents())(self.scope);
        } else {
          body.remove();
        }
  
        extend(self.scope, {
          title: options.title,
          buttons: options.buttons,
          subTitle: options.subTitle,
          cssClass: options.cssClass,
          $buttonTapped: function(button, event) {
            var result = (button.onTap || noop)(event);
            event = event.originalEvent || event; //jquery events
  
            if (!event.defaultPrevented) {
              responseDeferred.resolve(result);
            }
          }
        });
  
        self.show = function() {
          if (self.isShown) return;
  
          self.isShown = true;
          ionic.requestAnimationFrame(function() {
            //if hidden while waiting for raf, don't show
            if (!self.isShown) return;
  
            self.element.removeClass('popup-hidden');
            self.element.addClass('popup-showing active');
            focusInput(self.element);
          });
        };
        self.hide = function(callback) {
          callback = callback || noop;
          if (!self.isShown) return callback();
  
          self.isShown = false;
          self.element.removeClass('active');
          self.element.addClass('popup-hidden');
          $timeout(callback, 250);
        };
        self.remove = function() {
          if (self.removed) return;
  
          self.hide(function() {
            self.element.remove();
            self.scope.$destroy();
          });
  
          self.removed = true;
        };
  
        return self;
      });
    }
  
    function onHardwareBackButton(e) {
      popupStack[0] && popupStack[0].responseDeferred.resolve();
    }
  
    function showPopup(options) {
      var popupPromise = $ionicPopup._createPopup(options);
      var previousPopup = popupStack[0];
  
      if (previousPopup) {
        previousPopup.hide();
      }
  
      var resultPromise = $timeout(noop, previousPopup ? config.stackPushDelay : 0)
      .then(function() { return popupPromise; })
      .then(function(popup) {
        if (!previousPopup) {
          //Add popup-open & backdrop if this is first popup
          $ionicBody.addClass('popup-open');
          $ionicBackdrop.retain();
          //only show the backdrop on the first popup
          $ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction(
            onHardwareBackButton,
            PLATFORM_BACK_BUTTON_PRIORITY_POPUP
          );
        }
        popupStack.unshift(popup);
        popup.show();
  
        //DEPRECATED: notify the promise with an object with a close method
        popup.responseDeferred.notify({
          close: resultPromise.close
        });
  
        return popup.responseDeferred.promise.then(function(result) {
          var index = popupStack.indexOf(popup);
          if (index !== -1) {
            popupStack.splice(index, 1);
          }
          popup.remove();
  
          var previousPopup = popupStack[0];
          if (previousPopup) {
            previousPopup.show();
          } else {
            //Remove popup-open & backdrop if this is last popup
            $timeout(function() {
              // wait to remove this due to a 300ms delay native
              // click which would trigging whatever was underneath this
              $ionicBody.removeClass('popup-open');
            }, 400);
            $timeout(function() {
              $ionicBackdrop.release();
            }, config.stackPushDelay || 0);
            ($ionicPopup._backButtonActionDone || noop)();
          }
          return result;
        });
      });
  
      function close(result) {
        popupPromise.then(function(popup) {
          if (!popup.removed) {
            popup.responseDeferred.resolve(result);
          }
        });
      }
      resultPromise.close = close;
  
      return resultPromise;
    }
  
    function focusInput(element) {
      var focusOn = element[0].querySelector('[autofocus]');
      if (focusOn) {
        focusOn.focus();
      }
    }
  
    function showAlert(opts) {
      return showPopup(extend({
        buttons: [{
          text: opts.okText || 'OK',
          type: opts.okType || 'button-positive',
          onTap: function(e) {
            return true;
          }
        }]
      }, opts || {}));
    }
  
    function showConfirm(opts) {
      return showPopup(extend({
        buttons: [{
          text: opts.cancelText || 'Cancel',
          type: opts.cancelType || 'button-default',
          onTap: function(e) { return false; }
        }, {
          text: opts.okText || 'OK',
          type: opts.okType || 'button-positive',
          onTap: function(e) { return true; }
        }]
      }, opts || {}));
    }
  
    function showPrompt(opts) {
      var scope = $rootScope.$new(true);
      scope.data = {};
      var text = '';
      if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) {
        text = '<span>' + opts.template + '</span>';
        delete opts.template;
      }
      return showPopup(extend({
        template: text + '<input ng-model="data.response" type="' + (opts.inputType || 'text') +
          '" placeholder="' + (opts.inputPlaceholder || '') + '">',
        scope: scope,
        buttons: [{
          text: opts.cancelText || 'Cancel',
          type: opts.cancelType || 'button-default',
          onTap: function(e) {}
        }, {
          text: opts.okText || 'OK',
          type: opts.okType || 'button-positive',
          onTap: function(e) {
            return scope.data.response || '';
          }
        }]
      }, opts || {}));
    }
  }]);
  
  /**
   * @ngdoc service
   * @name $ionicPosition
   * @module ionic
   * @description
   * A set of utility methods that can be use to retrieve position of DOM elements.
   * It is meant to be used where we need to absolute-position DOM elements in
   * relation to other, existing elements (this is the case for tooltips, popovers, etc.).
   *
   * Adapted from [AngularUI Bootstrap](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js),
   * ([license](https://github.com/angular-ui/bootstrap/blob/master/LICENSE))
   */
  IonicModule
  .factory('$ionicPosition', ['$document', '$window', function($document, $window) {
  
    function getStyle(el, cssprop) {
      if (el.currentStyle) { //IE
        return el.currentStyle[cssprop];
      } else if ($window.getComputedStyle) {
        return $window.getComputedStyle(el)[cssprop];
      }
      // finally try and get inline style
      return el.style[cssprop];
    }
  
    /**
     * Checks if a given element is statically positioned
     * @param element - raw DOM element
     */
    function isStaticPositioned(element) {
      return (getStyle(element, 'position') || 'static') === 'static';
    }
  
    /**
     * returns the closest, non-statically positioned parentOffset of a given element
     * @param element
     */
    var parentOffsetEl = function(element) {
      var docDomEl = $document[0];
      var offsetParent = element.offsetParent || docDomEl;
      while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) {
        offsetParent = offsetParent.offsetParent;
      }
      return offsetParent || docDomEl;
    };
  
    return {
      /**
       * @ngdoc method
       * @name $ionicPosition#position
       * @description Get the current coordinates of the element, relative to the offset parent.
       * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/).
       * @param {element} element The element to get the position of.
       * @returns {object} Returns an object containing the properties top, left, width and height.
       */
      position: function(element) {
        var elBCR = this.offset(element);
        var offsetParentBCR = { top: 0, left: 0 };
        var offsetParentEl = parentOffsetEl(element[0]);
        if (offsetParentEl != $document[0]) {
          offsetParentBCR = this.offset(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).
     */
    '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 {object} 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 for the change to take.
     */
    'slide',
    'select',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#enableSlide
     * @param {boolean=} shouldEnable Whether to enable sliding the slidebox.
     * @returns {boolean} Whether sliding is enabled.
     */
    'enableSlide',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#previous
     * @description Go to the previous slide. Wraps around if at the beginning.
     */
    'previous',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#next
     * @description Go to the next slide. Wraps around if at the end.
     */
    'next',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#stop
     * @description Stop sliding. The slideBox will not move again until
     * explicitly told to do so.
     */
    'stop',
    'autoPlay',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#start
     * @description Start sliding again if the slideBox was stopped.
     */
    'start',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#currentIndex
     * @returns number The index of the current slide.
     */
    'currentIndex',
    'selected',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#slidesCount
     * @returns number The number of slides there are currently.
     */
    'slidesCount',
    'count',
    'loop'
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#$getByHandle
     * @param {string} handle
     * @returns `delegateInstance` A delegate instance that controls only the
     * {@link ionic.directive:ionSlideBox} directives with `delegate-handle` matching
     * the given handle.
     *
     * Example: `$ionicSlideBoxDelegate.$getByHandle('my-handle').stop();`
     */
  ]));
  
  
  /**
   * @ngdoc service
   * @name $ionicTabsDelegate
   * @module ionic
   *
   * @description
   * Delegate for controlling the {@link ionic.directive:ionTabs} directive.
   *
   * Methods called directly on the $ionicTabsDelegate service will control all ionTabs
   * directives. Use the {@link ionic.service:$ionicTabsDelegate#$getByHandle $getByHandle}
   * method to control specific ionTabs instances.
   *
   * @usage
   *
   * ```html
   * <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#$getByHandle
     * @param {string} handle
     * @returns `delegateInstance` A delegate instance that controls only the
     * {@link ionic.directive:ionTabs} directives with `delegate-handle` matching
     * the given handle.
     *
     * Example: `$ionicTabsDelegate.$getByHandle('my-handle').select(0);`
     */
  ]));
  
  
  // closure to keep things neat
  (function() {
    var templatesToCache = [];
  
  /**
   * @ngdoc service
   * @name $ionicTemplateCache
   * @module ionic
   * @description A service that preemptively caches template files to eliminate transition flicker and boost performance.
   * @usage
   * State templates are cached automatically, but you can optionally cache other templates.
   *
   * ```js
   * $ionicTemplateCache('myNgIncludeTemplate.html');
   * ```
   *
   * Optionally disable all preemptive caching with the `$ionicConfigProvider` or individual states by setting `prefetchTemplate`
   * in the `$state` definition
   *
   * ```js
   *   angular.module('myApp', ['ionic'])
   *   .config(function($stateProvider, $ionicConfigProvider) {
   *
   *     // disable preemptive template caching globally
   *     $ionicConfigProvider.templates.prefetch(false);
   *
   *     // disable individual states
   *     $stateProvider
   *       .state('tabs', {
   *         url: "/tab",
   *         abstract: true,
   *         prefetchTemplate: false,
   *         templateUrl: "tabs-templates/tabs.html"
   *       })
   *       .state('tabs.home', {
   *         url: "/home",
   *         views: {
   *           'home-tab': {
   *             prefetchTemplate: false,
   *             templateUrl: "tabs-templates/home.html",
   *             controller: 'HomeTabCtrl'
   *           }
   *         }
   *       });
   *   });
   * ```
   */
  IonicModule
  .factory('$ionicTemplateCache', [
  '$http',
  '$templateCache',
  '$timeout',
  function($http, $templateCache, $timeout) {
    var toCache = templatesToCache,
        hasRun;
  
    function $ionicTemplateCache(templates) {
      if (typeof templates === 'undefined') {
        return run();
      }
      if (isString(templates)) {
        templates = [templates];
      }
      forEach(templates, function(template) {
        toCache.push(template);
      });
      if (hasRun) {
        run();
      }
    }
  
    // run through methods - internal method
    function run() {
      $ionicTemplateCache._runCount++;
  
      hasRun = true;
      // ignore if race condition already zeroed out array
      if (toCache.length === 0) return;
  
      var i = 0;
      while (i < 4 && (template = toCache.pop())) {
        // note that inline templates are ignored by this request
        if (isString(template)) $http.get(template, { cache: $templateCache });
        i++;
      }
      // only preload 3 templates a second
      if (toCache.length) {
        $timeout(run, 1000);
      }
    }
  
    // exposing for testing
    $ionicTemplateCache._runCount = 0;
    // default method
    return $ionicTemplateCache;
  }])
  
  // Intercepts the $stateprovider.state() command to look for templateUrls that can be cached
  .config([
  '$stateProvider',
  '$ionicConfigProvider',
  function($stateProvider, $ionicConfigProvider) {
    var stateProviderState = $stateProvider.state;
    $stateProvider.state = function(stateName, definition) {
      // don't even bother if it's disabled. note, another config may run after this, so it's not a catch-all
      if (typeof definition === 'object') {
        var enabled = definition.prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
        if (enabled && isString(definition.templateUrl)) templatesToCache.push(definition.templateUrl);
        if (angular.isObject(definition.views)) {
          for (var key in definition.views) {
            enabled = definition.views[key].prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
            if (enabled && isString(definition.views[key].templateUrl)) templatesToCache.push(definition.views[key].templateUrl);
          }
        }
      }
      return stateProviderState.call($stateProvider, stateName, definition);
    };
  }])
  
  // process the templateUrls collected by the $stateProvider, adding them to the cache
  .run(['$ionicTemplateCache', function($ionicTemplateCache) {
    $ionicTemplateCache();
  }]);
  
  })();
  
  IonicModule
  .factory('$ionicTemplateLoader', [
    '$compile',
    '$controller',
    '$http',
    '$q',
    '$rootScope',
    '$templateCache',
  function($compile, $controller, $http, $q, $rootScope, $templateCache) {
  
    return {
      load: fetchTemplate,
      compile: loadAndCompile
    };
  
    function fetchTemplate(url) {
      return $http.get(url, {cache: $templateCache})
      .then(function(response) {
        return response.data && response.data.trim();
      });
    }
  
    function loadAndCompile(options) {
      options = extend({
        template: '',
        templateUrl: '',
        scope: null,
        controller: null,
        locals: {},
        appendTo: null
      }, options || {});
  
      var templatePromise = options.templateUrl ?
        this.load(options.templateUrl) :
        $q.when(options.template);
  
      return templatePromise.then(function(template) {
        var controller;
        var scope = options.scope || $rootScope.$new();
  
        //Incase template doesn't have just one root element, do this
        var element = jqLite('<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 HISTORY_CURSOR_ATTR = 'history-cursor';
    var VIEW_STATUS_ACTIVE = 'active';
    var VIEW_STATUS_CACHED = 'cached';
    var VIEW_STATUS_STAGED = 'stage';
  
    var transitionCounter = 0;
    var nextTransition, nextDirection;
    ionic.transition = ionic.transition || {};
    ionic.transition.isActive = false;
    var isActiveTimer;
    var cachedAttr = ionic.DomUtil.cachedAttr;
    var transitionPromises = [];
    var 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(onReflow, 16);
  
            } 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));
  
              // emit that the views have finished transitioning
              // each parent nav-view will update which views are active and cached
              switcher.emit('after', enteringData, leavingData);
  
              // 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);
                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 scope = enteringEle.scope();
            if (scope) {
              scope.$emit('$ionicView.' + step + 'Enter', enteringData);
              if (step == 'after') {
                scope.$emit('$ionicView.enter', enteringData);
              }
            }
  
            if (leavingEle) {
              scope = leavingEle.scope();
              if (scope) {
                scope.$emit('$ionicView.' + step + 'Leave', leavingData);
                if (step == 'after') {
                  scope.$emit('$ionicView.leave', leavingData);
                }
              }
  
            } else if (scope && leavingData && leavingData.viewId) {
              scope.$emit('$ionicNavView.' + step + 'Leave', leavingData);
              if (step == 'after') {
                scope.$emit('$ionicNavView.leave', leavingData);
              }
            }
          },
  
          cleanup: function(transData) {
            // check if any views should be removed
            if (leavingEle && transData.direction == 'back' && !$ionicConfig.views.forwardCache()) {
              // if they just navigated back we can destroy the forward view
              // do not remove forward views if cacheForwardViews config is true
              destroyViewEle(leavingEle);
            }
  
            var viewElements = navViewCtrl.getViewElements();
            var viewElementsLength = viewElements.length;
            var x, viewElement;
            var removeOldestAccess = (viewElementsLength - 1) > $ionicConfig.views.maxCache();
            var removableEle;
            var oldestAccess = Date.now();
  
            for (x = 0; x < viewElementsLength; x++) {
              viewElement = viewElements.eq(x);
  
              if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) {
                // remember what was the oldest element to be accessed so it can be destroyed
                oldestAccess = viewElement.data(DATA_VIEW_ACCESSED);
                removableEle = viewElements.eq(x);
  
              } else if (viewElement.data(DATA_DESTROY_ELE) && navViewAttr(viewElement) != VIEW_STATUS_ACTIVE) {
                destroyViewEle(viewElement);
              }
            }
  
            destroyViewEle(removableEle);
  
            if (enteringEle.data(DATA_NO_CACHE)) {
              enteringEle.data(DATA_DESTROY_ELE, true);
            }
          },
  
          enteringEle: function() { return enteringEle; },
          leavingEle: function() { return leavingEle; }
  
        };
  
        return switcher;
      },
  
      transitionEnd: function(navViewCtrls) {
        forEach(navViewCtrls, function(navViewCtrl) {
          navViewCtrl.transitionEnd();
        });
  
        ionicViewSwitcher.isTransitioning(false);
        $ionicClickBlock.hide();
        transitionPromises = [];
      },
  
      nextTransition: function(val) {
        nextTransition = val;
      },
  
      nextDirection: function(val) {
        nextDirection = val;
      },
  
      isTransitioning: function(val) {
        if (arguments.length) {
          ionic.transition.isActive = !!val;
          $timeout.cancel(isActiveTimer);
          if (val) {
            isActiveTimer = $timeout(function() {
              ionicViewSwitcher.isTransitioning(false);
            }, 999);
          }
        }
        return ionic.transition.isActive;
      },
  
      createViewEle: function(viewLocals) {
        var containerEle = $document[0].createElement('div');
        if (viewLocals && viewLocals.$template) {
          containerEle.innerHTML = viewLocals.$template;
          if (containerEle.children.length === 1) {
            containerEle.children[0].classList.add('pane');
            return jqLite(containerEle.children[0]);
          }
        }
        containerEle.className = "pane";
        return jqLite(containerEle);
      },
  
      viewEleIsActive: function(viewEle, isActiveAttr) {
        navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED);
      },
  
      getTransitionData: getTransitionData,
      navViewAttr: navViewAttr,
      destroyViewEle: destroyViewEle
  
    };
  
    return ionicViewSwitcher;
  
  
    function getViewElementIdentifier(locals, view) {
      if (viewState(locals).abstract) return viewState(locals).name;
      if (view) return view.stateId || view.viewId;
      return ionic.Utils.nextUid();
    }
  
    function viewState(locals) {
      return locals && locals.$$state && locals.$$state.self || {};
    }
  
    function getTransitionData(viewLocals, enteringEle, direction, view) {
      // Priority
      // 1) attribute directive on the button/link to this view
      // 2) entering element's attribute
      // 3) entering view's $state config property
      // 4) view registration data
      // 5) global config
      // 6) fallback value
  
      var state = viewState(viewLocals);
      var viewTransition = nextTransition || cachedAttr(enteringEle, 'view-transition') || state.viewTransition || $ionicConfig.views.transition() || 'ios';
      var navBarTransition = $ionicConfig.navBar.transition();
      direction = nextDirection || cachedAttr(enteringEle, 'view-direction') || state.viewDirection || direction || 'none';
  
      return extend(getViewData(view), {
        transition: viewTransition,
        navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition,
        direction: direction,
        shouldAnimate: (viewTransition !== 'none' && direction !== 'none')
      });
    }
  
    function getViewData(view) {
      view = view || {};
      return {
        viewId: view.viewId,
        historyId: view.historyId,
        stateId: view.stateId,
        stateName: view.stateName,
        stateParams: view.stateParams
      };
    }
  
    function navViewAttr(ele, value) {
      if (arguments.length > 1) {
        cachedAttr(ele, NAV_VIEW_ATTR, value);
      } else {
        return cachedAttr(ele, NAV_VIEW_ATTR);
      }
    }
  
    function destroyViewEle(ele) {
      // we found an element that should be removed
      // destroy its scope, then remove the element
      if (ele && ele.length) {
        var viewScope = ele.scope();
        if (viewScope) {
          viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW));
          viewScope.$destroy();
        }
        ele.remove();
      }
    }
  
  }]);
  
  /**
   * @private
   * Parts of Ionic requires that $scope data is attached to the element.
   * We do not want to disable adding $scope data to the $element when
   * $compileProvider.debugInfoEnabled(false) is used.
   */
  IonicModule.config(['$provide', function($provide) {
    $provide.decorator('$compile', ['$delegate', function($compile) {
       $compile.$$addScopeInfo = function $$addScopeInfo($element, scope, isolated, noTemplate) {
         var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope';
         $element.data(dataName, scope);
       };
       return $compile;
    }]);
  }]);
  
  /**
   * @private
   */
  IonicModule.config([
    '$provide',
  function($provide) {
    function $LocationDecorator($location, $timeout) {
  
      $location.__hash = $location.hash;
      //Fix: when window.location.hash is set, the scrollable area
      //found nearest to body's scrollTop is set to scroll to an element
      //with that ID.
      $location.hash = function(value) {
        if (isDefined(value)) {
          $timeout(function() {
            var scroll = document.querySelector('.scroll-content');
            if (scroll)
              scroll.scrollTop = 0;
          }, 0, false);
        }
        return $location.__hash(value);
      };
  
      return $location;
    }
  
    $provide.decorator('$location', ['$delegate', '$timeout', $LocationDecorator]);
  }]);
  
  IonicModule
  
  .controller('$ionicHeaderBar', [
    '$scope',
    '$element',
    '$attrs',
    '$q',
    '$ionicConfig',
    '$ionicHistory',
  function($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) {
    var TITLE = 'title';
    var BACK_TEXT = 'back-text';
    var BACK_BUTTON = 'back-button';
    var DEFAULT_TITLE = 'default-title';
    var PREVIOUS_TITLE = 'previous-title';
    var HIDE = 'hide';
  
    var self = this;
    var titleText = '';
    var previousTitleText = '';
    var titleLeft = 0;
    var titleRight = 0;
    var titleCss = '';
    var isBackEnabled = false;
    var isBackShown = true;
    var isNavBackShown = true;
    var isBackElementShown = false;
    var titleTextWidth = 0;
  
  
    self.beforeEnter = function(viewData) {
      $scope.$broadcast('$ionicView.beforeEnter', viewData);
    };
  
  
    self.title = function(newTitleText) {
      if (arguments.length && newTitleText !== titleText) {
        getEle(TITLE).innerHTML = newTitleText;
        titleText = newTitleText;
        titleTextWidth = 0;
      }
      return titleText;
    };
  
  
    self.enableBack = function(shouldEnable, disableReset) {
      // whether or not the back button show be visible, according
      // to the navigation and history
      if (arguments.length) {
        isBackEnabled = shouldEnable;
        if (!disableReset) self.updateBackButton();
      }
      return isBackEnabled;
    };
  
  
    self.showBack = function(shouldShow, disableReset) {
      // different from enableBack() because this will always have the back
      // visually hidden if false, even if the history says it should show
      if (arguments.length) {
        isBackShown = shouldShow;
        if (!disableReset) self.updateBackButton();
      }
      return isBackShown;
    };
  
  
    self.showNavBack = function(shouldShow) {
      // different from showBack() because this is for the entire nav bar's
      // setting for all of it's child headers. For internal use.
      isNavBackShown = shouldShow;
      self.updateBackButton();
    };
  
  
    self.updateBackButton = function() {
      if ((isBackShown && isNavBackShown && isBackEnabled) !== isBackElementShown) {
        isBackElementShown = isBackShown && isNavBackShown && isBackEnabled;
        var backBtnEle = getEle(BACK_BUTTON);
        backBtnEle && backBtnEle.classList[ isBackElementShown ? 'remove' : 'add' ](HIDE);
      }
    };
  
  
    self.titleTextWidth = function() {
      if (!titleTextWidth) {
        var bounds = ionic.DomUtil.getTextBounds(getEle(TITLE));
        titleTextWidth = Math.min(bounds && bounds.width || 30);
      }
      return titleTextWidth;
    };
  
  
    self.titleWidth = function() {
      var titleWidth = self.titleTextWidth();
      var offsetWidth = getEle(TITLE).offsetWidth;
      if (offsetWidth < titleWidth) {
        titleWidth = offsetWidth + (titleLeft - titleRight - 5);
      }
      return titleWidth;
    };
  
  
    self.titleTextX = function() {
      return ($element[0].offsetWidth / 2) - (self.titleWidth() / 2);
    };
  
  
    self.titleLeftRight = function() {
      return titleLeft - titleRight;
    };
  
  
    self.backButtonTextLeft = function() {
      var offsetLeft = 0;
      var ele = getEle(BACK_TEXT);
      while (ele) {
        offsetLeft += ele.offsetLeft;
        ele = ele.parentElement;
      }
      return offsetLeft;
    };
  
  
    self.resetBackButton = function(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();
        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: computedStyle.overflowX === 'scroll' ||
        computedStyle.overflowX === 'auto' ||
        self.scrollEl.style['overflow-x'] === 'scroll' ?
          calculateMaxValue(maxValues.left) : -1,
        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) {
      distance = ($attrs.distance || '2.5%').trim();
      isPercent = distance.indexOf('%') !== -1;
      return isPercent ?
      maximum * (1 - parseFloat(distance) / 100) :
      maximum - parseFloat(distance);
    }
  
  }]);
  
  
  /**
   * @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
   * <ion-content ng-controller="MyCtrl">
   *   <button class="button" ng-click="showDeleteButtons()"></button>
   *   <ion-list>
   *     <ion-item ng-repeat="i in items">
   *       {% raw %}Hello, {{i}}!{% endraw %}
   *       <ion-delete-button class="ion-minus-circled"></ion-delete-button>
   *     </ion-item>
   *   </ion-list>
   * </ion-content>
   * ```
   * ```js
   * function MyCtrl($scope, $ionicListDelegate) {
   *   $scope.showDeleteButtons = function() {
   *     $ionicListDelegate.showDelete(true);
   *   };
   * }
   * ```
   */
  IonicModule
  .service('$ionicListDelegate', ionic.DelegateService([
    /**
     * @ngdoc method
     * @name $ionicListDelegate#showReorder
     * @param {boolean=} showReorder Set whether or not this list is showing its reorder buttons.
     * @returns {boolean} Whether the reorder buttons are shown.
     */
    'showReorder',
    /**
     * @ngdoc method
     * @name $ionicListDelegate#showDelete
     * @param {boolean=} showDelete Set whether or not this list is showing its delete buttons.
     * @returns {boolean} Whether the delete buttons are shown.
     */
    'showDelete',
    /**
     * @ngdoc method
     * @name $ionicListDelegate#canSwipeItems
     * @param {boolean=} canSwipeItems Set whether or not this list is able to swipe to show
     * option buttons.
     * @returns {boolean} Whether the list is able to swipe to show option buttons.
     */
    'canSwipeItems',
    /**
     * @ngdoc method
     * @name $ionicListDelegate#closeOptionButtons
     * @description Closes any option buttons on the list that are swiped open.
     */
    'closeOptionButtons',
    /**
     * @ngdoc method
     * @name $ionicListDelegate#$getByHandle
     * @param {string} handle
     * @returns `delegateInstance` A delegate instance that controls only the
     * {@link ionic.directive:ionList} directives with `delegate-handle` matching
     * the given handle.
     *
     * Example: `$ionicListDelegate.$getByHandle('my-handle').showReorder(true);`
     */
  ]))
  
  .controller('$ionicList', [
    '$scope',
    '$attrs',
    '$ionicListDelegate',
    '$ionicHistory',
  function($scope, $attrs, $ionicListDelegate, $ionicHistory) {
    var self = this;
    var isSwipeable = true;
    var isReorderShown = false;
    var isDeleteShown = false;
  
    var deregisterInstance = $ionicListDelegate._registerInstance(
      self, $attrs.delegateHandle, function() {
        return $ionicHistory.isActiveScope($scope);
      }
    );
    $scope.$on('$destroy', deregisterInstance);
  
    self.showReorder = function(show) {
      if (arguments.length) {
        isReorderShown = !!show;
      }
      return isReorderShown;
    };
  
    self.showDelete = function(show) {
      if (arguments.length) {
        isDeleteShown = !!show;
      }
      return isDeleteShown;
    };
  
    self.canSwipeItems = function(can) {
      if (arguments.length) {
        isSwipeable = !!can;
      }
      return isSwipeable;
    };
  
    self.closeOptionButtons = function() {
      self.listView && self.listView.clearDragEffects();
    };
  }]);
  
  IonicModule
  
  .controller('$ionicNavBar', [
    '$scope',
    '$element',
    '$attrs',
    '$compile',
    '$timeout',
    '$ionicNavBarDelegate',
    '$ionicConfig',
    '$ionicHistory',
  function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $ionicConfig, $ionicHistory) {
  
    var CSS_HIDE = 'hide';
    var DATA_NAV_BAR_CTRL = '$ionNavBarController';
    var PRIMARY_BUTTONS = 'primaryButtons';
    var SECONDARY_BUTTONS = 'secondaryButtons';
    var BACK_BUTTON = 'backButton';
    var ITEM_TYPES = 'primaryButtons secondaryButtons leftButtons rightButtons title'.split(' ');
  
    var self = this;
    var headerBars = [];
    var navElementHtml = {};
    var isVisible = true;
    var queuedTransitionStart, queuedTransitionEnd, latestTransitionId;
  
    $element.parent().data(DATA_NAV_BAR_CTRL, self);
  
    var delegateHandle = $attrs.delegateHandle || 'navBar' + ionic.Utils.nextUid();
  
    var deregisterInstance = $ionicNavBarDelegate._registerInstance(self, delegateHandle);
  
  
    self.init = function() {
      $element.addClass('nav-bar-container');
      ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', $ionicConfig.views.transition());
  
      // create two nav bar blocks which will trade out which one is shown
      self.createHeaderBar(false);
      self.createHeaderBar(true);
  
      $scope.$emit('ionNavBar.init', delegateHandle);
    };
  
  
    self.createHeaderBar = function(isActive, navBarClass) {
      var containerEle = jqLite('<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] = self.createBackButtonElement(headerBarEle);
      navEle[BACK_BUTTON] = createNavElement(BACK_BUTTON);
      navEle[BACK_BUTTON] && headerBarEle.append(navEle[BACK_BUTTON]);
  
      // append title in the header, this is the rock to where buttons append
      headerBarEle.append(titleEle);
  
      forEach(ITEM_TYPES, function(itemType) {
        // create default button elements
        navEle[itemType] = createNavElement(itemType);
        // append and position buttons
        positionItem(navEle[itemType], itemType);
      });
  
      // add header-item to the root children
      for (var x = 0; x < headerBarEle[0].children.length; x++) {
        headerBarEle[0].children[x].classList.add('header-item');
      }
  
      // compile header and append to the DOM
      containerEle.append(headerBarEle);
      $element.append($compile(containerEle)($scope.$new()));
  
      var headerBarCtrl = headerBarEle.data('$ionHeaderBarController');
  
      var headerBarInstance = {
        isActive: isActive,
        title: function(newTitleText) {
          headerBarCtrl.title(newTitleText);
        },
        setItem: function(navBarItemEle, itemType) {
          // first make sure any exiting nav bar item has been removed
          headerBarInstance.removeItem(itemType);
  
          if (navBarItemEle) {
            if (itemType === 'title') {
              // clear out the text based title
              headerBarInstance.title("");
            }
  
            // there's a custom nav bar item
            positionItem(navBarItemEle, itemType);
  
            if (navEle[itemType]) {
              // make sure the default on this itemType is hidden
              navEle[itemType].addClass(CSS_HIDE);
            }
            lastViewItemEle[itemType] = navBarItemEle;
  
          } else if (navEle[itemType]) {
            // there's a default button for this side and no view button
            navEle[itemType].removeClass(CSS_HIDE);
          }
        },
        removeItem: function(itemType) {
          if (lastViewItemEle[itemType]) {
            lastViewItemEle[itemType].scope().$destroy();
            lastViewItemEle[itemType].remove();
            lastViewItemEle[itemType] = null;
          }
        },
        containerEle: function() {
          return containerEle;
        },
        headerBarEle: function() {
          return headerBarEle;
        },
        afterLeave: function() {
          forEach(ITEM_TYPES, function(itemType) {
            headerBarInstance.removeItem(itemType);
          });
          headerBarCtrl.resetBackButton();
        },
        controller: function() {
          return headerBarCtrl;
        },
        destroy: function() {
          forEach(ITEM_TYPES, function(itemType) {
            headerBarInstance.removeItem(itemType);
          });
          containerEle.scope().$destroy();
          for (var n in navEle) {
            if (navEle[n]) {
              navEle[n].removeData();
              navEle[n] = null;
            }
          }
          leftButtonsEle && leftButtonsEle.removeData();
          rightButtonsEle && rightButtonsEle.removeData();
          titleEle.removeData();
          headerBarEle.removeData();
          containerEle.remove();
          containerEle = headerBarEle = titleEle = leftButtonsEle = rightButtonsEle = null;
        }
      };
  
      function positionItem(ele, itemType) {
        if (!ele) return;
  
        if (itemType === 'title') {
          // title element
          titleEle.append(ele);
  
        } else if (itemType == 'rightButtons' ||
                  (itemType == SECONDARY_BUTTONS && $ionicConfig.navBar.positionSecondaryButtons() != 'left') ||
                  (itemType == PRIMARY_BUTTONS && $ionicConfig.navBar.positionPrimaryButtons() == 'right')) {
          // right side
          if (!rightButtonsEle) {
            rightButtonsEle = jqLite('<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) {
          navSwipeAttr(speed);
          navBarAttr(leavingHeaderBar, 'active');
          navBarAttr(enteringHeaderBar, 'cached');
          navBarTransition.shouldAnimate = shouldAnimate;
          navBarTransition.run(0);
          self.activeTransition = navBarTransition = null;
        },
        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);
      } else if (!shouldShow && isVisible) {
        $element.addClass(CSS_HIDE);
      }
      isVisible = shouldShow;
    };
  
  
    self.enable = function(val) {
      // set primary to show first
      self.visibleBar(val);
  
      // set non primary to hide second
      for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) {
        if ($ionicNavBarDelegate._instances[x] !== self) $ionicNavBarDelegate._instances[x].visibleBar(false);
      }
    };
  
  
    /**
     * @ngdoc method
     * @name $ionicNavBar#showBackButton
     * @description Show/hide the nav bar back button when there is a
     * back view. If the back button is not possible, for example, the
     * first view in the stack, then this will not force the back button
     * to show.
     */
    self.showBackButton = function(shouldShow) {
      for (var x = 0; x < headerBars.length; x++) {
        headerBars[x].controller().showNavBack(!!shouldShow);
      }
      $scope.$isBackButtonShown = !!shouldShow;
      return $scope.$isBackButtonShown;
    };
  
  
    /**
     * @ngdoc method
     * @name $ionicNavBar#showActiveBackButton
     * @description Show/hide only the active header bar's back button.
     */
    self.showActiveBackButton = function(shouldShow) {
      var headerBar = getOnScreenHeaderBar();
      headerBar && headerBar.controller().showBack(shouldShow);
    };
  
  
    self.title = function(newTitleText, headerBar) {
      if (isDefined(newTitleText)) {
        newTitleText = newTitleText || '';
        headerBar = headerBar || getOnScreenHeaderBar();
        headerBar && headerBar.title(newTitleText);
        $scope.$title = newTitleText;
        $ionicHistory.currentTitle(newTitleText);
      }
      return $scope.$title;
    };
  
  
    self.align = function(val, headerBar) {
      headerBar = headerBar || getOnScreenHeaderBar();
      headerBar && headerBar.controller().align(val);
    };
  
  
    self.hasTabsTop = function(isTabsTop) {
      $element[isTabsTop ? 'addClass' : 'removeClass']('nav-bar-tabs-top');
    };
  
  
    // 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',
  function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher, $ionicConfig, $ionicScrollDelegate) {
  
    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;
    var transitionDuration, transitionTiming;
  
    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);
  
      ionic.Platform.ready(function() {
        if (ionic.Platform.isWebView() && $ionicConfig.views.swipeBackEnabled()) {
          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);
      }
    };
  
  
    self.cacheCleanup = function() {
      var viewElements = $element.children();
      for (var x = 0, l = viewElements.length; x < l; x++) {
        if (viewElements.eq(x).data(DATA_DESTROY_ELE)) {
          $ionicViewSwitcher.destroyViewEle(viewElements.eq(x));
        }
      }
    };
  
  
    self.clearCache = function() {
      var viewElements = $element.children();
      var viewElement, viewScope;
  
      for (var x = 0, l = viewElements.length; x < l; x++) {
        viewElement = viewElements.eq(x);
        if (navViewAttr(viewElement) == VIEW_STATUS_CACHED) {
          $ionicViewSwitcher.destroyViewEle(viewElement);
        } else if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) {
          viewScope = viewElement.scope();
          viewScope && viewScope.$broadcast('$ionicView.clearCache');
        }
      }
  
    };
  
  
    self.getViewElements = function() {
      return $element.children();
    };
  
  
    self.appendViewElement = function(viewEle, viewLocals) {
      // compile the entering element and get the link function
      var linkFn = $compile(viewEle);
  
      $element.append(viewEle);
  
      var viewScope = $scope.$new();
  
      if (viewLocals && viewLocals.$$controller) {
        viewLocals.$scope = viewScope;
        var controller = $controller(viewLocals.$$controller, viewLocals);
        $element.children().data('$ngControllerController', controller);
      }
  
      linkFn(viewScope);
  
      return viewScope;
    };
  
  
    self.title = function(val) {
      var associatedNavBarCtrl = getAssociatedNavBarCtrl();
      associatedNavBarCtrl && associatedNavBarCtrl.title(val);
    };
  
  
    /**
     * @ngdoc method
     * @name $ionicNavView#enableBackButton
     * @description Enable/disable if the back button can be shown or not. For
     * example, the very first view in the navigation stack would not have a
     * back view, so the back button would be disabled.
     */
    self.enableBackButton = function(shouldEnable) {
      var associatedNavBarCtrl = getAssociatedNavBarCtrl();
      associatedNavBarCtrl && associatedNavBarCtrl.enableBackButton(shouldEnable);
    };
  
  
    /**
     * @ngdoc method
     * @name $ionicNavView#showBackButton
     * @description Show/hide the nav bar active back button. If the back button
     * is not possible this will not force the back button to show. The
     * `enableBackButton()` method handles if a back button is even possible or not.
     */
    self.showBackButton = function(shouldShow) {
      var associatedNavBarCtrl = getAssociatedNavBarCtrl();
      associatedNavBarCtrl && associatedNavBarCtrl.showActiveBackButton(shouldShow);
    };
  
  
    self.showBar = function(val) {
      var associatedNavBarCtrl = getAssociatedNavBarCtrl();
      associatedNavBarCtrl && associatedNavBarCtrl.showBar(val);
    };
  
  
    self.isPrimary = function(val) {
      if (arguments.length) {
        isPrimary = val;
      }
      return isPrimary;
    };
  
  
    self.direction = function(val) {
      if (arguments.length) {
        direction = val;
      }
      return direction;
    };
  
  
    self.initSwipeBack = function() {
      var swipeBackHitWidth = $ionicConfig.views.swipeBackHitWidth();
      var viewTransition, associatedNavBarCtrl, backView;
      var deregDragStart, deregDrag, deregRelease;
      var windowWidth, startDragX, dragPoints;
  
      function onDragStart(ev) {
        if (!isPrimary) return;
  
        startDragX = getDragX(ev);
        if (startDragX > swipeBackHitWidth) return;
  
        backView = $ionicHistory.backView();
  
        if (!backView || backView.historyId !== $ionicHistory.currentView().historyId) return;
  
        if (!windowWidth) windowWidth = window.innerWidth;
  
        self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(true);
  
        var registerData = {
          direction: 'back'
        };
  
        dragPoints = [];
  
        var switcher = $ionicViewSwitcher.create(self, registerData, backView, $ionicHistory.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)) {
            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 {
            navSwipeAttr(disableAnimation ? '' : 'fast');
            disableRenderStartViewId = null;
            viewTransition.cancel(!disableAnimation);
            associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.cancel(!disableAnimation, 'fast');
            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 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 handleTouchend() {
        // if this wasn't an overscroll, get out immediately
        if (!canOverscroll && !isDragging) {
          return;
        }
        // reset Y
        startY = null;
        // the user has overscrolled but went back to native scrolling
        if (!isDragging) {
          dragOffset = 0;
          isOverscrolling = false;
          setScrollLock(false);
          return true;
        }
        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;
        }
        return true;
      }
  
      function handleTouchmove(e) {
        // 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 = parseInt(e.touches[0].screenY, 10);
        }
  
        // how far have we dragged so far?
        deltaY = parseInt(e.touches[0].screenY, 10) - startY;
  
        // 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,parseInt(deltaY - dragOffset, 10) * -1);
          }
  
          // if we're not at overscroll 0 yet, 0 out
          if (lastOverscroll !== 0) {
            overscroll(0);
          }
  
          return true;
  
        } 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(parseInt((deltaY - dragOffset) / 3, 10));
  
        // 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] = 'translateY(' + val + 'px)';
        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(parseInt((easedT * (Y - from)) + from, 10));
  
          if (time < 1) {
            ionic.requestAnimationFrame(scroll);
  
          } else {
  
            if (Y < 5 && Y > -5) {
              isOverscrolling = false;
              setScrollLock(false);
            }
  
            callback && callback();
          }
        }
  
        // start scroll loop
        ionic.requestAnimationFrame(scroll);
      }
  
  
      self.init = function() {
        scrollParent = $element.parent().parent()[0];
        scrollChild = $element.parent()[0];
  
        if (!scrollParent.classList.contains('ionic-scroll') ||
            !scrollChild.classList.contains('scroll')) {
          throw new Error('Refresher must be immediate child of ion-content or ion-scroll');
        }
  
        ionic.on('touchmove', handleTouchmove, scrollChild);
        ionic.on('touchend', handleTouchend, scrollChild);
        ionic.on('scroll', handleScroll, scrollParent);
  
        // cleanup when done
        $scope.$on('$destroy', destroy);
      };
  
      function destroy() {
        ionic.off('touchmove', handleTouchmove, scrollChild);
        ionic.off('touchend', handleTouchend, scrollChild);
        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');
        $scope.$onRefresh();
      }
  
      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
  
    var element = self.element = scrollViewOptions.el;
    var $element = self.$element = jqLite(element);
    var scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions);
  
    //Attach self to element as a controller so other directives can require this controller
    //through `require: '$ionicScroll'
    //Also attach to parent so that sibling elements can require this
    ($element.parent().length ? $element.parent() : $element)
      .data('$$ionicScrollController', self);
  
    var deregisterInstance = $ionicScrollDelegate._registerInstance(
      self, scrollViewOptions.delegateHandle, function() {
        return $ionicHistory.isActiveScope($scope);
      }
    );
  
    if (!isDefined(scrollViewOptions.bouncing)) {
      ionic.Platform.ready(function() {
        if (scrollView.options) {
          scrollView.options.bouncing = true;
          if (ionic.Platform.isAndroid()) {
            // No bouncing by default on Android
            scrollView.options.bouncing = false;
            // Faster scroll decel
            scrollView.options.deceleration = 0.95;
          }
        }
      });
    }
  
    var resize = angular.bind(scrollView, scrollView.resize);
    ionic.on('resize', resize, $window);
  
  
    var scrollFunc = function(e) {
      var detail = (e.originalEvent || e).detail || {};
      $scope.$onScroll && $scope.$onScroll({
        event: e,
        scrollTop: detail.scrollTop || 0,
        scrollLeft: detail.scrollLeft || 0
      });
    };
  
    $element.on('scroll', scrollFunc);
  
    $scope.$on('$destroy', function() {
      deregisterInstance();
      scrollView && scrollView.__cleanup && scrollView.__cleanup();
      ionic.off('resize', resize, $window);
      $window.removeEventListener('resize', resize);
      $element.off('scroll', scrollFunc);
      scrollView = self.scrollView = scrollViewOptions = self._scrollViewOptions = scrollViewOptions.el = self._scrollViewOptions.el = $element = self.$element = element = null;
    });
  
    $timeout(function() {
      scrollView && scrollView.run && scrollView.run();
      $element.triggerHandler('scroll.init');
    });
  
    self.getScrollView = function() {
      return scrollView;
    };
  
    self.getScrollPosition = function() {
      return scrollView.getValues();
    };
  
    self.resize = function() {
      return $timeout(resize).then(function() {
        $element && $element.triggerHandler('scroll.resize');
      });
    };
  
    self.scrollTop = function(shouldAnimate) {
      self.resize().then(function() {
        scrollView.scrollTo(0, 0, !!shouldAnimate);
      });
    };
  
    self.scrollBottom = function(shouldAnimate) {
      self.resize().then(function() {
        var max = scrollView.getScrollMax();
        scrollView.scrollTo(max.left, max.top, !!shouldAnimate);
      });
    };
  
    self.scrollTo = function(left, top, shouldAnimate) {
      self.resize().then(function() {
        scrollView.scrollTo(left, top, !!shouldAnimate);
      });
    };
  
    self.zoomTo = function(zoom, shouldAnimate, originLeft, originTop) {
      self.resize().then(function() {
        scrollView.zoomTo(zoom, !!shouldAnimate, originLeft, originTop);
      });
    };
  
    self.zoomBy = function(zoom, shouldAnimate, originLeft, originTop) {
      self.resize().then(function() {
        scrollView.zoomBy(zoom, !!shouldAnimate, originLeft, originTop);
      });
    };
  
    self.scrollBy = function(left, top, shouldAnimate) {
      self.resize().then(function() {
        scrollView.scrollBy(left, top, !!shouldAnimate);
      });
    };
  
    self.anchorScroll = function(shouldAnimate) {
      self.resize().then(function() {
        var hash = $location.hash();
        var elm = hash && $document[0].getElementById(hash);
        if (!(hash && elm)) {
          scrollView.scrollTo(0,0, !!shouldAnimate);
          return;
        }
        var curElm = elm;
        var scrollLeft = 0, scrollTop = 0, levelsClimbed = 0;
        do {
          if (curElm !== null) scrollLeft += curElm.offsetLeft;
          if (curElm !== null) scrollTop += curElm.offsetTop;
          curElm = curElm.offsetParent;
          levelsClimbed++;
        } while (curElm.attributes != self.element.attributes && curElm.offsetParent);
        scrollView.scrollTo(scrollLeft, scrollTop, !!shouldAnimate);
      });
    };
  
    self.freezeScroll = scrollView.freeze;
  
    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',
  function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $ionicBody, $ionicHistory, $ionicScrollDelegate) {
    var self = this;
    var rightShowing, leftShowing, isDragging;
    var startX, lastX, offsetX, isAsideExposed;
    var enableMenuWithBackViews = true;
  
    self.$scope = $scope;
  
    self.initialize = function(options) {
      self.left = options.left;
      self.right = options.right;
      self.setContent(options.content);
      self.dragThresholdX = options.dragThresholdX || 10;
      $ionicHistory.registerHistory(self.$scope);
    };
  
    /**
     * Set the content view controller if not passed in the constructor options.
     *
     * @param {object} content
     */
    self.setContent = function(content) {
      if (content) {
        self.content = content;
  
        self.content.onDrag = function(e) {
          self._handleDrag(e);
        };
  
        self.content.endDrag = function(e) {
          self._endDrag(e);
        };
      }
    };
  
    self.isOpenLeft = function() {
      return self.getOpenAmount() > 0;
    };
  
    self.isOpenRight = function() {
      return self.getOpenAmount() < 0;
    };
  
    /**
     * Toggle the left menu to open 100%
     */
    self.toggleLeft = function(shouldOpen) {
      if (isAsideExposed || !self.left.isEnabled) return;
      var openAmount = self.getOpenAmount();
      if (arguments.length === 0) {
        shouldOpen = openAmount <= 0;
      }
      self.content.enableAnimation();
      if (!shouldOpen) {
        self.openPercentage(0);
      } else {
        self.openPercentage(100);
      }
    };
  
    /**
     * Toggle the right menu to open 100%
     */
    self.toggleRight = function(shouldOpen) {
      if (isAsideExposed || !self.right.isEnabled) return;
      var openAmount = self.getOpenAmount();
      if (arguments.length === 0) {
        shouldOpen = openAmount >= 0;
      }
      self.content.enableAnimation();
      if (!shouldOpen) {
        self.openPercentage(0);
      } else {
        self.openPercentage(-100);
      }
    };
  
    self.toggle = function(side) {
      if (side == 'right') {
        self.toggleRight();
      } else {
        self.toggleLeft();
      }
    };
  
    /**
     * Close all menus.
     */
    self.close = function() {
      self.openPercentage(0);
    };
  
    /**
     * @return {float} The amount the side menu is open, either positive or negative for left (positive), or right (negative)
     */
    self.getOpenAmount = function() {
      return self.content && self.content.getTranslateX() || 0;
    };
  
    /**
     * @return {float} The ratio of open amount over menu width. For example, a
     * menu of width 100 open 50 pixels would be open 50% or a ratio of 0.5. Value is negative
     * for right menu.
     */
    self.getOpenRatio = function() {
      var amount = self.getOpenAmount();
      if (amount >= 0) {
        return amount / self.left.width;
      }
      return amount / self.right.width;
    };
  
    self.isOpen = function() {
      return self.getOpenAmount() !== 0;
    };
  
    /**
     * @return {float} The percentage of open amount over menu width. For example, a
     * menu of width 100 open 50 pixels would be open 50%. Value is negative
     * for right menu.
     */
    self.getOpenPercentage = function() {
      return self.getOpenRatio() * 100;
    };
  
    /**
     * Open the menu with a given percentage amount.
     * @param {float} percentage The percentage (positive or negative for left/right) to open the menu.
     */
    self.openPercentage = function(percentage) {
      var p = percentage / 100;
  
      if (self.left && percentage >= 0) {
        self.openAmount(self.left.width * p);
      } else if (self.right && percentage < 0) {
        var maxRight = self.right.width;
        self.openAmount(self.right.width * p);
      }
  
      // add the CSS class "menu-open" if the percentage does not
      // equal 0, otherwise remove the class from the body element
      $ionicBody.enableClass((percentage !== 0), 'menu-open');
  
      freezeAllScrolls(false);
    };
  
    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);
  
      if (amount >= 0) {
        leftShowing = true;
        rightShowing = false;
  
        if (amount > 0) {
          // Push the z-index of the right menu down
          self.right && self.right.pushDown && self.right.pushDown();
          // Bring the z-index of the left menu up
          self.left && self.left.bringUp && self.left.bringUp();
        }
      } else {
        rightShowing = true;
        leftShowing = false;
  
        // Bring the z-index of the right menu up
        self.right && self.right.bringUp && self.right.bringUp();
        // Push the z-index of the left menu down
        self.left && self.left.pushDown && self.left.pushDown();
      }
    };
  
    /**
     * Given an event object, find the final resting position of this side
     * menu. For example, if the user "throws" the content to the right and
     * releases the touch, the left menu should snap open (animated, of course).
     *
     * @param {Event} e the gesture event to use for snapping
     */
    self.snapToRest = function(e) {
      // We want to animate at the end of this
      self.content.enableAnimation();
      isDragging = false;
  
      // Check how much the panel is open after the drag, and
      // what the drag velocity is
      var ratio = self.getOpenRatio();
  
      if (ratio === 0) {
        // Just to be safe
        self.openPercentage(0);
        return;
      }
  
      var velocityThreshold = 0.3;
      var velocityX = e.gesture.velocityX;
      var direction = e.gesture.direction;
  
      // Going right, less than half, too slow (snap back)
      if (ratio > 0 && ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
        self.openPercentage(0);
      }
  
      // Going left, more than half, too slow (snap back)
      else if (ratio > 0.5 && direction == 'left' && velocityX < velocityThreshold) {
        self.openPercentage(100);
      }
  
      // Going left, less than half, too slow (snap back)
      else if (ratio < 0 && ratio > -0.5 && direction == 'left' && velocityX < velocityThreshold) {
        self.openPercentage(0);
      }
  
      // Going right, more than half, too slow (snap back)
      else if (ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
        self.openPercentage(-100);
      }
  
      // Going right, more than half, or quickly (snap open)
      else if (direction == 'right' && ratio >= 0 && (ratio >= 0.5 || velocityX > velocityThreshold)) {
        self.openPercentage(100);
      }
  
      // Going left, more than half, or quickly (span open)
      else if (direction == 'left' && ratio <= 0 && (ratio <= -0.5 || velocityX > velocityThreshold)) {
        self.openPercentage(-100);
      }
  
      // Snap back for safety
      else {
        self.openPercentage(0);
      }
    };
  
    self.enableMenuWithBackViews = function(val) {
      if (arguments.length) {
        enableMenuWithBackViews = !!val;
      }
      return enableMenuWithBackViews;
    };
  
    self.isAsideExposed = function() {
      return !!isAsideExposed;
    };
  
    self.exposeAside = function(shouldExposeAside) {
      if (!(self.left && self.left.isEnabled) && !(self.right && self.right.isEnabled)) return;
      self.close();
      //Trigger a resize if it changed
      if (isAsideExposed !== shouldExposeAside) {
        ionic.trigger('resize', null, window);
      }
      isAsideExposed = shouldExposeAside;
      if (self.left && self.left.isEnabled) {
        // set the left marget width if it should be exposed
        // otherwise set false so there's no left margin
        self.content.setMarginLeft(isAsideExposed ? self.left.width : 0);
      } else if (self.right && self.right.isEnabled) {
        self.content.setMarginRight(isAsideExposed ? self.right.width : 0);
      }
  
      self.$scope.$emit('$ionicExposeAside', isAsideExposed);
    };
  
    self.activeAsideResizing = function(isResizing) {
      $ionicBody.enableClass(isResizing, 'aside-resizing');
    };
  
    // End a drag with the given event
    self._endDrag = function(e) {
      freezeAllScrolls(false);
  
      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));
        freezeAllScrolls(true);
      }
    };
  
    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 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,
          PLATFORM_BACK_BUTTON_PRIORITY_SIDE_MENU
        );
      }
    });
  
    var deregisterInstance = $ionicSideMenuDelegate._registerInstance(
      self, $attrs.delegateHandle, function() {
        return $ionicHistory.isActiveScope($scope);
      }
    );
  
    $scope.$on('$destroy', function() {
      deregisterInstance();
      deregisterBackButtonAction();
      self.$scope = null;
      if (self.content) {
        self.content.element = null;
        self.content = null;
      }
  
      // ensure scrolls are unfrozen
      freezeAllScrolls(false);
    });
  
    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) {
        var rIndex = 0;
        var rotateCircle = 0;
        var startTime;
        var svgEle = ele.querySelector('g');
        var circleEle = ele.querySelector('circle');
  
        function run() {
          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();
        };
  
      }
  
    };
  
    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',
    function($element, $attrs) {
      var spinnerName, spinner;
  
      this.init = function() {
        spinnerName = $attrs.icon || ionic.Platform.platform();
        spinner = spinners[spinnerName];
        if (!spinner) {
          spinnerName = 'ios';
          spinner = spinners.ios;
        }
  
        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] && animations[spinnerName]($element[0])();
      };
  
    }]);
  
  })(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 selectedTabIndex;
    self.tabs = [];
  
    self.selectedIndex = function() {
      return self.tabs.indexOf(selectedTab);
    };
    self.selectedTab = function() {
      return selectedTab;
    };
  
    self.add = function(tab) {
      $ionicHistory.registerHistory(tab);
      self.tabs.push(tab);
    };
  
    self.remove = function(tab) {
      var tabIndex = self.tabs.indexOf(tab);
      if (tabIndex === -1) {
        return;
      }
      //Use a field like '$tabSelected' so developers won't accidentally set it in controllers etc
      if (tab.$tabSelected) {
        self.deselect(tab);
        //Try to select a new tab if we're removing a tab
        if (self.tabs.length === 1) {
          //do nothing if there are no other tabs to select
        } else {
          //Select previous tab if it's the last tab, else select next tab
          var newTabIndex = tabIndex === self.tabs.length - 1 ? tabIndex - 1 : tabIndex + 1;
          self.select(self.tabs[newTabIndex]);
        }
      }
      self.tabs.splice(tabIndex, 1);
    };
  
    self.deselect = function(tab) {
      if (tab.$tabSelected) {
        selectedTab = selectedTabIndex = null;
        tab.$tabSelected = false;
        (tab.onDeselect || 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
          });
        }
      }
    };
  
    self.hasActiveScope = function() {
      for (var x = 0; x < self.tabs.length; x++) {
        if ($ionicHistory.isActiveScope(self.tabs[x])) {
          return true;
        }
      }
      return false;
    };
  
  }]);
  
  IonicModule
  .controller('$ionicView', [
    '$scope',
    '$element',
    '$attrs',
    '$compile',
    '$rootScope',
    '$ionicViewSwitcher',
  function($scope, $element, $attrs, $compile, $rootScope, $ionicViewSwitcher) {
    var self = this;
    var navElementHtml = {};
    var navViewCtrl;
    var navBarDelegateHandle;
    var hasViewHeaderBar;
    var deregisters = [];
    var viewTitle;
  
    var deregIonNavBarInit = $scope.$on('ionNavBar.init', function(ev, delegateHandle) {
      // this view has its own ion-nav-bar, remember the navBarDelegateHandle for this view
      ev.stopPropagation();
      navBarDelegateHandle = delegateHandle;
    });
  
  
    self.init = function() {
      deregIonNavBarInit();
  
      var modalCtrl = $element.inheritedData('$ionModalController');
      navViewCtrl = $element.inheritedData('$ionNavViewController');
  
      // don't bother if inside a modal or there's no parent navView
      if (!navViewCtrl || modalCtrl) return;
  
      // add listeners for when this view changes
      $scope.$on('$ionicView.beforeEnter', self.beforeEnter);
      $scope.$on('$ionicView.afterEnter', afterEnter);
      $scope.$on('$ionicView.beforeLeave', deregisterFns);
    };
  
    self.beforeEnter = function(ev, transData) {
      // this event was emitted, starting at intial ion-view, then bubbles up
      // only the first ion-view should do something with it, parent ion-views should ignore
      if (transData && !transData.viewNotified) {
        transData.viewNotified = true;
  
        if (!$rootScope.$$phase) $scope.$digest();
        viewTitle = isDefined($attrs.viewTitle) ? $attrs.viewTitle : $attrs.title;
  
        var navBarItems = {};
        for (var n in navElementHtml) {
          navBarItems[n] = generateNavBarItem(navElementHtml[n]);
        }
  
        navViewCtrl.beforeEnter(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-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
        }, 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/driftyco/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="{{photo.src}}">
   *     </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 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;
  
      //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 && !widthExpr) {
        heightData.computed = widthData.computed = true;
      } else {
        if (heightExpr) {
          parseDimensionAttr(heightExpr, heightData);
        } else {
          heightData.computed = true;
        }
        if (!widthExpr) widthExpr = '"100%"';
        parseDimensionAttr(widthExpr, widthData);
      }
  
      var afterItemsContainer = angular.element(
        scrollView.__content.querySelector('.collection-repeat-after-container')
      );
      if (!afterItemsContainer.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;
        });
        afterItemsContainer = angular.element('<span class="collection-repeat-after-container">');
        if (scrollView.options.scrollingX) {
          afterItemsContainer.addClass('horizontal');
        }
        afterItemsContainer.append(afterNodes);
        scrollView.__content.appendChild(afterItemsContainer[0]);
      }
  
      $$rAF(refreshDimensions);
      scrollCtrl.$element.one('scroll.init', refreshDimensions);
  
      var onWindowResize = ionic.animationFrameThrottle(validateResize);
      angular.element($window).on('resize', onWindowResize);
  
      scope.$on('$destroy', function() {
        angular.element($window).off('resize', onWindowResize);
        scrollCtrl.$element && scrollCtrl.$element.off('scroll.init', refreshDimensions);
  
        computedStyleNode && computedStyleNode.parentNode &&
          computedStyleNode.parentNode.removeChild(computedStyleNode);
        computedStyleScope && computedStyleScope.$destroy();
        computedStyleScope = computedStyleNode = null;
  
        repeatManager && repeatManager.destroy();
        repeatManager = null;
      });
  
      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,
        }));
      }
  
      scope.$watchCollection(listGetter, function(newValue) {
        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.
        $timeout(function() {
          if (newValue.length) {
            refreshDimensions();
          }
          getRepeatManager().refreshData(newValue);
        }, 0, false);
      });
  
      // Make sure this resize actually changed the size of the screen
      function validateResize() {
        var h = scrollView.__clientHeight, w = scrollView.__clientWidth;
        if (w && h && (validateResize.height !== h || validateResize.width !== w)) {
          validateResize.height = h;
          validateResize.width = w;
          refreshDimensions();
        }
      }
      function refreshDimensions() {
        if (heightData.computed || widthData.computed) {
          computeStyleDimensions();
        }
  
        if (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 (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 withoutQuotes = attrValue.replace(/(\'|\"|px|%)/g, '').trim();
        var isConstant = withoutQuotes.length && !/([a-zA-Z]|\$|:|\?)/.test(withoutQuotes);
        dimensionData.attrValue = attrValue;
  
        // If it's a constant, it's either a percent or just a constant pixel number.
        if (isConstant) {
          var intValue = parseInt(parsedValue());
  
          // For percents, store the percent getter on .getValue()
          if (attrValue.indexOf('%') > -1) {
            var decimalValue = intValue / 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 = intValue;
          }
  
        } 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(parseInt(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(parseInt(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 };
  
    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 estimatedItemsAcross;
  
      // 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(itemsAfterRepeater) {
        estimatedHeight = heightGetter(0, data[0]);
        estimatedWidth = widthGetter(0, data[0]);
  
        // 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) );
  
        (view.onRefreshLayout || angular.noop)();
        view.refreshDirection();
  
        // 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) {
          forceRerender();
        }
      };
  
      this.refreshData = function(newData) {
        data = newData;
        (view.onRefreshData || angular.noop)();
  
        isDataReady = true;
        if (isLayoutReady && isDataReady) {
          forceRerender();
          setTimeout(angular.bind(scrollView, scrollView.resize));
        }
      };
  
      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 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;
            item.scope.$broadcast('$collectionRepeatChange');
          }
        }
  
        // 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
                .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();
          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 getNextItem() {
        if (itemsLeaving.length)
          return itemsLeaving.pop();
        else if (itemsPool.length)
          return itemsPool.shift();
        return new RepeatItem();
      }
  
      function digestEnteringItems() {
        var item;
        var scope;
        var len;
        if (digestEnteringItems.running) return;
        digestEnteringItems.running = true;
  
        $$rAF(function process() {
          if( (len = itemsEntering.length) ) {
            var rootScopePhase = $rootScope.$$phase;
            var count = Math.floor(len / 1.25) || 1;
            while (count && itemsEntering.length) {
              item = itemsEntering.pop();
              if (item.isShown) {
                count--;
                if (!rootScopePhase) item.scope.$digest();
              }
            }
            $$rAF(process);
          } else {
            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 scrollViewSetDimensions = isVertical ?
          function() {
            scrollView.setDimensions(null, null, null, self.getContentSize(), true);
          } :
          function() {
            scrollView.setDimensions(null, null, self.getContentSize(), null, true);
          };
        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.rowStartIndex = i;
              dim.secondaryPos = 0;
              dim.primarySize = self.getItemPrimarySize(i, data[i]);
              dim.primaryPos = prevDimension.primaryPos + prevDimension.primarySize;
            } else {
              dim.rowStartIndex = prevDimension.rowStartIndex;
              dim.primarySize = prevDimension.primarySize;
              dim.primaryPos = prevDimension.primaryPos;
            }
          }
        }
  
        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() {
          // 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, len = data.length; i < len; 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.primarySize >= 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.primarySize > 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.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.
   *
   * Be aware that this directive gets its own child scope. If you do not understand why this
   * is important, you can read [https://docs.angularjs.org/guide/scope](https://docs.angularjs.org/guide/scope).
   *
   * @param {string=} delegate-handle The handle used to identify this scrollView
   * with {@link ionic.service:$ionicScrollDelegate}.
   * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'.
   * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true.
   * @param {boolean=} padding Whether to add padding to the content.
   * of the content.  Defaults to true on iOS, false on Android.
   * @param {boolean=} scroll Whether to allow scrolling of content.  Defaults to true.
   * @param {boolean=} overflow-scroll Whether to use overflow-scrolling instead of
   * Ionic scroll.
   * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true.
   * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true.
   * @param {string=} start-x Initial horizontal scroll position. Default 0.
   * @param {string=} start-y Initial vertical scroll position. Default 0.
   * @param {expression=} on-scroll Expression to evaluate when the content is scrolled.
   * @param {expression=} on-scroll-complete Expression to evaluate when a scroll action completes.
   * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges
   * of the content.  Defaults to true on iOS, false on Android.
   * @param {number=} scroll-event-interval Number of milliseconds between each firing of the 'on-scroll' expression. Default 10.
   */
  IonicModule
  .directive('ionContent', [
    '$timeout',
    '$controller',
    '$ionicBind',
    '$ionicConfig',
  function($timeout, $controller, $ionicBind, $ionicConfig) {
    return {
      restrict: 'E',
      require: '^?ionNavView',
      scope: true,
      priority: 800,
      compile: function(element, attr) {
        var innerElement;
  
        element.addClass('scroll-content ionic-scroll');
  
        if (attr.scroll != 'false') {
          //We cannot use normal transclude here because it breaks element.data()
          //inheritance on compile
          innerElement = jqLite('<div class="scroll"></div>');
          innerElement.append(element.contents());
          element.append(innerElement);
        } else {
          element.addClass('scroll-content-false');
        }
  
        return { pre: prelink };
        function prelink($scope, $element, $attr, navViewCtrl) {
          var parentScope = $scope.$parent;
          $scope.$watch(function() {
            return (parentScope.$hasHeader ? ' has-header' : '')  +
              (parentScope.$hasSubheader ? ' has-subheader' : '') +
              (parentScope.$hasFooter ? ' has-footer' : '') +
              (parentScope.$hasSubfooter ? ' has-subfooter' : '') +
              (parentScope.$hasTabs ? ' has-tabs' : '') +
              (parentScope.$hasTabsTop ? ' has-tabs-top' : '');
          }, function(className, oldClassName) {
            $element.removeClass(oldClassName);
            $element.addClass(className);
          });
  
          //Only this ionContent should use these variables from parent scopes
          $scope.$hasHeader = $scope.$hasSubheader =
            $scope.$hasFooter = $scope.$hasSubfooter =
            $scope.$hasTabs = $scope.$hasTabsTop =
            false;
          $ionicBind($scope, $attr, {
            $onScroll: '&onScroll',
            $onScrollComplete: '&onScrollComplete',
            hasBouncing: '@',
            padding: '@',
            direction: '@',
            scrollbarX: '@',
            scrollbarY: '@',
            startX: '@',
            startY: '@',
            scrollEventInterval: '@'
          });
          $scope.direction = $scope.direction || 'y';
  
          if (isDefined($attr.padding)) {
            $scope.$watch($attr.padding, function(newVal) {
                (innerElement || $element).toggleClass('padding', !!newVal);
            });
          }
  
          if ($attr.scroll === "false") {
            //do nothing
          } else if (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling()) {
            // use native scrolling
            $element.addClass('overflow-scroll');
          } else {
            var scrollViewOptions = {
              el: $element[0],
              delegateHandle: attr.delegateHandle,
              locking: (attr.locking || 'true') === 'true',
              bouncing: $scope.$eval($scope.hasBouncing),
              startX: $scope.$eval($scope.startX) || 0,
              startY: $scope.$eval($scope.startY) || 0,
              scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
              scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
              scrollingX: $scope.direction.indexOf('x') >= 0,
              scrollingY: $scope.direction.indexOf('y') >= 0,
              scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 10,
              scrollingComplete: function() {
                $scope.$onScrollComplete({
                  scrollTop: this.__scrollTop,
                  scrollLeft: this.__scrollLeft
                });
              }
            };
            $controller('$ionicScroll', {
              $scope: $scope,
              scrollViewOptions: scrollViewOptions
            });
  
            $scope.$on('$destroy', function() {
              scrollViewOptions.scrollingComplete = noop;
              delete scrollViewOptions.el;
              innerElement = null;
              $element = null;
              attr.$$element = null;
            });
          }
  
        }
      }
    };
  }]);
  
  /**
   * @ngdoc directive
   * @name exposeAsideWhen
   * @module ionic
   * @restrict A
   * @parent ionic.directive:ionSideMenus
   *
   * @description
   * It is common for a tablet application to hide a menu when in portrait mode, but to show the
   * same menu on the left side when the tablet is in landscape mode. The `exposeAsideWhen` attribute
   * directive can be used to accomplish a similar interface.
   *
   * By default, side menus are hidden underneath its side menu content, and can be opened by either
   * swiping the content left or right, or toggling a button to show the side menu. However, by adding the
   * `exposeAsideWhen` attribute directive to an {@link ionic.directive:ionSideMenu} element directive,
   * a side menu can be given instructions on "when" the menu should be exposed (always viewable). For
   * example, the `expose-aside-when="large"` attribute will keep the side menu hidden when the viewport's
   * width is less than `768px`, but when the viewport's width is `768px` or greater, the menu will then
   * always be shown and can no longer be opened or closed like it could when it was hidden for smaller
   * viewports.
   *
   * Using `large` as the attribute's value is a shortcut value to `(min-width:768px)` since it is
   * the most common use-case. However, for added flexibility, any valid media query could be added
   * as the value, such as `(min-width:600px)` or even multiple queries such as
   * `(min-width:750px) and (max-width:1200px)`.
  
   * @usage
   * ```html
   * <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) {
  
        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);
  
        checkAsideExpose();
  
        ionic.on('resize', onResize, $window);
  
        $scope.$on('$destroy', function(){
          ionic.off('resize', onResize, $window);
        });
  
      }
    };
  }]);
  
  
  var GESTURE_DIRECTIVES = 'onHold onTap onDoubleTap onTouch onRelease onDrag onDragUp onDragRight onDragDown onDragLeft onSwipe onSwipeUp onSwipeRight onSwipeDown onSwipeLeft'.split(' ');
  
  GESTURE_DIRECTIVES.forEach(function(name) {
    IonicModule.directive(name, gestureDirective(name));
  });
  
  
  /**
   * @ngdoc directive
   * @name onHold
   * @module ionic
   * @restrict A
   *
   * @description
   * Touch stays at the same location for 500ms. Similar to long touch events available for AngularJS and jQuery.
   *
   * @usage
   * ```html
   * <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 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 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>
   *   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>
   *   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() {
    return ['$ionicScrollDelegate', function($ionicScrollDelegate) {
      return {
        restrict: 'E',
        link: function($scope, $element, $attr) {
          if ($attr.noTapScroll == 'true') {
            return;
          }
          ionic.on('tap', onTap, $element[0]);
          $scope.$on('$destroy', function() {
            ionic.off('tap', onTap, $element[0]);
          });
  
          function onTap(e) {
            var depth = 3;
            var current = e.target;
            //Don't scroll to top in certain cases
            while (depth-- && current) {
              if (current.classList.contains('button') ||
                  current.tagName.match(/input|textarea|select/i) ||
                  current.isContentEditable) {
                return;
              }
              current = current.parentNode;
            }
            var touch = e.gesture && e.gesture.touches[0] || e.detail.touches[0];
            var bounds = $element[0].getBoundingClientRect();
            if (ionic.DomUtil.rectContains(
              touch.pageX, touch.pageY,
              bounds.left, bounds.top - 20,
              bounds.left + bounds.width, bounds.top + bounds.height
            )) {
              $ionicScrollDelegate.scrollTop(true);
            }
          }
        }
      };
    }];
  }
  
  function headerFooterBarDirective(isHeader) {
    return ['$document', '$timeout',function($document, $timeout) {
      return {
        restrict: 'E',
        controller: '$ionicHeaderBar',
        compile: function(tElement, $attr) {
          tElement.addClass(isHeader ? 'bar bar-header' : 'bar bar-footer');
          // top style tabs? if so, remove bottom border for seamless display
          $timeout(function() {
            if (isHeader && $document[0].getElementsByClassName('tabs-top').length) tElement.addClass('has-tabs-top');
          });
  
          return { pre: prelink };
          function prelink($scope, $element, $attr, ctrl) {
            if (isHeader) {
              $scope.$watch(function() { return $element[0].className; }, function(value) {
                var isShown = value.indexOf('ng-hide') === -1;
                var isSubheader = value.indexOf('bar-subheader') !== -1;
                $scope.$hasHeader = isShown && !isSubheader;
                $scope.$hasSubheader = isShown && isSubheader;
              });
              $scope.$on('$destroy', function() {
                delete $scope.$hasHeader;
                delete $scope.$hasSubheader;
              });
              ctrl.align();
              $scope.$on('$ionicHeader.align', function() {
                ionic.requestAnimationFrame(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);
              });
            }
          }
        }
      };
    }];
  }
  
  /**
   * @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 depreicated
   * 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;
        // if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling
        if (jsScrolling) {
          infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
        } 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 appropriate scroll event
        if (jsScrolling) {
          $scope.scrollingType = 'js-scrolling';
          scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds);
        } else {
          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(); });
        }
      }
    };
  }]);
  
  var ITEM_TPL_CONTENT_ANCHOR =
    '<a class="item-content" ng-href="{{$href()}}" target="{{$target()}}"></a>';
  var ITEM_TPL_CONTENT =
    '<div class="item-content"></div>';
  /**
  * @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', function() {
        var nextId = 0;
    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 ? ITEM_TPL_CONTENT_ANCHOR : ITEM_TPL_CONTENT);
          innerElement.append($element.contents());
  
          $element.append(innerElement);
          $element.addClass('item item-complex');
        } else {
          $element.addClass('item');
        }
  
        return function link($scope, $element, $attrs) {
          var listCtrl;
          $scope.$href = function() {
            return $attrs.href || $attrs.ngHref;
          };
          $scope.$target = function() {
            return $attrs.target || '_self';
          };
  
          var content = $element[0].querySelector('.item-content');
          if (content) {
            $scope.$on('$collectionRepeatChange', function() {
              content && (content.style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)');
            });
          }
        };
  
      }
    };
  });
  
  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() {
    return {
      restrict: 'E',
      require: ['^ionItem', '^?ionList'],
      //Run before anything else, so we can move it before other directives process
      //its location (eg ngIf relies on the location of the directive in the dom)
      priority: Number.MAX_VALUE,
      compile: function($element, $attr) {
        //Add the classes we need during the compile phase, so that they stay
        //even if something else like ngIf removes the element and re-addss it
        $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true);
        return function($scope, $element, $attr, ctrls) {
          var itemCtrl = ctrls[0];
          var listCtrl = ctrls[1];
          var container = jqLite(ITEM_TPL_DELETE_BUTTON);
          container.append($element);
          itemCtrl.$element.append(container).addClass('item-left-editable');
  
          if (listCtrl && listCtrl.showDelete()) {
            container.addClass('visible active');
          }
        };
      }
    };
  });
  
  
  IonicModule
  .directive('itemFloatingLabel', function() {
    return {
      restrict: 'C',
      link: function(scope, element) {
        var el = element[0];
        var input = el.querySelector('input, textarea');
        var inputLabel = el.querySelector('.input-label');
  
        if (!input || !inputLabel) return;
  
        var onInput = function() {
          if (input.value) {
            inputLabel.classList.add('has-input');
          } else {
            inputLabel.classList.remove('has-input');
          }
        };
  
        input.addEventListener('input', onInput);
  
        var ngModelCtrl = 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
  * 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', ['$compile', function($compile) {
    function stopPropagation(e) {
      e.stopPropagation();
    }
    return {
      restrict: 'E',
      require: '^ionItem',
      priority: Number.MAX_VALUE,
      compile: function($element, $attr) {
        $attr.$set('class', ($attr['class'] || '') + ' button', true);
        return function($scope, $element, $attr, itemCtrl) {
          if (!itemCtrl.optionsContainer) {
            itemCtrl.optionsContainer = jqLite(ITEM_TPL_OPTION_BUTTONS);
            itemCtrl.$element.append(itemCtrl.optionsContainer);
          }
          itemCtrl.optionsContainer.append($element);
  
          itemCtrl.$element.addClass('item-right-editable');
  
          //Don't bubble click up to main .item
          $element.on('click', stopPropagation);
        };
      }
    };
  }]);
  
  var ITEM_TPL_REORDER_BUTTON =
    '<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/driftyco/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, attrs) {
      ionic.on('native.keyboardshow', onShow, window);
      ionic.on('native.keyboardhide', onHide, window);
  
      //deprecated
      ionic.on('native.showkeyboard', onShow, window);
      ionic.on('native.hidekeyboard', onHide, window);
  
  
      var scrollCtrl;
  
      function onShow(e) {
        if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
          return;
        }
  
        //for testing
        var keyboardHeight = e.keyboardHeight || e.detail.keyboardHeight;
        element.css('bottom', keyboardHeight + "px");
        scrollCtrl = element.controller('$ionicScroll');
        if (scrollCtrl) {
          scrollCtrl.scrollView.__container.style.bottom = keyboardHeight + keyboardAttachGetClientHeight(element[0]) + "px";
        }
      }
  
      function onHide() {
        if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
          return;
        }
  
        element.css('bottom', '');
        if (scrollCtrl) {
          scrollCtrl.scrollView.__container.style.bottom = '';
        }
      }
  
      scope.$on('$destroy', function() {
        ionic.off('native.keyboardshow', onShow, window);
        ionic.off('native.keyboardhide', onHide, window);
  
        //deprecated
        ionic.off('native.showkeyboard', onShow, window);
        ionic.off('native.hidekeyboard', onHide, window);
      });
    };
  });
  
  function keyboardAttachGetClientHeight(element) {
    return element.clientHeight;
  }
  
  /**
  * @ngdoc directive
  * @name ionList
  * @module ionic
  * @delegate ionic.service:$ionicListDelegate
  * @codepen JsHjf
  * @restrict E
  * @description
  * The List is a widely used interface element in almost any mobile app, and can include
  * content ranging from basic text all the way to buttons, toggles, icons, and thumbnails.
  *
  * Both the list, which contains items, and the list items themselves can be any HTML
  * element. The containing element requires the `list` class and each list item requires
  * the `item` class.
  *
  * However, using the ionList and ionItem directives make it easy to support various
  * interaction modes such as swipe to edit, drag to reorder, and removing items.
  *
  * Related: {@link ionic.directive:ionItem}, {@link ionic.directive:ionOptionButton}
  * {@link ionic.directive:ionReorderButton}, {@link ionic.directive:ionDeleteButton}, [`list CSS documentation`](/docs/components/#list).
  *
  * @usage
  *
  * Basic Usage:
  *
  * ```html
  * <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>
   * ```
   */
  IonicModule
  .directive('menuClose', ['$ionicHistory', function($ionicHistory) {
    return {
      restrict: 'AC',
      link: function($scope, $element) {
        $element.bind('click', function() {
          var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
          if (sideMenuCtrl) {
            $ionicHistory.nextViewOptions({
              historyRoot: true,
              disableAnimate: true,
              expire: 300
            });
            sideMenuCtrl.close();
          }
        });
      }
    };
  }]);
  
  /**
   * @ngdoc directive
   * @name menuToggle
   * @module ionic
   * @restrict AC
   *
   * @description
   * Toggle a side menu on the given side.
   *
   * @usage
   * Below is an example of a link within a nav bar. Tapping this button
   * would open the given side menu, and tapping it again would close it.
   *
   * ```html
   * <ion-nav-bar>
   *   <ion-nav-buttons side="left">
   *    <button menu-toggle="left" 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-wrapper" ng-transclude></div>' +
                  '</div>'
    };
  }]);
  
  IonicModule
  .directive('ionModalView', function() {
    return {
      restrict: 'E',
      compile: function(element, attr) {
        element.addClass('modal');
      }
    };
  });
  
  /**
   * @ngdoc directive
   * @name ionNavBackButton
   * @module ionic
   * @restrict E
   * @parent ionNavBar
   * @description
   * Creates a back button inside an {@link ionic.directive:ionNavBar}.
   *
   * The back button will appear when the user is able to go back in the current navigation stack. By
   * default, the markup of the back button is automatically built using platform-appropriate defaults
   * (iOS back button icon on iOS and Android icon on Android).
   *
   * Additionally, the button is automatically set to `$ionicGoBack()` on click/tap. By default, the
   * app will navigate back one view when the back button is clicked.  More advanced behavior is also
   * possible, as outlined below.
   *
   * @usage
   *
   * Recommended markup for default settings:
   *
   * ```html
   * <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($event)');
        }
  
        buttonEle.className = 'button back-button hide buttons ' + (tElement.attr('class') || '');
        buttonEle.innerHTML = tElement.html() || '';
  
        var childNode;
        var hasIcon = hasIconClass(tElement[0]);
        var hasInnerText;
        var hasButtonText;
        var hasPreviousTitle;
  
        for (var x = 0; x < tElement[0].childNodes.length; x++) {
          childNode = tElement[0].childNodes[x];
          if (childNode.nodeType === 1) {
            if (hasIconClass(childNode)) {
              hasIcon = true;
            } else if (childNode.classList.contains('default-title')) {
              hasButtonText = true;
            } else if (childNode.classList.contains('previous-title')) {
              hasPreviousTitle = true;
            }
          } else if (!hasInnerText && childNode.nodeType === 3) {
            hasInnerText = !!childNode.nodeValue.trim();
          }
        }
  
        function hasIconClass(ele) {
          return /ion-|icon/.test(ele.className);
        }
  
        var defaultIcon = $ionicConfig.backButton.icon();
        if (!hasIcon && defaultIcon && defaultIcon !== 'none') {
          buttonEle.innerHTML = '<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.
   *
   * </table><br/>
   */
  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 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>
   *
   * @param {string=} name A view name. The name should be unique amongst the other views in the
   * same state. You can have views of the same name that live in different states. For more
   * information, see ui-router's
   * [ui-view documentation](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.directive:ui-view).
   */
  IonicModule
  .directive('ionNavView', [
    '$state',
    '$ionicConfig',
  function($state, $ionicConfig) {
    // IONIC's fork of Angular UI Router, v0.2.10
    // the navView handles registering views in the history and how to transition between them
    return {
      restrict: 'E',
      terminal: true,
      priority: 2000,
      transclude: true,
      controller: '$ionicNavView',
      compile: function(tElement, tAttrs, transclude) {
  
        // a nav view element is a container for numerous views
        tElement.addClass('view-container');
        ionic.DomUtil.cachedAttr(tElement, 'nav-view-transition', $ionicConfig.views.transition());
  
        return function($scope, $element, $attr, navViewCtrl) {
          var latestLocals;
  
          // Put in the compiled initial view
          transclude($scope, function(clone) {
            $element.append(clone);
          });
  
          var viewData = navViewCtrl.init();
  
          // listen for $stateChangeSuccess
          $scope.$on('$stateChangeSuccess', function() {
            updateView(false);
          });
          $scope.$on('$viewContentLoading', function() {
            updateView(false);
          });
  
          // initial load, ready go
          updateView(true);
  
  
          function updateView(firstTime) {
            // get the current local according to the $state
            var viewLocals = $state.$current && $state.$current.locals[viewData.name];
  
            // do not update THIS nav-view if its is not the container for the given state
            // if the viewLocals are the same as THIS latestLocals, then nothing to do
            if (!viewLocals || (!firstTime && viewLocals === latestLocals)) return;
  
            // update the latestLocals
            latestLocals = viewLocals;
            viewData.state = viewLocals.$$state;
  
            // register, update and transition to the new view
            navViewCtrl.register(viewLocals);
          }
  
        };
      }
    };
  }]);
  
  IonicModule
  
  .config(['$provide', function($provide) {
    $provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
      // drop the default ngClick directive
      $delegate.shift();
      return $delegate;
    }]);
  }])
  
  /**
   * @private
   */
  .factory('$ionicNgClick', ['$parse', function($parse) {
    return function(scope, element, clickExpr) {
      var clickHandler = angular.isFunction(clickExpr) ?
        clickExpr :
        $parse(clickExpr);
  
      element.on('click', function(event) {
        scope.$apply(function() {
          clickHandler(scope, {$event: (event)});
        });
      });
  
      // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
      // something else nearby.
      element.onclick = function(event) { };
    };
  }])
  
  .directive('ngClick', ['$ionicNgClick', function($ionicNgClick) {
    return function(scope, element, attr) {
      $ionicNgClick(scope, element, attr.ngClick);
    };
  }])
  
  .directive('ionStopEvent', function() {
    return {
      restrict: 'A',
      link: function(scope, element, attr) {
        element.bind(attr.ionStopEvent, eventStopPropagation);
      }
    };
  });
  function eventStopPropagation(e) {
    e.stopPropagation();
  }
  
  
  /**
   * @ngdoc directive
   * @name ionPane
   * @module ionic
   * @restrict E
   *
   * @description A simple container that fits content, with no side effects.  Adds the 'pane' class to the element.
   */
  IonicModule
  .directive('ionPane', function() {
    return {
      restrict: 'E',
      link: function(scope, element, attr) {
        element.addClass('pane');
      }
    };
  });
  
  /*
   * We don't document the ionPopover directive, we instead document
   * the $ionicPopover service
   */
  IonicModule
  .directive('ionPopover', [function() {
    return {
      restrict: 'E',
      transclude: true,
      replace: true,
      controller: [function(){}],
      template: '<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 any [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="item-content disable-pointer-events" ng-transclude></div>' +
          '<i class="radio-icon disable-pointer-events icon ion-checkmark"></i>' +
        '</label>',
  
      compile: function(element, attr) {
        if (attr.icon) {
          element.children().eq(2).removeClass('ion-checkmark').addClass(attr.icon);
        }
  
        var input = element.find('input');
        forEach({
            'name': attr.name,
            'value': attr.value,
            'disabled': attr.disabled,
            'ng-value': attr.ngValue,
            'ng-model': attr.ngModel,
            'ng-disabled': attr.ngDisabled,
            'ng-change': attr.ngChange
        }, function(value, name) {
          if (isDefined(value)) {
              input.attr(name, value);
            }
        });
  
        return function(scope, element, attr) {
          scope.getValue = function() {
            return scope.ngValue || attr.value;
          };
        };
      }
    };
  });
  
  
  /**
   * @ngdoc directive
   * @name ionRefresher
   * @module ionic
   * @restrict E
   * @parent ionic.directive:ionContent, ionic.directive:ionScroll
   * @description
   * Allows you to add pull-to-refresh to a scrollView.
   *
   * Place it as the first child of your {@link ionic.directive:ionContent} or
   * {@link ionic.directive:ionScroll} element.
   *
   * When refreshing is complete, $broadcast the 'scroll.refreshComplete' event
   * from your controller.
   *
   * @usage
   *
   * ```html
   * <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-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 depreicated 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) {
          $element[0].classList.add('js-scrolling');
  
          scrollCtrl._setRefresher(
            $scope,
            $element[0],
            refresherCtrl.getRefresherDomMethods()
          );
  
          $scope.$on('scroll.refreshComplete', function() {
            $scope.$evalAsync(function() {
              scrollCtrl.scrollView.finishPullToRefresh();
            });
          });
  
        } else {
          // Kick off native scrolling
          refresherCtrl.init();
        }
  
      }
    };
  }]);
  
  /**
   * @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 {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true.
   * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true.
   * @param {boolean=} zooming Whether to support pinch-to-zoom
   * @param {integer=} min-zoom The smallest zoom amount allowed (default is 0.5)
   * @param {integer=} max-zoom The largest zoom amount allowed (default is 3)
   * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges
   * of the content.  Defaults to true on iOS, false on Android.
   */
  IonicModule
  .directive('ionScroll', [
    '$timeout',
    '$controller',
    '$ionicBind',
  function($timeout, $controller, $ionicBind) {
    return {
      restrict: 'E',
      scope: true,
      controller: function() {},
      compile: function(element, attr) {
        element.addClass('scroll-view ionic-scroll');
  
        //We cannot transclude here because it breaks element.data() inheritance on compile
        var innerElement = jqLite('<div class="scroll"></div>');
        innerElement.append(element.contents());
        element.append(innerElement);
  
        return { pre: prelink };
        function prelink($scope, $element, $attr) {
          var scrollView, scrollCtrl;
  
          $ionicBind($scope, $attr, {
            direction: '@',
            paging: '@',
            $onScroll: '&onScroll',
            scroll: '@',
            scrollbarX: '@',
            scrollbarY: '@',
            zooming: '@',
            minZoom: '@',
            maxZoom: '@'
          });
          $scope.direction = $scope.direction || 'y';
  
          if (isDefined($attr.padding)) {
            $scope.$watch($attr.padding, function(newVal) {
              innerElement.toggleClass('padding', !!newVal);
            });
          }
          if($scope.$eval($scope.paging) === true) {
            innerElement.addClass('scroll-paging');
          }
  
          if(!$scope.direction) { $scope.direction = 'y'; }
          var isPaging = $scope.$eval($scope.paging) === true;
  
          var scrollViewOptions= {
            el: $element[0],
            delegateHandle: $attr.delegateHandle,
            locking: ($attr.locking || 'true') === 'true',
            bouncing: $scope.$eval($attr.hasBouncing),
            paging: isPaging,
            scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
            scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
            scrollingX: $scope.direction.indexOf('x') >= 0,
            scrollingY: $scope.direction.indexOf('y') >= 0,
            zooming: $scope.$eval($scope.zooming) === true,
            maxZoom: $scope.$eval($scope.maxZoom) || 3,
            minZoom: $scope.$eval($scope.minZoom) || 0.5,
            preventDefault: true
          };
          if (isPaging) {
            scrollViewOptions.speedMultiplier = 0.8;
            scrollViewOptions.bouncing = false;
          }
  
          scrollCtrl = $controller('$ionicScroll', {
            $scope: $scope,
            scrollViewOptions: scrollViewOptions
          });
          scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView;
        }
      }
    };
  }]);
  
  /**
   * @ngdoc directive
   * @name ionSideMenu
   * @module ionic
   * @restrict E
   * @parent ionic.directive:ionSideMenus
   *
   * @description
   * A container for a side menu, sibling to an {@link ionic.directive:ionSideMenuContent} directive.
   *
   * @usage
   * ```html
   * <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(e) {},
            endDrag: function(e) {},
            getTranslateX: function() {
              return $scope.sideMenuContentTranslateX || 0;
            },
            setTranslateX: ionic.animationFrameThrottle(function(amount) {
              var xTransform = content.offsetX + amount;
              $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + xTransform + 'px,0,0)';
              $timeout(function() {
                $scope.sideMenuContentTranslateX = amount;
              });
            }),
            setMarginLeft: ionic.animationFrameThrottle(function(amount) {
              if (amount) {
                amount = parseInt(amount, 10);
                $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amount + 'px,0,0)';
                $element[0].style.width = ($window.innerWidth - amount) + 'px';
                content.offsetX = amount;
              } else {
                $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
                $element[0].style.width = '';
                content.offsetX = 0;
              }
              ionic.trigger('resize', null, window);
            }),
            setMarginRight: ionic.animationFrameThrottle(function(amount) {
              if (amount) {
                amount = parseInt(amount, 10);
                $element[0].style.width = ($window.innerWidth - amount) + 'px';
                content.offsetX = amount;
              } else {
                $element[0].style.width = '';
                content.offsetX = 0;
              }
              // reset incase left gets grabby
              $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
            }),
            enableAnimation: function() {
              $scope.animationEnabled = true;
              $element[0].classList.add('menu-animated');
            },
            disableAnimation: function() {
              $scope.animationEnabled = false;
              $element[0].classList.remove('menu-animated');
            },
            offsetX: 0
          };
  
          sideMenuCtrl.setContent(content);
  
          // add gesture handlers
          var gestureOpts = { stop_browser_behavior: false };
          var contentTapGesture = $ionicGesture.on('tap', onContentTap, $element, gestureOpts);
          var dragRightGesture = $ionicGesture.on('dragright', onDragX, $element, gestureOpts);
          var dragLeftGesture = $ionicGesture.on('dragleft', onDragX, $element, gestureOpts);
          var dragUpGesture = $ionicGesture.on('dragup', onDragY, $element, gestureOpts);
          var dragDownGesture = $ionicGesture.on('dragdown', onDragY, $element, gestureOpts);
          var releaseGesture = $ionicGesture.on('release', onDragRelease, $element, gestureOpts);
  
          // Cleanup
          $scope.$on('$destroy', function() {
            if (content) {
              content.element = null;
              content = null;
            }
            $ionicGesture.off(dragLeftGesture, 'dragleft', onDragX);
            $ionicGesture.off(dragRightGesture, 'dragright', onDragX);
            $ionicGesture.off(dragUpGesture, 'dragup', onDragY);
            $ionicGesture.off(dragDownGesture, 'dragdown', onDragY);
            $ionicGesture.off(releaseGesture, 'release', onDragRelease);
            $ionicGesture.off(contentTapGesture, 'tap', onContentTap);
          });
        }
      }
    };
  }]);
  
  IonicModule
  
  /**
   * @ngdoc directive
   * @name ionSideMenus
   * @module ionic
   * @delegate ionic.service:$ionicSideMenuDelegate
   * @restrict E
   *
   * @description
   * A container element for side menu(s) and the main content. Allows the left and/or right side menu
   * to be toggled by dragging the main content area side to side.
   *
   * To automatically close an opened menu, you can add the {@link ionic.directive:menuClose} attribute
   * directive. The `menu-close` attribute is usually added to links and buttons within
   * `ion-side-menu-content`, so that when the element is clicked, the opened side menu will
   * automatically close.
   *
   * "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,
   * an `<ion-side-menu-content>` for the center content,
   * and one or more `<ion-side-menu>` directives.
   *
   * ```html
   * <ion-side-menus>
   *   <!-- Center content -->
   *   <ion-side-menu-content ng-controller="ContentController">
   *   </ion-side-menu-content>
   *
   *   <!-- Left menu -->
   *   <ion-side-menu side="left">
   *   </ion-side-menu>
   *
   *   <!-- 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
   * @delegate ionic.service:$ionicSlideBoxDelegate
   * @restrict E
   * @description
   * The Slide Box is a multi-page container where each page can be swiped or dragged between:
   *
   * ![SlideBox](http://ionicframework.com.s3.amazonaws.com/docs/controllers/slideBox.gif)
   *
   * @usage
   * ```html
   * <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()}}"`.
   * @param {expression=} pager-click Expression to call when a pager is clicked (if show-pager is true). Is passed the 'index' variable.
   * @param {expression=} on-slide-changed Expression called whenever the slide is changed.  Is passed an '$index' variable.
   * @param {expression=} active-slide Model to bind the current slide to.
   */
  IonicModule
  .directive('ionSlideBox', [
    '$timeout',
    '$compile',
    '$ionicSlideBoxDelegate',
    '$ionicHistory',
  function($timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory) {
    return {
      restrict: 'E',
      replace: true,
      transclude: true,
      scope: {
        autoPlay: '=',
        doesContinue: '@',
        slideInterval: '@',
        showPager: '@',
        pagerClick: '&',
        disableScroll: '@',
        onSlideChanged: '&',
        activeSlide: '=?'
      },
      controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
        var _this = this;
  
        var continuous = $scope.$eval($scope.doesContinue) === true;
        var shouldAutoPlay = isDefined($attrs.autoPlay) ? !!$scope.autoPlay : false;
        var slideInterval = shouldAutoPlay ? $scope.$eval($scope.slideInterval) || 4000 : 0;
  
        var slider = new ionic.views.Slider({
          el: $element[0],
          auto: slideInterval,
          continuous: continuous,
          startSlide: $scope.activeSlide,
          slidesChanged: function() {
            $scope.currentSlide = slider.currentIndex();
  
            // Try to trigger a digest
            $timeout(function() {});
          },
          callback: function(slideIndex) {
            $scope.currentSlide = slideIndex;
            $scope.onSlideChanged({ index: $scope.currentSlide, $index: $scope.currentSlide});
            $scope.$parent.$broadcast('slideBox.slideChanged', slideIndex);
            $scope.activeSlide = slideIndex;
            // Try to trigger a digest
            $timeout(function() {});
          }
        });
  
        slider.enableSlide($scope.$eval($attrs.disableScroll) !== true);
  
        $scope.$watch('activeSlide', function(nv) {
          if (isDefined(nv)) {
            slider.slide(nv);
          }
        });
  
        $scope.$on('slideBox.nextSlide', function() {
          slider.next();
        });
  
        $scope.$on('slideBox.prevSlide', function() {
          slider.prev();
        });
  
        $scope.$on('slideBox.setSlide', function(e, index) {
          slider.slide(index);
        });
  
        //Exposed for testing
        this.__slider = slider;
  
        var deregisterInstance = $ionicSlideBoxDelegate._registerInstance(
          slider, $attrs.delegateHandle, function() {
            return $ionicHistory.isActiveScope($scope);
          }
        );
        $scope.$on('$destroy', deregisterInstance);
  
        this.slidesCount = function() {
          return slider.slidesCount();
        };
  
        this.onPagerClick = function(index) {
          console.log('pagerClick', 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, slideBoxCtrl) {
        $attr.$observe('showPager', function(show) {
          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, attr) {
        element.addClass('slider-slide');
        return function($scope, $element, $attr) {
        };
      }
    };
  })
  
  .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 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);
      }
    };
  });
  
  /**
   * @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;
  
          $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="{\'tab-item-active\': isTabActive(), \'has-badge\':badge, \'tab-hidden\':isHidden()}" ' +
        ' ng-disabled="disabled()" class="tab-item">' +
        '<span class="badge {{badgeStyle}}" ng-if="badge">{{badge}}</span>' +
        '<i class="icon {{getIconOn()}}" ng-if="getIconOn() && isTabActive()"></i>' +
        '<i class="icon {{getIconOff()}}" ng-if="getIconOff() && !isTabActive()"></i>' +
        '<span class="tab-title" ng-bind-html="title"></span>' +
      '</a>',
      scope: {
        title: '@',
        icon: '@',
        iconOn: '@',
        iconOff: '@',
        badge: '=',
        hidden: '@',
        disabled: '&',
        badgeStyle: '@',
        'class': '@'
      },
      compile: function(element, attr, transclude) {
        return function link($scope, $element, $attrs, ctrls) {
          var tabsCtrl = ctrls[0],
            tabCtrl = ctrls[1];
  
          //Remove title attribute so browser-tooltip does not apear
          $element[0].removeAttribute('title');
  
          $scope.selectTab = function(e) {
            e.preventDefault();
            tabsCtrl.select(tabCtrl.$scope, true);
          };
          if (!$attrs.ngClick) {
            $element.on('click', function(event) {
              $scope.$apply(function() {
                $scope.selectTab(event);
              });
            });
          }
  
          $scope.isHidden = function() {
            if ($attrs.hidden === 'true' || $attrs.hidden === true) return true;
            return false;
          };
  
          $scope.getIconOn = function() {
            return $scope.iconOn || $scope.icon;
          };
          $scope.getIconOff = function() {
            return $scope.iconOff || $scope.icon;
          };
  
          $scope.isTabActive = function() {
            return tabsCtrl.selectedTab() === tabCtrl.$scope;
          };
        };
      }
    };
  }]);
  
  /**
   * @ngdoc directive
   * @name ionTabs
   * @module ionic
   * @delegate ionic.service:$ionicTabsDelegate
   * @restrict E
   * @codepen 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 [$ionicConfigProvider](docs/api/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-only">
   *
   *   <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',
    '$ionicHistory',
  function($ionicTabsDelegate, $ionicConfig, $ionicHistory) {
    return {
      restrict: 'E',
      scope: true,
      controller: '$ionicTabs',
      compile: function(tElement) {
        //We cannot use regular transclude here because it breaks element.data()
        //inheritance on compile
        var innerElement = jqLite('<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 selectedTab = tabsCtrl.selectedTab();
            if (selectedTab) {
              selectedTab.$emit(ev.name.replace('NavView', 'View'), 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 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
        }, 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 exists its parent {@link ionic.directive:ionNavView}, the view also emits view
   * information, such as its title, whether the back button should show or not, whether the
   * corresponding {@link ionic.directive:ionNavBar} should show or not, which transition the view
   * should use to animate, and which direction to animate.
   *
   * *Views are cached to improve performance.* When a view is navigated away from, its element is
   * left in the DOM, and its scope is disconnected from the `$watch` cycle. When navigating to a
   * view that is already cached, its scope is reconnected, and the existing element, which was
   * left in the DOM, becomes active again. This can be disabled, or the maximum number of cached
   * views changed in {@link ionic.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.
   *
   * <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>
   * </table>
   *
   * ## Caching
   *
   * Caching can be disabled and enabled in multiple ways. By default, Ionic will
   * cache a maximum of 10 views. You can optionally choose to disable caching at
   * either an individual view basis, or by global configuration. Please see the
   * _Caching_ section in {@link ionic.directive:ionNavView} for more info.
   *
   * @param {string=} view-title A text-only title to display on the parent {@link ionic.directive:ionNavBar}.
   * For an HTML title, such as an image, see {@link ionic.directive:ionNavTitle} instead.
   * @param {boolean=} cache-view If this view should be allowed to be cached or not.
   * Please see the _Caching_ section in {@link ionic.directive:ionNavView} for
   * more info. Default `true`
   * @param {boolean=} hide-back-button Whether to hide the back button on the parent
   * {@link ionic.directive:ionNavBar} by default.
   * @param {boolean=} hide-nav-bar Whether to hide the parent
   * {@link ionic.directive:ionNavBar} by default.
   */
  IonicModule
  .directive('ionView', function() {
    return {
      restrict: 'EA',
      priority: 1000,
      controller: '$ionicView',
      compile: function(tElement) {
        tElement.addClass('pane');
        tElement[0].removeAttribute('title');
        return function link($scope, $element, $attrs, viewCtrl) {
          viewCtrl.init();
        };
      }
    };
  });
  
  })();