summaryrefslogtreecommitdiff
path: root/www/lib/ionic/js/ionic-angular.js
diff options
context:
space:
mode:
authorPliable Pixels <pliablepixels@gmail.com>2017-09-20 16:15:18 -0400
committerPliable Pixels <pliablepixels@gmail.com>2017-09-20 16:15:18 -0400
commit676270d21beed31d767a06c89522198c77d5d865 (patch)
tree902772af01bfbcf80955f0351a5aae9eb029b9b0 /www/lib/ionic/js/ionic-angular.js
Initial commit
Diffstat (limited to 'www/lib/ionic/js/ionic-angular.js')
-rw-r--r--www/lib/ionic/js/ionic-angular.js14399
1 files changed, 14399 insertions, 0 deletions
diff --git a/www/lib/ionic/js/ionic-angular.js b/www/lib/ionic/js/ionic-angular.js
new file mode 100644
index 00000000..eba49d27
--- /dev/null
+++ b/www/lib/ionic/js/ionic-angular.js
@@ -0,0 +1,14399 @@
+/*!
+ * Copyright 2015 Drifty Co.
+ * http://drifty.com/
+ *
+ * Ionic, v1.3.3
+ * A powerful HTML5 mobile app framework.
+ * http://ionicframework.com/
+ *
+ * By @maxlynch, @benjsperry, @adamdbradley <3
+ *
+ * Licensed under the MIT license. Please see LICENSE for more information.
+ *
+ */
+
+(function() {
+/* eslint no-unused-vars:0 */
+var IonicModule = angular.module('ionic', ['ngAnimate', 'ngSanitize', 'ui.router', 'ngIOS9UIWebViewPatch']),
+ extend = angular.extend,
+ forEach = angular.forEach,
+ isDefined = angular.isDefined,
+ isNumber = angular.isNumber,
+ isString = angular.isString,
+ jqLite = angular.element,
+ noop = angular.noop;
+
+/**
+ * @ngdoc service
+ * @name $ionicActionSheet
+ * @module ionic
+ * @description
+ * The Action Sheet is a slide-up pane that lets the user choose from a set of options.
+ * Dangerous options are highlighted in red and made obvious.
+ *
+ * There are easy ways to cancel out of the action sheet, such as tapping the backdrop or even
+ * hitting escape on the keyboard for desktop testing.
+ *
+ * ![Action Sheet](http://ionicframework.com.s3.amazonaws.com/docs/controllers/actionSheet.gif)
+ *
+ * @usage
+ * To trigger an Action Sheet in your code, use the $ionicActionSheet service in your angular controllers:
+ *
+ * ```js
+ * angular.module('mySuperApp', ['ionic'])
+ * .controller(function($scope, $ionicActionSheet, $timeout) {
+ *
+ * // Triggered on a button click, or some other target
+ * $scope.show = function() {
+ *
+ * // Show the action sheet
+ * var hideSheet = $ionicActionSheet.show({
+ * buttons: [
+ * { text: '<b>Share</b> This' },
+ * { text: 'Move' }
+ * ],
+ * destructiveText: 'Delete',
+ * titleText: 'Modify your album',
+ * cancelText: 'Cancel',
+ * cancel: function() {
+ // add cancel code..
+ },
+ * buttonClicked: function(index) {
+ * return true;
+ * }
+ * });
+ *
+ * // For example's sake, hide the sheet after two seconds
+ * $timeout(function() {
+ * hideSheet();
+ * }, 2000);
+ *
+ * };
+ * });
+ * ```
+ *
+ */
+IonicModule
+.factory('$ionicActionSheet', [
+ '$rootScope',
+ '$compile',
+ '$animate',
+ '$timeout',
+ '$ionicTemplateLoader',
+ '$ionicPlatform',
+ '$ionicBody',
+ 'IONIC_BACK_PRIORITY',
+function($rootScope, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform, $ionicBody, IONIC_BACK_PRIORITY) {
+
+ return {
+ show: actionSheet
+ };
+
+ /**
+ * @ngdoc method
+ * @name $ionicActionSheet#show
+ * @description
+ * Load and return a new action sheet.
+ *
+ * A new isolated scope will be created for the
+ * action sheet and the new element will be appended into the body.
+ *
+ * @param {object} options The options for this ActionSheet. Properties:
+ *
+ * - `[Object]` `buttons` Which buttons to show. Each button is an object with a `text` field.
+ * - `{string}` `titleText` The title to show on the action sheet.
+ * - `{string=}` `cancelText` the text for a 'cancel' button on the action sheet.
+ * - `{string=}` `destructiveText` The text for a 'danger' on the action sheet.
+ * - `{function=}` `cancel` Called if the cancel button is pressed, the backdrop is tapped or
+ * the hardware back button is pressed.
+ * - `{function=}` `buttonClicked` Called when one of the non-destructive buttons is clicked,
+ * with the index of the button that was clicked and the button object. Return true to close
+ * the action sheet, or false to keep it opened.
+ * - `{function=}` `destructiveButtonClicked` Called when the destructive button is clicked.
+ * Return true to close the action sheet, or false to keep it opened.
+ * - `{boolean=}` `cancelOnStateChange` Whether to cancel the actionSheet when navigating
+ * to a new state. Default true.
+ * - `{string}` `cssClass` The custom CSS class name.
+ *
+ * @returns {function} `hideSheet` A function which, when called, hides & cancels the action sheet.
+ */
+ function actionSheet(opts) {
+ var scope = $rootScope.$new(true);
+
+ extend(scope, {
+ cancel: noop,
+ destructiveButtonClicked: noop,
+ buttonClicked: noop,
+ $deregisterBackButton: noop,
+ buttons: [],
+ cancelOnStateChange: true
+ }, opts || {});
+
+ function textForIcon(text) {
+ if (text && /icon/.test(text)) {
+ scope.$actionSheetHasIcon = true;
+ }
+ }
+
+ for (var x = 0; x < scope.buttons.length; x++) {
+ textForIcon(scope.buttons[x].text);
+ }
+ textForIcon(scope.cancelText);
+ textForIcon(scope.destructiveText);
+
+ // Compile the template
+ var element = scope.element = $compile('<ion-action-sheet ng-class="cssClass" buttons="buttons"></ion-action-sheet>')(scope);
+
+ // Grab the sheet element for animation
+ var sheetEl = jqLite(element[0].querySelector('.action-sheet-wrapper'));
+
+ var stateChangeListenDone = scope.cancelOnStateChange ?
+ $rootScope.$on('$stateChangeSuccess', function() { scope.cancel(); }) :
+ noop;
+
+ // removes the actionSheet from the screen
+ scope.removeSheet = function(done) {
+ if (scope.removed) return;
+
+ scope.removed = true;
+ sheetEl.removeClass('action-sheet-up');
+ $timeout(function() {
+ // wait to remove this due to a 300ms delay native
+ // click which would trigging whatever was underneath this
+ $ionicBody.removeClass('action-sheet-open');
+ }, 400);
+ scope.$deregisterBackButton();
+ stateChangeListenDone();
+
+ $animate.removeClass(element, 'active').then(function() {
+ scope.$destroy();
+ element.remove();
+ // scope.cancel.$scope is defined near the bottom
+ scope.cancel.$scope = sheetEl = null;
+ (done || noop)(opts.buttons);
+ });
+ };
+
+ scope.showSheet = function(done) {
+ if (scope.removed) return;
+
+ $ionicBody.append(element)
+ .addClass('action-sheet-open');
+
+ $animate.addClass(element, 'active').then(function() {
+ if (scope.removed) return;
+ (done || noop)();
+ });
+ $timeout(function() {
+ if (scope.removed) return;
+ sheetEl.addClass('action-sheet-up');
+ }, 20, false);
+ };
+
+ // registerBackButtonAction returns a callback to deregister the action
+ scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(
+ function() {
+ $timeout(scope.cancel);
+ },
+ IONIC_BACK_PRIORITY.actionSheet
+ );
+
+ // called when the user presses the cancel button
+ scope.cancel = function() {
+ // after the animation is out, call the cancel callback
+ scope.removeSheet(opts.cancel);
+ };
+
+ scope.buttonClicked = function(index) {
+ // Check if the button click event returned true, which means
+ // we can close the action sheet
+ if (opts.buttonClicked(index, opts.buttons[index]) === true) {
+ scope.removeSheet();
+ }
+ };
+
+ scope.destructiveButtonClicked = function() {
+ // Check if the destructive button click event returned true, which means
+ // we can close the action sheet
+ if (opts.destructiveButtonClicked() === true) {
+ scope.removeSheet();
+ }
+ };
+
+ scope.showSheet();
+
+ // Expose the scope on $ionicActionSheet's return value for the sake
+ // of testing it.
+ scope.cancel.$scope = scope;
+
+ return scope.cancel;
+ }
+}]);
+
+
+jqLite.prototype.addClass = function(cssClasses) {
+ var x, y, cssClass, el, splitClasses, existingClasses;
+ if (cssClasses && cssClasses != 'ng-scope' && cssClasses != 'ng-isolate-scope') {
+ for (x = 0; x < this.length; x++) {
+ el = this[x];
+ if (el.setAttribute) {
+
+ if (cssClasses.indexOf(' ') < 0 && el.classList.add) {
+ el.classList.add(cssClasses);
+ } else {
+ existingClasses = (' ' + (el.getAttribute('class') || '') + ' ')
+ .replace(/[\n\t]/g, " ");
+ splitClasses = cssClasses.split(' ');
+
+ for (y = 0; y < splitClasses.length; y++) {
+ cssClass = splitClasses[y].trim();
+ if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) {
+ existingClasses += cssClass + ' ';
+ }
+ }
+ el.setAttribute('class', existingClasses.trim());
+ }
+ }
+ }
+ }
+ return this;
+};
+
+jqLite.prototype.removeClass = function(cssClasses) {
+ var x, y, splitClasses, cssClass, el;
+ if (cssClasses) {
+ for (x = 0; x < this.length; x++) {
+ el = this[x];
+ if (el.getAttribute) {
+ if (cssClasses.indexOf(' ') < 0 && el.classList.remove) {
+ el.classList.remove(cssClasses);
+ } else {
+ splitClasses = cssClasses.split(' ');
+
+ for (y = 0; y < splitClasses.length; y++) {
+ cssClass = splitClasses[y];
+ el.setAttribute('class', (
+ (" " + (el.getAttribute('class') || '') + " ")
+ .replace(/[\n\t]/g, " ")
+ .replace(" " + cssClass.trim() + " ", " ")).trim()
+ );
+ }
+ }
+ }
+ }
+ }
+ return this;
+};
+
+/**
+ * @ngdoc service
+ * @name $ionicBackdrop
+ * @module ionic
+ * @description
+ * Shows and hides a backdrop over the UI. Appears behind popups, loading,
+ * and other overlays.
+ *
+ * Often, multiple UI components require a backdrop, but only one backdrop is
+ * ever needed in the DOM at a time.
+ *
+ * Therefore, each component that requires the backdrop to be shown calls
+ * `$ionicBackdrop.retain()` when it wants the backdrop, then `$ionicBackdrop.release()`
+ * when it is done with the backdrop.
+ *
+ * For each time `retain` is called, the backdrop will be shown until `release` is called.
+ *
+ * For example, if `retain` is called three times, the backdrop will be shown until `release`
+ * is called three times.
+ *
+ * **Notes:**
+ * - The backdrop service will broadcast 'backdrop.shown' and 'backdrop.hidden' events from the root scope,
+ * this is useful for alerting native components not in html.
+ *
+ * @usage
+ *
+ * ```js
+ * function MyController($scope, $ionicBackdrop, $timeout, $rootScope) {
+ * //Show a backdrop for one second
+ * $scope.action = function() {
+ * $ionicBackdrop.retain();
+ * $timeout(function() {
+ * $ionicBackdrop.release();
+ * }, 1000);
+ * };
+ *
+ * // Execute action on backdrop disappearing
+ * $scope.$on('backdrop.hidden', function() {
+ * // Execute action
+ * });
+ *
+ * // Execute action on backdrop appearing
+ * $scope.$on('backdrop.shown', function() {
+ * // Execute action
+ * });
+ *
+ * }
+ * ```
+ */
+IonicModule
+.factory('$ionicBackdrop', [
+ '$document', '$timeout', '$$rAF', '$rootScope',
+function($document, $timeout, $$rAF, $rootScope) {
+
+ var el = jqLite('<div class="backdrop">');
+ var backdropHolds = 0;
+
+ $document[0].body.appendChild(el[0]);
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicBackdrop#retain
+ * @description Retains the backdrop.
+ */
+ retain: retain,
+ /**
+ * @ngdoc method
+ * @name $ionicBackdrop#release
+ * @description
+ * Releases the backdrop.
+ */
+ release: release,
+
+ getElement: getElement,
+
+ // exposed for testing
+ _element: el
+ };
+
+ function retain() {
+ backdropHolds++;
+ if (backdropHolds === 1) {
+ el.addClass('visible');
+ $rootScope.$broadcast('backdrop.shown');
+ $$rAF(function() {
+ // If we're still at >0 backdropHolds after async...
+ if (backdropHolds >= 1) el.addClass('active');
+ });
+ }
+ }
+ function release() {
+ if (backdropHolds === 1) {
+ el.removeClass('active');
+ $rootScope.$broadcast('backdrop.hidden');
+ $timeout(function() {
+ // If we're still at 0 backdropHolds after async...
+ if (backdropHolds === 0) el.removeClass('visible');
+ }, 400, false);
+ }
+ backdropHolds = Math.max(0, backdropHolds - 1);
+ }
+
+ function getElement() {
+ return el;
+ }
+
+}]);
+
+/**
+ * @private
+ */
+IonicModule
+.factory('$ionicBind', ['$parse', '$interpolate', function($parse, $interpolate) {
+ var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
+ return function(scope, attrs, bindDefinition) {
+ forEach(bindDefinition || {}, function(definition, scopeName) {
+ //Adapted from angular.js $compile
+ var match = definition.match(LOCAL_REGEXP) || [],
+ attrName = match[3] || scopeName,
+ mode = match[1], // @, =, or &
+ parentGet,
+ unwatch;
+
+ switch (mode) {
+ case '@':
+ if (!attrs[attrName]) {
+ return;
+ }
+ attrs.$observe(attrName, function(value) {
+ scope[scopeName] = value;
+ });
+ // we trigger an interpolation to ensure
+ // the value is there for use immediately
+ if (attrs[attrName]) {
+ scope[scopeName] = $interpolate(attrs[attrName])(scope);
+ }
+ break;
+
+ case '=':
+ if (!attrs[attrName]) {
+ return;
+ }
+ unwatch = scope.$watch(attrs[attrName], function(value) {
+ scope[scopeName] = value;
+ });
+ //Destroy parent scope watcher when this scope is destroyed
+ scope.$on('$destroy', unwatch);
+ break;
+
+ case '&':
+ /* jshint -W044 */
+ if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) {
+ throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' +
+ attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.');
+ }
+ parentGet = $parse(attrs[attrName]);
+ scope[scopeName] = function(locals) {
+ return parentGet(scope, locals);
+ };
+ break;
+ }
+ });
+ };
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicBody
+ * @module ionic
+ * @description An angular utility service to easily and efficiently
+ * add and remove CSS classes from the document's body element.
+ */
+IonicModule
+.factory('$ionicBody', ['$document', function($document) {
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicBody#addClass
+ * @description Add a class to the document's body element.
+ * @param {string} class Each argument will be added to the body element.
+ * @returns {$ionicBody} The $ionicBody service so methods can be chained.
+ */
+ addClass: function() {
+ for (var x = 0; x < arguments.length; x++) {
+ $document[0].body.classList.add(arguments[x]);
+ }
+ return this;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicBody#removeClass
+ * @description Remove a class from the document's body element.
+ * @param {string} class Each argument will be removed from the body element.
+ * @returns {$ionicBody} The $ionicBody service so methods can be chained.
+ */
+ removeClass: function() {
+ for (var x = 0; x < arguments.length; x++) {
+ $document[0].body.classList.remove(arguments[x]);
+ }
+ return this;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicBody#enableClass
+ * @description Similar to the `add` method, except the first parameter accepts a boolean
+ * value determining if the class should be added or removed. Rather than writing user code,
+ * such as "if true then add the class, else then remove the class", this method can be
+ * given a true or false value which reduces redundant code.
+ * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed.
+ * @param {string} class Each remaining argument would be added or removed depending on
+ * the first argument.
+ * @returns {$ionicBody} The $ionicBody service so methods can be chained.
+ */
+ enableClass: function(shouldEnableClass) {
+ var args = Array.prototype.slice.call(arguments).slice(1);
+ if (shouldEnableClass) {
+ this.addClass.apply(this, args);
+ } else {
+ this.removeClass.apply(this, args);
+ }
+ return this;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicBody#append
+ * @description Append a child to the document's body.
+ * @param {element} element The element to be appended to the body. The passed in element
+ * can be either a jqLite element, or a DOM element.
+ * @returns {$ionicBody} The $ionicBody service so methods can be chained.
+ */
+ append: function(ele) {
+ $document[0].body.appendChild(ele.length ? ele[0] : ele);
+ return this;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicBody#get
+ * @description Get the document's body element.
+ * @returns {element} Returns the document's body element.
+ */
+ get: function() {
+ return $document[0].body;
+ }
+ };
+}]);
+
+IonicModule
+.factory('$ionicClickBlock', [
+ '$document',
+ '$ionicBody',
+ '$timeout',
+function($document, $ionicBody, $timeout) {
+ var CSS_HIDE = 'click-block-hide';
+ var cbEle, fallbackTimer, pendingShow;
+
+ function preventClick(ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
+
+ function addClickBlock() {
+ if (pendingShow) {
+ if (cbEle) {
+ cbEle.classList.remove(CSS_HIDE);
+ } else {
+ cbEle = $document[0].createElement('div');
+ cbEle.className = 'click-block';
+ $ionicBody.append(cbEle);
+ cbEle.addEventListener('touchstart', preventClick);
+ cbEle.addEventListener('mousedown', preventClick);
+ }
+ pendingShow = false;
+ }
+ }
+
+ function removeClickBlock() {
+ cbEle && cbEle.classList.add(CSS_HIDE);
+ }
+
+ return {
+ show: function(autoExpire) {
+ pendingShow = true;
+ $timeout.cancel(fallbackTimer);
+ fallbackTimer = $timeout(this.hide, autoExpire || 310, false);
+ addClickBlock();
+ },
+ hide: function() {
+ pendingShow = false;
+ $timeout.cancel(fallbackTimer);
+ removeClickBlock();
+ }
+ };
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicGesture
+ * @module ionic
+ * @description An angular service exposing ionic
+ * {@link ionic.utility:ionic.EventController}'s gestures.
+ */
+IonicModule
+.factory('$ionicGesture', [function() {
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicGesture#on
+ * @description Add an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#onGesture}.
+ * @param {string} eventType The gesture event to listen for.
+ * @param {function(e)} callback The function to call when the gesture
+ * happens.
+ * @param {element} $element The angular element to listen for the event on.
+ * @param {object} options object.
+ * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on).
+ */
+ on: function(eventType, cb, $element, options) {
+ return window.ionic.onGesture(eventType, cb, $element[0], options);
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicGesture#off
+ * @description Remove an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#offGesture}.
+ * @param {ionic.Gesture} gesture The gesture that should be removed.
+ * @param {string} eventType The gesture event to remove the listener for.
+ * @param {function(e)} callback The listener to remove.
+ */
+ off: function(gesture, eventType, cb) {
+ return window.ionic.offGesture(gesture, eventType, cb);
+ }
+ };
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicHistory
+ * @module ionic
+ * @description
+ * $ionicHistory keeps track of views as the user navigates through an app. Similar to the way a
+ * browser behaves, an Ionic app is able to keep track of the previous view, the current view, and
+ * the forward view (if there is one). However, a typical web browser only keeps track of one
+ * history stack in a linear fashion.
+ *
+ * Unlike a traditional browser environment, apps and webapps have parallel independent histories,
+ * such as with tabs. Should a user navigate few pages deep on one tab, and then switch to a new
+ * tab and back, the back button relates not to the previous tab, but to the previous pages
+ * visited within _that_ tab.
+ *
+ * `$ionicHistory` facilitates this parallel history architecture.
+ */
+
+IonicModule
+.factory('$ionicHistory', [
+ '$rootScope',
+ '$state',
+ '$location',
+ '$window',
+ '$timeout',
+ '$ionicViewSwitcher',
+ '$ionicNavViewDelegate',
+function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ionicNavViewDelegate) {
+
+ // history actions while navigating views
+ var ACTION_INITIAL_VIEW = 'initialView';
+ var ACTION_NEW_VIEW = 'newView';
+ var ACTION_MOVE_BACK = 'moveBack';
+ var ACTION_MOVE_FORWARD = 'moveForward';
+
+ // direction of navigation
+ var DIRECTION_BACK = 'back';
+ var DIRECTION_FORWARD = 'forward';
+ var DIRECTION_ENTER = 'enter';
+ var DIRECTION_EXIT = 'exit';
+ var DIRECTION_SWAP = 'swap';
+ var DIRECTION_NONE = 'none';
+
+ var stateChangeCounter = 0;
+ var lastStateId, nextViewOptions, deregisterStateChangeListener, nextViewExpireTimer, forcedNav;
+
+ var viewHistory = {
+ histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1 } },
+ views: {},
+ backView: null,
+ forwardView: null,
+ currentView: null
+ };
+
+ var View = function() {};
+ View.prototype.initialize = function(data) {
+ if (data) {
+ for (var name in data) this[name] = data[name];
+ return this;
+ }
+ return null;
+ };
+ View.prototype.go = function() {
+
+ if (this.stateName) {
+ return $state.go(this.stateName, this.stateParams);
+ }
+
+ if (this.url && this.url !== $location.url()) {
+
+ if (viewHistory.backView === this) {
+ return $window.history.go(-1);
+ } else if (viewHistory.forwardView === this) {
+ return $window.history.go(1);
+ }
+
+ $location.url(this.url);
+ }
+
+ return null;
+ };
+ View.prototype.destroy = function() {
+ if (this.scope) {
+ this.scope.$destroy && this.scope.$destroy();
+ this.scope = null;
+ }
+ };
+
+
+ function getViewById(viewId) {
+ return (viewId ? viewHistory.views[ viewId ] : null);
+ }
+
+ function getBackView(view) {
+ return (view ? getViewById(view.backViewId) : null);
+ }
+
+ function getForwardView(view) {
+ return (view ? getViewById(view.forwardViewId) : null);
+ }
+
+ function getHistoryById(historyId) {
+ return (historyId ? viewHistory.histories[ historyId ] : null);
+ }
+
+ function getHistory(scope) {
+ var histObj = getParentHistoryObj(scope);
+
+ if (!viewHistory.histories[ histObj.historyId ]) {
+ // this history object exists in parent scope, but doesn't
+ // exist in the history data yet
+ viewHistory.histories[ histObj.historyId ] = {
+ historyId: histObj.historyId,
+ parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId,
+ stack: [],
+ cursor: -1
+ };
+ }
+ return getHistoryById(histObj.historyId);
+ }
+
+ function getParentHistoryObj(scope) {
+ var parentScope = scope;
+ while (parentScope) {
+ if (parentScope.hasOwnProperty('$historyId')) {
+ // this parent scope has a historyId
+ return { historyId: parentScope.$historyId, scope: parentScope };
+ }
+ // nothing found keep climbing up
+ parentScope = parentScope.$parent;
+ }
+ // no history for the parent, use the root
+ return { historyId: 'root', scope: $rootScope };
+ }
+
+ function setNavViews(viewId) {
+ viewHistory.currentView = getViewById(viewId);
+ viewHistory.backView = getBackView(viewHistory.currentView);
+ viewHistory.forwardView = getForwardView(viewHistory.currentView);
+ }
+
+ function getCurrentStateId() {
+ var id;
+ if ($state && $state.current && $state.current.name) {
+ id = $state.current.name;
+ if ($state.params) {
+ for (var key in $state.params) {
+ if ($state.params.hasOwnProperty(key) && $state.params[key]) {
+ id += "_" + key + "=" + $state.params[key];
+ }
+ }
+ }
+ return id;
+ }
+ // if something goes wrong make sure its got a unique stateId
+ return ionic.Utils.nextUid();
+ }
+
+ function getCurrentStateParams() {
+ var rtn;
+ if ($state && $state.params) {
+ for (var key in $state.params) {
+ if ($state.params.hasOwnProperty(key)) {
+ rtn = rtn || {};
+ rtn[key] = $state.params[key];
+ }
+ }
+ }
+ return rtn;
+ }
+
+
+ return {
+
+ register: function(parentScope, viewLocals) {
+
+ var currentStateId = getCurrentStateId(),
+ hist = getHistory(parentScope),
+ currentView = viewHistory.currentView,
+ backView = viewHistory.backView,
+ forwardView = viewHistory.forwardView,
+ viewId = null,
+ action = null,
+ direction = DIRECTION_NONE,
+ historyId = hist.historyId,
+ url = $location.url(),
+ tmp, x, ele;
+
+ if (lastStateId !== currentStateId) {
+ lastStateId = currentStateId;
+ stateChangeCounter++;
+ }
+
+ if (forcedNav) {
+ // we've previously set exactly what to do
+ viewId = forcedNav.viewId;
+ action = forcedNav.action;
+ direction = forcedNav.direction;
+ forcedNav = null;
+
+ } else if (backView && backView.stateId === currentStateId) {
+ // they went back one, set the old current view as a forward view
+ viewId = backView.viewId;
+ historyId = backView.historyId;
+ action = ACTION_MOVE_BACK;
+ if (backView.historyId === currentView.historyId) {
+ // went back in the same history
+ direction = DIRECTION_BACK;
+
+ } else if (currentView) {
+ direction = DIRECTION_EXIT;
+
+ tmp = getHistoryById(backView.historyId);
+ if (tmp && tmp.parentHistoryId === currentView.historyId) {
+ direction = DIRECTION_ENTER;
+
+ } else {
+ tmp = getHistoryById(currentView.historyId);
+ if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
+ direction = DIRECTION_SWAP;
+ }
+ }
+ }
+
+ } else if (forwardView && forwardView.stateId === currentStateId) {
+ // they went to the forward one, set the forward view to no longer a forward view
+ viewId = forwardView.viewId;
+ historyId = forwardView.historyId;
+ action = ACTION_MOVE_FORWARD;
+ if (forwardView.historyId === currentView.historyId) {
+ direction = DIRECTION_FORWARD;
+
+ } else if (currentView) {
+ direction = DIRECTION_EXIT;
+
+ if (currentView.historyId === hist.parentHistoryId) {
+ direction = DIRECTION_ENTER;
+
+ } else {
+ tmp = getHistoryById(currentView.historyId);
+ if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
+ direction = DIRECTION_SWAP;
+ }
+ }
+ }
+
+ tmp = getParentHistoryObj(parentScope);
+ if (forwardView.historyId && tmp.scope) {
+ // if a history has already been created by the forward view then make sure it stays the same
+ tmp.scope.$historyId = forwardView.historyId;
+ historyId = forwardView.historyId;
+ }
+
+ } else if (currentView && currentView.historyId !== historyId &&
+ hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length &&
+ hist.stack[hist.cursor].stateId === currentStateId) {
+ // they just changed to a different history and the history already has views in it
+ var switchToView = hist.stack[hist.cursor];
+ viewId = switchToView.viewId;
+ historyId = switchToView.historyId;
+ action = ACTION_MOVE_BACK;
+ direction = DIRECTION_SWAP;
+
+ tmp = getHistoryById(currentView.historyId);
+ if (tmp && tmp.parentHistoryId === historyId) {
+ direction = DIRECTION_EXIT;
+
+ } else {
+ tmp = getHistoryById(historyId);
+ if (tmp && tmp.parentHistoryId === currentView.historyId) {
+ direction = DIRECTION_ENTER;
+ }
+ }
+
+ // if switching to a different history, and the history of the view we're switching
+ // to has an existing back view from a different history than itself, then
+ // it's back view would be better represented using the current view as its back view
+ tmp = getViewById(switchToView.backViewId);
+ if (tmp && switchToView.historyId !== tmp.historyId) {
+ // the new view is being removed from it's old position in the history and being placed at the top,
+ // so we need to update any views that reference it as a backview, otherwise there will be infinitely loops
+ var viewIds = Object.keys(viewHistory.views);
+ viewIds.forEach(function(viewId) {
+ var view = viewHistory.views[viewId];
+ if ((view.backViewId === switchToView.viewId) && (view.historyId !== switchToView.historyId)) {
+ view.backViewId = null;
+ }
+ });
+
+ hist.stack[hist.cursor].backViewId = currentView.viewId;
+ }
+
+ } else {
+
+ // create an element from the viewLocals template
+ ele = $ionicViewSwitcher.createViewEle(viewLocals);
+ if (this.isAbstractEle(ele, viewLocals)) {
+ return {
+ action: 'abstractView',
+ direction: DIRECTION_NONE,
+ ele: ele
+ };
+ }
+
+ // set a new unique viewId
+ viewId = ionic.Utils.nextUid();
+
+ if (currentView) {
+ // set the forward view if there is a current view (ie: if its not the first view)
+ currentView.forwardViewId = viewId;
+
+ action = ACTION_NEW_VIEW;
+
+ // check if there is a new forward view within the same history
+ if (forwardView && currentView.stateId !== forwardView.stateId &&
+ currentView.historyId === forwardView.historyId) {
+ // they navigated to a new view but the stack already has a forward view
+ // since its a new view remove any forwards that existed
+ tmp = getHistoryById(forwardView.historyId);
+ if (tmp) {
+ // the forward has a history
+ for (x = tmp.stack.length - 1; x >= forwardView.index; x--) {
+ // starting from the end destroy all forwards in this history from this point
+ var stackItem = tmp.stack[x];
+ stackItem && stackItem.destroy && stackItem.destroy();
+ tmp.stack.splice(x);
+ }
+ historyId = forwardView.historyId;
+ }
+ }
+
+ // its only moving forward if its in the same history
+ if (hist.historyId === currentView.historyId) {
+ direction = DIRECTION_FORWARD;
+
+ } else if (currentView.historyId !== hist.historyId) {
+ // DB: this is a new view in a different tab
+ direction = DIRECTION_ENTER;
+
+ tmp = getHistoryById(currentView.historyId);
+ if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
+ direction = DIRECTION_SWAP;
+
+ } else {
+ tmp = getHistoryById(tmp.parentHistoryId);
+ if (tmp && tmp.historyId === hist.historyId) {
+ direction = DIRECTION_EXIT;
+ }
+ }
+ }
+
+ } else {
+ // there's no current view, so this must be the initial view
+ action = ACTION_INITIAL_VIEW;
+ }
+
+ if (stateChangeCounter < 2) {
+ // views that were spun up on the first load should not animate
+ direction = DIRECTION_NONE;
+ }
+
+ // add the new view
+ viewHistory.views[viewId] = this.createView({
+ viewId: viewId,
+ index: hist.stack.length,
+ historyId: hist.historyId,
+ backViewId: (currentView && currentView.viewId ? currentView.viewId : null),
+ forwardViewId: null,
+ stateId: currentStateId,
+ stateName: this.currentStateName(),
+ stateParams: getCurrentStateParams(),
+ url: url,
+ canSwipeBack: canSwipeBack(ele, viewLocals)
+ });
+
+ // add the new view to this history's stack
+ hist.stack.push(viewHistory.views[viewId]);
+ }
+
+ deregisterStateChangeListener && deregisterStateChangeListener();
+ $timeout.cancel(nextViewExpireTimer);
+ if (nextViewOptions) {
+ if (nextViewOptions.disableAnimate) direction = DIRECTION_NONE;
+ if (nextViewOptions.disableBack) viewHistory.views[viewId].backViewId = null;
+ if (nextViewOptions.historyRoot) {
+ for (x = 0; x < hist.stack.length; x++) {
+ if (hist.stack[x].viewId === viewId) {
+ hist.stack[x].index = 0;
+ hist.stack[x].backViewId = hist.stack[x].forwardViewId = null;
+ } else {
+ delete viewHistory.views[hist.stack[x].viewId];
+ }
+ }
+ hist.stack = [viewHistory.views[viewId]];
+ }
+ nextViewOptions = null;
+ }
+
+ setNavViews(viewId);
+
+ if (viewHistory.backView && historyId == viewHistory.backView.historyId && currentStateId == viewHistory.backView.stateId && url == viewHistory.backView.url) {
+ for (x = 0; x < hist.stack.length; x++) {
+ if (hist.stack[x].viewId == viewId) {
+ action = 'dupNav';
+ direction = DIRECTION_NONE;
+ if (x > 0) {
+ hist.stack[x - 1].forwardViewId = null;
+ }
+ viewHistory.forwardView = null;
+ viewHistory.currentView.index = viewHistory.backView.index;
+ viewHistory.currentView.backViewId = viewHistory.backView.backViewId;
+ viewHistory.backView = getBackView(viewHistory.backView);
+ hist.stack.splice(x, 1);
+ break;
+ }
+ }
+ }
+
+ hist.cursor = viewHistory.currentView.index;
+
+ return {
+ viewId: viewId,
+ action: action,
+ direction: direction,
+ historyId: historyId,
+ enableBack: this.enabledBack(viewHistory.currentView),
+ isHistoryRoot: (viewHistory.currentView.index === 0),
+ ele: ele
+ };
+ },
+
+ registerHistory: function(scope) {
+ scope.$historyId = ionic.Utils.nextUid();
+ },
+
+ createView: function(data) {
+ var newView = new View();
+ return newView.initialize(data);
+ },
+
+ getViewById: getViewById,
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#viewHistory
+ * @description The app's view history data, such as all the views and histories, along
+ * with how they are ordered and linked together within the navigation stack.
+ * @returns {object} Returns an object containing the apps view history data.
+ */
+ viewHistory: function() {
+ return viewHistory;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#currentView
+ * @description The app's current view.
+ * @returns {object} Returns the current view.
+ */
+ currentView: function(view) {
+ if (arguments.length) {
+ viewHistory.currentView = view;
+ }
+ return viewHistory.currentView;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#currentHistoryId
+ * @description The ID of the history stack which is the parent container of the current view.
+ * @returns {string} Returns the current history ID.
+ */
+ currentHistoryId: function() {
+ return viewHistory.currentView ? viewHistory.currentView.historyId : null;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#currentTitle
+ * @description Gets and sets the current view's title.
+ * @param {string=} val The title to update the current view with.
+ * @returns {string} Returns the current view's title.
+ */
+ currentTitle: function(val) {
+ if (viewHistory.currentView) {
+ if (arguments.length) {
+ viewHistory.currentView.title = val;
+ }
+ return viewHistory.currentView.title;
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#backView
+ * @description Returns the view that was before the current view in the history stack.
+ * If the user navigated from View A to View B, then View A would be the back view, and
+ * View B would be the current view.
+ * @returns {object} Returns the back view.
+ */
+ backView: function(view) {
+ if (arguments.length) {
+ viewHistory.backView = view;
+ }
+ return viewHistory.backView;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#backTitle
+ * @description Gets the back view's title.
+ * @returns {string} Returns the back view's title.
+ */
+ backTitle: function(view) {
+ var backView = (view && getViewById(view.backViewId)) || viewHistory.backView;
+ return backView && backView.title;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#forwardView
+ * @description Returns the view that was in front of the current view in the history stack.
+ * A forward view would exist if the user navigated from View A to View B, then
+ * navigated back to View A. At this point then View B would be the forward view, and View
+ * A would be the current view.
+ * @returns {object} Returns the forward view.
+ */
+ forwardView: function(view) {
+ if (arguments.length) {
+ viewHistory.forwardView = view;
+ }
+ return viewHistory.forwardView;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#currentStateName
+ * @description Returns the current state name.
+ * @returns {string}
+ */
+ currentStateName: function() {
+ return ($state && $state.current ? $state.current.name : null);
+ },
+
+ isCurrentStateNavView: function(navView) {
+ return !!($state && $state.current && $state.current.views && $state.current.views[navView]);
+ },
+
+ goToHistoryRoot: function(historyId) {
+ if (historyId) {
+ var hist = getHistoryById(historyId);
+ if (hist && hist.stack.length) {
+ if (viewHistory.currentView && viewHistory.currentView.viewId === hist.stack[0].viewId) {
+ return;
+ }
+ forcedNav = {
+ viewId: hist.stack[0].viewId,
+ action: ACTION_MOVE_BACK,
+ direction: DIRECTION_BACK
+ };
+ hist.stack[0].go();
+ }
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#goBack
+ * @param {number=} backCount Optional negative integer setting how many views to go
+ * back. By default it'll go back one view by using the value `-1`. To go back two
+ * views you would use `-2`. If the number goes farther back than the number of views
+ * in the current history's stack then it'll go to the first view in the current history's
+ * stack. If the number is zero or greater then it'll do nothing. It also does not
+ * cross history stacks, meaning it can only go as far back as the current history.
+ * @description Navigates the app to the back view, if a back view exists.
+ */
+ goBack: function(backCount) {
+ if (isDefined(backCount) && backCount !== -1) {
+ if (backCount > -1) return;
+
+ var currentHistory = viewHistory.histories[this.currentHistoryId()];
+ var newCursor = currentHistory.cursor + backCount + 1;
+ if (newCursor < 1) {
+ newCursor = 1;
+ }
+
+ currentHistory.cursor = newCursor;
+ setNavViews(currentHistory.stack[newCursor].viewId);
+
+ var cursor = newCursor - 1;
+ var clearStateIds = [];
+ var fwdView = getViewById(currentHistory.stack[cursor].forwardViewId);
+ while (fwdView) {
+ clearStateIds.push(fwdView.stateId || fwdView.viewId);
+ cursor++;
+ if (cursor >= currentHistory.stack.length) break;
+ fwdView = getViewById(currentHistory.stack[cursor].forwardViewId);
+ }
+
+ var self = this;
+ if (clearStateIds.length) {
+ $timeout(function() {
+ self.clearCache(clearStateIds);
+ }, 300);
+ }
+ }
+
+ viewHistory.backView && viewHistory.backView.go();
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#removeBackView
+ * @description Remove the previous view from the history completely, including the
+ * cached element and scope (if they exist).
+ */
+ removeBackView: function() {
+ var self = this;
+ var currentHistory = viewHistory.histories[this.currentHistoryId()];
+ var currentCursor = currentHistory.cursor;
+
+ var currentView = currentHistory.stack[currentCursor];
+ var backView = currentHistory.stack[currentCursor - 1];
+ var replacementView = currentHistory.stack[currentCursor - 2];
+
+ // fail if we dont have enough views in the history
+ if (!backView || !replacementView) {
+ return;
+ }
+
+ // remove the old backView and the cached element/scope
+ currentHistory.stack.splice(currentCursor - 1, 1);
+ self.clearCache([backView.viewId]);
+ // make the replacementView and currentView point to each other (bypass the old backView)
+ currentView.backViewId = replacementView.viewId;
+ currentView.index = currentView.index - 1;
+ replacementView.forwardViewId = currentView.viewId;
+ // update the cursor and set new backView
+ viewHistory.backView = replacementView;
+ currentHistory.currentCursor += -1;
+ },
+
+ enabledBack: function(view) {
+ var backView = getBackView(view);
+ return !!(backView && backView.historyId === view.historyId);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#clearHistory
+ * @description Clears out the app's entire history, except for the current view.
+ */
+ clearHistory: function() {
+ var
+ histories = viewHistory.histories,
+ currentView = viewHistory.currentView;
+
+ if (histories) {
+ for (var historyId in histories) {
+
+ if (histories[historyId].stack) {
+ histories[historyId].stack = [];
+ histories[historyId].cursor = -1;
+ }
+
+ if (currentView && currentView.historyId === historyId) {
+ currentView.backViewId = currentView.forwardViewId = null;
+ histories[historyId].stack.push(currentView);
+ } else if (histories[historyId].destroy) {
+ histories[historyId].destroy();
+ }
+
+ }
+ }
+
+ for (var viewId in viewHistory.views) {
+ if (viewId !== currentView.viewId) {
+ delete viewHistory.views[viewId];
+ }
+ }
+
+ if (currentView) {
+ setNavViews(currentView.viewId);
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#clearCache
+ * @return promise
+ * @description Removes all cached views within every {@link ionic.directive:ionNavView}.
+ * This both removes the view element from the DOM, and destroy it's scope.
+ */
+ clearCache: function(stateIds) {
+ return $timeout(function() {
+ $ionicNavViewDelegate._instances.forEach(function(instance) {
+ instance.clearCache(stateIds);
+ });
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicHistory#nextViewOptions
+ * @description Sets options for the next view. This method can be useful to override
+ * certain view/transition defaults right before a view transition happens. For example,
+ * the {@link ionic.directive:menuClose} directive uses this method internally to ensure
+ * an animated view transition does not happen when a side menu is open, and also sets
+ * the next view as the root of its history stack. After the transition these options
+ * are set back to null.
+ *
+ * Available options:
+ *
+ * * `disableAnimate`: Do not animate the next transition.
+ * * `disableBack`: The next view should forget its back view, and set it to null.
+ * * `historyRoot`: The next view should become the root view in its history stack.
+ *
+ * ```js
+ * $ionicHistory.nextViewOptions({
+ * disableAnimate: true,
+ * disableBack: true
+ * });
+ * ```
+ */
+ nextViewOptions: function(opts) {
+ deregisterStateChangeListener && deregisterStateChangeListener();
+ if (arguments.length) {
+ $timeout.cancel(nextViewExpireTimer);
+ if (opts === null) {
+ nextViewOptions = opts;
+ } else {
+ nextViewOptions = nextViewOptions || {};
+ extend(nextViewOptions, opts);
+ if (nextViewOptions.expire) {
+ deregisterStateChangeListener = $rootScope.$on('$stateChangeSuccess', function() {
+ nextViewExpireTimer = $timeout(function() {
+ nextViewOptions = null;
+ }, nextViewOptions.expire);
+ });
+ }
+ }
+ }
+ return nextViewOptions;
+ },
+
+ isAbstractEle: function(ele, viewLocals) {
+ if (viewLocals && viewLocals.$$state && viewLocals.$$state.self['abstract']) {
+ return true;
+ }
+ return !!(ele && (isAbstractTag(ele) || isAbstractTag(ele.children())));
+ },
+
+ isActiveScope: function(scope) {
+ if (!scope) return false;
+
+ var climbScope = scope;
+ var currentHistoryId = this.currentHistoryId();
+ var foundHistoryId;
+
+ while (climbScope) {
+ if (climbScope.$$disconnected) {
+ return false;
+ }
+
+ if (!foundHistoryId && climbScope.hasOwnProperty('$historyId')) {
+ foundHistoryId = true;
+ }
+
+ if (currentHistoryId) {
+ if (climbScope.hasOwnProperty('$historyId') && currentHistoryId == climbScope.$historyId) {
+ return true;
+ }
+ if (climbScope.hasOwnProperty('$activeHistoryId')) {
+ if (currentHistoryId == climbScope.$activeHistoryId) {
+ if (climbScope.hasOwnProperty('$historyId')) {
+ return true;
+ }
+ if (!foundHistoryId) {
+ return true;
+ }
+ }
+ }
+ }
+
+ if (foundHistoryId && climbScope.hasOwnProperty('$activeHistoryId')) {
+ foundHistoryId = false;
+ }
+
+ climbScope = climbScope.$parent;
+ }
+
+ return currentHistoryId ? currentHistoryId == 'root' : true;
+ }
+
+ };
+
+ function isAbstractTag(ele) {
+ return ele && ele.length && /ion-side-menus|ion-tabs/i.test(ele[0].tagName);
+ }
+
+ function canSwipeBack(ele, viewLocals) {
+ if (viewLocals && viewLocals.$$state && viewLocals.$$state.self.canSwipeBack === false) {
+ return false;
+ }
+ if (ele && ele.attr('can-swipe-back') === 'false') {
+ return false;
+ }
+ var eleChild = ele.find('ion-view');
+ if (eleChild && eleChild.attr('can-swipe-back') === 'false') {
+ return false;
+ }
+ return true;
+ }
+
+}])
+
+.run([
+ '$rootScope',
+ '$state',
+ '$location',
+ '$document',
+ '$ionicPlatform',
+ '$ionicHistory',
+ 'IONIC_BACK_PRIORITY',
+function($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory, IONIC_BACK_PRIORITY) {
+
+ // always reset the keyboard state when change stage
+ $rootScope.$on('$ionicView.beforeEnter', function() {
+ ionic.keyboard && ionic.keyboard.hide && ionic.keyboard.hide();
+ });
+
+ $rootScope.$on('$ionicHistory.change', function(e, data) {
+ if (!data) return null;
+
+ var viewHistory = $ionicHistory.viewHistory();
+
+ var hist = (data.historyId ? viewHistory.histories[ data.historyId ] : null);
+ if (hist && hist.cursor > -1 && hist.cursor < hist.stack.length) {
+ // the history they're going to already exists
+ // go to it's last view in its stack
+ var view = hist.stack[ hist.cursor ];
+ return view.go(data);
+ }
+
+ // this history does not have a URL, but it does have a uiSref
+ // figure out its URL from the uiSref
+ if (!data.url && data.uiSref) {
+ data.url = $state.href(data.uiSref);
+ }
+
+ if (data.url) {
+ // don't let it start with a #, messes with $location.url()
+ if (data.url.indexOf('#') === 0) {
+ data.url = data.url.replace('#', '');
+ }
+ if (data.url !== $location.url()) {
+ // we've got a good URL, ready GO!
+ $location.url(data.url);
+ }
+ }
+ });
+
+ $rootScope.$ionicGoBack = function(backCount) {
+ $ionicHistory.goBack(backCount);
+ };
+
+ // Set the document title when a new view is shown
+ $rootScope.$on('$ionicView.afterEnter', function(ev, data) {
+ if (data && data.title) {
+ $document[0].title = data.title;
+ }
+ });
+
+ // Triggered when devices with a hardware back button (Android) is clicked by the user
+ // This is a Cordova/Phonegap platform specifc method
+ function onHardwareBackButton(e) {
+ var backView = $ionicHistory.backView();
+ if (backView) {
+ // there is a back view, go to it
+ backView.go();
+ } else {
+ // there is no back view, so close the app instead
+ ionic.Platform.exitApp();
+ }
+ e.preventDefault();
+ return false;
+ }
+ $ionicPlatform.registerBackButtonAction(
+ onHardwareBackButton,
+ IONIC_BACK_PRIORITY.view
+ );
+
+}]);
+
+/**
+ * @ngdoc provider
+ * @name $ionicConfigProvider
+ * @module ionic
+ * @description
+ * Ionic automatically takes platform configurations into account to adjust things like what
+ * transition style to use and whether tab icons should show on the top or bottom. For example,
+ * iOS will move forward by transitioning the entering view from right to center and the leaving
+ * view from center to left. However, Android will transition with the entering view going from
+ * bottom to center, covering the previous view, which remains stationary. It should be noted
+ * that when a platform is not iOS or Android, then it'll default to iOS. So if you are
+ * developing on a desktop browser, it's going to take on iOS default configs.
+ *
+ * These configs can be changed using the `$ionicConfigProvider` during the configuration phase
+ * of your app. Additionally, `$ionicConfig` can also set and get config values during the run
+ * phase and within the app itself.
+ *
+ * By default, all base config variables are set to `'platform'`, which means it'll take on the
+ * default config of the platform on which it's running. Config variables can be set at this
+ * level so all platforms follow the same setting, rather than its platform config.
+ * The following code would set the same config variable for all platforms:
+ *
+ * ```js
+ * $ionicConfigProvider.views.maxCache(10);
+ * ```
+ *
+ * Additionally, each platform can have its own config within the `$ionicConfigProvider.platform`
+ * property. The config below would only apply to Android devices.
+ *
+ * ```js
+ * $ionicConfigProvider.platform.android.views.maxCache(5);
+ * ```
+ *
+ * @usage
+ * ```js
+ * var myApp = angular.module('reallyCoolApp', ['ionic']);
+ *
+ * myApp.config(function($ionicConfigProvider) {
+ * $ionicConfigProvider.views.maxCache(5);
+ *
+ * // note that you can also chain configs
+ * $ionicConfigProvider.backButton.text('Go Back').icon('ion-chevron-left');
+ * });
+ * ```
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#views.transition
+ * @description Animation style when transitioning between views. Default `platform`.
+ *
+ * @param {string} transition Which style of view transitioning to use.
+ *
+ * * `platform`: Dynamically choose the correct transition style depending on the platform
+ * the app is running from. If the platform is not `ios` or `android` then it will default
+ * to `ios`.
+ * * `ios`: iOS style transition.
+ * * `android`: Android style transition.
+ * * `none`: Do not perform animated transitions.
+ *
+ * @returns {string} value
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#views.maxCache
+ * @description Maximum number of view elements to cache in the DOM. When the max number is
+ * exceeded, the view with the longest time period since it was accessed is removed. Views that
+ * stay in the DOM cache the view's scope, current state, and scroll position. The scope is
+ * disconnected from the `$watch` cycle when it is cached and reconnected when it enters again.
+ * When the maximum cache is `0`, the leaving view's element will be removed from the DOM after
+ * each view transition, and the next time the same view is shown, it will have to re-compile,
+ * attach to the DOM, and link the element again. This disables caching, in effect.
+ * @param {number} maxNumber Maximum number of views to retain. Default `10`.
+ * @returns {number} How many views Ionic will hold onto until the a view is removed.
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#views.forwardCache
+ * @description By default, when navigating, views that were recently visited are cached, and
+ * the same instance data and DOM elements are referenced when navigating back. However, when
+ * navigating back in the history, the "forward" views are removed from the cache. If you
+ * navigate forward to the same view again, it'll create a new DOM element and controller
+ * instance. Basically, any forward views are reset each time. Set this config to `true` to have
+ * forward views cached and not reset on each load.
+ * @param {boolean} value
+ * @returns {boolean}
+ */
+
+ /**
+ * @ngdoc method
+ * @name $ionicConfigProvider#views.swipeBackEnabled
+ * @description By default on iOS devices, swipe to go back functionality is enabled by default.
+ * This method can be used to disable it globally, or on a per-view basis.
+ * Note: This functionality is only supported on iOS.
+ * @param {boolean} value
+ * @returns {boolean}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#scrolling.jsScrolling
+ * @description Whether to use JS or Native scrolling. Defaults to native scrolling. Setting this to
+ * `true` has the same effect as setting each `ion-content` to have `overflow-scroll='false'`.
+ * @param {boolean} value Defaults to `false` as of Ionic 1.2
+ * @returns {boolean}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#backButton.icon
+ * @description Back button icon.
+ * @param {string} value
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#backButton.text
+ * @description Back button text.
+ * @param {string} value Defaults to `Back`.
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#backButton.previousTitleText
+ * @description If the previous title text should become the back button text. This
+ * is the default for iOS.
+ * @param {boolean} value
+ * @returns {boolean}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#form.checkbox
+ * @description Checkbox style. Android defaults to `square` and iOS defaults to `circle`.
+ * @param {string} value
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#form.toggle
+ * @description Toggle item style. Android defaults to `small` and iOS defaults to `large`.
+ * @param {string} value
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#spinner.icon
+ * @description Default spinner icon to use.
+ * @param {string} value Can be: `android`, `ios`, `ios-small`, `bubbles`, `circles`, `crescent`,
+ * `dots`, `lines`, `ripple`, or `spiral`.
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#tabs.style
+ * @description Tab style. Android defaults to `striped` and iOS defaults to `standard`.
+ * @param {string} value Available values include `striped` and `standard`.
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#tabs.position
+ * @description Tab position. Android defaults to `top` and iOS defaults to `bottom`.
+ * @param {string} value Available values include `top` and `bottom`.
+ * @returns {string}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#templates.maxPrefetch
+ * @description Sets the maximum number of templates to prefetch from the templateUrls defined in
+ * $stateProvider.state. If set to `0`, the user will have to wait
+ * for a template to be fetched the first time when navigating to a new page. Default `30`.
+ * @param {integer} value Max number of template to prefetch from the templateUrls defined in
+ * `$stateProvider.state()`.
+ * @returns {integer}
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#navBar.alignTitle
+ * @description Which side of the navBar to align the title. Default `center`.
+ *
+ * @param {string} value side of the navBar to align the title.
+ *
+ * * `platform`: Dynamically choose the correct title style depending on the platform
+ * the app is running from. If the platform is `ios`, it will default to `center`.
+ * If the platform is `android`, it will default to `left`. If the platform is not
+ * `ios` or `android`, it will default to `center`.
+ *
+ * * `left`: Left align the title in the navBar
+ * * `center`: Center align the title in the navBar
+ * * `right`: Right align the title in the navBar.
+ *
+ * @returns {string} value
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#navBar.positionPrimaryButtons
+ * @description Which side of the navBar to align the primary navBar buttons. Default `left`.
+ *
+ * @param {string} value side of the navBar to align the primary navBar buttons.
+ *
+ * * `platform`: Dynamically choose the correct title style depending on the platform
+ * the app is running from. If the platform is `ios`, it will default to `left`.
+ * If the platform is `android`, it will default to `right`. If the platform is not
+ * `ios` or `android`, it will default to `left`.
+ *
+ * * `left`: Left align the primary navBar buttons in the navBar
+ * * `right`: Right align the primary navBar buttons in the navBar.
+ *
+ * @returns {string} value
+ */
+
+/**
+ * @ngdoc method
+ * @name $ionicConfigProvider#navBar.positionSecondaryButtons
+ * @description Which side of the navBar to align the secondary navBar buttons. Default `right`.
+ *
+ * @param {string} value side of the navBar to align the secondary navBar buttons.
+ *
+ * * `platform`: Dynamically choose the correct title style depending on the platform
+ * the app is running from. If the platform is `ios`, it will default to `right`.
+ * If the platform is `android`, it will default to `right`. If the platform is not
+ * `ios` or `android`, it will default to `right`.
+ *
+ * * `left`: Left align the secondary navBar buttons in the navBar
+ * * `right`: Right align the secondary navBar buttons in the navBar.
+ *
+ * @returns {string} value
+ */
+
+IonicModule
+.provider('$ionicConfig', function() {
+
+ var provider = this;
+ provider.platform = {};
+ var PLATFORM = 'platform';
+
+ var configProperties = {
+ views: {
+ maxCache: PLATFORM,
+ forwardCache: PLATFORM,
+ transition: PLATFORM,
+ swipeBackEnabled: PLATFORM,
+ swipeBackHitWidth: PLATFORM
+ },
+ navBar: {
+ alignTitle: PLATFORM,
+ positionPrimaryButtons: PLATFORM,
+ positionSecondaryButtons: PLATFORM,
+ transition: PLATFORM
+ },
+ backButton: {
+ icon: PLATFORM,
+ text: PLATFORM,
+ previousTitleText: PLATFORM
+ },
+ form: {
+ checkbox: PLATFORM,
+ toggle: PLATFORM
+ },
+ scrolling: {
+ jsScrolling: PLATFORM
+ },
+ spinner: {
+ icon: PLATFORM
+ },
+ tabs: {
+ style: PLATFORM,
+ position: PLATFORM
+ },
+ templates: {
+ maxPrefetch: PLATFORM
+ },
+ platform: {}
+ };
+ createConfig(configProperties, provider, '');
+
+
+
+ // Default
+ // -------------------------
+ setPlatformConfig('default', {
+
+ views: {
+ maxCache: 10,
+ forwardCache: false,
+ transition: 'ios',
+ swipeBackEnabled: true,
+ swipeBackHitWidth: 45
+ },
+
+ navBar: {
+ alignTitle: 'center',
+ positionPrimaryButtons: 'left',
+ positionSecondaryButtons: 'right',
+ transition: 'view'
+ },
+
+ backButton: {
+ icon: 'ion-ios-arrow-back',
+ text: 'Back',
+ previousTitleText: true
+ },
+
+ form: {
+ checkbox: 'circle',
+ toggle: 'large'
+ },
+
+ scrolling: {
+ jsScrolling: true
+ },
+
+ spinner: {
+ icon: 'ios'
+ },
+
+ tabs: {
+ style: 'standard',
+ position: 'bottom'
+ },
+
+ templates: {
+ maxPrefetch: 30
+ }
+
+ });
+
+
+
+ // iOS (it is the default already)
+ // -------------------------
+ setPlatformConfig('ios', {});
+
+
+
+ // Android
+ // -------------------------
+ setPlatformConfig('android', {
+
+ views: {
+ transition: 'android',
+ swipeBackEnabled: false
+ },
+
+ navBar: {
+ alignTitle: 'left',
+ positionPrimaryButtons: 'right',
+ positionSecondaryButtons: 'right'
+ },
+
+ backButton: {
+ icon: 'ion-android-arrow-back',
+ text: false,
+ previousTitleText: false
+ },
+
+ form: {
+ checkbox: 'square',
+ toggle: 'small'
+ },
+
+ spinner: {
+ icon: 'android'
+ },
+
+ tabs: {
+ style: 'striped',
+ position: 'top'
+ },
+
+ scrolling: {
+ jsScrolling: false
+ }
+ });
+
+ // Windows Phone
+ // -------------------------
+ setPlatformConfig('windowsphone', {
+ //scrolling: {
+ // jsScrolling: false
+ //}
+ spinner: {
+ icon: 'android'
+ }
+ });
+
+
+ provider.transitions = {
+ views: {},
+ navBar: {}
+ };
+
+
+ // iOS Transitions
+ // -----------------------
+ provider.transitions.views.ios = function(enteringEle, leavingEle, direction, shouldAnimate) {
+
+ function setStyles(ele, opacity, x, boxShadowOpacity) {
+ var css = {};
+ css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0;
+ css.opacity = opacity;
+ if (boxShadowOpacity > -1) {
+ css.boxShadow = '0 0 10px rgba(0,0,0,' + (d.shouldAnimate ? boxShadowOpacity * 0.45 : 0.3) + ')';
+ }
+ css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
+ ionic.DomUtil.cachedStyles(ele, css);
+ }
+
+ var d = {
+ run: function(step) {
+ if (direction == 'forward') {
+ setStyles(enteringEle, 1, (1 - step) * 99, 1 - step); // starting at 98% prevents a flicker
+ setStyles(leavingEle, (1 - 0.1 * step), step * -33, -1);
+
+ } else if (direction == 'back') {
+ setStyles(enteringEle, (1 - 0.1 * (1 - step)), (1 - step) * -33, -1);
+ setStyles(leavingEle, 1, step * 100, 1 - step);
+
+ } else {
+ // swap, enter, exit
+ setStyles(enteringEle, 1, 0, -1);
+ setStyles(leavingEle, 0, 0, -1);
+ }
+ },
+ shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back')
+ };
+
+ return d;
+ };
+
+ provider.transitions.navBar.ios = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {
+
+ function setStyles(ctrl, opacity, titleX, backTextX) {
+ var css = {};
+ css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : '0ms';
+ css.opacity = opacity === 1 ? '' : opacity;
+
+ ctrl.setCss('buttons-left', css);
+ ctrl.setCss('buttons-right', css);
+ ctrl.setCss('back-button', css);
+
+ css[ionic.CSS.TRANSFORM] = 'translate3d(' + backTextX + 'px,0,0)';
+ ctrl.setCss('back-text', css);
+
+ css[ionic.CSS.TRANSFORM] = 'translate3d(' + titleX + 'px,0,0)';
+ ctrl.setCss('title', css);
+ }
+
+ function enter(ctrlA, ctrlB, step) {
+ if (!ctrlA || !ctrlB) return;
+ var titleX = (ctrlA.titleTextX() + ctrlA.titleWidth()) * (1 - step);
+ var backTextX = (ctrlB && (ctrlB.titleTextX() - ctrlA.backButtonTextLeft()) * (1 - step)) || 0;
+ setStyles(ctrlA, step, titleX, backTextX);
+ }
+
+ function leave(ctrlA, ctrlB, step) {
+ if (!ctrlA || !ctrlB) return;
+ var titleX = (-(ctrlA.titleTextX() - ctrlB.backButtonTextLeft()) - (ctrlA.titleLeftRight())) * step;
+ setStyles(ctrlA, 1 - step, titleX, 0);
+ }
+
+ var d = {
+ run: function(step) {
+ var enteringHeaderCtrl = enteringHeaderBar.controller();
+ var leavingHeaderCtrl = leavingHeaderBar && leavingHeaderBar.controller();
+ if (d.direction == 'back') {
+ leave(enteringHeaderCtrl, leavingHeaderCtrl, 1 - step);
+ enter(leavingHeaderCtrl, enteringHeaderCtrl, 1 - step);
+ } else {
+ enter(enteringHeaderCtrl, leavingHeaderCtrl, step);
+ leave(leavingHeaderCtrl, enteringHeaderCtrl, step);
+ }
+ },
+ direction: direction,
+ shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back')
+ };
+
+ return d;
+ };
+
+
+ // Android Transitions
+ // -----------------------
+
+ provider.transitions.views.android = function(enteringEle, leavingEle, direction, shouldAnimate) {
+ shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back');
+
+ function setStyles(ele, x, opacity) {
+ var css = {};
+ css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0;
+ css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
+ css.opacity = opacity;
+ ionic.DomUtil.cachedStyles(ele, css);
+ }
+
+ var d = {
+ run: function(step) {
+ if (direction == 'forward') {
+ setStyles(enteringEle, (1 - step) * 99, 1); // starting at 98% prevents a flicker
+ setStyles(leavingEle, step * -100, 1);
+
+ } else if (direction == 'back') {
+ setStyles(enteringEle, (1 - step) * -100, 1);
+ setStyles(leavingEle, step * 100, 1);
+
+ } else {
+ // swap, enter, exit
+ setStyles(enteringEle, 0, 1);
+ setStyles(leavingEle, 0, 0);
+ }
+ },
+ shouldAnimate: shouldAnimate
+ };
+
+ return d;
+ };
+
+ provider.transitions.navBar.android = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {
+
+ function setStyles(ctrl, opacity) {
+ if (!ctrl) return;
+ var css = {};
+ css.opacity = opacity === 1 ? '' : opacity;
+
+ ctrl.setCss('buttons-left', css);
+ ctrl.setCss('buttons-right', css);
+ ctrl.setCss('back-button', css);
+ ctrl.setCss('back-text', css);
+ ctrl.setCss('title', css);
+ }
+
+ return {
+ run: function(step) {
+ setStyles(enteringHeaderBar.controller(), step);
+ setStyles(leavingHeaderBar && leavingHeaderBar.controller(), 1 - step);
+ },
+ shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back')
+ };
+ };
+
+
+ // No Transition
+ // -----------------------
+
+ provider.transitions.views.none = function(enteringEle, leavingEle) {
+ return {
+ run: function(step) {
+ provider.transitions.views.android(enteringEle, leavingEle, false, false).run(step);
+ },
+ shouldAnimate: false
+ };
+ };
+
+ provider.transitions.navBar.none = function(enteringHeaderBar, leavingHeaderBar) {
+ return {
+ run: function(step) {
+ provider.transitions.navBar.ios(enteringHeaderBar, leavingHeaderBar, false, false).run(step);
+ provider.transitions.navBar.android(enteringHeaderBar, leavingHeaderBar, false, false).run(step);
+ },
+ shouldAnimate: false
+ };
+ };
+
+
+ // private: used to set platform configs
+ function setPlatformConfig(platformName, platformConfigs) {
+ configProperties.platform[platformName] = platformConfigs;
+ provider.platform[platformName] = {};
+
+ addConfig(configProperties, configProperties.platform[platformName]);
+
+ createConfig(configProperties.platform[platformName], provider.platform[platformName], '');
+ }
+
+
+ // private: used to recursively add new platform configs
+ function addConfig(configObj, platformObj) {
+ for (var n in configObj) {
+ if (n != PLATFORM && configObj.hasOwnProperty(n)) {
+ if (angular.isObject(configObj[n])) {
+ if (!isDefined(platformObj[n])) {
+ platformObj[n] = {};
+ }
+ addConfig(configObj[n], platformObj[n]);
+
+ } else if (!isDefined(platformObj[n])) {
+ platformObj[n] = null;
+ }
+ }
+ }
+ }
+
+
+ // private: create methods for each config to get/set
+ function createConfig(configObj, providerObj, platformPath) {
+ forEach(configObj, function(value, namespace) {
+
+ if (angular.isObject(configObj[namespace])) {
+ // recursively drill down the config object so we can create a method for each one
+ providerObj[namespace] = {};
+ createConfig(configObj[namespace], providerObj[namespace], platformPath + '.' + namespace);
+
+ } else {
+ // create a method for the provider/config methods that will be exposed
+ providerObj[namespace] = function(newValue) {
+ if (arguments.length) {
+ configObj[namespace] = newValue;
+ return providerObj;
+ }
+ if (configObj[namespace] == PLATFORM) {
+ // if the config is set to 'platform', then get this config's platform value
+ var platformConfig = stringObj(configProperties.platform, ionic.Platform.platform() + platformPath + '.' + namespace);
+ if (platformConfig || platformConfig === false) {
+ return platformConfig;
+ }
+ // didnt find a specific platform config, now try the default
+ return stringObj(configProperties.platform, 'default' + platformPath + '.' + namespace);
+ }
+ return configObj[namespace];
+ };
+ }
+
+ });
+ }
+
+ function stringObj(obj, str) {
+ str = str.split(".");
+ for (var i = 0; i < str.length; i++) {
+ if (obj && isDefined(obj[str[i]])) {
+ obj = obj[str[i]];
+ } else {
+ return null;
+ }
+ }
+ return obj;
+ }
+
+ provider.setPlatformConfig = setPlatformConfig;
+
+
+ // private: Service definition for internal Ionic use
+ /**
+ * @ngdoc service
+ * @name $ionicConfig
+ * @module ionic
+ * @private
+ */
+ provider.$get = function() {
+ return provider;
+ };
+})
+// Fix for URLs in Cordova apps on Windows Phone
+// http://blogs.msdn.com/b/msdn_answers/archive/2015/02/10/
+// running-cordova-apps-on-windows-and-windows-phone-8-1-using-ionic-angularjs-and-other-frameworks.aspx
+.config(['$compileProvider', function($compileProvider) {
+ $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|sms|tel|geo|ftp|mailto|file|ghttps?|ms-appx-web|ms-appx|x-wmapp0):/);
+ $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|content|blob|ms-appx|ms-appx-web|x-wmapp0):|data:image\//);
+}]);
+
+
+var LOADING_TPL =
+ '<div class="loading-container">' +
+ '<div class="loading">' +
+ '</div>' +
+ '</div>';
+
+/**
+ * @ngdoc service
+ * @name $ionicLoading
+ * @module ionic
+ * @description
+ * An overlay that can be used to indicate activity while blocking user
+ * interaction.
+ *
+ * @usage
+ * ```js
+ * angular.module('LoadingApp', ['ionic'])
+ * .controller('LoadingCtrl', function($scope, $ionicLoading) {
+ * $scope.show = function() {
+ * $ionicLoading.show({
+ * template: 'Loading...',
+ * duration: 3000
+ * }).then(function(){
+ * console.log("The loading indicator is now displayed");
+ * });
+ * };
+ * $scope.hide = function(){
+ * $ionicLoading.hide().then(function(){
+ * console.log("The loading indicator is now hidden");
+ * });
+ * };
+ * });
+ * ```
+ */
+/**
+ * @ngdoc object
+ * @name $ionicLoadingConfig
+ * @module ionic
+ * @description
+ * Set the default options to be passed to the {@link ionic.service:$ionicLoading} service.
+ *
+ * @usage
+ * ```js
+ * var app = angular.module('myApp', ['ionic'])
+ * app.constant('$ionicLoadingConfig', {
+ * template: 'Default Loading Template...'
+ * });
+ * app.controller('AppCtrl', function($scope, $ionicLoading) {
+ * $scope.showLoading = function() {
+ * //options default to values in $ionicLoadingConfig
+ * $ionicLoading.show().then(function(){
+ * console.log("The loading indicator is now displayed");
+ * });
+ * };
+ * });
+ * ```
+ */
+IonicModule
+.constant('$ionicLoadingConfig', {
+ template: '<ion-spinner></ion-spinner>'
+})
+.factory('$ionicLoading', [
+ '$ionicLoadingConfig',
+ '$ionicBody',
+ '$ionicTemplateLoader',
+ '$ionicBackdrop',
+ '$timeout',
+ '$q',
+ '$log',
+ '$compile',
+ '$ionicPlatform',
+ '$rootScope',
+ 'IONIC_BACK_PRIORITY',
+function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform, $rootScope, IONIC_BACK_PRIORITY) {
+
+ var loaderInstance;
+ //default values
+ var deregisterBackAction = noop;
+ var deregisterStateListener1 = noop;
+ var deregisterStateListener2 = noop;
+ var loadingShowDelay = $q.when();
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicLoading#show
+ * @description Shows a loading indicator. If the indicator is already shown,
+ * it will set the options given and keep the indicator shown.
+ * @returns {promise} A promise which is resolved when the loading indicator is presented.
+ * @param {object} opts The options for the loading indicator. Available properties:
+ * - `{string=}` `template` The html content of the indicator.
+ * - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator.
+ * - `{object=}` `scope` The scope to be a child of. Default: creates a child of $rootScope.
+ * - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown.
+ * - `{boolean=}` `hideOnStateChange` Whether to hide the loading spinner when navigating
+ * to a new state. Default false.
+ * - `{number=}` `delay` How many milliseconds to delay showing the indicator. By default there is no delay.
+ * - `{number=}` `duration` How many milliseconds to wait until automatically
+ * hiding the indicator. By default, the indicator will be shown until `.hide()` is called.
+ */
+ show: showLoader,
+ /**
+ * @ngdoc method
+ * @name $ionicLoading#hide
+ * @description Hides the loading indicator, if shown.
+ * @returns {promise} A promise which is resolved when the loading indicator is hidden.
+ */
+ hide: hideLoader,
+ /**
+ * @private for testing
+ */
+ _getLoader: getLoader
+ };
+
+ function getLoader() {
+ if (!loaderInstance) {
+ loaderInstance = $ionicTemplateLoader.compile({
+ template: LOADING_TPL,
+ appendTo: $ionicBody.get()
+ })
+ .then(function(self) {
+ self.show = function(options) {
+ var templatePromise = options.templateUrl ?
+ $ionicTemplateLoader.load(options.templateUrl) :
+ //options.content: deprecated
+ $q.when(options.template || options.content || '');
+
+ self.scope = options.scope || self.scope;
+
+ if (!self.isShown) {
+ //options.showBackdrop: deprecated
+ self.hasBackdrop = !options.noBackdrop && options.showBackdrop !== false;
+ if (self.hasBackdrop) {
+ $ionicBackdrop.retain();
+ $ionicBackdrop.getElement().addClass('backdrop-loading');
+ }
+ }
+
+ if (options.duration) {
+ $timeout.cancel(self.durationTimeout);
+ self.durationTimeout = $timeout(
+ angular.bind(self, self.hide),
+ +options.duration
+ );
+ }
+
+ deregisterBackAction();
+ //Disable hardware back button while loading
+ deregisterBackAction = $ionicPlatform.registerBackButtonAction(
+ noop,
+ IONIC_BACK_PRIORITY.loading
+ );
+
+ templatePromise.then(function(html) {
+ if (html) {
+ var loading = self.element.children();
+ loading.html(html);
+ $compile(loading.contents())(self.scope);
+ }
+
+ //Don't show until template changes
+ if (self.isShown) {
+ self.element.addClass('visible');
+ ionic.requestAnimationFrame(function() {
+ if (self.isShown) {
+ self.element.addClass('active');
+ $ionicBody.addClass('loading-active');
+ }
+ });
+ }
+ });
+
+ self.isShown = true;
+ };
+ self.hide = function() {
+
+ deregisterBackAction();
+ if (self.isShown) {
+ if (self.hasBackdrop) {
+ $ionicBackdrop.release();
+ $ionicBackdrop.getElement().removeClass('backdrop-loading');
+ }
+ self.element.removeClass('active');
+ $ionicBody.removeClass('loading-active');
+ self.element.removeClass('visible');
+ ionic.requestAnimationFrame(function() {
+ !self.isShown && self.element.removeClass('visible');
+ });
+ }
+ $timeout.cancel(self.durationTimeout);
+ self.isShown = false;
+ var loading = self.element.children();
+ loading.html("");
+ };
+
+ return self;
+ });
+ }
+ return loaderInstance;
+ }
+
+ function showLoader(options) {
+ options = extend({}, $ionicLoadingConfig || {}, options || {});
+ // use a default delay of 100 to avoid some issues reported on github
+ // https://github.com/driftyco/ionic/issues/3717
+ var delay = options.delay || options.showDelay || 0;
+
+ deregisterStateListener1();
+ deregisterStateListener2();
+ if (options.hideOnStateChange) {
+ deregisterStateListener1 = $rootScope.$on('$stateChangeSuccess', hideLoader);
+ deregisterStateListener2 = $rootScope.$on('$stateChangeError', hideLoader);
+ }
+
+ //If loading.show() was called previously, cancel it and show with our new options
+ $timeout.cancel(loadingShowDelay);
+ loadingShowDelay = $timeout(noop, delay);
+ return loadingShowDelay.then(getLoader).then(function(loader) {
+ return loader.show(options);
+ });
+ }
+
+ function hideLoader() {
+ deregisterStateListener1();
+ deregisterStateListener2();
+ $timeout.cancel(loadingShowDelay);
+ return getLoader().then(function(loader) {
+ return loader.hide();
+ });
+ }
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicModal
+ * @module ionic
+ * @codepen gblny
+ * @description
+ *
+ * Related: {@link ionic.controller:ionicModal ionicModal controller}.
+ *
+ * The Modal is a content pane that can go over the user's main view
+ * temporarily. Usually used for making a choice or editing an item.
+ *
+ * Put the content of the modal inside of an `<ion-modal-view>` element.
+ *
+ * **Notes:**
+ * - A modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
+ * scope, passing in itself as an event argument. Both the modal.removed and modal.hidden events are
+ * called when the modal is removed.
+ *
+ * - This example assumes your modal is in your main index file or another template file. If it is in its own
+ * template file, remove the script tags and call it by file name.
+ *
+ * @usage
+ * ```html
+ * <script id="my-modal.html" type="text/ng-template">
+ * <ion-modal-view>
+ * <ion-header-bar>
+ * <h1 class="title">My Modal title</h1>
+ * </ion-header-bar>
+ * <ion-content>
+ * Hello!
+ * </ion-content>
+ * </ion-modal-view>
+ * </script>
+ * ```
+ * ```js
+ * angular.module('testApp', ['ionic'])
+ * .controller('MyController', function($scope, $ionicModal) {
+ * $ionicModal.fromTemplateUrl('my-modal.html', {
+ * scope: $scope,
+ * animation: 'slide-in-up'
+ * }).then(function(modal) {
+ * $scope.modal = modal;
+ * });
+ * $scope.openModal = function() {
+ * $scope.modal.show();
+ * };
+ * $scope.closeModal = function() {
+ * $scope.modal.hide();
+ * };
+ * // Cleanup the modal when we're done with it!
+ * $scope.$on('$destroy', function() {
+ * $scope.modal.remove();
+ * });
+ * // Execute action on hide modal
+ * $scope.$on('modal.hidden', function() {
+ * // Execute action
+ * });
+ * // Execute action on remove modal
+ * $scope.$on('modal.removed', function() {
+ * // Execute action
+ * });
+ * });
+ * ```
+ */
+IonicModule
+.factory('$ionicModal', [
+ '$rootScope',
+ '$ionicBody',
+ '$compile',
+ '$timeout',
+ '$ionicPlatform',
+ '$ionicTemplateLoader',
+ '$$q',
+ '$log',
+ '$ionicClickBlock',
+ '$window',
+ 'IONIC_BACK_PRIORITY',
+function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $$q, $log, $ionicClickBlock, $window, IONIC_BACK_PRIORITY) {
+
+ /**
+ * @ngdoc controller
+ * @name ionicModal
+ * @module ionic
+ * @description
+ * Instantiated by the {@link ionic.service:$ionicModal} service.
+ *
+ * Be sure to call [remove()](#remove) when you are done with each modal
+ * to clean it up and avoid memory leaks.
+ *
+ * Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
+ * scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are
+ * called when the modal is removed.
+ */
+ var ModalView = ionic.views.Modal.inherit({
+ /**
+ * @ngdoc method
+ * @name ionicModal#initialize
+ * @description Creates a new modal controller instance.
+ * @param {object} options An options object with the following properties:
+ * - `{object=}` `scope` The scope to be a child of.
+ * Default: creates a child of $rootScope.
+ * - `{string=}` `animation` The animation to show & hide with.
+ * Default: 'slide-in-up'
+ * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
+ * the modal when shown. Will only show the keyboard on iOS, to force the keyboard to show
+ * on Android, please use the [Ionic keyboard plugin](https://github.com/driftyco/ionic-plugin-keyboard#keyboardshow).
+ * Default: false.
+ * - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop.
+ * Default: true.
+ * - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware
+ * back button on Android and similar devices. Default: true.
+ */
+ initialize: function(opts) {
+ ionic.views.Modal.prototype.initialize.call(this, opts);
+ this.animation = opts.animation || 'slide-in-up';
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionicModal#show
+ * @description Show this modal instance.
+ * @returns {promise} A promise which is resolved when the modal is finished animating in.
+ */
+ show: function(target) {
+ var self = this;
+
+ if (self.scope.$$destroyed) {
+ $log.error('Cannot call ' + self.viewType + '.show() after remove(). Please create a new ' + self.viewType + ' instance.');
+ return $$q.when();
+ }
+
+ // on iOS, clicks will sometimes bleed through/ghost click on underlying
+ // elements
+ $ionicClickBlock.show(600);
+ stack.add(self);
+
+ var modalEl = jqLite(self.modalEl);
+
+ self.el.classList.remove('hide');
+ $timeout(function() {
+ if (!self._isShown) return;
+ $ionicBody.addClass(self.viewType + '-open');
+ }, 400, false);
+
+ if (!self.el.parentElement) {
+ modalEl.addClass(self.animation);
+ $ionicBody.append(self.el);
+ }
+
+ // if modal was closed while the keyboard was up, reset scroll view on
+ // next show since we can only resize it once it's visible
+ var scrollCtrl = modalEl.data('$$ionicScrollController');
+ scrollCtrl && scrollCtrl.resize();
+
+ if (target && self.positionView) {
+ self.positionView(target, modalEl);
+ // set up a listener for in case the window size changes
+
+ self._onWindowResize = function() {
+ if (self._isShown) self.positionView(target, modalEl);
+ };
+ ionic.on('resize', self._onWindowResize, window);
+ }
+
+ modalEl.addClass('ng-enter active')
+ .removeClass('ng-leave ng-leave-active');
+
+ self._isShown = true;
+ self._deregisterBackButton = $ionicPlatform.registerBackButtonAction(
+ self.hardwareBackButtonClose ? angular.bind(self, self.hide) : noop,
+ IONIC_BACK_PRIORITY.modal
+ );
+
+ ionic.views.Modal.prototype.show.call(self);
+
+ $timeout(function() {
+ if (!self._isShown) return;
+ modalEl.addClass('ng-enter-active');
+ ionic.trigger('resize');
+ self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self);
+ self.el.classList.add('active');
+ self.scope.$broadcast('$ionicHeader.align');
+ self.scope.$broadcast('$ionicFooter.align');
+ self.scope.$broadcast('$ionic.modalPresented');
+ }, 20);
+
+ return $timeout(function() {
+ if (!self._isShown) return;
+ self.$el.on('touchmove', function(e) {
+ //Don't allow scrolling while open by dragging on backdrop
+ var isInScroll = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'scroll');
+ if (!isInScroll) {
+ e.preventDefault();
+ }
+ });
+ //After animating in, allow hide on backdrop click
+ self.$el.on('click', function(e) {
+ if (self.backdropClickToClose && e.target === self.el && stack.isHighest(self)) {
+ self.hide();
+ }
+ });
+ }, 400);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionicModal#hide
+ * @description Hide this modal instance.
+ * @returns {promise} A promise which is resolved when the modal is finished animating out.
+ */
+ hide: function() {
+ var self = this;
+ var modalEl = jqLite(self.modalEl);
+
+ // on iOS, clicks will sometimes bleed through/ghost click on underlying
+ // elements
+ $ionicClickBlock.show(600);
+ stack.remove(self);
+
+ self.el.classList.remove('active');
+ modalEl.addClass('ng-leave');
+
+ $timeout(function() {
+ if (self._isShown) return;
+ modalEl.addClass('ng-leave-active')
+ .removeClass('ng-enter ng-enter-active active');
+
+ self.scope.$broadcast('$ionic.modalRemoved');
+ }, 20, false);
+
+ self.$el.off('click');
+ self._isShown = false;
+ self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self);
+ self._deregisterBackButton && self._deregisterBackButton();
+
+ ionic.views.Modal.prototype.hide.call(self);
+
+ // clean up event listeners
+ if (self.positionView) {
+ ionic.off('resize', self._onWindowResize, window);
+ }
+
+ return $timeout(function() {
+ if (!modalStack.length) {
+ $ionicBody.removeClass(self.viewType + '-open');
+ }
+ self.el.classList.add('hide');
+ }, self.hideDelay || 320);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionicModal#remove
+ * @description Remove this modal instance from the DOM and clean up.
+ * @returns {promise} A promise which is resolved when the modal is finished animating out.
+ */
+ remove: function() {
+ var self = this,
+ deferred, promise;
+ self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self);
+
+ // Only hide modal, when it is actually shown!
+ // The hide function shows a click-block-div for a split second, because on iOS,
+ // clicks will sometimes bleed through/ghost click on underlying elements.
+ // However, this will make the app unresponsive for short amount of time.
+ // We don't want that, if the modal window is already hidden.
+ if (self._isShown) {
+ promise = self.hide();
+ } else {
+ deferred = $$q.defer();
+ deferred.resolve();
+ promise = deferred.promise;
+ }
+
+ return promise.then(function() {
+ self.scope.$destroy();
+ self.$el.remove();
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name ionicModal#isShown
+ * @returns boolean Whether this modal is currently shown.
+ */
+ isShown: function() {
+ return !!this._isShown;
+ }
+ });
+
+ var createModal = function(templateString, options) {
+ // Create a new scope for the modal
+ var scope = options.scope && options.scope.$new() || $rootScope.$new(true);
+
+ options.viewType = options.viewType || 'modal';
+
+ extend(scope, {
+ $hasHeader: false,
+ $hasSubheader: false,
+ $hasFooter: false,
+ $hasSubfooter: false,
+ $hasTabs: false,
+ $hasTabsTop: false
+ });
+
+ // Compile the template
+ var element = $compile('<ion-' + options.viewType + '>' + templateString + '</ion-' + options.viewType + '>')(scope);
+
+ options.$el = element;
+ options.el = element[0];
+ options.modalEl = options.el.querySelector('.' + options.viewType);
+ var modal = new ModalView(options);
+
+ modal.scope = scope;
+
+ // If this wasn't a defined scope, we can assign the viewType to the isolated scope
+ // we created
+ if (!options.scope) {
+ scope[ options.viewType ] = modal;
+ }
+
+ return modal;
+ };
+
+ var modalStack = [];
+ var stack = {
+ add: function(modal) {
+ modalStack.push(modal);
+ },
+ remove: function(modal) {
+ var index = modalStack.indexOf(modal);
+ if (index > -1 && index < modalStack.length) {
+ modalStack.splice(index, 1);
+ }
+ },
+ isHighest: function(modal) {
+ var index = modalStack.indexOf(modal);
+ return (index > -1 && index === modalStack.length - 1);
+ }
+ };
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicModal#fromTemplate
+ * @param {string} templateString The template string to use as the modal's
+ * content.
+ * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
+ * @returns {object} An instance of an {@link ionic.controller:ionicModal}
+ * controller.
+ */
+ fromTemplate: function(templateString, options) {
+ var modal = createModal(templateString, options || {});
+ return modal;
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicModal#fromTemplateUrl
+ * @param {string} templateUrl The url to load the template from.
+ * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
+ * options object.
+ * @returns {promise} A promise that will be resolved with an instance of
+ * an {@link ionic.controller:ionicModal} controller.
+ */
+ fromTemplateUrl: function(url, options, _) {
+ var cb;
+ //Deprecated: allow a callback as second parameter. Now we return a promise.
+ if (angular.isFunction(options)) {
+ cb = options;
+ options = _;
+ }
+ return $ionicTemplateLoader.load(url).then(function(templateString) {
+ var modal = createModal(templateString, options || {});
+ cb && cb(modal);
+ return modal;
+ });
+ },
+
+ stack: stack
+ };
+}]);
+
+
+/**
+ * @ngdoc service
+ * @name $ionicNavBarDelegate
+ * @module ionic
+ * @description
+ * Delegate for controlling the {@link ionic.directive:ionNavBar} directive.
+ *
+ * @usage
+ *
+ * ```html
+ * <body ng-controller="MyCtrl">
+ * <ion-nav-bar>
+ * <button ng-click="setNavTitle('banana')">
+ * Set title to banana!
+ * </button>
+ * </ion-nav-bar>
+ * </body>
+ * ```
+ * ```js
+ * function MyCtrl($scope, $ionicNavBarDelegate) {
+ * $scope.setNavTitle = function(title) {
+ * $ionicNavBarDelegate.title(title);
+ * }
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicNavBarDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicNavBarDelegate#align
+ * @description Aligns the title with the buttons in a given direction.
+ * @param {string=} direction The direction to the align the title text towards.
+ * Available: 'left', 'right', 'center'. Default: 'center'.
+ */
+ 'align',
+ /**
+ * @ngdoc method
+ * @name $ionicNavBarDelegate#showBackButton
+ * @description
+ * Set/get whether the {@link ionic.directive:ionNavBackButton} is shown
+ * (if it exists and there is a previous view that can be navigated to).
+ * @param {boolean=} show Whether to show the back button.
+ * @returns {boolean} Whether the back button is shown.
+ */
+ 'showBackButton',
+ /**
+ * @ngdoc method
+ * @name $ionicNavBarDelegate#showBar
+ * @description
+ * Set/get whether the {@link ionic.directive:ionNavBar} is shown.
+ * @param {boolean} show Whether to show the bar.
+ * @returns {boolean} Whether the bar is shown.
+ */
+ 'showBar',
+ /**
+ * @ngdoc method
+ * @name $ionicNavBarDelegate#title
+ * @description
+ * Set the title for the {@link ionic.directive:ionNavBar}.
+ * @param {string} title The new title to show.
+ */
+ 'title',
+
+ // DEPRECATED, as of v1.0.0-beta14 -------
+ 'changeTitle',
+ 'setTitle',
+ 'getTitle',
+ 'back',
+ 'getPreviousTitle'
+ // END DEPRECATED -------
+]));
+
+
+IonicModule
+.service('$ionicNavViewDelegate', ionic.DelegateService([
+ 'clearCache'
+]));
+
+
+
+/**
+ * @ngdoc service
+ * @name $ionicPlatform
+ * @module ionic
+ * @description
+ * An angular abstraction of {@link ionic.utility:ionic.Platform}.
+ *
+ * Used to detect the current platform, as well as do things like override the
+ * Android back button in PhoneGap/Cordova.
+ */
+IonicModule
+.constant('IONIC_BACK_PRIORITY', {
+ view: 100,
+ sideMenu: 150,
+ modal: 200,
+ actionSheet: 300,
+ popup: 400,
+ loading: 500
+})
+.provider('$ionicPlatform', function() {
+ return {
+ $get: ['$q', '$ionicScrollDelegate', function($q, $ionicScrollDelegate) {
+ var self = {
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#onHardwareBackButton
+ * @description
+ * Some platforms have a hardware back button, so this is one way to
+ * bind to it.
+ * @param {function} callback the callback to trigger when this event occurs
+ */
+ onHardwareBackButton: function(cb) {
+ ionic.Platform.ready(function() {
+ document.addEventListener('backbutton', cb, false);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#offHardwareBackButton
+ * @description
+ * Remove an event listener for the backbutton.
+ * @param {function} callback The listener function that was
+ * originally bound.
+ */
+ offHardwareBackButton: function(fn) {
+ ionic.Platform.ready(function() {
+ document.removeEventListener('backbutton', fn);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#registerBackButtonAction
+ * @description
+ * Register a hardware back button action. Only one action will execute
+ * when the back button is clicked, so this method decides which of
+ * the registered back button actions has the highest priority.
+ *
+ * For example, if an actionsheet is showing, the back button should
+ * close the actionsheet, but it should not also go back a page view
+ * or close a modal which may be open.
+ *
+ * The priorities for the existing back button hooks are as follows:
+ * Return to previous view = 100
+ * Close side menu = 150
+ * Dismiss modal = 200
+ * Close action sheet = 300
+ * Dismiss popup = 400
+ * Dismiss loading overlay = 500
+ *
+ * Your back button action will override each of the above actions
+ * whose priority is less than the priority you provide. For example,
+ * an action assigned a priority of 101 will override the 'return to
+ * previous view' action, but not any of the other actions.
+ *
+ * @param {function} callback Called when the back button is pressed,
+ * if this listener is the highest priority.
+ * @param {number} priority Only the highest priority will execute.
+ * @param {*=} actionId The id to assign this action. Default: a
+ * random unique id.
+ * @returns {function} A function that, when called, will deregister
+ * this backButtonAction.
+ */
+ $backButtonActions: {},
+ registerBackButtonAction: function(fn, priority, actionId) {
+
+ if (!self._hasBackButtonHandler) {
+ // add a back button listener if one hasn't been setup yet
+ self.$backButtonActions = {};
+ self.onHardwareBackButton(self.hardwareBackButtonClick);
+ self._hasBackButtonHandler = true;
+ }
+
+ var action = {
+ id: (actionId ? actionId : ionic.Utils.nextUid()),
+ priority: (priority ? priority : 0),
+ fn: fn
+ };
+ self.$backButtonActions[action.id] = action;
+
+ // return a function to de-register this back button action
+ return function() {
+ delete self.$backButtonActions[action.id];
+ };
+ },
+
+ /**
+ * @private
+ */
+ hardwareBackButtonClick: function(e) {
+ // loop through all the registered back button actions
+ // and only run the last one of the highest priority
+ var priorityAction, actionId;
+ for (actionId in self.$backButtonActions) {
+ if (!priorityAction || self.$backButtonActions[actionId].priority >= priorityAction.priority) {
+ priorityAction = self.$backButtonActions[actionId];
+ }
+ }
+ if (priorityAction) {
+ priorityAction.fn(e);
+ return priorityAction;
+ }
+ },
+
+ is: function(type) {
+ return ionic.Platform.is(type);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#on
+ * @description
+ * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`,
+ * `offline`, etc. More information about available event types can be found in
+ * [Cordova's event documentation](https://cordova.apache.org/docs/en/latest/cordova/events/events.html).
+ * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/latest/cordova/events/events.html).
+ * @param {function} callback Called when the Cordova event is fired.
+ * @returns {function} Returns a deregistration function to remove the event listener.
+ */
+ on: function(type, cb) {
+ ionic.Platform.ready(function() {
+ document.addEventListener(type, cb, false);
+ });
+ return function() {
+ ionic.Platform.ready(function() {
+ document.removeEventListener(type, cb);
+ });
+ };
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPlatform#ready
+ * @description
+ * Trigger a callback once the device is ready,
+ * or immediately if the device is already ready.
+ * @param {function=} callback The function to call.
+ * @returns {promise} A promise which is resolved when the device is ready.
+ */
+ ready: function(cb) {
+ var q = $q.defer();
+
+ ionic.Platform.ready(function() {
+
+ window.addEventListener('statusTap', function() {
+ $ionicScrollDelegate.scrollTop(true);
+ });
+
+ q.resolve();
+ cb && cb();
+ });
+
+ return q.promise;
+ }
+ };
+
+ return self;
+ }]
+ };
+
+});
+
+/**
+ * @ngdoc service
+ * @name $ionicPopover
+ * @module ionic
+ * @description
+ *
+ * Related: {@link ionic.controller:ionicPopover ionicPopover controller}.
+ *
+ * The Popover is a view that floats above an app’s content. Popovers provide an
+ * easy way to present or gather information from the user and are
+ * commonly used in the following situations:
+ *
+ * - Show more info about the current view
+ * - Select a commonly used tool or configuration
+ * - Present a list of actions to perform inside one of your views
+ *
+ * Put the content of the popover inside of an `<ion-popover-view>` element.
+ *
+ * @usage
+ * ```html
+ * <p>
+ * <button ng-click="openPopover($event)">Open Popover</button>
+ * </p>
+ *
+ * <script id="my-popover.html" type="text/ng-template">
+ * <ion-popover-view>
+ * <ion-header-bar>
+ * <h1 class="title">My Popover Title</h1>
+ * </ion-header-bar>
+ * <ion-content>
+ * Hello!
+ * </ion-content>
+ * </ion-popover-view>
+ * </script>
+ * ```
+ * ```js
+ * angular.module('testApp', ['ionic'])
+ * .controller('MyController', function($scope, $ionicPopover) {
+ *
+ * // .fromTemplate() method
+ * var template = '<ion-popover-view><ion-header-bar> <h1 class="title">My Popover Title</h1> </ion-header-bar> <ion-content> Hello! </ion-content></ion-popover-view>';
+ *
+ * $scope.popover = $ionicPopover.fromTemplate(template, {
+ * scope: $scope
+ * });
+ *
+ * // .fromTemplateUrl() method
+ * $ionicPopover.fromTemplateUrl('my-popover.html', {
+ * scope: $scope
+ * }).then(function(popover) {
+ * $scope.popover = popover;
+ * });
+ *
+ *
+ * $scope.openPopover = function($event) {
+ * $scope.popover.show($event);
+ * };
+ * $scope.closePopover = function() {
+ * $scope.popover.hide();
+ * };
+ * //Cleanup the popover when we're done with it!
+ * $scope.$on('$destroy', function() {
+ * $scope.popover.remove();
+ * });
+ * // Execute action on hidden popover
+ * $scope.$on('popover.hidden', function() {
+ * // Execute action
+ * });
+ * // Execute action on remove popover
+ * $scope.$on('popover.removed', function() {
+ * // Execute action
+ * });
+ * });
+ * ```
+ */
+
+
+IonicModule
+.factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', '$window',
+function($ionicModal, $ionicPosition, $document, $window) {
+
+ var POPOVER_BODY_PADDING = 6;
+
+ var POPOVER_OPTIONS = {
+ viewType: 'popover',
+ hideDelay: 1,
+ animation: 'none',
+ positionView: positionView
+ };
+
+ function positionView(target, popoverEle) {
+ var targetEle = jqLite(target.target || target);
+ var buttonOffset = $ionicPosition.offset(targetEle);
+ var popoverWidth = popoverEle.prop('offsetWidth');
+ var popoverHeight = popoverEle.prop('offsetHeight');
+ // Use innerWidth and innerHeight, because clientWidth and clientHeight
+ // doesn't work consistently for body on all platforms
+ var bodyWidth = $window.innerWidth;
+ var bodyHeight = $window.innerHeight;
+
+ var popoverCSS = {
+ left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2
+ };
+ var arrowEle = jqLite(popoverEle[0].querySelector('.popover-arrow'));
+
+ if (popoverCSS.left < POPOVER_BODY_PADDING) {
+ popoverCSS.left = POPOVER_BODY_PADDING;
+ } else if (popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) {
+ popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING;
+ }
+
+ // If the popover when popped down stretches past bottom of screen,
+ // make it pop up if there's room above
+ if (buttonOffset.top + buttonOffset.height + popoverHeight > bodyHeight &&
+ buttonOffset.top - popoverHeight > 0) {
+ popoverCSS.top = buttonOffset.top - popoverHeight;
+ popoverEle.addClass('popover-bottom');
+ } else {
+ popoverCSS.top = buttonOffset.top + buttonOffset.height;
+ popoverEle.removeClass('popover-bottom');
+ }
+
+ arrowEle.css({
+ left: buttonOffset.left + buttonOffset.width / 2 -
+ arrowEle.prop('offsetWidth') / 2 - popoverCSS.left + 'px'
+ });
+
+ popoverEle.css({
+ top: popoverCSS.top + 'px',
+ left: popoverCSS.left + 'px',
+ marginLeft: '0',
+ opacity: '1'
+ });
+
+ }
+
+ /**
+ * @ngdoc controller
+ * @name ionicPopover
+ * @module ionic
+ * @description
+ * Instantiated by the {@link ionic.service:$ionicPopover} service.
+ *
+ * Be sure to call [remove()](#remove) when you are done with each popover
+ * to clean it up and avoid memory leaks.
+ *
+ * Note: a popover will broadcast 'popover.shown', 'popover.hidden', and 'popover.removed' events from its originating
+ * scope, passing in itself as an event argument. Both the popover.removed and popover.hidden events are
+ * called when the popover is removed.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#initialize
+ * @description Creates a new popover controller instance.
+ * @param {object} options An options object with the following properties:
+ * - `{object=}` `scope` The scope to be a child of.
+ * Default: creates a child of $rootScope.
+ * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
+ * the popover when shown. Default: false.
+ * - `{boolean=}` `backdropClickToClose` Whether to close the popover on clicking the backdrop.
+ * Default: true.
+ * - `{boolean=}` `hardwareBackButtonClose` Whether the popover can be closed using the hardware
+ * back button on Android and similar devices. Default: true.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#show
+ * @description Show this popover instance.
+ * @param {$event} $event The $event or target element which the popover should align
+ * itself next to.
+ * @returns {promise} A promise which is resolved when the popover is finished animating in.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#hide
+ * @description Hide this popover instance.
+ * @returns {promise} A promise which is resolved when the popover is finished animating out.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#remove
+ * @description Remove this popover instance from the DOM and clean up.
+ * @returns {promise} A promise which is resolved when the popover is finished animating out.
+ */
+
+ /**
+ * @ngdoc method
+ * @name ionicPopover#isShown
+ * @returns boolean Whether this popover is currently shown.
+ */
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicPopover#fromTemplate
+ * @param {string} templateString The template string to use as the popovers's
+ * content.
+ * @param {object} options Options to be passed to the initialize method.
+ * @returns {object} An instance of an {@link ionic.controller:ionicPopover}
+ * controller (ionicPopover is built on top of $ionicPopover).
+ */
+ fromTemplate: function(templateString, options) {
+ return $ionicModal.fromTemplate(templateString, ionic.Utils.extend({}, POPOVER_OPTIONS, options));
+ },
+ /**
+ * @ngdoc method
+ * @name $ionicPopover#fromTemplateUrl
+ * @param {string} templateUrl The url to load the template from.
+ * @param {object} options Options to be passed to the initialize method.
+ * @returns {promise} A promise that will be resolved with an instance of
+ * an {@link ionic.controller:ionicPopover} controller (ionicPopover is built on top of $ionicPopover).
+ */
+ fromTemplateUrl: function(url, options) {
+ return $ionicModal.fromTemplateUrl(url, ionic.Utils.extend({}, POPOVER_OPTIONS, options));
+ }
+ };
+
+}]);
+
+
+var POPUP_TPL =
+ '<div class="popup-container" ng-class="cssClass">' +
+ '<div class="popup">' +
+ '<div class="popup-head">' +
+ '<h3 class="popup-title" ng-bind-html="title"></h3>' +
+ '<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
+ '</div>' +
+ '<div class="popup-body">' +
+ '</div>' +
+ '<div class="popup-buttons" ng-show="buttons.length">' +
+ '<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
+ '</div>' +
+ '</div>' +
+ '</div>';
+
+/**
+ * @ngdoc service
+ * @name $ionicPopup
+ * @module ionic
+ * @restrict E
+ * @codepen zkmhJ
+ * @description
+ *
+ * The Ionic Popup service allows programmatically creating and showing popup
+ * windows that require the user to respond in order to continue.
+ *
+ * The popup system has support for more flexible versions of the built in `alert()`, `prompt()`,
+ * and `confirm()` functions that users are used to, in addition to allowing popups with completely
+ * custom content and look.
+ *
+ * An input can be given an `autofocus` attribute so it automatically receives focus when
+ * the popup first shows. However, depending on certain use-cases this can cause issues with
+ * the tap/click system, which is why Ionic prefers using the `autofocus` attribute as
+ * an opt-in feature and not the default.
+ *
+ * @usage
+ * A few basic examples, see below for details about all of the options available.
+ *
+ * ```js
+ *angular.module('mySuperApp', ['ionic'])
+ *.controller('PopupCtrl',function($scope, $ionicPopup, $timeout) {
+ *
+ * // Triggered on a button click, or some other target
+ * $scope.showPopup = function() {
+ * $scope.data = {};
+ *
+ * // An elaborate, custom popup
+ * var myPopup = $ionicPopup.show({
+ * template: '<input type="password" ng-model="data.wifi">',
+ * title: 'Enter Wi-Fi Password',
+ * subTitle: 'Please use normal things',
+ * scope: $scope,
+ * buttons: [
+ * { text: 'Cancel' },
+ * {
+ * text: '<b>Save</b>',
+ * type: 'button-positive',
+ * onTap: function(e) {
+ * if (!$scope.data.wifi) {
+ * //don't allow the user to close unless he enters wifi password
+ * e.preventDefault();
+ * } else {
+ * return $scope.data.wifi;
+ * }
+ * }
+ * }
+ * ]
+ * });
+ *
+ * myPopup.then(function(res) {
+ * console.log('Tapped!', res);
+ * });
+ *
+ * $timeout(function() {
+ * myPopup.close(); //close the popup after 3 seconds for some reason
+ * }, 3000);
+ * };
+ *
+ * // A confirm dialog
+ * $scope.showConfirm = function() {
+ * var confirmPopup = $ionicPopup.confirm({
+ * title: 'Consume Ice Cream',
+ * template: 'Are you sure you want to eat this ice cream?'
+ * });
+ *
+ * confirmPopup.then(function(res) {
+ * if(res) {
+ * console.log('You are sure');
+ * } else {
+ * console.log('You are not sure');
+ * }
+ * });
+ * };
+ *
+ * // An alert dialog
+ * $scope.showAlert = function() {
+ * var alertPopup = $ionicPopup.alert({
+ * title: 'Don\'t eat that!',
+ * template: 'It might taste good'
+ * });
+ *
+ * alertPopup.then(function(res) {
+ * console.log('Thank you for not eating my delicious ice cream cone');
+ * });
+ * };
+ *});
+ *```
+ */
+
+IonicModule
+.factory('$ionicPopup', [
+ '$ionicTemplateLoader',
+ '$ionicBackdrop',
+ '$q',
+ '$timeout',
+ '$rootScope',
+ '$ionicBody',
+ '$compile',
+ '$ionicPlatform',
+ '$ionicModal',
+ 'IONIC_BACK_PRIORITY',
+function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicBody, $compile, $ionicPlatform, $ionicModal, IONIC_BACK_PRIORITY) {
+ //TODO allow this to be configured
+ var config = {
+ stackPushDelay: 75
+ };
+ var popupStack = [];
+
+ var $ionicPopup = {
+ /**
+ * @ngdoc method
+ * @description
+ * Show a complex popup. This is the master show function for all popups.
+ *
+ * A complex popup has a `buttons` array, with each button having a `text` and `type`
+ * field, in addition to an `onTap` function. The `onTap` function, called when
+ * the corresponding button on the popup is tapped, will by default close the popup
+ * and resolve the popup promise with its return value. If you wish to prevent the
+ * default and keep the popup open on button tap, call `event.preventDefault()` on the
+ * passed in tap event. Details below.
+ *
+ * @name $ionicPopup#show
+ * @param {object} options The options for the new popup, of the form:
+ *
+ * ```
+ * {
+ * title: '', // String. The title of the popup.
+ * cssClass: '', // String, The custom CSS class name
+ * subTitle: '', // String (optional). The sub-title of the popup.
+ * template: '', // String (optional). The html template to place in the popup body.
+ * templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
+ * scope: null, // Scope (optional). A scope to link to the popup content.
+ * buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer.
+ * text: 'Cancel',
+ * type: 'button-default',
+ * onTap: function(e) {
+ * // e.preventDefault() will stop the popup from closing when tapped.
+ * e.preventDefault();
+ * }
+ * }, {
+ * text: 'OK',
+ * type: 'button-positive',
+ * onTap: function(e) {
+ * // Returning a value will cause the promise to resolve with the given value.
+ * return scope.data.response;
+ * }
+ * }]
+ * }
+ * ```
+ *
+ * @returns {object} A promise which is resolved when the popup is closed. Has an additional
+ * `close` function, which can be used to programmatically close the popup.
+ */
+ show: showPopup,
+
+ /**
+ * @ngdoc method
+ * @name $ionicPopup#alert
+ * @description Show a simple alert popup with a message and one button that the user can
+ * tap to close the popup.
+ *
+ * @param {object} options The options for showing the alert, of the form:
+ *
+ * ```
+ * {
+ * title: '', // String. The title of the popup.
+ * cssClass: '', // String, The custom CSS class name
+ * subTitle: '', // String (optional). The sub-title of the popup.
+ * template: '', // String (optional). The html template to place in the popup body.
+ * templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
+ * okText: '', // String (default: 'OK'). The text of the OK button.
+ * okType: '', // String (default: 'button-positive'). The type of the OK button.
+ * }
+ * ```
+ *
+ * @returns {object} A promise which is resolved when the popup is closed. Has one additional
+ * function `close`, which can be called with any value to programmatically close the popup
+ * with the given value.
+ */
+ alert: showAlert,
+
+ /**
+ * @ngdoc method
+ * @name $ionicPopup#confirm
+ * @description
+ * Show a simple confirm popup with a Cancel and OK button.
+ *
+ * Resolves the promise with true if the user presses the OK button, and false if the
+ * user presses the Cancel button.
+ *
+ * @param {object} options The options for showing the confirm popup, of the form:
+ *
+ * ```
+ * {
+ * title: '', // String. The title of the popup.
+ * cssClass: '', // String, The custom CSS class name
+ * subTitle: '', // String (optional). The sub-title of the popup.
+ * template: '', // String (optional). The html template to place in the popup body.
+ * templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
+ * cancelText: '', // String (default: 'Cancel'). The text of the Cancel button.
+ * cancelType: '', // String (default: 'button-default'). The type of the Cancel button.
+ * okText: '', // String (default: 'OK'). The text of the OK button.
+ * okType: '', // String (default: 'button-positive'). The type of the OK button.
+ * }
+ * ```
+ *
+ * @returns {object} A promise which is resolved when the popup is closed. Has one additional
+ * function `close`, which can be called with any value to programmatically close the popup
+ * with the given value.
+ */
+ confirm: showConfirm,
+
+ /**
+ * @ngdoc method
+ * @name $ionicPopup#prompt
+ * @description Show a simple prompt popup, which has an input, OK button, and Cancel button.
+ * Resolves the promise with the value of the input if the user presses OK, and with undefined
+ * if the user presses Cancel.
+ *
+ * ```javascript
+ * $ionicPopup.prompt({
+ * title: 'Password Check',
+ * template: 'Enter your secret password',
+ * inputType: 'password',
+ * inputPlaceholder: 'Your password'
+ * }).then(function(res) {
+ * console.log('Your password is', res);
+ * });
+ * ```
+ * @param {object} options The options for showing the prompt popup, of the form:
+ *
+ * ```
+ * {
+ * title: '', // String. The title of the popup.
+ * cssClass: '', // String, The custom CSS class name
+ * subTitle: '', // String (optional). The sub-title of the popup.
+ * template: '', // String (optional). The html template to place in the popup body.
+ * templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
+ * inputType: // String (default: 'text'). The type of input to use
+ * defaultText: // String (default: ''). The initial value placed into the input.
+ * maxLength: // Integer (default: null). Specify a maxlength attribute for the input.
+ * inputPlaceholder: // String (default: ''). A placeholder to use for the input.
+ * cancelText: // String (default: 'Cancel'. The text of the Cancel button.
+ * cancelType: // String (default: 'button-default'). The type of the Cancel button.
+ * okText: // String (default: 'OK'). The text of the OK button.
+ * okType: // String (default: 'button-positive'). The type of the OK button.
+ * }
+ * ```
+ *
+ * @returns {object} A promise which is resolved when the popup is closed. Has one additional
+ * function `close`, which can be called with any value to programmatically close the popup
+ * with the given value.
+ */
+ prompt: showPrompt,
+ /**
+ * @private for testing
+ */
+ _createPopup: createPopup,
+ _popupStack: popupStack
+ };
+
+ return $ionicPopup;
+
+ function createPopup(options) {
+ options = extend({
+ scope: null,
+ title: '',
+ buttons: []
+ }, options || {});
+
+ var self = {};
+ self.scope = (options.scope || $rootScope).$new();
+ self.element = jqLite(POPUP_TPL);
+ self.responseDeferred = $q.defer();
+
+ $ionicBody.get().appendChild(self.element[0]);
+ $compile(self.element)(self.scope);
+
+ extend(self.scope, {
+ title: options.title,
+ buttons: options.buttons,
+ subTitle: options.subTitle,
+ cssClass: options.cssClass,
+ $buttonTapped: function(button, event) {
+ var result = (button.onTap || noop).apply(self, [event]);
+ event = event.originalEvent || event; //jquery events
+
+ if (!event.defaultPrevented) {
+ self.responseDeferred.resolve(result);
+ }
+ }
+ });
+
+ $q.when(
+ options.templateUrl ?
+ $ionicTemplateLoader.load(options.templateUrl) :
+ (options.template || options.content || '')
+ ).then(function(template) {
+ var popupBody = jqLite(self.element[0].querySelector('.popup-body'));
+ if (template) {
+ popupBody.html(template);
+ $compile(popupBody.contents())(self.scope);
+ } else {
+ popupBody.remove();
+ }
+ });
+
+ self.show = function() {
+ if (self.isShown || self.removed) return;
+
+ $ionicModal.stack.add(self);
+ self.isShown = true;
+ ionic.requestAnimationFrame(function() {
+ //if hidden while waiting for raf, don't show
+ if (!self.isShown) return;
+
+ self.element.removeClass('popup-hidden');
+ self.element.addClass('popup-showing active');
+ focusInput(self.element);
+ });
+ };
+
+ self.hide = function(callback) {
+ callback = callback || noop;
+ if (!self.isShown) return callback();
+
+ $ionicModal.stack.remove(self);
+ self.isShown = false;
+ self.element.removeClass('active');
+ self.element.addClass('popup-hidden');
+ $timeout(callback, 250, false);
+ };
+
+ self.remove = function() {
+ if (self.removed) return;
+
+ self.hide(function() {
+ self.element.remove();
+ self.scope.$destroy();
+ });
+
+ self.removed = true;
+ };
+
+ return self;
+ }
+
+ function onHardwareBackButton() {
+ var last = popupStack[popupStack.length - 1];
+ last && last.responseDeferred.resolve();
+ }
+
+ function showPopup(options) {
+ var popup = $ionicPopup._createPopup(options);
+ var showDelay = 0;
+
+ if (popupStack.length > 0) {
+ showDelay = config.stackPushDelay;
+ $timeout(popupStack[popupStack.length - 1].hide, showDelay, false);
+ } else {
+ //Add popup-open & backdrop if this is first popup
+ $ionicBody.addClass('popup-open');
+ $ionicBackdrop.retain();
+ //only show the backdrop on the first popup
+ $ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction(
+ onHardwareBackButton,
+ IONIC_BACK_PRIORITY.popup
+ );
+ }
+
+ // Expose a 'close' method on the returned promise
+ popup.responseDeferred.promise.close = function popupClose(result) {
+ if (!popup.removed) popup.responseDeferred.resolve(result);
+ };
+ //DEPRECATED: notify the promise with an object with a close method
+ popup.responseDeferred.notify({ close: popup.responseDeferred.close });
+
+ doShow();
+
+ return popup.responseDeferred.promise;
+
+ function doShow() {
+ popupStack.push(popup);
+ $timeout(popup.show, showDelay, false);
+
+ popup.responseDeferred.promise.then(function(result) {
+ var index = popupStack.indexOf(popup);
+ if (index !== -1) {
+ popupStack.splice(index, 1);
+ }
+
+ popup.remove();
+
+ if (popupStack.length > 0) {
+ popupStack[popupStack.length - 1].show();
+ } else {
+ $ionicBackdrop.release();
+ //Remove popup-open & backdrop if this is last popup
+ $timeout(function() {
+ // wait to remove this due to a 300ms delay native
+ // click which would trigging whatever was underneath this
+ if (!popupStack.length) {
+ $ionicBody.removeClass('popup-open');
+ }
+ }, 400, false);
+ ($ionicPopup._backButtonActionDone || noop)();
+ }
+
+
+ return result;
+ });
+
+ }
+
+ }
+
+ function focusInput(element) {
+ var focusOn = element[0].querySelector('[autofocus]');
+ if (focusOn) {
+ focusOn.focus();
+ }
+ }
+
+ function showAlert(opts) {
+ return showPopup(extend({
+ buttons: [{
+ text: opts.okText || 'OK',
+ type: opts.okType || 'button-positive',
+ onTap: function() {
+ return true;
+ }
+ }]
+ }, opts || {}));
+ }
+
+ function showConfirm(opts) {
+ return showPopup(extend({
+ buttons: [{
+ text: opts.cancelText || 'Cancel',
+ type: opts.cancelType || 'button-default',
+ onTap: function() { return false; }
+ }, {
+ text: opts.okText || 'OK',
+ type: opts.okType || 'button-positive',
+ onTap: function() { return true; }
+ }]
+ }, opts || {}));
+ }
+
+ function showPrompt(opts) {
+ var scope = $rootScope.$new(true);
+ scope.data = {};
+ scope.data.fieldtype = opts.inputType ? opts.inputType : 'text';
+ scope.data.response = opts.defaultText ? opts.defaultText : '';
+ scope.data.placeholder = opts.inputPlaceholder ? opts.inputPlaceholder : '';
+ scope.data.maxlength = opts.maxLength ? parseInt(opts.maxLength) : '';
+ var text = '';
+ if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) {
+ text = '<span>' + opts.template + '</span>';
+ delete opts.template;
+ }
+ return showPopup(extend({
+ template: text + '<input ng-model="data.response" '
+ + 'type="{{ data.fieldtype }}"'
+ + 'maxlength="{{ data.maxlength }}"'
+ + 'placeholder="{{ data.placeholder }}"'
+ + '>',
+ scope: scope,
+ buttons: [{
+ text: opts.cancelText || 'Cancel',
+ type: opts.cancelType || 'button-default',
+ onTap: function() {}
+ }, {
+ text: opts.okText || 'OK',
+ type: opts.okType || 'button-positive',
+ onTap: function() {
+ return scope.data.response || '';
+ }
+ }]
+ }, opts || {}));
+ }
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicPosition
+ * @module ionic
+ * @description
+ * A set of utility methods that can be use to retrieve position of DOM elements.
+ * It is meant to be used where we need to absolute-position DOM elements in
+ * relation to other, existing elements (this is the case for tooltips, popovers, etc.).
+ *
+ * Adapted from [AngularUI Bootstrap](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js),
+ * ([license](https://github.com/angular-ui/bootstrap/blob/master/LICENSE))
+ */
+IonicModule
+.factory('$ionicPosition', ['$document', '$window', function($document, $window) {
+
+ function getStyle(el, cssprop) {
+ if (el.currentStyle) { //IE
+ return el.currentStyle[cssprop];
+ } else if ($window.getComputedStyle) {
+ return $window.getComputedStyle(el)[cssprop];
+ }
+ // finally try and get inline style
+ return el.style[cssprop];
+ }
+
+ /**
+ * Checks if a given element is statically positioned
+ * @param element - raw DOM element
+ */
+ function isStaticPositioned(element) {
+ return (getStyle(element, 'position') || 'static') === 'static';
+ }
+
+ /**
+ * returns the closest, non-statically positioned parentOffset of a given element
+ * @param element
+ */
+ var parentOffsetEl = function(element) {
+ var docDomEl = $document[0];
+ var offsetParent = element.offsetParent || docDomEl;
+ while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) {
+ offsetParent = offsetParent.offsetParent;
+ }
+ return offsetParent || docDomEl;
+ };
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $ionicPosition#position
+ * @description Get the current coordinates of the element, relative to the offset parent.
+ * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/).
+ * @param {element} element The element to get the position of.
+ * @returns {object} Returns an object containing the properties top, left, width and height.
+ */
+ position: function(element) {
+ var elBCR = this.offset(element);
+ var offsetParentBCR = { top: 0, left: 0 };
+ var offsetParentEl = parentOffsetEl(element[0]);
+ if (offsetParentEl != $document[0]) {
+ offsetParentBCR = this.offset(jqLite(offsetParentEl));
+ offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
+ offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
+ }
+
+ var boundingClientRect = element[0].getBoundingClientRect();
+ return {
+ width: boundingClientRect.width || element.prop('offsetWidth'),
+ height: boundingClientRect.height || element.prop('offsetHeight'),
+ top: elBCR.top - offsetParentBCR.top,
+ left: elBCR.left - offsetParentBCR.left
+ };
+ },
+
+ /**
+ * @ngdoc method
+ * @name $ionicPosition#offset
+ * @description Get the current coordinates of the element, relative to the document.
+ * Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/).
+ * @param {element} element The element to get the offset of.
+ * @returns {object} Returns an object containing the properties top, left, width and height.
+ */
+ offset: function(element) {
+ var boundingClientRect = element[0].getBoundingClientRect();
+ return {
+ width: boundingClientRect.width || element.prop('offsetWidth'),
+ height: boundingClientRect.height || element.prop('offsetHeight'),
+ top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
+ left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
+ };
+ }
+
+ };
+}]);
+
+
+/**
+ * @ngdoc service
+ * @name $ionicScrollDelegate
+ * @module ionic
+ * @description
+ * Delegate for controlling scrollViews (created by
+ * {@link ionic.directive:ionContent} and
+ * {@link ionic.directive:ionScroll} directives).
+ *
+ * Methods called directly on the $ionicScrollDelegate service will control all scroll
+ * views. Use the {@link ionic.service:$ionicScrollDelegate#$getByHandle $getByHandle}
+ * method to control specific scrollViews.
+ *
+ * @usage
+ *
+ * ```html
+ * <body ng-controller="MainCtrl">
+ * <ion-content>
+ * <button ng-click="scrollTop()">Scroll to Top!</button>
+ * </ion-content>
+ * </body>
+ * ```
+ * ```js
+ * function MainCtrl($scope, $ionicScrollDelegate) {
+ * $scope.scrollTop = function() {
+ * $ionicScrollDelegate.scrollTop();
+ * };
+ * }
+ * ```
+ *
+ * Example of advanced usage, with two scroll areas using `delegate-handle`
+ * for fine control.
+ *
+ * ```html
+ * <body ng-controller="MainCtrl">
+ * <ion-content delegate-handle="mainScroll">
+ * <button ng-click="scrollMainToTop()">
+ * Scroll content to top!
+ * </button>
+ * <ion-scroll delegate-handle="small" style="height: 100px;">
+ * <button ng-click="scrollSmallToTop()">
+ * Scroll small area to top!
+ * </button>
+ * </ion-scroll>
+ * </ion-content>
+ * </body>
+ * ```
+ * ```js
+ * function MainCtrl($scope, $ionicScrollDelegate) {
+ * $scope.scrollMainToTop = function() {
+ * $ionicScrollDelegate.$getByHandle('mainScroll').scrollTop();
+ * };
+ * $scope.scrollSmallToTop = function() {
+ * $ionicScrollDelegate.$getByHandle('small').scrollTop();
+ * };
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicScrollDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#resize
+ * @description Tell the scrollView to recalculate the size of its container.
+ */
+ 'resize',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#scrollTop
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'scrollTop',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#scrollBottom
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'scrollBottom',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#scrollTo
+ * @param {number} left The x-value to scroll to.
+ * @param {number} top The y-value to scroll to.
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'scrollTo',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#scrollBy
+ * @param {number} left The x-offset to scroll by.
+ * @param {number} top The y-offset to scroll by.
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'scrollBy',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#zoomTo
+ * @param {number} level Level to zoom to.
+ * @param {boolean=} animate Whether to animate the zoom.
+ * @param {number=} originLeft Zoom in at given left coordinate.
+ * @param {number=} originTop Zoom in at given top coordinate.
+ */
+ 'zoomTo',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#zoomBy
+ * @param {number} factor The factor to zoom by.
+ * @param {boolean=} animate Whether to animate the zoom.
+ * @param {number=} originLeft Zoom in at given left coordinate.
+ * @param {number=} originTop Zoom in at given top coordinate.
+ */
+ 'zoomBy',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#getScrollPosition
+ * @returns {object} The scroll position of this view, with the following properties:
+ * - `{number}` `left` The distance the user has scrolled from the left (starts at 0).
+ * - `{number}` `top` The distance the user has scrolled from the top (starts at 0).
+ * - `{number}` `zoom` The current zoom level.
+ */
+ 'getScrollPosition',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#anchorScroll
+ * @description Tell the scrollView to scroll to the element with an id
+ * matching window.location.hash.
+ *
+ * If no matching element is found, it will scroll to top.
+ *
+ * @param {boolean=} shouldAnimate Whether the scroll should animate.
+ */
+ 'anchorScroll',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#freezeScroll
+ * @description Does not allow this scroll view to scroll either x or y.
+ * @param {boolean=} shouldFreeze Should this scroll view be prevented from scrolling or not.
+ * @returns {boolean} If the scroll view is being prevented from scrolling or not.
+ */
+ 'freezeScroll',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#freezeAllScrolls
+ * @description Does not allow any of the app's scroll views to scroll either x or y.
+ * @param {boolean=} shouldFreeze Should all app scrolls be prevented from scrolling or not.
+ */
+ 'freezeAllScrolls',
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#getScrollView
+ * @returns {object} The scrollView associated with this delegate.
+ */
+ 'getScrollView'
+ /**
+ * @ngdoc method
+ * @name $ionicScrollDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * scrollViews with `delegate-handle` matching the given handle.
+ *
+ * Example: `$ionicScrollDelegate.$getByHandle('my-handle').scrollTop();`
+ */
+]));
+
+
+/**
+ * @ngdoc service
+ * @name $ionicSideMenuDelegate
+ * @module ionic
+ *
+ * @description
+ * Delegate for controlling the {@link ionic.directive:ionSideMenus} directive.
+ *
+ * Methods called directly on the $ionicSideMenuDelegate service will control all side
+ * menus. Use the {@link ionic.service:$ionicSideMenuDelegate#$getByHandle $getByHandle}
+ * method to control specific ionSideMenus instances.
+ *
+ * @usage
+ *
+ * ```html
+ * <body ng-controller="MainCtrl">
+ * <ion-side-menus>
+ * <ion-side-menu-content>
+ * Content!
+ * <button ng-click="toggleLeftSideMenu()">
+ * Toggle Left Side Menu
+ * </button>
+ * </ion-side-menu-content>
+ * <ion-side-menu side="left">
+ * Left Menu!
+ * <ion-side-menu>
+ * </ion-side-menus>
+ * </body>
+ * ```
+ * ```js
+ * function MainCtrl($scope, $ionicSideMenuDelegate) {
+ * $scope.toggleLeftSideMenu = function() {
+ * $ionicSideMenuDelegate.toggleLeft();
+ * };
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicSideMenuDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#toggleLeft
+ * @description Toggle the left side menu (if it exists).
+ * @param {boolean=} isOpen Whether to open or close the menu.
+ * Default: Toggles the menu.
+ */
+ 'toggleLeft',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#toggleRight
+ * @description Toggle the right side menu (if it exists).
+ * @param {boolean=} isOpen Whether to open or close the menu.
+ * Default: Toggles the menu.
+ */
+ 'toggleRight',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#getOpenRatio
+ * @description Gets the ratio of open amount over menu width. For example, a
+ * menu of width 100 that is opened by 50 pixels is 50% opened, and would return
+ * a ratio of 0.5.
+ *
+ * @returns {float} 0 if nothing is open, between 0 and 1 if left menu is
+ * opened/opening, and between 0 and -1 if right menu is opened/opening.
+ */
+ 'getOpenRatio',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#isOpen
+ * @returns {boolean} Whether either the left or right menu is currently opened.
+ */
+ 'isOpen',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#isOpenLeft
+ * @returns {boolean} Whether the left menu is currently opened.
+ */
+ 'isOpenLeft',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#isOpenRight
+ * @returns {boolean} Whether the right menu is currently opened.
+ */
+ 'isOpenRight',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#canDragContent
+ * @param {boolean=} canDrag Set whether the content can or cannot be dragged to open
+ * side menus.
+ * @returns {boolean} Whether the content can be dragged to open side menus.
+ */
+ 'canDragContent',
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#edgeDragThreshold
+ * @param {boolean|number=} value Set whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Accepts three different values:
+ * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu.
+ * - If true is given, the default number of pixels (25) is used as the maximum allowed distance.
+ * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed.
+ * @returns {boolean} Whether the drag can start only from within the edge of screen threshold.
+ */
+ 'edgeDragThreshold'
+ /**
+ * @ngdoc method
+ * @name $ionicSideMenuDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * {@link ionic.directive:ionSideMenus} directives with `delegate-handle` matching
+ * the given handle.
+ *
+ * Example: `$ionicSideMenuDelegate.$getByHandle('my-handle').toggleLeft();`
+ */
+]));
+
+
+/**
+ * @ngdoc service
+ * @name $ionicSlideBoxDelegate
+ * @module ionic
+ * @description
+ * Delegate that controls the {@link ionic.directive:ionSlideBox} directive.
+ *
+ * Methods called directly on the $ionicSlideBoxDelegate service will control all slide boxes. Use the {@link ionic.service:$ionicSlideBoxDelegate#$getByHandle $getByHandle}
+ * method to control specific slide box instances.
+ *
+ * @usage
+ *
+ * ```html
+ * <ion-view>
+ * <ion-slide-box>
+ * <ion-slide>
+ * <div class="box blue">
+ * <button ng-click="nextSlide()">Next slide!</button>
+ * </div>
+ * </ion-slide>
+ * <ion-slide>
+ * <div class="box red">
+ * Slide 2!
+ * </div>
+ * </ion-slide>
+ * </ion-slide-box>
+ * </ion-view>
+ * ```
+ * ```js
+ * function MyCtrl($scope, $ionicSlideBoxDelegate) {
+ * $scope.nextSlide = function() {
+ * $ionicSlideBoxDelegate.next();
+ * }
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicSlideBoxDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#update
+ * @description
+ * Update the slidebox (for example if using Angular with ng-repeat,
+ * resize it for the elements inside).
+ */
+ 'update',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#slide
+ * @param {number} to The index to slide to.
+ * @param {number=} speed The number of milliseconds the change should take.
+ */
+ 'slide',
+ 'select',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#enableSlide
+ * @param {boolean=} shouldEnable Whether to enable sliding the slidebox.
+ * @returns {boolean} Whether sliding is enabled.
+ */
+ 'enableSlide',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#previous
+ * @param {number=} speed The number of milliseconds the change should take.
+ * @description Go to the previous slide. Wraps around if at the beginning.
+ */
+ 'previous',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#next
+ * @param {number=} speed The number of milliseconds the change should take.
+ * @description Go to the next slide. Wraps around if at the end.
+ */
+ 'next',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#stop
+ * @description Stop sliding. The slideBox will not move again until
+ * explicitly told to do so.
+ */
+ 'stop',
+ 'autoPlay',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#start
+ * @description Start sliding again if the slideBox was stopped.
+ */
+ 'start',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#currentIndex
+ * @returns number The index of the current slide.
+ */
+ 'currentIndex',
+ 'selected',
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#slidesCount
+ * @returns number The number of slides there are currently.
+ */
+ 'slidesCount',
+ 'count',
+ 'loop'
+ /**
+ * @ngdoc method
+ * @name $ionicSlideBoxDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * {@link ionic.directive:ionSlideBox} directives with `delegate-handle` matching
+ * the given handle.
+ *
+ * Example: `$ionicSlideBoxDelegate.$getByHandle('my-handle').stop();`
+ */
+]));
+
+
+/**
+ * @ngdoc service
+ * @name $ionicTabsDelegate
+ * @module ionic
+ *
+ * @description
+ * Delegate for controlling the {@link ionic.directive:ionTabs} directive.
+ *
+ * Methods called directly on the $ionicTabsDelegate service will control all ionTabs
+ * directives. Use the {@link ionic.service:$ionicTabsDelegate#$getByHandle $getByHandle}
+ * method to control specific ionTabs instances.
+ *
+ * @usage
+ *
+ * ```html
+ * <body ng-controller="MyCtrl">
+ * <ion-tabs>
+ *
+ * <ion-tab title="Tab 1">
+ * Hello tab 1!
+ * <button ng-click="selectTabWithIndex(1)">Select tab 2!</button>
+ * </ion-tab>
+ * <ion-tab title="Tab 2">Hello tab 2!</ion-tab>
+ *
+ * </ion-tabs>
+ * </body>
+ * ```
+ * ```js
+ * function MyCtrl($scope, $ionicTabsDelegate) {
+ * $scope.selectTabWithIndex = function(index) {
+ * $ionicTabsDelegate.select(index);
+ * }
+ * }
+ * ```
+ */
+IonicModule
+.service('$ionicTabsDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicTabsDelegate#select
+ * @description Select the tab matching the given index.
+ *
+ * @param {number} index Index of the tab to select.
+ */
+ 'select',
+ /**
+ * @ngdoc method
+ * @name $ionicTabsDelegate#selectedIndex
+ * @returns `number` The index of the selected tab, or -1.
+ */
+ 'selectedIndex',
+ /**
+ * @ngdoc method
+ * @name $ionicTabsDelegate#showBar
+ * @description
+ * Set/get whether the {@link ionic.directive:ionTabs} is shown
+ * @param {boolean} show Whether to show the bar.
+ * @returns {boolean} Whether the bar is shown.
+ */
+ 'showBar'
+ /**
+ * @ngdoc method
+ * @name $ionicTabsDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * {@link ionic.directive:ionTabs} directives with `delegate-handle` matching
+ * the given handle.
+ *
+ * Example: `$ionicTabsDelegate.$getByHandle('my-handle').select(0);`
+ */
+]));
+
+// closure to keep things neat
+(function() {
+ var templatesToCache = [];
+
+/**
+ * @ngdoc service
+ * @name $ionicTemplateCache
+ * @module ionic
+ * @description A service that preemptively caches template files to eliminate transition flicker and boost performance.
+ * @usage
+ * State templates are cached automatically, but you can optionally cache other templates.
+ *
+ * ```js
+ * $ionicTemplateCache('myNgIncludeTemplate.html');
+ * ```
+ *
+ * Optionally disable all preemptive caching with the `$ionicConfigProvider` or individual states by setting `prefetchTemplate`
+ * in the `$state` definition
+ *
+ * ```js
+ * angular.module('myApp', ['ionic'])
+ * .config(function($stateProvider, $ionicConfigProvider) {
+ *
+ * // disable preemptive template caching globally
+ * $ionicConfigProvider.templates.prefetch(false);
+ *
+ * // disable individual states
+ * $stateProvider
+ * .state('tabs', {
+ * url: "/tab",
+ * abstract: true,
+ * prefetchTemplate: false,
+ * templateUrl: "tabs-templates/tabs.html"
+ * })
+ * .state('tabs.home', {
+ * url: "/home",
+ * views: {
+ * 'home-tab': {
+ * prefetchTemplate: false,
+ * templateUrl: "tabs-templates/home.html",
+ * controller: 'HomeTabCtrl'
+ * }
+ * }
+ * });
+ * });
+ * ```
+ */
+IonicModule
+.factory('$ionicTemplateCache', [
+'$http',
+'$templateCache',
+'$timeout',
+function($http, $templateCache, $timeout) {
+ var toCache = templatesToCache,
+ hasRun;
+
+ function $ionicTemplateCache(templates) {
+ if (typeof templates === 'undefined') {
+ return run();
+ }
+ if (isString(templates)) {
+ templates = [templates];
+ }
+ forEach(templates, function(template) {
+ toCache.push(template);
+ });
+ if (hasRun) {
+ run();
+ }
+ }
+
+ // run through methods - internal method
+ function run() {
+ var template;
+ $ionicTemplateCache._runCount++;
+
+ hasRun = true;
+ // ignore if race condition already zeroed out array
+ if (toCache.length === 0) return;
+
+ var i = 0;
+ while (i < 4 && (template = toCache.pop())) {
+ // note that inline templates are ignored by this request
+ if (isString(template)) $http.get(template, { cache: $templateCache });
+ i++;
+ }
+ // only preload 3 templates a second
+ if (toCache.length) {
+ $timeout(run, 1000);
+ }
+ }
+
+ // exposing for testing
+ $ionicTemplateCache._runCount = 0;
+ // default method
+ return $ionicTemplateCache;
+}])
+
+// Intercepts the $stateprovider.state() command to look for templateUrls that can be cached
+.config([
+'$stateProvider',
+'$ionicConfigProvider',
+function($stateProvider, $ionicConfigProvider) {
+ var stateProviderState = $stateProvider.state;
+ $stateProvider.state = function(stateName, definition) {
+ // don't even bother if it's disabled. note, another config may run after this, so it's not a catch-all
+ if (typeof definition === 'object') {
+ var enabled = definition.prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
+ if (enabled && isString(definition.templateUrl)) templatesToCache.push(definition.templateUrl);
+ if (angular.isObject(definition.views)) {
+ for (var key in definition.views) {
+ enabled = definition.views[key].prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
+ if (enabled && isString(definition.views[key].templateUrl)) templatesToCache.push(definition.views[key].templateUrl);
+ }
+ }
+ }
+ return stateProviderState.call($stateProvider, stateName, definition);
+ };
+}])
+
+// process the templateUrls collected by the $stateProvider, adding them to the cache
+.run(['$ionicTemplateCache', function($ionicTemplateCache) {
+ $ionicTemplateCache();
+}]);
+
+})();
+
+IonicModule
+.factory('$ionicTemplateLoader', [
+ '$compile',
+ '$controller',
+ '$http',
+ '$q',
+ '$rootScope',
+ '$templateCache',
+function($compile, $controller, $http, $q, $rootScope, $templateCache) {
+
+ return {
+ load: fetchTemplate,
+ compile: loadAndCompile
+ };
+
+ function fetchTemplate(url) {
+ return $http.get(url, {cache: $templateCache})
+ .then(function(response) {
+ return response.data && response.data.trim();
+ });
+ }
+
+ function loadAndCompile(options) {
+ options = extend({
+ template: '',
+ templateUrl: '',
+ scope: null,
+ controller: null,
+ locals: {},
+ appendTo: null
+ }, options || {});
+
+ var templatePromise = options.templateUrl ?
+ this.load(options.templateUrl) :
+ $q.when(options.template);
+
+ return templatePromise.then(function(template) {
+ var controller;
+ var scope = options.scope || $rootScope.$new();
+
+ //Incase template doesn't have just one root element, do this
+ var element = jqLite('<div>').html(template).contents();
+
+ if (options.controller) {
+ controller = $controller(
+ options.controller,
+ extend(options.locals, {
+ $scope: scope
+ })
+ );
+ element.children().data('$ngControllerController', controller);
+ }
+ if (options.appendTo) {
+ jqLite(options.appendTo).append(element);
+ }
+
+ $compile(element)(scope);
+
+ return {
+ element: element,
+ scope: scope
+ };
+ });
+ }
+
+}]);
+
+/**
+ * @private
+ * DEPRECATED, as of v1.0.0-beta14 -------
+ */
+IonicModule
+.factory('$ionicViewService', ['$ionicHistory', '$log', function($ionicHistory, $log) {
+
+ function warn(oldMethod, newMethod) {
+ $log.warn('$ionicViewService' + oldMethod + ' is deprecated, please use $ionicHistory' + newMethod + ' instead: http://ionicframework.com/docs/nightly/api/service/$ionicHistory/');
+ }
+
+ warn('', '');
+
+ var methodsMap = {
+ getCurrentView: 'currentView',
+ getBackView: 'backView',
+ getForwardView: 'forwardView',
+ getCurrentStateName: 'currentStateName',
+ nextViewOptions: 'nextViewOptions',
+ clearHistory: 'clearHistory'
+ };
+
+ forEach(methodsMap, function(newMethod, oldMethod) {
+ methodsMap[oldMethod] = function() {
+ warn('.' + oldMethod, '.' + newMethod);
+ return $ionicHistory[newMethod].apply(this, arguments);
+ };
+ });
+
+ return methodsMap;
+
+}]);
+
+/**
+ * @private
+ * TODO document
+ */
+
+IonicModule.factory('$ionicViewSwitcher', [
+ '$timeout',
+ '$document',
+ '$q',
+ '$ionicClickBlock',
+ '$ionicConfig',
+ '$ionicNavBarDelegate',
+function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDelegate) {
+
+ var TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
+ var DATA_NO_CACHE = '$noCache';
+ var DATA_DESTROY_ELE = '$destroyEle';
+ var DATA_ELE_IDENTIFIER = '$eleId';
+ var DATA_VIEW_ACCESSED = '$accessed';
+ var DATA_FALLBACK_TIMER = '$fallbackTimer';
+ var DATA_VIEW = '$viewData';
+ var NAV_VIEW_ATTR = 'nav-view';
+ var VIEW_STATUS_ACTIVE = 'active';
+ var VIEW_STATUS_CACHED = 'cached';
+ var VIEW_STATUS_STAGED = 'stage';
+
+ var transitionCounter = 0;
+ var nextTransition, nextDirection;
+ ionic.transition = ionic.transition || {};
+ ionic.transition.isActive = false;
+ var isActiveTimer;
+ var cachedAttr = ionic.DomUtil.cachedAttr;
+ var transitionPromises = [];
+ var defaultTimeout = 1100;
+
+ var ionicViewSwitcher = {
+
+ create: function(navViewCtrl, viewLocals, enteringView, leavingView, renderStart, renderEnd) {
+ // get a reference to an entering/leaving element if they exist
+ // loop through to see if the view is already in the navViewElement
+ var enteringEle, leavingEle;
+ var transitionId = ++transitionCounter;
+ var alreadyInDom;
+
+ var switcher = {
+
+ init: function(registerData, callback) {
+ ionicViewSwitcher.isTransitioning(true);
+
+ switcher.loadViewElements(registerData);
+
+ switcher.render(registerData, function() {
+ callback && callback();
+ });
+ },
+
+ loadViewElements: function(registerData) {
+ var x, l, viewEle;
+ var viewElements = navViewCtrl.getViewElements();
+ var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView);
+ var navViewActiveEleId = navViewCtrl.activeEleId();
+
+ for (x = 0, l = viewElements.length; x < l; x++) {
+ viewEle = viewElements.eq(x);
+
+ if (viewEle.data(DATA_ELE_IDENTIFIER) === enteringEleIdentifier) {
+ // we found an existing element in the DOM that should be entering the view
+ if (viewEle.data(DATA_NO_CACHE)) {
+ // the existing element should not be cached, don't use it
+ viewEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier + ionic.Utils.nextUid());
+ viewEle.data(DATA_DESTROY_ELE, true);
+
+ } else {
+ enteringEle = viewEle;
+ }
+
+ } else if (isDefined(navViewActiveEleId) && viewEle.data(DATA_ELE_IDENTIFIER) === navViewActiveEleId) {
+ leavingEle = viewEle;
+ }
+
+ if (enteringEle && leavingEle) break;
+ }
+
+ alreadyInDom = !!enteringEle;
+
+ if (!alreadyInDom) {
+ // still no existing element to use
+ // create it using existing template/scope/locals
+ enteringEle = registerData.ele || ionicViewSwitcher.createViewEle(viewLocals);
+
+ // existing elements in the DOM are looked up by their state name and state id
+ enteringEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier);
+ }
+
+ if (renderEnd) {
+ navViewCtrl.activeEleId(enteringEleIdentifier);
+ }
+
+ registerData.ele = null;
+ },
+
+ render: function(registerData, callback) {
+ if (alreadyInDom) {
+ // it was already found in the DOM, just reconnect the scope
+ ionic.Utils.reconnectScope(enteringEle.scope());
+
+ } else {
+ // the entering element is not already in the DOM
+ // set that the entering element should be "staged" and its
+ // styles of where this element will go before it hits the DOM
+ navViewAttr(enteringEle, VIEW_STATUS_STAGED);
+
+ var enteringData = getTransitionData(viewLocals, enteringEle, registerData.direction, enteringView);
+ var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none;
+ transitionFn(enteringEle, null, enteringData.direction, true).run(0);
+
+ enteringEle.data(DATA_VIEW, {
+ viewId: enteringData.viewId,
+ historyId: enteringData.historyId,
+ stateName: enteringData.stateName,
+ stateParams: enteringData.stateParams
+ });
+
+ // if the current state has cache:false
+ // or the element has cache-view="false" attribute
+ if (viewState(viewLocals).cache === false || viewState(viewLocals).cache === 'false' ||
+ enteringEle.attr('cache-view') == 'false' || $ionicConfig.views.maxCache() === 0) {
+ enteringEle.data(DATA_NO_CACHE, true);
+ }
+
+ // append the entering element to the DOM, create a new scope and run link
+ var viewScope = navViewCtrl.appendViewElement(enteringEle, viewLocals);
+
+ delete enteringData.direction;
+ delete enteringData.transition;
+ viewScope.$emit('$ionicView.loaded', enteringData);
+ }
+
+ // update that this view was just accessed
+ enteringEle.data(DATA_VIEW_ACCESSED, Date.now());
+
+ callback && callback();
+ },
+
+ transition: function(direction, enableBack, allowAnimate) {
+ var deferred;
+ var enteringData = getTransitionData(viewLocals, enteringEle, direction, enteringView);
+ var leavingData = extend(extend({}, enteringData), getViewData(leavingView));
+ enteringData.transitionId = leavingData.transitionId = transitionId;
+ enteringData.fromCache = !!alreadyInDom;
+ enteringData.enableBack = !!enableBack;
+ enteringData.renderStart = renderStart;
+ enteringData.renderEnd = renderEnd;
+
+ cachedAttr(enteringEle.parent(), 'nav-view-transition', enteringData.transition);
+ cachedAttr(enteringEle.parent(), 'nav-view-direction', enteringData.direction);
+
+ // cancel any previous transition complete fallbacks
+ $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
+
+ // get the transition ready and see if it'll animate
+ var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none;
+ var viewTransition = transitionFn(enteringEle, leavingEle, enteringData.direction,
+ enteringData.shouldAnimate && allowAnimate && renderEnd);
+
+ if (viewTransition.shouldAnimate) {
+ // attach transitionend events (and fallback timer)
+ enteringEle.on(TRANSITIONEND_EVENT, completeOnTransitionEnd);
+ enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, defaultTimeout));
+ $ionicClickBlock.show(defaultTimeout);
+ }
+
+ if (renderStart) {
+ // notify the views "before" the transition starts
+ switcher.emit('before', enteringData, leavingData);
+
+ // stage entering element, opacity 0, no transition duration
+ navViewAttr(enteringEle, VIEW_STATUS_STAGED);
+
+ // render the elements in the correct location for their starting point
+ viewTransition.run(0);
+ }
+
+ if (renderEnd) {
+ // create a promise so we can keep track of when all transitions finish
+ // only required if this transition should complete
+ deferred = $q.defer();
+ transitionPromises.push(deferred.promise);
+ }
+
+ if (renderStart && renderEnd) {
+ // CSS "auto" transitioned, not manually transitioned
+ // wait a frame so the styles apply before auto transitioning
+ $timeout(function() {
+ ionic.requestAnimationFrame(onReflow);
+ });
+ } else if (!renderEnd) {
+ // just the start of a manual transition
+ // but it will not render the end of the transition
+ navViewAttr(enteringEle, 'entering');
+ navViewAttr(leavingEle, 'leaving');
+
+ // return the transition run method so each step can be ran manually
+ return {
+ run: viewTransition.run,
+ cancel: function(shouldAnimate) {
+ if (shouldAnimate) {
+ enteringEle.on(TRANSITIONEND_EVENT, cancelOnTransitionEnd);
+ enteringEle.data(DATA_FALLBACK_TIMER, $timeout(cancelTransition, defaultTimeout));
+ $ionicClickBlock.show(defaultTimeout);
+ } else {
+ cancelTransition();
+ }
+ viewTransition.shouldAnimate = shouldAnimate;
+ viewTransition.run(0);
+ viewTransition = null;
+ }
+ };
+
+ } else if (renderEnd) {
+ // just the end of a manual transition
+ // happens after the manual transition has completed
+ // and a full history change has happened
+ onReflow();
+ }
+
+
+ function onReflow() {
+ // remove that we're staging the entering element so it can auto transition
+ navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE);
+ navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED);
+
+ // start the auto transition and let the CSS take over
+ viewTransition.run(1);
+
+ // trigger auto transitions on the associated nav bars
+ $ionicNavBarDelegate._instances.forEach(function(instance) {
+ instance.triggerTransitionStart(transitionId);
+ });
+
+ if (!viewTransition.shouldAnimate) {
+ // no animated auto transition
+ transitionComplete();
+ }
+ }
+
+ // Make sure that transitionend events bubbling up from children won't fire
+ // transitionComplete. Will only go forward if ev.target == the element listening.
+ function completeOnTransitionEnd(ev) {
+ if (ev.target !== this) return;
+ transitionComplete();
+ }
+ function transitionComplete() {
+ if (transitionComplete.x) return;
+ transitionComplete.x = true;
+
+ enteringEle.off(TRANSITIONEND_EVENT, completeOnTransitionEnd);
+ $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
+ leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER));
+
+ // resolve that this one transition (there could be many w/ nested views)
+ deferred && deferred.resolve(navViewCtrl);
+
+ // the most recent transition added has completed and all the active
+ // transition promises should be added to the services array of promises
+ if (transitionId === transitionCounter) {
+ $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd);
+
+ // emit that the views have finished transitioning
+ // each parent nav-view will update which views are active and cached
+ switcher.emit('after', enteringData, leavingData);
+ switcher.cleanup(enteringData);
+ }
+
+ // tell the nav bars that the transition has ended
+ $ionicNavBarDelegate._instances.forEach(function(instance) {
+ instance.triggerTransitionEnd();
+ });
+
+
+ // remove any references that could cause memory issues
+ nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null;
+ }
+
+ // Make sure that transitionend events bubbling up from children won't fire
+ // transitionComplete. Will only go forward if ev.target == the element listening.
+ function cancelOnTransitionEnd(ev) {
+ if (ev.target !== this) return;
+ cancelTransition();
+ }
+ function cancelTransition() {
+ navViewAttr(enteringEle, VIEW_STATUS_CACHED);
+ navViewAttr(leavingEle, VIEW_STATUS_ACTIVE);
+ enteringEle.off(TRANSITIONEND_EVENT, cancelOnTransitionEnd);
+ $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
+ ionicViewSwitcher.transitionEnd([navViewCtrl]);
+ }
+
+ },
+
+ emit: function(step, enteringData, leavingData) {
+ var enteringScope = getScopeForElement(enteringEle, enteringData);
+ var leavingScope = getScopeForElement(leavingEle, leavingData);
+
+ var prefixesAreEqual;
+
+ if ( !enteringData.viewId || enteringData.abstractView ) {
+ // it's an abstract view, so treat it accordingly
+
+ // we only get access to the leaving scope once in the transition,
+ // so dispatch all events right away if it exists
+ if ( leavingScope ) {
+ leavingScope.$emit('$ionicView.beforeLeave', leavingData);
+ leavingScope.$emit('$ionicView.leave', leavingData);
+ leavingScope.$emit('$ionicView.afterLeave', leavingData);
+ leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData);
+ leavingScope.$broadcast('$ionicParentView.leave', leavingData);
+ leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData);
+ }
+ }
+ else {
+ // it's a regular view, so do the normal process
+ if (step == 'after') {
+ if (enteringScope) {
+ enteringScope.$emit('$ionicView.enter', enteringData);
+ enteringScope.$broadcast('$ionicParentView.enter', enteringData);
+ }
+
+ if (leavingScope) {
+ leavingScope.$emit('$ionicView.leave', leavingData);
+ leavingScope.$broadcast('$ionicParentView.leave', leavingData);
+ }
+ else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) {
+ // we only want to dispatch this when we are doing a single-tier
+ // state change such as changing a tab, so compare the state
+ // for the same state-prefix but different suffix
+ prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
+ if ( prefixesAreEqual ) {
+ enteringScope.$emit('$ionicNavView.leave', leavingData);
+ }
+ }
+ }
+
+ if (enteringScope) {
+ enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData);
+ enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData);
+ }
+
+ if (leavingScope) {
+ leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData);
+ leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData);
+
+ } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) {
+ // we only want to dispatch this when we are doing a single-tier
+ // state change such as changing a tab, so compare the state
+ // for the same state-prefix but different suffix
+ prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
+ if ( prefixesAreEqual ) {
+ enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData);
+ }
+ }
+ }
+ },
+
+ cleanup: function(transData) {
+ // check if any views should be removed
+ if (leavingEle && transData.direction == 'back' && !$ionicConfig.views.forwardCache()) {
+ // if they just navigated back we can destroy the forward view
+ // do not remove forward views if cacheForwardViews config is true
+ destroyViewEle(leavingEle);
+ }
+
+ var viewElements = navViewCtrl.getViewElements();
+ var viewElementsLength = viewElements.length;
+ var x, viewElement;
+ var removeOldestAccess = (viewElementsLength - 1) > $ionicConfig.views.maxCache();
+ var removableEle;
+ var oldestAccess = Date.now();
+
+ for (x = 0; x < viewElementsLength; x++) {
+ viewElement = viewElements.eq(x);
+
+ if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) {
+ // remember what was the oldest element to be accessed so it can be destroyed
+ oldestAccess = viewElement.data(DATA_VIEW_ACCESSED);
+ removableEle = viewElements.eq(x);
+
+ } else if (viewElement.data(DATA_DESTROY_ELE) && navViewAttr(viewElement) != VIEW_STATUS_ACTIVE) {
+ destroyViewEle(viewElement);
+ }
+ }
+
+ destroyViewEle(removableEle);
+
+ if (enteringEle.data(DATA_NO_CACHE)) {
+ enteringEle.data(DATA_DESTROY_ELE, true);
+ }
+ },
+
+ enteringEle: function() { return enteringEle; },
+ leavingEle: function() { return leavingEle; }
+
+ };
+
+ return switcher;
+ },
+
+ transitionEnd: function(navViewCtrls) {
+ forEach(navViewCtrls, function(navViewCtrl) {
+ navViewCtrl.transitionEnd();
+ });
+
+ ionicViewSwitcher.isTransitioning(false);
+ $ionicClickBlock.hide();
+ transitionPromises = [];
+ },
+
+ nextTransition: function(val) {
+ nextTransition = val;
+ },
+
+ nextDirection: function(val) {
+ nextDirection = val;
+ },
+
+ isTransitioning: function(val) {
+ if (arguments.length) {
+ ionic.transition.isActive = !!val;
+ $timeout.cancel(isActiveTimer);
+ if (val) {
+ isActiveTimer = $timeout(function() {
+ ionicViewSwitcher.isTransitioning(false);
+ }, 999);
+ }
+ }
+ return ionic.transition.isActive;
+ },
+
+ createViewEle: function(viewLocals) {
+ var containerEle = $document[0].createElement('div');
+ if (viewLocals && viewLocals.$template) {
+ containerEle.innerHTML = viewLocals.$template;
+ if (containerEle.children.length === 1) {
+ containerEle.children[0].classList.add('pane');
+ if ( viewLocals.$$state && viewLocals.$$state.self && viewLocals.$$state.self['abstract'] ) {
+ angular.element(containerEle.children[0]).attr("abstract", "true");
+ }
+ else {
+ if ( viewLocals.$$state && viewLocals.$$state.self ) {
+ angular.element(containerEle.children[0]).attr("state", viewLocals.$$state.self.name);
+ }
+
+ }
+ return jqLite(containerEle.children[0]);
+ }
+ }
+ containerEle.className = "pane";
+ return jqLite(containerEle);
+ },
+
+ viewEleIsActive: function(viewEle, isActiveAttr) {
+ navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED);
+ },
+
+ getTransitionData: getTransitionData,
+ navViewAttr: navViewAttr,
+ destroyViewEle: destroyViewEle
+
+ };
+
+ return ionicViewSwitcher;
+
+
+ function getViewElementIdentifier(locals, view) {
+ if (viewState(locals)['abstract']) return viewState(locals).name;
+ if (view) return view.stateId || view.viewId;
+ return ionic.Utils.nextUid();
+ }
+
+ function viewState(locals) {
+ return locals && locals.$$state && locals.$$state.self || {};
+ }
+
+ function getTransitionData(viewLocals, enteringEle, direction, view) {
+ // Priority
+ // 1) attribute directive on the button/link to this view
+ // 2) entering element's attribute
+ // 3) entering view's $state config property
+ // 4) view registration data
+ // 5) global config
+ // 6) fallback value
+
+ var state = viewState(viewLocals);
+ var viewTransition = nextTransition || cachedAttr(enteringEle, 'view-transition') || state.viewTransition || $ionicConfig.views.transition() || 'ios';
+ var navBarTransition = $ionicConfig.navBar.transition();
+ direction = nextDirection || cachedAttr(enteringEle, 'view-direction') || state.viewDirection || direction || 'none';
+
+ return extend(getViewData(view), {
+ transition: viewTransition,
+ navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition,
+ direction: direction,
+ shouldAnimate: (viewTransition !== 'none' && direction !== 'none')
+ });
+ }
+
+ function getViewData(view) {
+ view = view || {};
+ return {
+ viewId: view.viewId,
+ historyId: view.historyId,
+ stateId: view.stateId,
+ stateName: view.stateName,
+ stateParams: view.stateParams
+ };
+ }
+
+ function navViewAttr(ele, value) {
+ if (arguments.length > 1) {
+ cachedAttr(ele, NAV_VIEW_ATTR, value);
+ } else {
+ return cachedAttr(ele, NAV_VIEW_ATTR);
+ }
+ }
+
+ function destroyViewEle(ele) {
+ // we found an element that should be removed
+ // destroy its scope, then remove the element
+ if (ele && ele.length) {
+ var viewScope = ele.scope();
+ if (viewScope) {
+ viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW));
+ viewScope.$destroy();
+ }
+ ele.remove();
+ }
+ }
+
+ function compareStatePrefixes(enteringStateName, exitingStateName) {
+ var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.');
+ var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.');
+
+ // if either of the prefixes are empty, just return false
+ if ( enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0 ) {
+ return false;
+ }
+
+ var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex);
+ var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex);
+
+ return enteringPrefix === exitingPrefix;
+ }
+
+ function getScopeForElement(element, stateData) {
+ if ( !element ) {
+ return null;
+ }
+ // check if it's abstract
+ var attributeValue = angular.element(element).attr("abstract");
+ var stateValue = angular.element(element).attr("state");
+
+ if ( attributeValue !== "true" ) {
+ // it's not an abstract view, so make sure the element
+ // matches the state. Due to abstract view weirdness,
+ // sometimes it doesn't. If it doesn't, don't dispatch events
+ // so leave the scope undefined
+ if ( stateValue === stateData.stateName ) {
+ return angular.element(element).scope();
+ }
+ return null;
+ }
+ else {
+ // it is an abstract element, so look for element with the "state" attributeValue
+ // set to the name of the stateData state
+ var elements = aggregateNavViewChildren(element);
+ for ( var i = 0; i < elements.length; i++ ) {
+ var state = angular.element(elements[i]).attr("state");
+ if ( state === stateData.stateName ) {
+ stateData.abstractView = true;
+ return angular.element(elements[i]).scope();
+ }
+ }
+ // we didn't find a match, so return null
+ return null;
+ }
+ }
+
+ function aggregateNavViewChildren(element) {
+ var aggregate = [];
+ var navViews = angular.element(element).find("ion-nav-view");
+ for ( var i = 0; i < navViews.length; i++ ) {
+ var children = angular.element(navViews[i]).children();
+ var childrenAggregated = [];
+ for ( var j = 0; j < children.length; j++ ) {
+ childrenAggregated = childrenAggregated.concat(children[j]);
+ }
+ aggregate = aggregate.concat(childrenAggregated);
+ }
+ return aggregate;
+ }
+
+}]);
+
+/**
+ * ================== angular-ios9-uiwebview.patch.js v1.1.1 ==================
+ *
+ * This patch works around iOS9 UIWebView regression that causes infinite digest
+ * errors in Angular.
+ *
+ * The patch can be applied to Angular 1.2.0 – 1.4.5. Newer versions of Angular
+ * have the workaround baked in.
+ *
+ * To apply this patch load/bundle this file with your application and add a
+ * dependency on the "ngIOS9UIWebViewPatch" module to your main app module.
+ *
+ * For example:
+ *
+ * ```
+ * angular.module('myApp', ['ngRoute'])`
+ * ```
+ *
+ * becomes
+ *
+ * ```
+ * angular.module('myApp', ['ngRoute', 'ngIOS9UIWebViewPatch'])
+ * ```
+ *
+ *
+ * More info:
+ * - https://openradar.appspot.com/22186109
+ * - https://github.com/angular/angular.js/issues/12241
+ * - https://github.com/driftyco/ionic/issues/4082
+ *
+ *
+ * @license AngularJS
+ * (c) 2010-2015 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+
+angular.module('ngIOS9UIWebViewPatch', ['ng']).config(['$provide', function($provide) {
+ 'use strict';
+
+ $provide.decorator('$browser', ['$delegate', '$window', function($delegate, $window) {
+
+ if (isIOS9UIWebView($window.navigator.userAgent)) {
+ return applyIOS9Shim($delegate);
+ }
+
+ return $delegate;
+
+ function isIOS9UIWebView(userAgent) {
+ return /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent);
+ }
+
+ function applyIOS9Shim(browser) {
+ var pendingLocationUrl = null;
+ var originalUrlFn = browser.url;
+
+ browser.url = function() {
+ if (arguments.length) {
+ pendingLocationUrl = arguments[0];
+ return originalUrlFn.apply(browser, arguments);
+ }
+
+ return pendingLocationUrl || originalUrlFn.apply(browser, arguments);
+ };
+
+ window.addEventListener('popstate', clearPendingLocationUrl, false);
+ window.addEventListener('hashchange', clearPendingLocationUrl, false);
+
+ function clearPendingLocationUrl() {
+ pendingLocationUrl = null;
+ }
+
+ return browser;
+ }
+ }]);
+}]);
+
+/**
+ * @private
+ * Parts of Ionic requires that $scope data is attached to the element.
+ * We do not want to disable adding $scope data to the $element when
+ * $compileProvider.debugInfoEnabled(false) is used.
+ */
+IonicModule.config(['$provide', function($provide) {
+ $provide.decorator('$compile', ['$delegate', function($compile) {
+ $compile.$$addScopeInfo = function $$addScopeInfo($element, scope, isolated, noTemplate) {
+ var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope';
+ $element.data(dataName, scope);
+ };
+ return $compile;
+ }]);
+}]);
+
+/**
+ * @private
+ */
+IonicModule.config([
+ '$provide',
+function($provide) {
+ function $LocationDecorator($location, $timeout) {
+
+ $location.__hash = $location.hash;
+ //Fix: when window.location.hash is set, the scrollable area
+ //found nearest to body's scrollTop is set to scroll to an element
+ //with that ID.
+ $location.hash = function(value) {
+ if (isDefined(value) && value.length > 0) {
+ $timeout(function() {
+ var scroll = document.querySelector('.scroll-content');
+ if (scroll) {
+ scroll.scrollTop = 0;
+ }
+ }, 0, false);
+ }
+ return $location.__hash(value);
+ };
+
+ return $location;
+ }
+
+ $provide.decorator('$location', ['$delegate', '$timeout', $LocationDecorator]);
+}]);
+
+IonicModule
+
+.controller('$ionicHeaderBar', [
+ '$scope',
+ '$element',
+ '$attrs',
+ '$q',
+ '$ionicConfig',
+ '$ionicHistory',
+function($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) {
+ var TITLE = 'title';
+ var BACK_TEXT = 'back-text';
+ var BACK_BUTTON = 'back-button';
+ var DEFAULT_TITLE = 'default-title';
+ var PREVIOUS_TITLE = 'previous-title';
+ var HIDE = 'hide';
+
+ var self = this;
+ var titleText = '';
+ var previousTitleText = '';
+ var titleLeft = 0;
+ var titleRight = 0;
+ var titleCss = '';
+ var isBackEnabled = false;
+ var isBackShown = true;
+ var isNavBackShown = true;
+ var isBackElementShown = false;
+ var titleTextWidth = 0;
+
+
+ self.beforeEnter = function(viewData) {
+ $scope.$broadcast('$ionicView.beforeEnter', viewData);
+ };
+
+
+ self.title = function(newTitleText) {
+ if (arguments.length && newTitleText !== titleText) {
+ getEle(TITLE).innerHTML = newTitleText;
+ titleText = newTitleText;
+ titleTextWidth = 0;
+ }
+ return titleText;
+ };
+
+
+ self.enableBack = function(shouldEnable, disableReset) {
+ // whether or not the back button show be visible, according
+ // to the navigation and history
+ if (arguments.length) {
+ isBackEnabled = shouldEnable;
+ if (!disableReset) self.updateBackButton();
+ }
+ return isBackEnabled;
+ };
+
+
+ self.showBack = function(shouldShow, disableReset) {
+ // different from enableBack() because this will always have the back
+ // visually hidden if false, even if the history says it should show
+ if (arguments.length) {
+ isBackShown = shouldShow;
+ if (!disableReset) self.updateBackButton();
+ }
+ return isBackShown;
+ };
+
+
+ self.showNavBack = function(shouldShow) {
+ // different from showBack() because this is for the entire nav bar's
+ // setting for all of it's child headers. For internal use.
+ isNavBackShown = shouldShow;
+ self.updateBackButton();
+ };
+
+
+ self.updateBackButton = function() {
+ var ele;
+ if ((isBackShown && isNavBackShown && isBackEnabled) !== isBackElementShown) {
+ isBackElementShown = isBackShown && isNavBackShown && isBackEnabled;
+ ele = getEle(BACK_BUTTON);
+ ele && ele.classList[ isBackElementShown ? 'remove' : 'add' ](HIDE);
+ }
+
+ if (isBackEnabled) {
+ ele = ele || getEle(BACK_BUTTON);
+ if (ele) {
+ if (self.backButtonIcon !== $ionicConfig.backButton.icon()) {
+ ele = getEle(BACK_BUTTON + ' .icon');
+ if (ele) {
+ self.backButtonIcon = $ionicConfig.backButton.icon();
+ ele.className = 'icon ' + self.backButtonIcon;
+ }
+ }
+
+ if (self.backButtonText !== $ionicConfig.backButton.text()) {
+ ele = getEle(BACK_BUTTON + ' .back-text');
+ if (ele) {
+ ele.textContent = self.backButtonText = $ionicConfig.backButton.text();
+ }
+ }
+ }
+ }
+ };
+
+
+ self.titleTextWidth = function() {
+ var element = getEle(TITLE);
+ if ( element ) {
+ // If the element has a nav-bar-title, use that instead
+ // to calculate the width of the title
+ var children = angular.element(element).children();
+ for ( var i = 0; i < children.length; i++ ) {
+ if ( angular.element(children[i]).hasClass('nav-bar-title') ) {
+ element = children[i];
+ break;
+ }
+ }
+ }
+ var bounds = ionic.DomUtil.getTextBounds(element);
+ titleTextWidth = Math.min(bounds && bounds.width || 30);
+ return titleTextWidth;
+ };
+
+
+ self.titleWidth = function() {
+ var titleWidth = self.titleTextWidth();
+ var offsetWidth = getEle(TITLE).offsetWidth;
+ if (offsetWidth < titleWidth) {
+ titleWidth = offsetWidth + (titleLeft - titleRight - 5);
+ }
+ return titleWidth;
+ };
+
+
+ self.titleTextX = function() {
+ return ($element[0].offsetWidth / 2) - (self.titleWidth() / 2);
+ };
+
+
+ self.titleLeftRight = function() {
+ return titleLeft - titleRight;
+ };
+
+
+ self.backButtonTextLeft = function() {
+ var offsetLeft = 0;
+ var ele = getEle(BACK_TEXT);
+ while (ele) {
+ offsetLeft += ele.offsetLeft;
+ ele = ele.parentElement;
+ }
+ return offsetLeft;
+ };
+
+
+ self.resetBackButton = function(viewData) {
+ if ($ionicConfig.backButton.previousTitleText()) {
+ var previousTitleEle = getEle(PREVIOUS_TITLE);
+ if (previousTitleEle) {
+ previousTitleEle.classList.remove(HIDE);
+
+ var view = (viewData && $ionicHistory.getViewById(viewData.viewId));
+ var newPreviousTitleText = $ionicHistory.backTitle(view);
+
+ if (newPreviousTitleText !== previousTitleText) {
+ previousTitleText = previousTitleEle.innerHTML = newPreviousTitleText;
+ }
+ }
+ var defaultTitleEle = getEle(DEFAULT_TITLE);
+ if (defaultTitleEle) {
+ defaultTitleEle.classList.remove(HIDE);
+ }
+ }
+ };
+
+
+ self.align = function(textAlign) {
+ var titleEle = getEle(TITLE);
+
+ textAlign = textAlign || $attrs.alignTitle || $ionicConfig.navBar.alignTitle();
+
+ var widths = self.calcWidths(textAlign, false);
+
+ if (isBackShown && previousTitleText && $ionicConfig.backButton.previousTitleText()) {
+ var previousTitleWidths = self.calcWidths(textAlign, true);
+
+ var availableTitleWidth = $element[0].offsetWidth - previousTitleWidths.titleLeft - previousTitleWidths.titleRight;
+
+ if (self.titleTextWidth() <= availableTitleWidth) {
+ widths = previousTitleWidths;
+ }
+ }
+
+ return self.updatePositions(titleEle, widths.titleLeft, widths.titleRight, widths.buttonsLeft, widths.buttonsRight, widths.css, widths.showPrevTitle);
+ };
+
+
+ self.calcWidths = function(textAlign, isPreviousTitle) {
+ var titleEle = getEle(TITLE);
+ var backBtnEle = getEle(BACK_BUTTON);
+ var x, y, z, b, c, d, childSize, bounds;
+ var childNodes = $element[0].childNodes;
+ var buttonsLeft = 0;
+ var buttonsRight = 0;
+ var isCountRightOfTitle;
+ var updateTitleLeft = 0;
+ var updateTitleRight = 0;
+ var updateCss = '';
+ var backButtonWidth = 0;
+
+ // Compute how wide the left children are
+ // Skip all titles (there may still be two titles, one leaving the dom)
+ // Once we encounter a titleEle, realize we are now counting the right-buttons, not left
+ for (x = 0; x < childNodes.length; x++) {
+ c = childNodes[x];
+
+ childSize = 0;
+ if (c.nodeType == 1) {
+ // element node
+ if (c === titleEle) {
+ isCountRightOfTitle = true;
+ continue;
+ }
+
+ if (c.classList.contains(HIDE)) {
+ continue;
+ }
+
+ if (isBackShown && c === backBtnEle) {
+
+ for (y = 0; y < c.childNodes.length; y++) {
+ b = c.childNodes[y];
+
+ if (b.nodeType == 1) {
+
+ if (b.classList.contains(BACK_TEXT)) {
+ for (z = 0; z < b.children.length; z++) {
+ d = b.children[z];
+
+ if (isPreviousTitle) {
+ if (d.classList.contains(DEFAULT_TITLE)) continue;
+ backButtonWidth += d.offsetWidth;
+ } else {
+ if (d.classList.contains(PREVIOUS_TITLE)) continue;
+ backButtonWidth += d.offsetWidth;
+ }
+ }
+
+ } else {
+ backButtonWidth += b.offsetWidth;
+ }
+
+ } else if (b.nodeType == 3 && b.nodeValue.trim()) {
+ bounds = ionic.DomUtil.getTextBounds(b);
+ backButtonWidth += bounds && bounds.width || 0;
+ }
+
+ }
+ childSize = backButtonWidth || c.offsetWidth;
+
+ } else {
+ // not the title, not the back button, not a hidden element
+ childSize = c.offsetWidth;
+ }
+
+ } else if (c.nodeType == 3 && c.nodeValue.trim()) {
+ // text node
+ bounds = ionic.DomUtil.getTextBounds(c);
+ childSize = bounds && bounds.width || 0;
+ }
+
+ if (isCountRightOfTitle) {
+ buttonsRight += childSize;
+ } else {
+ buttonsLeft += childSize;
+ }
+ }
+
+ // Size and align the header titleEle based on the sizes of the left and
+ // right children, and the desired alignment mode
+ if (textAlign == 'left') {
+ updateCss = 'title-left';
+ if (buttonsLeft) {
+ updateTitleLeft = buttonsLeft + 15;
+ }
+ if (buttonsRight) {
+ updateTitleRight = buttonsRight + 15;
+ }
+
+ } else if (textAlign == 'right') {
+ updateCss = 'title-right';
+ if (buttonsLeft) {
+ updateTitleLeft = buttonsLeft + 15;
+ }
+ if (buttonsRight) {
+ updateTitleRight = buttonsRight + 15;
+ }
+
+ } else {
+ // center the default
+ var margin = Math.max(buttonsLeft, buttonsRight) + 10;
+ if (margin > 10) {
+ updateTitleLeft = updateTitleRight = margin;
+ }
+ }
+
+ return {
+ backButtonWidth: backButtonWidth,
+ buttonsLeft: buttonsLeft,
+ buttonsRight: buttonsRight,
+ titleLeft: updateTitleLeft,
+ titleRight: updateTitleRight,
+ showPrevTitle: isPreviousTitle,
+ css: updateCss
+ };
+ };
+
+
+ self.updatePositions = function(titleEle, updateTitleLeft, updateTitleRight, buttonsLeft, buttonsRight, updateCss, showPreviousTitle) {
+ var deferred = $q.defer();
+
+ // only make DOM updates when there are actual changes
+ if (titleEle) {
+ if (updateTitleLeft !== titleLeft) {
+ titleEle.style.left = updateTitleLeft ? updateTitleLeft + 'px' : '';
+ titleLeft = updateTitleLeft;
+ }
+ if (updateTitleRight !== titleRight) {
+ titleEle.style.right = updateTitleRight ? updateTitleRight + 'px' : '';
+ titleRight = updateTitleRight;
+ }
+
+ if (updateCss !== titleCss) {
+ updateCss && titleEle.classList.add(updateCss);
+ titleCss && titleEle.classList.remove(titleCss);
+ titleCss = updateCss;
+ }
+ }
+
+ if ($ionicConfig.backButton.previousTitleText()) {
+ var prevTitle = getEle(PREVIOUS_TITLE);
+ var defaultTitle = getEle(DEFAULT_TITLE);
+
+ prevTitle && prevTitle.classList[ showPreviousTitle ? 'remove' : 'add'](HIDE);
+ defaultTitle && defaultTitle.classList[ showPreviousTitle ? 'add' : 'remove'](HIDE);
+ }
+
+ ionic.requestAnimationFrame(function() {
+ if (titleEle && titleEle.offsetWidth + 10 < titleEle.scrollWidth) {
+ var minRight = buttonsRight + 5;
+ var testRight = $element[0].offsetWidth - titleLeft - self.titleTextWidth() - 20;
+ updateTitleRight = testRight < minRight ? minRight : testRight;
+ if (updateTitleRight !== titleRight) {
+ titleEle.style.right = updateTitleRight + 'px';
+ titleRight = updateTitleRight;
+ }
+ }
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ };
+
+
+ self.setCss = function(elementClassname, css) {
+ ionic.DomUtil.cachedStyles(getEle(elementClassname), css);
+ };
+
+
+ var eleCache = {};
+ function getEle(className) {
+ if (!eleCache[className]) {
+ eleCache[className] = $element[0].querySelector('.' + className);
+ }
+ return eleCache[className];
+ }
+
+
+ $scope.$on('$destroy', function() {
+ for (var n in eleCache) eleCache[n] = null;
+ });
+
+}]);
+
+IonicModule
+.controller('$ionInfiniteScroll', [
+ '$scope',
+ '$attrs',
+ '$element',
+ '$timeout',
+function($scope, $attrs, $element, $timeout) {
+ var self = this;
+ self.isLoading = false;
+
+ $scope.icon = function() {
+ return isDefined($attrs.icon) ? $attrs.icon : 'ion-load-d';
+ };
+
+ $scope.spinner = function() {
+ return isDefined($attrs.spinner) ? $attrs.spinner : '';
+ };
+
+ $scope.$on('scroll.infiniteScrollComplete', function() {
+ finishInfiniteScroll();
+ });
+
+ $scope.$on('$destroy', function() {
+ if (self.scrollCtrl && self.scrollCtrl.$element) self.scrollCtrl.$element.off('scroll', self.checkBounds);
+ if (self.scrollEl && self.scrollEl.removeEventListener) {
+ self.scrollEl.removeEventListener('scroll', self.checkBounds);
+ }
+ });
+
+ // debounce checking infinite scroll events
+ self.checkBounds = ionic.Utils.throttle(checkInfiniteBounds, 300);
+
+ function onInfinite() {
+ ionic.requestAnimationFrame(function() {
+ $element[0].classList.add('active');
+ });
+ self.isLoading = true;
+ $scope.$parent && $scope.$parent.$apply($attrs.onInfinite || '');
+ }
+
+ function finishInfiniteScroll() {
+ ionic.requestAnimationFrame(function() {
+ $element[0].classList.remove('active');
+ });
+ $timeout(function() {
+ if (self.jsScrolling) self.scrollView.resize();
+ // only check bounds again immediately if the page isn't cached (scroll el has height)
+ if ((self.jsScrolling && self.scrollView.__container && self.scrollView.__container.offsetHeight > 0) ||
+ !self.jsScrolling) {
+ self.checkBounds();
+ }
+ }, 30, false);
+ self.isLoading = false;
+ }
+
+ // check if we've scrolled far enough to trigger an infinite scroll
+ function checkInfiniteBounds() {
+ if (self.isLoading) return;
+ var maxScroll = {};
+
+ if (self.jsScrolling) {
+ maxScroll = self.getJSMaxScroll();
+ var scrollValues = self.scrollView.getValues();
+ if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) ||
+ (maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) {
+ onInfinite();
+ }
+ } else {
+ maxScroll = self.getNativeMaxScroll();
+ if ((
+ maxScroll.left !== -1 &&
+ self.scrollEl.scrollLeft >= maxScroll.left - self.scrollEl.clientWidth
+ ) || (
+ maxScroll.top !== -1 &&
+ self.scrollEl.scrollTop >= maxScroll.top - self.scrollEl.clientHeight
+ )) {
+ onInfinite();
+ }
+ }
+ }
+
+ // determine the threshold at which we should fire an infinite scroll
+ // note: this gets processed every scroll event, can it be cached?
+ self.getJSMaxScroll = function() {
+ var maxValues = self.scrollView.getScrollMax();
+ return {
+ left: self.scrollView.options.scrollingX ?
+ calculateMaxValue(maxValues.left) :
+ -1,
+ top: self.scrollView.options.scrollingY ?
+ calculateMaxValue(maxValues.top) :
+ -1
+ };
+ };
+
+ self.getNativeMaxScroll = function() {
+ var maxValues = {
+ left: self.scrollEl.scrollWidth,
+ top: self.scrollEl.scrollHeight
+ };
+ var computedStyle = window.getComputedStyle(self.scrollEl) || {};
+ return {
+ left: maxValues.left &&
+ (computedStyle.overflowX === 'scroll' ||
+ computedStyle.overflowX === 'auto' ||
+ self.scrollEl.style['overflow-x'] === 'scroll') ?
+ calculateMaxValue(maxValues.left) : -1,
+ top: maxValues.top &&
+ (computedStyle.overflowY === 'scroll' ||
+ computedStyle.overflowY === 'auto' ||
+ self.scrollEl.style['overflow-y'] === 'scroll' ) ?
+ calculateMaxValue(maxValues.top) : -1
+ };
+ };
+
+ // determine pixel refresh distance based on % or value
+ function calculateMaxValue(maximum) {
+ var distance = ($attrs.distance || '2.5%').trim();
+ var isPercent = distance.indexOf('%') !== -1;
+ return isPercent ?
+ maximum * (1 - parseFloat(distance) / 100) :
+ maximum - parseFloat(distance);
+ }
+
+ //for testing
+ self.__finishInfiniteScroll = finishInfiniteScroll;
+
+}]);
+
+/**
+ * @ngdoc service
+ * @name $ionicListDelegate
+ * @module ionic
+ *
+ * @description
+ * Delegate for controlling the {@link ionic.directive:ionList} directive.
+ *
+ * Methods called directly on the $ionicListDelegate service will control all lists.
+ * Use the {@link ionic.service:$ionicListDelegate#$getByHandle $getByHandle}
+ * method to control specific ionList instances.
+ *
+ * @usage
+ * ```html
+ * {% raw %}
+ * <ion-content ng-controller="MyCtrl">
+ * <button class="button" ng-click="showDeleteButtons()"></button>
+ * <ion-list>
+ * <ion-item ng-repeat="i in items">
+ * Hello, {{i}}!
+ * <ion-delete-button class="ion-minus-circled"></ion-delete-button>
+ * </ion-item>
+ * </ion-list>
+ * </ion-content>
+ * {% endraw %}
+ * ```
+
+ * ```js
+ * function MyCtrl($scope, $ionicListDelegate) {
+ * $scope.showDeleteButtons = function() {
+ * $ionicListDelegate.showDelete(true);
+ * };
+ * }
+ * ```
+ */
+IonicModule.service('$ionicListDelegate', ionic.DelegateService([
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#showReorder
+ * @param {boolean=} showReorder Set whether or not this list is showing its reorder buttons.
+ * @returns {boolean} Whether the reorder buttons are shown.
+ */
+ 'showReorder',
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#showDelete
+ * @param {boolean=} showDelete Set whether or not this list is showing its delete buttons.
+ * @returns {boolean} Whether the delete buttons are shown.
+ */
+ 'showDelete',
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#canSwipeItems
+ * @param {boolean=} canSwipeItems Set whether or not this list is able to swipe to show
+ * option buttons.
+ * @returns {boolean} Whether the list is able to swipe to show option buttons.
+ */
+ 'canSwipeItems',
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#closeOptionButtons
+ * @description Closes any option buttons on the list that are swiped open.
+ */
+ 'closeOptionButtons'
+ /**
+ * @ngdoc method
+ * @name $ionicListDelegate#$getByHandle
+ * @param {string} handle
+ * @returns `delegateInstance` A delegate instance that controls only the
+ * {@link ionic.directive:ionList} directives with `delegate-handle` matching
+ * the given handle.
+ *
+ * Example: `$ionicListDelegate.$getByHandle('my-handle').showReorder(true);`
+ */
+]))
+
+.controller('$ionicList', [
+ '$scope',
+ '$attrs',
+ '$ionicListDelegate',
+ '$ionicHistory',
+function($scope, $attrs, $ionicListDelegate, $ionicHistory) {
+ var self = this;
+ var isSwipeable = true;
+ var isReorderShown = false;
+ var isDeleteShown = false;
+
+ var deregisterInstance = $ionicListDelegate._registerInstance(
+ self, $attrs.delegateHandle, function() {
+ return $ionicHistory.isActiveScope($scope);
+ }
+ );
+ $scope.$on('$destroy', deregisterInstance);
+
+ self.showReorder = function(show) {
+ if (arguments.length) {
+ isReorderShown = !!show;
+ }
+ return isReorderShown;
+ };
+
+ self.showDelete = function(show) {
+ if (arguments.length) {
+ isDeleteShown = !!show;
+ }
+ return isDeleteShown;
+ };
+
+ self.canSwipeItems = function(can) {
+ if (arguments.length) {
+ isSwipeable = !!can;
+ }
+ return isSwipeable;
+ };
+
+ self.closeOptionButtons = function() {
+ self.listView && self.listView.clearDragEffects();
+ };
+}]);
+
+IonicModule
+
+.controller('$ionicNavBar', [
+ '$scope',
+ '$element',
+ '$attrs',
+ '$compile',
+ '$timeout',
+ '$ionicNavBarDelegate',
+ '$ionicConfig',
+ '$ionicHistory',
+function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $ionicConfig, $ionicHistory) {
+
+ var CSS_HIDE = 'hide';
+ var DATA_NAV_BAR_CTRL = '$ionNavBarController';
+ var PRIMARY_BUTTONS = 'primaryButtons';
+ var SECONDARY_BUTTONS = 'secondaryButtons';
+ var BACK_BUTTON = 'backButton';
+ var ITEM_TYPES = 'primaryButtons secondaryButtons leftButtons rightButtons title'.split(' ');
+
+ var self = this;
+ var headerBars = [];
+ var navElementHtml = {};
+ var isVisible = true;
+ var queuedTransitionStart, queuedTransitionEnd, latestTransitionId;
+
+ $element.parent().data(DATA_NAV_BAR_CTRL, self);
+
+ var delegateHandle = $attrs.delegateHandle || 'navBar' + ionic.Utils.nextUid();
+
+ var deregisterInstance = $ionicNavBarDelegate._registerInstance(self, delegateHandle);
+
+
+ self.init = function() {
+ $element.addClass('nav-bar-container');
+ ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', $ionicConfig.views.transition());
+
+ // create two nav bar blocks which will trade out which one is shown
+ self.createHeaderBar(false);
+ self.createHeaderBar(true);
+
+ $scope.$emit('ionNavBar.init', delegateHandle);
+ };
+
+
+ self.createHeaderBar = function(isActive) {
+ var containerEle = jqLite('<div class="nav-bar-block">');
+ ionic.DomUtil.cachedAttr(containerEle, 'nav-bar', isActive ? 'active' : 'cached');
+
+ var alignTitle = $attrs.alignTitle || $ionicConfig.navBar.alignTitle();
+ var headerBarEle = jqLite('<ion-header-bar>').addClass($attrs['class']).attr('align-title', alignTitle);
+ if (isDefined($attrs.noTapScroll)) headerBarEle.attr('no-tap-scroll', $attrs.noTapScroll);
+ var titleEle = jqLite('<div class="title title-' + alignTitle + '">');
+ var navEle = {};
+ var lastViewItemEle = {};
+ var leftButtonsEle, rightButtonsEle;
+
+ navEle[BACK_BUTTON] = createNavElement(BACK_BUTTON);
+ navEle[BACK_BUTTON] && headerBarEle.append(navEle[BACK_BUTTON]);
+
+ // append title in the header, this is the rock to where buttons append
+ headerBarEle.append(titleEle);
+
+ forEach(ITEM_TYPES, function(itemType) {
+ // create default button elements
+ navEle[itemType] = createNavElement(itemType);
+ // append and position buttons
+ positionItem(navEle[itemType], itemType);
+ });
+
+ // add header-item to the root children
+ for (var x = 0; x < headerBarEle[0].children.length; x++) {
+ headerBarEle[0].children[x].classList.add('header-item');
+ }
+
+ // compile header and append to the DOM
+ containerEle.append(headerBarEle);
+ $element.append($compile(containerEle)($scope.$new()));
+
+ var headerBarCtrl = headerBarEle.data('$ionHeaderBarController');
+ headerBarCtrl.backButtonIcon = $ionicConfig.backButton.icon();
+ headerBarCtrl.backButtonText = $ionicConfig.backButton.text();
+
+ var headerBarInstance = {
+ isActive: isActive,
+ title: function(newTitleText) {
+ headerBarCtrl.title(newTitleText);
+ },
+ setItem: function(navBarItemEle, itemType) {
+ // first make sure any exiting nav bar item has been removed
+ headerBarInstance.removeItem(itemType);
+
+ if (navBarItemEle) {
+ if (itemType === 'title') {
+ // clear out the text based title
+ headerBarInstance.title("");
+ }
+
+ // there's a custom nav bar item
+ positionItem(navBarItemEle, itemType);
+
+ if (navEle[itemType]) {
+ // make sure the default on this itemType is hidden
+ navEle[itemType].addClass(CSS_HIDE);
+ }
+ lastViewItemEle[itemType] = navBarItemEle;
+
+ } else if (navEle[itemType]) {
+ // there's a default button for this side and no view button
+ navEle[itemType].removeClass(CSS_HIDE);
+ }
+ },
+ removeItem: function(itemType) {
+ if (lastViewItemEle[itemType]) {
+ lastViewItemEle[itemType].scope().$destroy();
+ lastViewItemEle[itemType].remove();
+ lastViewItemEle[itemType] = null;
+ }
+ },
+ containerEle: function() {
+ return containerEle;
+ },
+ headerBarEle: function() {
+ return headerBarEle;
+ },
+ afterLeave: function() {
+ forEach(ITEM_TYPES, function(itemType) {
+ headerBarInstance.removeItem(itemType);
+ });
+ headerBarCtrl.resetBackButton();
+ },
+ controller: function() {
+ return headerBarCtrl;
+ },
+ destroy: function() {
+ forEach(ITEM_TYPES, function(itemType) {
+ headerBarInstance.removeItem(itemType);
+ });
+ containerEle.scope().$destroy();
+ for (var n in navEle) {
+ if (navEle[n]) {
+ navEle[n].removeData();
+ navEle[n] = null;
+ }
+ }
+ leftButtonsEle && leftButtonsEle.removeData();
+ rightButtonsEle && rightButtonsEle.removeData();
+ titleEle.removeData();
+ headerBarEle.removeData();
+ containerEle.remove();
+ containerEle = headerBarEle = titleEle = leftButtonsEle = rightButtonsEle = null;
+ }
+ };
+
+ function positionItem(ele, itemType) {
+ if (!ele) return;
+
+ if (itemType === 'title') {
+ // title element
+ titleEle.append(ele);
+
+ } else if (itemType == 'rightButtons' ||
+ (itemType == SECONDARY_BUTTONS && $ionicConfig.navBar.positionSecondaryButtons() != 'left') ||
+ (itemType == PRIMARY_BUTTONS && $ionicConfig.navBar.positionPrimaryButtons() == 'right')) {
+ // right side
+ if (!rightButtonsEle) {
+ rightButtonsEle = jqLite('<div class="buttons buttons-right">');
+ headerBarEle.append(rightButtonsEle);
+ }
+ if (itemType == SECONDARY_BUTTONS) {
+ rightButtonsEle.append(ele);
+ } else {
+ rightButtonsEle.prepend(ele);
+ }
+
+ } else {
+ // left side
+ if (!leftButtonsEle) {
+ leftButtonsEle = jqLite('<div class="buttons buttons-left">');
+ if (navEle[BACK_BUTTON]) {
+ navEle[BACK_BUTTON].after(leftButtonsEle);
+ } else {
+ headerBarEle.prepend(leftButtonsEle);
+ }
+ }
+ if (itemType == SECONDARY_BUTTONS) {
+ leftButtonsEle.append(ele);
+ } else {
+ leftButtonsEle.prepend(ele);
+ }
+ }
+
+ }
+
+ headerBars.push(headerBarInstance);
+
+ return headerBarInstance;
+ };
+
+
+ self.navElement = function(type, html) {
+ if (isDefined(html)) {
+ navElementHtml[type] = html;
+ }
+ return navElementHtml[type];
+ };
+
+
+ self.update = function(viewData) {
+ var showNavBar = !viewData.hasHeaderBar && viewData.showNavBar;
+ viewData.transition = $ionicConfig.views.transition();
+
+ if (!showNavBar) {
+ viewData.direction = 'none';
+ }
+
+ self.enable(showNavBar);
+ var enteringHeaderBar = self.isInitialized ? getOffScreenHeaderBar() : getOnScreenHeaderBar();
+ var leavingHeaderBar = self.isInitialized ? getOnScreenHeaderBar() : null;
+ var enteringHeaderCtrl = enteringHeaderBar.controller();
+
+ // update if the entering header should show the back button or not
+ enteringHeaderCtrl.enableBack(viewData.enableBack, true);
+ enteringHeaderCtrl.showBack(viewData.showBack, true);
+ enteringHeaderCtrl.updateBackButton();
+
+ // update the entering header bar's title
+ self.title(viewData.title, enteringHeaderBar);
+
+ self.showBar(showNavBar);
+
+ // update the nav bar items, depending if the view has their own or not
+ if (viewData.navBarItems) {
+ forEach(ITEM_TYPES, function(itemType) {
+ enteringHeaderBar.setItem(viewData.navBarItems[itemType], itemType);
+ });
+ }
+
+ // begin transition of entering and leaving header bars
+ self.transition(enteringHeaderBar, leavingHeaderBar, viewData);
+
+ self.isInitialized = true;
+ navSwipeAttr('');
+ };
+
+
+ self.transition = function(enteringHeaderBar, leavingHeaderBar, viewData) {
+ var enteringHeaderBarCtrl = enteringHeaderBar.controller();
+ var transitionFn = $ionicConfig.transitions.navBar[viewData.navBarTransition] || $ionicConfig.transitions.navBar.none;
+ var transitionId = viewData.transitionId;
+
+ enteringHeaderBarCtrl.beforeEnter(viewData);
+
+ var navBarTransition = transitionFn(enteringHeaderBar, leavingHeaderBar, viewData.direction, viewData.shouldAnimate && self.isInitialized);
+
+ ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', viewData.navBarTransition);
+ ionic.DomUtil.cachedAttr($element, 'nav-bar-direction', viewData.direction);
+
+ if (navBarTransition.shouldAnimate && viewData.renderEnd) {
+ navBarAttr(enteringHeaderBar, 'stage');
+ } else {
+ navBarAttr(enteringHeaderBar, 'entering');
+ navBarAttr(leavingHeaderBar, 'leaving');
+ }
+
+ enteringHeaderBarCtrl.resetBackButton(viewData);
+
+ navBarTransition.run(0);
+
+ self.activeTransition = {
+ run: function(step) {
+ navBarTransition.shouldAnimate = false;
+ navBarTransition.direction = 'back';
+ navBarTransition.run(step);
+ },
+ cancel: function(shouldAnimate, speed, cancelData) {
+ navSwipeAttr(speed);
+ navBarAttr(leavingHeaderBar, 'active');
+ navBarAttr(enteringHeaderBar, 'cached');
+ navBarTransition.shouldAnimate = shouldAnimate;
+ navBarTransition.run(0);
+ self.activeTransition = navBarTransition = null;
+
+ var runApply;
+ if (cancelData.showBar !== self.showBar()) {
+ self.showBar(cancelData.showBar);
+ }
+ if (cancelData.showBackButton !== self.showBackButton()) {
+ self.showBackButton(cancelData.showBackButton);
+ }
+ if (runApply) {
+ $scope.$apply();
+ }
+ },
+ complete: function(shouldAnimate, speed) {
+ navSwipeAttr(speed);
+ navBarTransition.shouldAnimate = shouldAnimate;
+ navBarTransition.run(1);
+ queuedTransitionEnd = transitionEnd;
+ }
+ };
+
+ $timeout(enteringHeaderBarCtrl.align, 16);
+
+ queuedTransitionStart = function() {
+ if (latestTransitionId !== transitionId) return;
+
+ navBarAttr(enteringHeaderBar, 'entering');
+ navBarAttr(leavingHeaderBar, 'leaving');
+
+ navBarTransition.run(1);
+
+ queuedTransitionEnd = function() {
+ if (latestTransitionId == transitionId || !navBarTransition.shouldAnimate) {
+ transitionEnd();
+ }
+ };
+
+ queuedTransitionStart = null;
+ };
+
+ function transitionEnd() {
+ for (var x = 0; x < headerBars.length; x++) {
+ headerBars[x].isActive = false;
+ }
+ enteringHeaderBar.isActive = true;
+
+ navBarAttr(enteringHeaderBar, 'active');
+ navBarAttr(leavingHeaderBar, 'cached');
+
+ self.activeTransition = navBarTransition = queuedTransitionEnd = null;
+ }
+
+ queuedTransitionStart();
+ };
+
+
+ self.triggerTransitionStart = function(triggerTransitionId) {
+ latestTransitionId = triggerTransitionId;
+ queuedTransitionStart && queuedTransitionStart();
+ };
+
+
+ self.triggerTransitionEnd = function() {
+ queuedTransitionEnd && queuedTransitionEnd();
+ };
+
+
+ self.showBar = function(shouldShow) {
+ if (arguments.length) {
+ self.visibleBar(shouldShow);
+ $scope.$parent.$hasHeader = !!shouldShow;
+ }
+ return !!$scope.$parent.$hasHeader;
+ };
+
+
+ self.visibleBar = function(shouldShow) {
+ if (shouldShow && !isVisible) {
+ $element.removeClass(CSS_HIDE);
+ self.align();
+ } else if (!shouldShow && isVisible) {
+ $element.addClass(CSS_HIDE);
+ }
+ isVisible = shouldShow;
+ };
+
+
+ self.enable = function(val) {
+ // set primary to show first
+ self.visibleBar(val);
+
+ // set non primary to hide second
+ for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) {
+ if ($ionicNavBarDelegate._instances[x] !== self) $ionicNavBarDelegate._instances[x].visibleBar(false);
+ }
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $ionicNavBar#showBackButton
+ * @description Show/hide the nav bar back button when there is a
+ * back view. If the back button is not possible, for example, the
+ * first view in the stack, then this will not force the back button
+ * to show.
+ */
+ self.showBackButton = function(shouldShow) {
+ if (arguments.length) {
+ for (var x = 0; x < headerBars.length; x++) {
+ headerBars[x].controller().showNavBack(!!shouldShow);
+ }
+ $scope.$isBackButtonShown = !!shouldShow;
+ }
+ return $scope.$isBackButtonShown;
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $ionicNavBar#showActiveBackButton
+ * @description Show/hide only the active header bar's back button.
+ */
+ self.showActiveBackButton = function(shouldShow) {
+ var headerBar = getOnScreenHeaderBar();
+ if (headerBar) {
+ if (arguments.length) {
+ return headerBar.controller().showBack(shouldShow);
+ }
+ return headerBar.controller().showBack();
+ }
+ };
+
+
+ self.title = function(newTitleText, headerBar) {
+ if (isDefined(newTitleText)) {
+ newTitleText = newTitleText || '';
+ headerBar = headerBar || getOnScreenHeaderBar();
+ headerBar && headerBar.title(newTitleText);
+ $scope.$title = newTitleText;
+ $ionicHistory.currentTitle(newTitleText);
+ }
+ return $scope.$title;
+ };
+
+
+ self.align = function(val, headerBar) {
+ headerBar = headerBar || getOnScreenHeaderBar();
+ headerBar && headerBar.controller().align(val);
+ };
+
+
+ self.hasTabsTop = function(isTabsTop) {
+ $element[isTabsTop ? 'addClass' : 'removeClass']('nav-bar-tabs-top');
+ };
+
+ self.hasBarSubheader = function(isBarSubheader) {
+ $element[isBarSubheader ? 'addClass' : 'removeClass']('nav-bar-has-subheader');
+ };
+
+ // DEPRECATED, as of v1.0.0-beta14 -------
+ self.changeTitle = function(val) {
+ deprecatedWarning('changeTitle(val)', 'title(val)');
+ self.title(val);
+ };
+ self.setTitle = function(val) {
+ deprecatedWarning('setTitle(val)', 'title(val)');
+ self.title(val);
+ };
+ self.getTitle = function() {
+ deprecatedWarning('getTitle()', 'title()');
+ return self.title();
+ };
+ self.back = function() {
+ deprecatedWarning('back()', '$ionicHistory.goBack()');
+ $ionicHistory.goBack();
+ };
+ self.getPreviousTitle = function() {
+ deprecatedWarning('getPreviousTitle()', '$ionicHistory.backTitle()');
+ $ionicHistory.goBack();
+ };
+ function deprecatedWarning(oldMethod, newMethod) {
+ var warn = console.warn || console.log;
+ warn && warn.call(console, 'navBarController.' + oldMethod + ' is deprecated, please use ' + newMethod + ' instead');
+ }
+ // END DEPRECATED -------
+
+
+ function createNavElement(type) {
+ if (navElementHtml[type]) {
+ return jqLite(navElementHtml[type]);
+ }
+ }
+
+
+ function getOnScreenHeaderBar() {
+ for (var x = 0; x < headerBars.length; x++) {
+ if (headerBars[x].isActive) return headerBars[x];
+ }
+ }
+
+
+ function getOffScreenHeaderBar() {
+ for (var x = 0; x < headerBars.length; x++) {
+ if (!headerBars[x].isActive) return headerBars[x];
+ }
+ }
+
+
+ function navBarAttr(ctrl, val) {
+ ctrl && ionic.DomUtil.cachedAttr(ctrl.containerEle(), 'nav-bar', val);
+ }
+
+ function navSwipeAttr(val) {
+ ionic.DomUtil.cachedAttr($element, 'nav-swipe', val);
+ }
+
+
+ $scope.$on('$destroy', function() {
+ $scope.$parent.$hasHeader = false;
+ $element.parent().removeData(DATA_NAV_BAR_CTRL);
+ for (var x = 0; x < headerBars.length; x++) {
+ headerBars[x].destroy();
+ }
+ $element.remove();
+ $element = headerBars = null;
+ deregisterInstance();
+ });
+
+}]);
+
+IonicModule
+.controller('$ionicNavView', [
+ '$scope',
+ '$element',
+ '$attrs',
+ '$compile',
+ '$controller',
+ '$ionicNavBarDelegate',
+ '$ionicNavViewDelegate',
+ '$ionicHistory',
+ '$ionicViewSwitcher',
+ '$ionicConfig',
+ '$ionicScrollDelegate',
+ '$ionicSideMenuDelegate',
+function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher, $ionicConfig, $ionicScrollDelegate, $ionicSideMenuDelegate) {
+
+ var DATA_ELE_IDENTIFIER = '$eleId';
+ var DATA_DESTROY_ELE = '$destroyEle';
+ var DATA_NO_CACHE = '$noCache';
+ var VIEW_STATUS_ACTIVE = 'active';
+ var VIEW_STATUS_CACHED = 'cached';
+
+ var self = this;
+ var direction;
+ var isPrimary = false;
+ var navBarDelegate;
+ var activeEleId;
+ var navViewAttr = $ionicViewSwitcher.navViewAttr;
+ var disableRenderStartViewId, disableAnimation;
+
+ self.scope = $scope;
+ self.element = $element;
+
+ self.init = function() {
+ var navViewName = $attrs.name || '';
+
+ // Find the details of the parent view directive (if any) and use it
+ // to derive our own qualified view name, then hang our own details
+ // off the DOM so child directives can find it.
+ var parent = $element.parent().inheritedData('$uiView');
+ var parentViewName = ((parent && parent.state) ? parent.state.name : '');
+ if (navViewName.indexOf('@') < 0) navViewName = navViewName + '@' + parentViewName;
+
+ var viewData = { name: navViewName, state: null };
+ $element.data('$uiView', viewData);
+
+ var deregisterInstance = $ionicNavViewDelegate._registerInstance(self, $attrs.delegateHandle);
+ $scope.$on('$destroy', function() {
+ deregisterInstance();
+
+ // ensure no scrolls have been left frozen
+ if (self.isSwipeFreeze) {
+ $ionicScrollDelegate.freezeAllScrolls(false);
+ }
+ });
+
+ $scope.$on('$ionicHistory.deselect', self.cacheCleanup);
+ $scope.$on('$ionicTabs.top', onTabsTop);
+ $scope.$on('$ionicSubheader', onBarSubheader);
+
+ $scope.$on('$ionicTabs.beforeLeave', onTabsLeave);
+ $scope.$on('$ionicTabs.afterLeave', onTabsLeave);
+ $scope.$on('$ionicTabs.leave', onTabsLeave);
+
+ ionic.Platform.ready(function() {
+ if ( ionic.Platform.isWebView() && ionic.Platform.isIOS() ) {
+ self.initSwipeBack();
+ }
+ });
+
+ return viewData;
+ };
+
+
+ self.register = function(viewLocals) {
+ var leavingView = extend({}, $ionicHistory.currentView());
+
+ // register that a view is coming in and get info on how it should transition
+ var registerData = $ionicHistory.register($scope, viewLocals);
+
+ // update which direction
+ self.update(registerData);
+
+ // begin rendering and transitioning
+ var enteringView = $ionicHistory.getViewById(registerData.viewId) || {};
+
+ var renderStart = (disableRenderStartViewId !== registerData.viewId);
+ self.render(registerData, viewLocals, enteringView, leavingView, renderStart, true);
+ };
+
+
+ self.update = function(registerData) {
+ // always reset that this is the primary navView
+ isPrimary = true;
+
+ // remember what direction this navView should use
+ // this may get updated later by a child navView
+ direction = registerData.direction;
+
+ var parentNavViewCtrl = $element.parent().inheritedData('$ionNavViewController');
+ if (parentNavViewCtrl) {
+ // this navView is nested inside another one
+ // update the parent to use this direction and not
+ // the other it originally was set to
+
+ // inform the parent navView that it is not the primary navView
+ parentNavViewCtrl.isPrimary(false);
+
+ if (direction === 'enter' || direction === 'exit') {
+ // they're entering/exiting a history
+ // find parent navViewController
+ parentNavViewCtrl.direction(direction);
+
+ if (direction === 'enter') {
+ // reset the direction so this navView doesn't animate
+ // because it's parent will
+ direction = 'none';
+ }
+ }
+ }
+ };
+
+
+ self.render = function(registerData, viewLocals, enteringView, leavingView, renderStart, renderEnd) {
+ // register the view and figure out where it lives in the various
+ // histories and nav stacks, along with how views should enter/leave
+ var switcher = $ionicViewSwitcher.create(self, viewLocals, enteringView, leavingView, renderStart, renderEnd);
+
+ // init the rendering of views for this navView directive
+ switcher.init(registerData, function() {
+ // the view is now compiled, in the dom and linked, now lets transition the views.
+ // this uses a callback incase THIS nav-view has a nested nav-view, and after the NESTED
+ // nav-view links, the NESTED nav-view would update which direction THIS nav-view should use
+
+ // kick off the transition of views
+ switcher.transition(self.direction(), registerData.enableBack, !disableAnimation);
+
+ // reset private vars for next time
+ disableRenderStartViewId = disableAnimation = null;
+ });
+
+ };
+
+
+ self.beforeEnter = function(transitionData) {
+ if (isPrimary) {
+ // only update this nav-view's nav-bar if this is the primary nav-view
+ navBarDelegate = transitionData.navBarDelegate;
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.update(transitionData);
+ navSwipeAttr('');
+ }
+ };
+
+
+ self.activeEleId = function(eleId) {
+ if (arguments.length) {
+ activeEleId = eleId;
+ }
+ return activeEleId;
+ };
+
+
+ self.transitionEnd = function() {
+ var viewElements = $element.children();
+ var x, l, viewElement;
+
+ for (x = 0, l = viewElements.length; x < l; x++) {
+ viewElement = viewElements.eq(x);
+
+ if (viewElement.data(DATA_ELE_IDENTIFIER) === activeEleId) {
+ // this is the active element
+ navViewAttr(viewElement, VIEW_STATUS_ACTIVE);
+
+ } else if (navViewAttr(viewElement) === 'leaving' || navViewAttr(viewElement) === VIEW_STATUS_ACTIVE || navViewAttr(viewElement) === VIEW_STATUS_CACHED) {
+ // this is a leaving element or was the former active element, or is an cached element
+ if (viewElement.data(DATA_DESTROY_ELE) || viewElement.data(DATA_NO_CACHE)) {
+ // this element shouldn't stay cached
+ $ionicViewSwitcher.destroyViewEle(viewElement);
+
+ } else {
+ // keep in the DOM, mark as cached
+ navViewAttr(viewElement, VIEW_STATUS_CACHED);
+
+ // disconnect the leaving scope
+ ionic.Utils.disconnectScope(viewElement.scope());
+ }
+ }
+ }
+
+ navSwipeAttr('');
+
+ // ensure no scrolls have been left frozen
+ if (self.isSwipeFreeze) {
+ $ionicScrollDelegate.freezeAllScrolls(false);
+ }
+ };
+
+
+ function onTabsLeave(ev, data) {
+ var viewElements = $element.children();
+ var viewElement, viewScope;
+
+ for (var x = 0, l = viewElements.length; x < l; x++) {
+ viewElement = viewElements.eq(x);
+ if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) {
+ viewScope = viewElement.scope();
+ viewScope && viewScope.$emit(ev.name.replace('Tabs', 'View'), data);
+ viewScope && viewScope.$broadcast(ev.name.replace('Tabs', 'ParentView'), data);
+ break;
+ }
+ }
+ }
+
+
+ self.cacheCleanup = function() {
+ var viewElements = $element.children();
+ for (var x = 0, l = viewElements.length; x < l; x++) {
+ if (viewElements.eq(x).data(DATA_DESTROY_ELE)) {
+ $ionicViewSwitcher.destroyViewEle(viewElements.eq(x));
+ }
+ }
+ };
+
+
+ self.clearCache = function(stateIds) {
+ var viewElements = $element.children();
+ var viewElement, viewScope, x, l, y, eleIdentifier;
+
+ for (x = 0, l = viewElements.length; x < l; x++) {
+ viewElement = viewElements.eq(x);
+
+ if (stateIds) {
+ eleIdentifier = viewElement.data(DATA_ELE_IDENTIFIER);
+
+ for (y = 0; y < stateIds.length; y++) {
+ if (eleIdentifier === stateIds[y]) {
+ $ionicViewSwitcher.destroyViewEle(viewElement);
+ }
+ }
+ continue;
+ }
+
+ if (navViewAttr(viewElement) == VIEW_STATUS_CACHED) {
+ $ionicViewSwitcher.destroyViewEle(viewElement);
+
+ } else if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) {
+ viewScope = viewElement.scope();
+ viewScope && viewScope.$broadcast('$ionicView.clearCache');
+ }
+
+ }
+ };
+
+
+ self.getViewElements = function() {
+ return $element.children();
+ };
+
+
+ self.appendViewElement = function(viewEle, viewLocals) {
+ // compile the entering element and get the link function
+ var linkFn = $compile(viewEle);
+
+ $element.append(viewEle);
+
+ var viewScope = $scope.$new();
+
+ if (viewLocals && viewLocals.$$controller) {
+ viewLocals.$scope = viewScope;
+ var controller = $controller(viewLocals.$$controller, viewLocals);
+ if (viewLocals.$$controllerAs) {
+ viewScope[viewLocals.$$controllerAs] = controller;
+ }
+ $element.children().data('$ngControllerController', controller);
+ }
+
+ linkFn(viewScope);
+
+ return viewScope;
+ };
+
+
+ self.title = function(val) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.title(val);
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $ionicNavView#enableBackButton
+ * @description Enable/disable if the back button can be shown or not. For
+ * example, the very first view in the navigation stack would not have a
+ * back view, so the back button would be disabled.
+ */
+ self.enableBackButton = function(shouldEnable) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.enableBackButton(shouldEnable);
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $ionicNavView#showBackButton
+ * @description Show/hide the nav bar active back button. If the back button
+ * is not possible this will not force the back button to show. The
+ * `enableBackButton()` method handles if a back button is even possible or not.
+ */
+ self.showBackButton = function(shouldShow) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ if (associatedNavBarCtrl) {
+ if (arguments.length) {
+ return associatedNavBarCtrl.showActiveBackButton(shouldShow);
+ }
+ return associatedNavBarCtrl.showActiveBackButton();
+ }
+ return true;
+ };
+
+
+ self.showBar = function(val) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ if (associatedNavBarCtrl) {
+ if (arguments.length) {
+ return associatedNavBarCtrl.showBar(val);
+ }
+ return associatedNavBarCtrl.showBar();
+ }
+ return true;
+ };
+
+
+ self.isPrimary = function(val) {
+ if (arguments.length) {
+ isPrimary = val;
+ }
+ return isPrimary;
+ };
+
+
+ self.direction = function(val) {
+ if (arguments.length) {
+ direction = val;
+ }
+ return direction;
+ };
+
+
+ self.initSwipeBack = function() {
+ var swipeBackHitWidth = $ionicConfig.views.swipeBackHitWidth();
+ var viewTransition, associatedNavBarCtrl, backView;
+ var deregDragStart, deregDrag, deregRelease;
+ var windowWidth, startDragX, dragPoints;
+ var cancelData = {};
+
+ function onDragStart(ev) {
+ if (!isPrimary || !$ionicConfig.views.swipeBackEnabled() || $ionicSideMenuDelegate.isOpenRight() ) return;
+
+
+ startDragX = getDragX(ev);
+ if (startDragX > swipeBackHitWidth) return;
+
+ backView = $ionicHistory.backView();
+
+ var currentView = $ionicHistory.currentView();
+
+ if (!backView || backView.historyId !== currentView.historyId || currentView.canSwipeBack === false) return;
+
+ if (!windowWidth) windowWidth = window.innerWidth;
+
+ self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(true);
+
+ var registerData = {
+ direction: 'back'
+ };
+
+ dragPoints = [];
+
+ cancelData = {
+ showBar: self.showBar(),
+ showBackButton: self.showBackButton()
+ };
+
+ var switcher = $ionicViewSwitcher.create(self, registerData, backView, currentView, true, false);
+ switcher.loadViewElements(registerData);
+ switcher.render(registerData);
+
+ viewTransition = switcher.transition('back', $ionicHistory.enabledBack(backView), true);
+
+ associatedNavBarCtrl = getAssociatedNavBarCtrl();
+
+ deregDrag = ionic.onGesture('drag', onDrag, $element[0]);
+ deregRelease = ionic.onGesture('release', onRelease, $element[0]);
+ }
+
+ function onDrag(ev) {
+ if (isPrimary && viewTransition) {
+ var dragX = getDragX(ev);
+
+ dragPoints.push({
+ t: Date.now(),
+ x: dragX
+ });
+
+ if (dragX >= windowWidth - 15) {
+ onRelease(ev);
+
+ } else {
+ var step = Math.min(Math.max(getSwipeCompletion(dragX), 0), 1);
+ viewTransition.run(step);
+ associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.run(step);
+ }
+
+ }
+ }
+
+ function onRelease(ev) {
+ if (isPrimary && viewTransition && dragPoints && dragPoints.length > 1) {
+
+ var now = Date.now();
+ var releaseX = getDragX(ev);
+ var startDrag = dragPoints[dragPoints.length - 1];
+
+ for (var x = dragPoints.length - 2; x >= 0; x--) {
+ if (now - startDrag.t > 200) {
+ break;
+ }
+ startDrag = dragPoints[x];
+ }
+
+ var isSwipingRight = (releaseX >= dragPoints[dragPoints.length - 2].x);
+ var releaseSwipeCompletion = getSwipeCompletion(releaseX);
+ var velocity = Math.abs(startDrag.x - releaseX) / (now - startDrag.t);
+
+ // private variables because ui-router has no way to pass custom data using $state.go
+ disableRenderStartViewId = backView.viewId;
+ disableAnimation = (releaseSwipeCompletion < 0.03 || releaseSwipeCompletion > 0.97);
+
+ if (isSwipingRight && (releaseSwipeCompletion > 0.5 || velocity > 0.1)) {
+ // complete view transition on release
+ var speed = (velocity > 0.5 || velocity < 0.05 || releaseX > windowWidth - 45) ? 'fast' : 'slow';
+ navSwipeAttr(disableAnimation ? '' : speed);
+ backView.go();
+ associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.complete(!disableAnimation, speed);
+
+ } else {
+ // cancel view transition on release
+ navSwipeAttr(disableAnimation ? '' : 'fast');
+ disableRenderStartViewId = null;
+ viewTransition.cancel(!disableAnimation);
+ associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.cancel(!disableAnimation, 'fast', cancelData);
+ disableAnimation = null;
+ }
+
+ }
+
+ ionic.offGesture(deregDrag, 'drag', onDrag);
+ ionic.offGesture(deregRelease, 'release', onRelease);
+
+ windowWidth = viewTransition = dragPoints = null;
+
+ self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(false);
+ }
+
+ function getDragX(ev) {
+ return ionic.tap.pointerCoord(ev.gesture.srcEvent).x;
+ }
+
+ function getSwipeCompletion(dragX) {
+ return (dragX - startDragX) / windowWidth;
+ }
+
+ deregDragStart = ionic.onGesture('dragstart', onDragStart, $element[0]);
+
+ $scope.$on('$destroy', function() {
+ ionic.offGesture(deregDragStart, 'dragstart', onDragStart);
+ ionic.offGesture(deregDrag, 'drag', onDrag);
+ ionic.offGesture(deregRelease, 'release', onRelease);
+ self.element = viewTransition = associatedNavBarCtrl = null;
+ });
+ };
+
+
+ function navSwipeAttr(val) {
+ ionic.DomUtil.cachedAttr($element, 'nav-swipe', val);
+ }
+
+
+ function onTabsTop(ev, isTabsTop) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.hasTabsTop(isTabsTop);
+ }
+
+ function onBarSubheader(ev, isBarSubheader) {
+ var associatedNavBarCtrl = getAssociatedNavBarCtrl();
+ associatedNavBarCtrl && associatedNavBarCtrl.hasBarSubheader(isBarSubheader);
+ }
+
+ function getAssociatedNavBarCtrl() {
+ if (navBarDelegate) {
+ for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) {
+ if ($ionicNavBarDelegate._instances[x].$$delegateHandle == navBarDelegate) {
+ return $ionicNavBarDelegate._instances[x];
+ }
+ }
+ }
+ return $element.inheritedData('$ionNavBarController');
+ }
+
+}]);
+
+IonicModule
+.controller('$ionicRefresher', [
+ '$scope',
+ '$attrs',
+ '$element',
+ '$ionicBind',
+ '$timeout',
+ function($scope, $attrs, $element, $ionicBind, $timeout) {
+ var self = this,
+ isDragging = false,
+ isOverscrolling = false,
+ dragOffset = 0,
+ lastOverscroll = 0,
+ ptrThreshold = 60,
+ activated = false,
+ scrollTime = 500,
+ startY = null,
+ deltaY = null,
+ canOverscroll = true,
+ scrollParent,
+ scrollChild;
+
+ if (!isDefined($attrs.pullingIcon)) {
+ $attrs.$set('pullingIcon', 'ion-android-arrow-down');
+ }
+
+ $scope.showSpinner = !isDefined($attrs.refreshingIcon) && $attrs.spinner != 'none';
+
+ $scope.showIcon = isDefined($attrs.refreshingIcon);
+
+ $ionicBind($scope, $attrs, {
+ pullingIcon: '@',
+ pullingText: '@',
+ refreshingIcon: '@',
+ refreshingText: '@',
+ spinner: '@',
+ disablePullingRotation: '@',
+ $onRefresh: '&onRefresh',
+ $onPulling: '&onPulling'
+ });
+
+ function handleMousedown(e) {
+ e.touches = e.touches || [{
+ screenX: e.screenX,
+ screenY: e.screenY
+ }];
+ // Mouse needs this
+ startY = Math.floor(e.touches[0].screenY);
+ }
+
+ function handleTouchstart(e) {
+ e.touches = e.touches || [{
+ screenX: e.screenX,
+ screenY: e.screenY
+ }];
+
+ startY = e.touches[0].screenY;
+ }
+
+ function handleTouchend() {
+ // reset Y
+ startY = null;
+ // if this wasn't an overscroll, get out immediately
+ if (!canOverscroll && !isDragging) {
+ return;
+ }
+ // the user has overscrolled but went back to native scrolling
+ if (!isDragging) {
+ dragOffset = 0;
+ isOverscrolling = false;
+ setScrollLock(false);
+ } else {
+ isDragging = false;
+ dragOffset = 0;
+
+ // the user has scroll far enough to trigger a refresh
+ if (lastOverscroll > ptrThreshold) {
+ start();
+ scrollTo(ptrThreshold, scrollTime);
+
+ // the user has overscrolled but not far enough to trigger a refresh
+ } else {
+ scrollTo(0, scrollTime, deactivate);
+ isOverscrolling = false;
+ }
+ }
+ }
+
+ function handleTouchmove(e) {
+ e.touches = e.touches || [{
+ screenX: e.screenX,
+ screenY: e.screenY
+ }];
+
+ // Force mouse events to have had a down event first
+ if (!startY && e.type == 'mousemove') {
+ return;
+ }
+
+ // if multitouch or regular scroll event, get out immediately
+ if (!canOverscroll || e.touches.length > 1) {
+ return;
+ }
+ //if this is a new drag, keep track of where we start
+ if (startY === null) {
+ startY = e.touches[0].screenY;
+ }
+
+ deltaY = e.touches[0].screenY - startY;
+
+ // how far have we dragged so far?
+ // kitkat fix for touchcancel events http://updates.html5rocks.com/2014/05/A-More-Compatible-Smoother-Touch
+ // Only do this if we're not on crosswalk
+ if (ionic.Platform.isAndroid() && ionic.Platform.version() === 4.4 && !ionic.Platform.isCrosswalk() && scrollParent.scrollTop === 0 && deltaY > 0) {
+ isDragging = true;
+ e.preventDefault();
+ }
+
+
+ // if we've dragged up and back down in to native scroll territory
+ if (deltaY - dragOffset <= 0 || scrollParent.scrollTop !== 0) {
+
+ if (isOverscrolling) {
+ isOverscrolling = false;
+ setScrollLock(false);
+ }
+
+ if (isDragging) {
+ nativescroll(scrollParent, deltaY - dragOffset * -1);
+ }
+
+ // if we're not at overscroll 0 yet, 0 out
+ if (lastOverscroll !== 0) {
+ overscroll(0);
+ }
+ return;
+
+ } else if (deltaY > 0 && scrollParent.scrollTop === 0 && !isOverscrolling) {
+ // starting overscroll, but drag started below scrollTop 0, so we need to offset the position
+ dragOffset = deltaY;
+ }
+
+ // prevent native scroll events while overscrolling
+ e.preventDefault();
+
+ // if not overscrolling yet, initiate overscrolling
+ if (!isOverscrolling) {
+ isOverscrolling = true;
+ setScrollLock(true);
+ }
+
+ isDragging = true;
+ // overscroll according to the user's drag so far
+ overscroll((deltaY - dragOffset) / 3);
+
+ // update the icon accordingly
+ if (!activated && lastOverscroll > ptrThreshold) {
+ activated = true;
+ ionic.requestAnimationFrame(activate);
+
+ } else if (activated && lastOverscroll < ptrThreshold) {
+ activated = false;
+ ionic.requestAnimationFrame(deactivate);
+ }
+ }
+
+ function handleScroll(e) {
+ // canOverscrol is used to greatly simplify the drag handler during normal scrolling
+ canOverscroll = (e.target.scrollTop === 0) || isDragging;
+ }
+
+ function overscroll(val) {
+ scrollChild.style[ionic.CSS.TRANSFORM] = 'translate3d(0px, ' + val + 'px, 0px)';
+ lastOverscroll = val;
+ }
+
+ function nativescroll(target, newScrollTop) {
+ // creates a scroll event that bubbles, can be cancelled, and with its view
+ // and detail property initialized to window and 1, respectively
+ target.scrollTop = newScrollTop;
+ var e = document.createEvent("UIEvents");
+ e.initUIEvent("scroll", true, true, window, 1);
+ target.dispatchEvent(e);
+ }
+
+ function setScrollLock(enabled) {
+ // set the scrollbar to be position:fixed in preparation to overscroll
+ // or remove it so the app can be natively scrolled
+ if (enabled) {
+ ionic.requestAnimationFrame(function() {
+ scrollChild.classList.add('overscroll');
+ show();
+ });
+
+ } else {
+ ionic.requestAnimationFrame(function() {
+ scrollChild.classList.remove('overscroll');
+ hide();
+ deactivate();
+ });
+ }
+ }
+
+ $scope.$on('scroll.refreshComplete', function() {
+ // prevent the complete from firing before the scroll has started
+ $timeout(function() {
+
+ ionic.requestAnimationFrame(tail);
+
+ // scroll back to home during tail animation
+ scrollTo(0, scrollTime, deactivate);
+
+ // return to native scrolling after tail animation has time to finish
+ $timeout(function() {
+
+ if (isOverscrolling) {
+ isOverscrolling = false;
+ setScrollLock(false);
+ }
+
+ }, scrollTime);
+
+ }, scrollTime);
+ });
+
+ function scrollTo(Y, duration, callback) {
+ // scroll animation loop w/ easing
+ // credit https://gist.github.com/dezinezync/5487119
+ var start = Date.now(),
+ from = lastOverscroll;
+
+ if (from === Y) {
+ callback();
+ return; /* Prevent scrolling to the Y point if already there */
+ }
+
+ // decelerating to zero velocity
+ function easeOutCubic(t) {
+ return (--t) * t * t + 1;
+ }
+
+ // scroll loop
+ function scroll() {
+ var currentTime = Date.now(),
+ time = Math.min(1, ((currentTime - start) / duration)),
+ // where .5 would be 50% of time on a linear scale easedT gives a
+ // fraction based on the easing method
+ easedT = easeOutCubic(time);
+
+ overscroll(Math.floor((easedT * (Y - from)) + from));
+
+ if (time < 1) {
+ ionic.requestAnimationFrame(scroll);
+
+ } else {
+
+ if (Y < 5 && Y > -5) {
+ isOverscrolling = false;
+ setScrollLock(false);
+ }
+
+ callback && callback();
+ }
+ }
+
+ // start scroll loop
+ ionic.requestAnimationFrame(scroll);
+ }
+
+
+ var touchStartEvent, touchMoveEvent, touchEndEvent;
+ if (window.navigator.pointerEnabled) {
+ touchStartEvent = 'pointerdown';
+ touchMoveEvent = 'pointermove';
+ touchEndEvent = 'pointerup';
+ } else if (window.navigator.msPointerEnabled) {
+ touchStartEvent = 'MSPointerDown';
+ touchMoveEvent = 'MSPointerMove';
+ touchEndEvent = 'MSPointerUp';
+ } else {
+ touchStartEvent = 'touchstart';
+ touchMoveEvent = 'touchmove';
+ touchEndEvent = 'touchend';
+ }
+
+ self.init = function() {
+ scrollParent = $element.parent().parent()[0];
+ scrollChild = $element.parent()[0];
+
+ if (!scrollParent || !scrollParent.classList.contains('ionic-scroll') ||
+ !scrollChild || !scrollChild.classList.contains('scroll')) {
+ throw new Error('Refresher must be immediate child of ion-content or ion-scroll');
+ }
+
+
+ ionic.on(touchStartEvent, handleTouchstart, scrollChild);
+ ionic.on(touchMoveEvent, handleTouchmove, scrollChild);
+ ionic.on(touchEndEvent, handleTouchend, scrollChild);
+ ionic.on('mousedown', handleMousedown, scrollChild);
+ ionic.on('mousemove', handleTouchmove, scrollChild);
+ ionic.on('mouseup', handleTouchend, scrollChild);
+ ionic.on('scroll', handleScroll, scrollParent);
+
+ // cleanup when done
+ $scope.$on('$destroy', destroy);
+ };
+
+ function destroy() {
+ if ( scrollChild ) {
+ ionic.off(touchStartEvent, handleTouchstart, scrollChild);
+ ionic.off(touchMoveEvent, handleTouchmove, scrollChild);
+ ionic.off(touchEndEvent, handleTouchend, scrollChild);
+ ionic.off('mousedown', handleMousedown, scrollChild);
+ ionic.off('mousemove', handleTouchmove, scrollChild);
+ ionic.off('mouseup', handleTouchend, scrollChild);
+ }
+ if ( scrollParent ) {
+ ionic.off('scroll', handleScroll, scrollParent);
+ }
+ scrollParent = null;
+ scrollChild = null;
+ }
+
+ // DOM manipulation and broadcast methods shared by JS and Native Scrolling
+ // getter used by JS Scrolling
+ self.getRefresherDomMethods = function() {
+ return {
+ activate: activate,
+ deactivate: deactivate,
+ start: start,
+ show: show,
+ hide: hide,
+ tail: tail
+ };
+ };
+
+ function activate() {
+ $element[0].classList.add('active');
+ $scope.$onPulling();
+ }
+
+ function deactivate() {
+ // give tail 150ms to finish
+ $timeout(function() {
+ // deactivateCallback
+ $element.removeClass('active refreshing refreshing-tail');
+ if (activated) activated = false;
+ }, 150);
+ }
+
+ function start() {
+ // startCallback
+ $element[0].classList.add('refreshing');
+ var q = $scope.$onRefresh();
+
+ if (q && q.then) {
+ q['finally'](function() {
+ $scope.$broadcast('scroll.refreshComplete');
+ });
+ }
+ }
+
+ function show() {
+ // showCallback
+ $element[0].classList.remove('invisible');
+ }
+
+ function hide() {
+ // showCallback
+ $element[0].classList.add('invisible');
+ }
+
+ function tail() {
+ // tailCallback
+ $element[0].classList.add('refreshing-tail');
+ }
+
+ // for testing
+ self.__handleTouchmove = handleTouchmove;
+ self.__getScrollChild = function() { return scrollChild; };
+ self.__getScrollParent = function() { return scrollParent; };
+ }
+]);
+
+/**
+ * @private
+ */
+IonicModule
+
+.controller('$ionicScroll', [
+ '$scope',
+ 'scrollViewOptions',
+ '$timeout',
+ '$window',
+ '$location',
+ '$document',
+ '$ionicScrollDelegate',
+ '$ionicHistory',
+function($scope,
+ scrollViewOptions,
+ $timeout,
+ $window,
+ $location,
+ $document,
+ $ionicScrollDelegate,
+ $ionicHistory) {
+
+ var self = this;
+ // for testing
+ self.__timeout = $timeout;
+
+ self._scrollViewOptions = scrollViewOptions; //for testing
+ self.isNative = function() {
+ return !!scrollViewOptions.nativeScrolling;
+ };
+
+ var element = self.element = scrollViewOptions.el;
+ var $element = self.$element = jqLite(element);
+ var scrollView;
+ if (self.isNative()) {
+ scrollView = self.scrollView = new ionic.views.ScrollNative(scrollViewOptions);
+ } else {
+ scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions);
+ }
+
+
+ //Attach self to element as a controller so other directives can require this controller
+ //through `require: '$ionicScroll'
+ //Also attach to parent so that sibling elements can require this
+ ($element.parent().length ? $element.parent() : $element)
+ .data('$$ionicScrollController', self);
+
+ var deregisterInstance = $ionicScrollDelegate._registerInstance(
+ self, scrollViewOptions.delegateHandle, function() {
+ return $ionicHistory.isActiveScope($scope);
+ }
+ );
+
+ if (!isDefined(scrollViewOptions.bouncing)) {
+ ionic.Platform.ready(function() {
+ if (scrollView && scrollView.options) {
+ scrollView.options.bouncing = true;
+ if (ionic.Platform.isAndroid()) {
+ // No bouncing by default on Android
+ scrollView.options.bouncing = false;
+ // Faster scroll decel
+ scrollView.options.deceleration = 0.95;
+ }
+ }
+ });
+ }
+
+ var resize = angular.bind(scrollView, scrollView.resize);
+ angular.element($window).on('resize', resize);
+
+ var scrollFunc = function(e) {
+ var detail = (e.originalEvent || e).detail || {};
+ $scope.$onScroll && $scope.$onScroll({
+ event: e,
+ scrollTop: detail.scrollTop || 0,
+ scrollLeft: detail.scrollLeft || 0
+ });
+ };
+
+ $element.on('scroll', scrollFunc);
+
+ $scope.$on('$destroy', function() {
+ deregisterInstance();
+ scrollView && scrollView.__cleanup && scrollView.__cleanup();
+ angular.element($window).off('resize', resize);
+ if ( $element ) {
+ $element.off('scroll', scrollFunc);
+ }
+ if ( self._scrollViewOptions ) {
+ self._scrollViewOptions.el = null;
+ }
+ if ( scrollViewOptions ) {
+ scrollViewOptions.el = null;
+ }
+
+ scrollView = self.scrollView = scrollViewOptions = self._scrollViewOptions = element = self.$element = $element = null;
+ });
+
+ $timeout(function() {
+ scrollView && scrollView.run && scrollView.run();
+ });
+
+ self.getScrollView = function() {
+ return scrollView;
+ };
+
+ self.getScrollPosition = function() {
+ return scrollView.getValues();
+ };
+
+ self.resize = function() {
+ return $timeout(resize, 0, false).then(function() {
+ $element && $element.triggerHandler('scroll-resize');
+ });
+ };
+
+ self.scrollTop = function(shouldAnimate) {
+ self.resize().then(function() {
+ if (!scrollView) {
+ return;
+ }
+ scrollView.scrollTo(0, 0, !!shouldAnimate);
+ });
+ };
+
+ self.scrollBottom = function(shouldAnimate) {
+ self.resize().then(function() {
+ if (!scrollView) {
+ return;
+ }
+ var max = scrollView.getScrollMax();
+ scrollView.scrollTo(max.left, max.top, !!shouldAnimate);
+ });
+ };
+
+ self.scrollTo = function(left, top, shouldAnimate) {
+ self.resize().then(function() {
+ if (!scrollView) {
+ return;
+ }
+ scrollView.scrollTo(left, top, !!shouldAnimate);
+ });
+ };
+
+ self.zoomTo = function(zoom, shouldAnimate, originLeft, originTop) {
+ self.resize().then(function() {
+ if (!scrollView) {
+ return;
+ }
+ scrollView.zoomTo(zoom, !!shouldAnimate, originLeft, originTop);
+ });
+ };
+
+ self.zoomBy = function(zoom, shouldAnimate, originLeft, originTop) {
+ self.resize().then(function() {
+ if (!scrollView) {
+ return;
+ }
+ scrollView.zoomBy(zoom, !!shouldAnimate, originLeft, originTop);
+ });
+ };
+
+ self.scrollBy = function(left, top, shouldAnimate) {
+ self.resize().then(function() {
+ if (!scrollView) {
+ return;
+ }
+ scrollView.scrollBy(left, top, !!shouldAnimate);
+ });
+ };
+
+ self.anchorScroll = function(shouldAnimate) {
+ self.resize().then(function() {
+ if (!scrollView) {
+ return;
+ }
+ var hash = $location.hash();
+ var elm = hash && $document[0].getElementById(hash);
+ if (!(hash && elm)) {
+ scrollView.scrollTo(0, 0, !!shouldAnimate);
+ return;
+ }
+ var curElm = elm;
+ var scrollLeft = 0, scrollTop = 0;
+ do {
+ if (curElm !== null) scrollLeft += curElm.offsetLeft;
+ if (curElm !== null) scrollTop += curElm.offsetTop;
+ curElm = curElm.offsetParent;
+ } while (curElm.attributes != self.element.attributes && curElm.offsetParent);
+ scrollView.scrollTo(scrollLeft, scrollTop, !!shouldAnimate);
+ });
+ };
+
+ self.freezeScroll = scrollView.freeze;
+ self.freezeScrollShut = scrollView.freezeShut;
+
+ self.freezeAllScrolls = function(shouldFreeze) {
+ for (var i = 0; i < $ionicScrollDelegate._instances.length; i++) {
+ $ionicScrollDelegate._instances[i].freezeScroll(shouldFreeze);
+ }
+ };
+
+
+ /**
+ * @private
+ */
+ self._setRefresher = function(refresherScope, refresherElement, refresherMethods) {
+ self.refresher = refresherElement;
+ var refresherHeight = self.refresher.clientHeight || 60;
+ scrollView.activatePullToRefresh(
+ refresherHeight,
+ refresherMethods
+ );
+ };
+
+}]);
+
+IonicModule
+.controller('$ionicSideMenus', [
+ '$scope',
+ '$attrs',
+ '$ionicSideMenuDelegate',
+ '$ionicPlatform',
+ '$ionicBody',
+ '$ionicHistory',
+ '$ionicScrollDelegate',
+ 'IONIC_BACK_PRIORITY',
+ '$rootScope',
+function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $ionicBody, $ionicHistory, $ionicScrollDelegate, IONIC_BACK_PRIORITY, $rootScope) {
+ var self = this;
+ var rightShowing, leftShowing, isDragging;
+ var startX, lastX, offsetX, isAsideExposed;
+ var enableMenuWithBackViews = true;
+
+ self.$scope = $scope;
+
+ self.initialize = function(options) {
+ self.left = options.left;
+ self.right = options.right;
+ self.setContent(options.content);
+ self.dragThresholdX = options.dragThresholdX || 10;
+ $ionicHistory.registerHistory(self.$scope);
+ };
+
+ /**
+ * Set the content view controller if not passed in the constructor options.
+ *
+ * @param {object} content
+ */
+ self.setContent = function(content) {
+ if (content) {
+ self.content = content;
+
+ self.content.onDrag = function(e) {
+ self._handleDrag(e);
+ };
+
+ self.content.endDrag = function(e) {
+ self._endDrag(e);
+ };
+ }
+ };
+
+ self.isOpenLeft = function() {
+ return self.getOpenAmount() > 0;
+ };
+
+ self.isOpenRight = function() {
+ return self.getOpenAmount() < 0;
+ };
+
+ /**
+ * Toggle the left menu to open 100%
+ */
+ self.toggleLeft = function(shouldOpen) {
+ if (isAsideExposed || !self.left.isEnabled) return;
+ var openAmount = self.getOpenAmount();
+ if (arguments.length === 0) {
+ shouldOpen = openAmount <= 0;
+ }
+ self.content.enableAnimation();
+ if (!shouldOpen) {
+ self.openPercentage(0);
+ $rootScope.$emit('$ionicSideMenuClose', 'left');
+ } else {
+ self.openPercentage(100);
+ $rootScope.$emit('$ionicSideMenuOpen', 'left');
+ }
+ };
+
+ /**
+ * Toggle the right menu to open 100%
+ */
+ self.toggleRight = function(shouldOpen) {
+ if (isAsideExposed || !self.right.isEnabled) return;
+ var openAmount = self.getOpenAmount();
+ if (arguments.length === 0) {
+ shouldOpen = openAmount >= 0;
+ }
+ self.content.enableAnimation();
+ if (!shouldOpen) {
+ self.openPercentage(0);
+ $rootScope.$emit('$ionicSideMenuClose', 'right');
+ } else {
+ self.openPercentage(-100);
+ $rootScope.$emit('$ionicSideMenuOpen', 'right');
+ }
+ };
+
+ self.toggle = function(side) {
+ if (side == 'right') {
+ self.toggleRight();
+ } else {
+ self.toggleLeft();
+ }
+ };
+
+ /**
+ * Close all menus.
+ */
+ self.close = function() {
+ self.openPercentage(0);
+ $rootScope.$emit('$ionicSideMenuClose', 'left');
+ $rootScope.$emit('$ionicSideMenuClose', 'right');
+ };
+
+ /**
+ * @return {float} The amount the side menu is open, either positive or negative for left (positive), or right (negative)
+ */
+ self.getOpenAmount = function() {
+ return self.content && self.content.getTranslateX() || 0;
+ };
+
+ /**
+ * @return {float} The ratio of open amount over menu width. For example, a
+ * menu of width 100 open 50 pixels would be open 50% or a ratio of 0.5. Value is negative
+ * for right menu.
+ */
+ self.getOpenRatio = function() {
+ var amount = self.getOpenAmount();
+ if (amount >= 0) {
+ return amount / self.left.width;
+ }
+ return amount / self.right.width;
+ };
+
+ self.isOpen = function() {
+ return self.getOpenAmount() !== 0;
+ };
+
+ /**
+ * @return {float} The percentage of open amount over menu width. For example, a
+ * menu of width 100 open 50 pixels would be open 50%. Value is negative
+ * for right menu.
+ */
+ self.getOpenPercentage = function() {
+ return self.getOpenRatio() * 100;
+ };
+
+ /**
+ * Open the menu with a given percentage amount.
+ * @param {float} percentage The percentage (positive or negative for left/right) to open the menu.
+ */
+ self.openPercentage = function(percentage) {
+ var p = percentage / 100;
+
+ if (self.left && percentage >= 0) {
+ self.openAmount(self.left.width * p);
+ } else if (self.right && percentage < 0) {
+ self.openAmount(self.right.width * p);
+ }
+
+ // add the CSS class "menu-open" if the percentage does not
+ // equal 0, otherwise remove the class from the body element
+ $ionicBody.enableClass((percentage !== 0), 'menu-open');
+
+ self.content.setCanScroll(percentage == 0);
+ };
+
+ /*
+ function freezeAllScrolls(shouldFreeze) {
+ if (shouldFreeze && !self.isScrollFreeze) {
+ $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);
+
+ } else if (!shouldFreeze && self.isScrollFreeze) {
+ $ionicScrollDelegate.freezeAllScrolls(false);
+ }
+ self.isScrollFreeze = shouldFreeze;
+ }
+ */
+
+ /**
+ * Open the menu the given pixel amount.
+ * @param {float} amount the pixel amount to open the menu. Positive value for left menu,
+ * negative value for right menu (only one menu will be visible at a time).
+ */
+ self.openAmount = function(amount) {
+ var maxLeft = self.left && self.left.width || 0;
+ var maxRight = self.right && self.right.width || 0;
+
+ // Check if we can move to that side, depending if the left/right panel is enabled
+ if (!(self.left && self.left.isEnabled) && amount > 0) {
+ self.content.setTranslateX(0);
+ return;
+ }
+
+ if (!(self.right && self.right.isEnabled) && amount < 0) {
+ self.content.setTranslateX(0);
+ return;
+ }
+
+ if (leftShowing && amount > maxLeft) {
+ self.content.setTranslateX(maxLeft);
+ return;
+ }
+
+ if (rightShowing && amount < -maxRight) {
+ self.content.setTranslateX(-maxRight);
+ return;
+ }
+
+ self.content.setTranslateX(amount);
+
+ leftShowing = amount > 0;
+ rightShowing = amount < 0;
+
+ if (amount > 0) {
+ // Push the z-index of the right menu down
+ self.right && self.right.pushDown && self.right.pushDown();
+ // Bring the z-index of the left menu up
+ self.left && self.left.bringUp && self.left.bringUp();
+ } else {
+ // Bring the z-index of the right menu up
+ self.right && self.right.bringUp && self.right.bringUp();
+ // Push the z-index of the left menu down
+ self.left && self.left.pushDown && self.left.pushDown();
+ }
+ };
+
+ /**
+ * Given an event object, find the final resting position of this side
+ * menu. For example, if the user "throws" the content to the right and
+ * releases the touch, the left menu should snap open (animated, of course).
+ *
+ * @param {Event} e the gesture event to use for snapping
+ */
+ self.snapToRest = function(e) {
+ // We want to animate at the end of this
+ self.content.enableAnimation();
+ isDragging = false;
+
+ // Check how much the panel is open after the drag, and
+ // what the drag velocity is
+ var ratio = self.getOpenRatio();
+
+ if (ratio === 0) {
+ // Just to be safe
+ self.openPercentage(0);
+ return;
+ }
+
+ var velocityThreshold = 0.3;
+ var velocityX = e.gesture.velocityX;
+ var direction = e.gesture.direction;
+
+ // Going right, less than half, too slow (snap back)
+ if (ratio > 0 && ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
+ self.openPercentage(0);
+ }
+
+ // Going left, more than half, too slow (snap back)
+ else if (ratio > 0.5 && direction == 'left' && velocityX < velocityThreshold) {
+ self.openPercentage(100);
+ }
+
+ // Going left, less than half, too slow (snap back)
+ else if (ratio < 0 && ratio > -0.5 && direction == 'left' && velocityX < velocityThreshold) {
+ self.openPercentage(0);
+ }
+
+ // Going right, more than half, too slow (snap back)
+ else if (ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
+ self.openPercentage(-100);
+ }
+
+ // Going right, more than half, or quickly (snap open)
+ else if (direction == 'right' && ratio >= 0 && (ratio >= 0.5 || velocityX > velocityThreshold)) {
+ self.openPercentage(100);
+ }
+
+ // Going left, more than half, or quickly (span open)
+ else if (direction == 'left' && ratio <= 0 && (ratio <= -0.5 || velocityX > velocityThreshold)) {
+ self.openPercentage(-100);
+ }
+
+ // Snap back for safety
+ else {
+ self.openPercentage(0);
+ }
+ };
+
+ self.enableMenuWithBackViews = function(val) {
+ if (arguments.length) {
+ enableMenuWithBackViews = !!val;
+ }
+ return enableMenuWithBackViews;
+ };
+
+ self.isAsideExposed = function() {
+ return !!isAsideExposed;
+ };
+
+ self.exposeAside = function(shouldExposeAside) {
+ if (!(self.left && self.left.isEnabled) && !(self.right && self.right.isEnabled)) return;
+ self.close();
+
+ isAsideExposed = shouldExposeAside;
+ if ((self.left && self.left.isEnabled) && (self.right && self.right.isEnabled)) {
+ self.content.setMarginLeftAndRight(isAsideExposed ? self.left.width : 0, isAsideExposed ? self.right.width : 0);
+ } else if (self.left && self.left.isEnabled) {
+ // set the left marget width if it should be exposed
+ // otherwise set false so there's no left margin
+ self.content.setMarginLeft(isAsideExposed ? self.left.width : 0);
+ } else if (self.right && self.right.isEnabled) {
+ self.content.setMarginRight(isAsideExposed ? self.right.width : 0);
+ }
+ self.$scope.$emit('$ionicExposeAside', isAsideExposed);
+ };
+
+ self.activeAsideResizing = function(isResizing) {
+ $ionicBody.enableClass(isResizing, 'aside-resizing');
+ };
+
+ // End a drag with the given event
+ self._endDrag = function(e) {
+ if (isAsideExposed) return;
+
+ if (isDragging) {
+ self.snapToRest(e);
+ }
+ startX = null;
+ lastX = null;
+ offsetX = null;
+ };
+
+ // Handle a drag event
+ self._handleDrag = function(e) {
+ if (isAsideExposed || !$scope.dragContent) return;
+
+ // If we don't have start coords, grab and store them
+ if (!startX) {
+ startX = e.gesture.touches[0].pageX;
+ lastX = startX;
+ } else {
+ // Grab the current tap coords
+ lastX = e.gesture.touches[0].pageX;
+ }
+
+ // Calculate difference from the tap points
+ if (!isDragging && Math.abs(lastX - startX) > self.dragThresholdX) {
+ // if the difference is greater than threshold, start dragging using the current
+ // point as the starting point
+ startX = lastX;
+
+ isDragging = true;
+ // Initialize dragging
+ self.content.disableAnimation();
+ offsetX = self.getOpenAmount();
+ }
+
+ if (isDragging) {
+ self.openAmount(offsetX + (lastX - startX));
+ //self.content.setCanScroll(false);
+ }
+ };
+
+ self.canDragContent = function(canDrag) {
+ if (arguments.length) {
+ $scope.dragContent = !!canDrag;
+ }
+ return $scope.dragContent;
+ };
+
+ self.edgeThreshold = 25;
+ self.edgeThresholdEnabled = false;
+ self.edgeDragThreshold = function(value) {
+ if (arguments.length) {
+ if (isNumber(value) && value > 0) {
+ self.edgeThreshold = value;
+ self.edgeThresholdEnabled = true;
+ } else {
+ self.edgeThresholdEnabled = !!value;
+ }
+ }
+ return self.edgeThresholdEnabled;
+ };
+
+ self.isDraggableTarget = function(e) {
+ //Only restrict edge when sidemenu is closed and restriction is enabled
+ var shouldOnlyAllowEdgeDrag = self.edgeThresholdEnabled && !self.isOpen();
+ var startX = e.gesture.startEvent && e.gesture.startEvent.center &&
+ e.gesture.startEvent.center.pageX;
+
+ var dragIsWithinBounds = !shouldOnlyAllowEdgeDrag ||
+ startX <= self.edgeThreshold ||
+ startX >= self.content.element.offsetWidth - self.edgeThreshold;
+
+ var backView = $ionicHistory.backView();
+ var menuEnabled = enableMenuWithBackViews ? true : !backView;
+ if (!menuEnabled) {
+ var currentView = $ionicHistory.currentView() || {};
+ return (dragIsWithinBounds && (backView.historyId !== currentView.historyId));
+ }
+
+ return ($scope.dragContent || self.isOpen()) &&
+ dragIsWithinBounds &&
+ !e.gesture.srcEvent.defaultPrevented &&
+ menuEnabled &&
+ !e.target.tagName.match(/input|textarea|select|object|embed/i) &&
+ !e.target.isContentEditable &&
+ !(e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-scroll') == 'true');
+ };
+
+ $scope.sideMenuContentTranslateX = 0;
+
+ var deregisterBackButtonAction = noop;
+ var closeSideMenu = angular.bind(self, self.close);
+
+ $scope.$watch(function() {
+ return self.getOpenAmount() !== 0;
+ }, function(isOpen) {
+ deregisterBackButtonAction();
+ if (isOpen) {
+ deregisterBackButtonAction = $ionicPlatform.registerBackButtonAction(
+ closeSideMenu,
+ IONIC_BACK_PRIORITY.sideMenu
+ );
+ }
+ });
+
+ var deregisterInstance = $ionicSideMenuDelegate._registerInstance(
+ self, $attrs.delegateHandle, function() {
+ return $ionicHistory.isActiveScope($scope);
+ }
+ );
+
+ $scope.$on('$destroy', function() {
+ deregisterInstance();
+ deregisterBackButtonAction();
+ self.$scope = null;
+ if (self.content) {
+ self.content.setCanScroll(true);
+ self.content.element = null;
+ self.content = null;
+ }
+ });
+
+ self.initialize({
+ left: {
+ width: 275
+ },
+ right: {
+ width: 275
+ }
+ });
+
+}]);
+
+(function(ionic) {
+
+ var TRANSLATE32 = 'translate(32,32)';
+ var STROKE_OPACITY = 'stroke-opacity';
+ var ROUND = 'round';
+ var INDEFINITE = 'indefinite';
+ var DURATION = '750ms';
+ var NONE = 'none';
+ var SHORTCUTS = {
+ a: 'animate',
+ an: 'attributeName',
+ at: 'animateTransform',
+ c: 'circle',
+ da: 'stroke-dasharray',
+ os: 'stroke-dashoffset',
+ f: 'fill',
+ lc: 'stroke-linecap',
+ rc: 'repeatCount',
+ sw: 'stroke-width',
+ t: 'transform',
+ v: 'values'
+ };
+
+ var SPIN_ANIMATION = {
+ v: '0,32,32;360,32,32',
+ an: 'transform',
+ type: 'rotate',
+ rc: INDEFINITE,
+ dur: DURATION
+ };
+
+ function createSvgElement(tagName, data, parent, spinnerName) {
+ var ele = document.createElement(SHORTCUTS[tagName] || tagName);
+ var k, x, y;
+
+ for (k in data) {
+
+ if (angular.isArray(data[k])) {
+ for (x = 0; x < data[k].length; x++) {
+ if (data[k][x].fn) {
+ for (y = 0; y < data[k][x].t; y++) {
+ createSvgElement(k, data[k][x].fn(y, spinnerName), ele, spinnerName);
+ }
+ } else {
+ createSvgElement(k, data[k][x], ele, spinnerName);
+ }
+ }
+
+ } else {
+ setSvgAttribute(ele, k, data[k]);
+ }
+ }
+
+ parent.appendChild(ele);
+ }
+
+ function setSvgAttribute(ele, k, v) {
+ ele.setAttribute(SHORTCUTS[k] || k, v);
+ }
+
+ function animationValues(strValues, i) {
+ var values = strValues.split(';');
+ var back = values.slice(i);
+ var front = values.slice(0, values.length - back.length);
+ values = back.concat(front).reverse();
+ return values.join(';') + ';' + values[0];
+ }
+
+ var IOS_SPINNER = {
+ sw: 4,
+ lc: ROUND,
+ line: [{
+ fn: function(i, spinnerName) {
+ return {
+ y1: spinnerName == 'ios' ? 17 : 12,
+ y2: spinnerName == 'ios' ? 29 : 20,
+ t: TRANSLATE32 + ' rotate(' + (30 * i + (i < 6 ? 180 : -180)) + ')',
+ a: [{
+ fn: function() {
+ return {
+ an: STROKE_OPACITY,
+ dur: DURATION,
+ v: animationValues('0;.1;.15;.25;.35;.45;.55;.65;.7;.85;1', i),
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }]
+ };
+ },
+ t: 12
+ }]
+ };
+
+ var spinners = {
+
+ android: {
+ c: [{
+ sw: 6,
+ da: 128,
+ os: 82,
+ r: 26,
+ cx: 32,
+ cy: 32,
+ f: NONE
+ }]
+ },
+
+ ios: IOS_SPINNER,
+
+ 'ios-small': IOS_SPINNER,
+
+ bubbles: {
+ sw: 0,
+ c: [{
+ fn: function(i) {
+ return {
+ cx: 24 * Math.cos(2 * Math.PI * i / 8),
+ cy: 24 * Math.sin(2 * Math.PI * i / 8),
+ t: TRANSLATE32,
+ a: [{
+ fn: function() {
+ return {
+ an: 'r',
+ dur: DURATION,
+ v: animationValues('1;2;3;4;5;6;7;8', i),
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }]
+ };
+ },
+ t: 8
+ }]
+ },
+
+ circles: {
+
+ c: [{
+ fn: function(i) {
+ return {
+ r: 5,
+ cx: 24 * Math.cos(2 * Math.PI * i / 8),
+ cy: 24 * Math.sin(2 * Math.PI * i / 8),
+ t: TRANSLATE32,
+ sw: 0,
+ a: [{
+ fn: function() {
+ return {
+ an: 'fill-opacity',
+ dur: DURATION,
+ v: animationValues('.3;.3;.3;.4;.7;.85;.9;1', i),
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }]
+ };
+ },
+ t: 8
+ }]
+ },
+
+ crescent: {
+ c: [{
+ sw: 4,
+ da: 128,
+ os: 82,
+ r: 26,
+ cx: 32,
+ cy: 32,
+ f: NONE,
+ at: [SPIN_ANIMATION]
+ }]
+ },
+
+ dots: {
+
+ c: [{
+ fn: function(i) {
+ return {
+ cx: 16 + (16 * i),
+ cy: 32,
+ sw: 0,
+ a: [{
+ fn: function() {
+ return {
+ an: 'fill-opacity',
+ dur: DURATION,
+ v: animationValues('.5;.6;.8;1;.8;.6;.5', i),
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }, {
+ fn: function() {
+ return {
+ an: 'r',
+ dur: DURATION,
+ v: animationValues('4;5;6;5;4;3;3', i),
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }]
+ };
+ },
+ t: 3
+ }]
+ },
+
+ lines: {
+ sw: 7,
+ lc: ROUND,
+ line: [{
+ fn: function(i) {
+ return {
+ x1: 10 + (i * 14),
+ x2: 10 + (i * 14),
+ a: [{
+ fn: function() {
+ return {
+ an: 'y1',
+ dur: DURATION,
+ v: animationValues('16;18;28;18;16', i),
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }, {
+ fn: function() {
+ return {
+ an: 'y2',
+ dur: DURATION,
+ v: animationValues('48;44;36;46;48', i),
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }, {
+ fn: function() {
+ return {
+ an: STROKE_OPACITY,
+ dur: DURATION,
+ v: animationValues('1;.8;.5;.4;1', i),
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }]
+ };
+ },
+ t: 4
+ }]
+ },
+
+ ripple: {
+ f: NONE,
+ 'fill-rule': 'evenodd',
+ sw: 3,
+ circle: [{
+ fn: function(i) {
+ return {
+ cx: 32,
+ cy: 32,
+ a: [{
+ fn: function() {
+ return {
+ an: 'r',
+ begin: (i * -1) + 's',
+ dur: '2s',
+ v: '0;24',
+ keyTimes: '0;1',
+ keySplines: '0.1,0.2,0.3,1',
+ calcMode: 'spline',
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }, {
+ fn: function() {
+ return {
+ an: STROKE_OPACITY,
+ begin: (i * -1) + 's',
+ dur: '2s',
+ v: '.2;1;.2;0',
+ rc: INDEFINITE
+ };
+ },
+ t: 1
+ }]
+ };
+ },
+ t: 2
+ }]
+ },
+
+ spiral: {
+ defs: [{
+ linearGradient: [{
+ id: 'sGD',
+ gradientUnits: 'userSpaceOnUse',
+ x1: 55, y1: 46, x2: 2, y2: 46,
+ stop: [{
+ offset: 0.1,
+ class: 'stop1'
+ }, {
+ offset: 1,
+ class: 'stop2'
+ }]
+ }]
+ }],
+ g: [{
+ sw: 4,
+ lc: ROUND,
+ f: NONE,
+ path: [{
+ stroke: 'url(#sGD)',
+ d: 'M4,32 c0,15,12,28,28,28c8,0,16-4,21-9'
+ }, {
+ d: 'M60,32 C60,16,47.464,4,32,4S4,16,4,32'
+ }],
+ at: [SPIN_ANIMATION]
+ }]
+ }
+
+ };
+
+ var animations = {
+
+ android: function(ele) {
+ // Note that this is called as a function, not a constructor.
+ var self = {};
+
+ this.stop = false;
+
+ var rIndex = 0;
+ var rotateCircle = 0;
+ var startTime;
+ var svgEle = ele.querySelector('g');
+ var circleEle = ele.querySelector('circle');
+
+ function run() {
+ if (self.stop) return;
+
+ var v = easeInOutCubic(Date.now() - startTime, 650);
+ var scaleX = 1;
+ var translateX = 0;
+ var dasharray = (188 - (58 * v));
+ var dashoffset = (182 - (182 * v));
+
+ if (rIndex % 2) {
+ scaleX = -1;
+ translateX = -64;
+ dasharray = (128 - (-58 * v));
+ dashoffset = (182 * v);
+ }
+
+ var rotateLine = [0, -101, -90, -11, -180, 79, -270, -191][rIndex];
+
+ setSvgAttribute(circleEle, 'da', Math.max(Math.min(dasharray, 188), 128));
+ setSvgAttribute(circleEle, 'os', Math.max(Math.min(dashoffset, 182), 0));
+ setSvgAttribute(circleEle, 't', 'scale(' + scaleX + ',1) translate(' + translateX + ',0) rotate(' + rotateLine + ',32,32)');
+
+ rotateCircle += 4.1;
+ if (rotateCircle > 359) rotateCircle = 0;
+ setSvgAttribute(svgEle, 't', 'rotate(' + rotateCircle + ',32,32)');
+
+ if (v >= 1) {
+ rIndex++;
+ if (rIndex > 7) rIndex = 0;
+ startTime = Date.now();
+ }
+
+ ionic.requestAnimationFrame(run);
+ }
+
+ return function() {
+ startTime = Date.now();
+ run();
+ return self;
+ };
+
+ }
+
+ };
+
+ function easeInOutCubic(t, c) {
+ t /= c / 2;
+ if (t < 1) return 1 / 2 * t * t * t;
+ t -= 2;
+ return 1 / 2 * (t * t * t + 2);
+ }
+
+
+ IonicModule
+ .controller('$ionicSpinner', [
+ '$element',
+ '$attrs',
+ '$ionicConfig',
+ function($element, $attrs, $ionicConfig) {
+ var spinnerName, anim;
+
+ this.init = function() {
+ spinnerName = $attrs.icon || $ionicConfig.spinner.icon();
+
+ var container = document.createElement('div');
+ createSvgElement('svg', {
+ viewBox: '0 0 64 64',
+ g: [spinners[spinnerName]]
+ }, container, spinnerName);
+
+ // Specifically for animations to work,
+ // Android 4.3 and below requires the element to be
+ // added as an html string, rather than dynmically
+ // building up the svg element and appending it.
+ $element.html(container.innerHTML);
+
+ this.start();
+
+ return spinnerName;
+ };
+
+ this.start = function() {
+ animations[spinnerName] && (anim = animations[spinnerName]($element[0])());
+ };
+
+ this.stop = function() {
+ animations[spinnerName] && (anim.stop = true);
+ };
+
+ }]);
+
+})(ionic);
+
+IonicModule
+.controller('$ionicTab', [
+ '$scope',
+ '$ionicHistory',
+ '$attrs',
+ '$location',
+ '$state',
+function($scope, $ionicHistory, $attrs, $location, $state) {
+ this.$scope = $scope;
+
+ //All of these exposed for testing
+ this.hrefMatchesState = function() {
+ return $attrs.href && $location.path().indexOf(
+ $attrs.href.replace(/^#/, '').replace(/\/$/, '')
+ ) === 0;
+ };
+ this.srefMatchesState = function() {
+ return $attrs.uiSref && $state.includes($attrs.uiSref.split('(')[0]);
+ };
+ this.navNameMatchesState = function() {
+ return this.navViewName && $ionicHistory.isCurrentStateNavView(this.navViewName);
+ };
+
+ this.tabMatchesState = function() {
+ return this.hrefMatchesState() || this.srefMatchesState() || this.navNameMatchesState();
+ };
+}]);
+
+IonicModule
+.controller('$ionicTabs', [
+ '$scope',
+ '$element',
+ '$ionicHistory',
+function($scope, $element, $ionicHistory) {
+ var self = this;
+ var selectedTab = null;
+ var previousSelectedTab = null;
+ var selectedTabIndex;
+ var isVisible = true;
+ self.tabs = [];
+
+ self.selectedIndex = function() {
+ return self.tabs.indexOf(selectedTab);
+ };
+ self.selectedTab = function() {
+ return selectedTab;
+ };
+ self.previousSelectedTab = function() {
+ return previousSelectedTab;
+ };
+
+ self.add = function(tab) {
+ $ionicHistory.registerHistory(tab);
+ self.tabs.push(tab);
+ };
+
+ self.remove = function(tab) {
+ var tabIndex = self.tabs.indexOf(tab);
+ if (tabIndex === -1) {
+ return;
+ }
+ //Use a field like '$tabSelected' so developers won't accidentally set it in controllers etc
+ if (tab.$tabSelected) {
+ self.deselect(tab);
+ //Try to select a new tab if we're removing a tab
+ if (self.tabs.length === 1) {
+ //Do nothing if there are no other tabs to select
+ } else {
+ //Select previous tab if it's the last tab, else select next tab
+ var newTabIndex = tabIndex === self.tabs.length - 1 ? tabIndex - 1 : tabIndex + 1;
+ self.select(self.tabs[newTabIndex]);
+ }
+ }
+ self.tabs.splice(tabIndex, 1);
+ };
+
+ self.deselect = function(tab) {
+ if (tab.$tabSelected) {
+ previousSelectedTab = selectedTab;
+ selectedTab = selectedTabIndex = null;
+ tab.$tabSelected = false;
+ (tab.onDeselect || noop)();
+ tab.$broadcast && tab.$broadcast('$ionicHistory.deselect');
+ }
+ };
+
+ self.select = function(tab, shouldEmitEvent) {
+ var tabIndex;
+ if (isNumber(tab)) {
+ tabIndex = tab;
+ if (tabIndex >= self.tabs.length) return;
+ tab = self.tabs[tabIndex];
+ } else {
+ tabIndex = self.tabs.indexOf(tab);
+ }
+
+ if (arguments.length === 1) {
+ shouldEmitEvent = !!(tab.navViewName || tab.uiSref);
+ }
+
+ if (selectedTab && selectedTab.$historyId == tab.$historyId) {
+ if (shouldEmitEvent) {
+ $ionicHistory.goToHistoryRoot(tab.$historyId);
+ }
+
+ } else if (selectedTabIndex !== tabIndex) {
+ forEach(self.tabs, function(tab) {
+ self.deselect(tab);
+ });
+
+ selectedTab = tab;
+ selectedTabIndex = tabIndex;
+
+ if (self.$scope && self.$scope.$parent) {
+ self.$scope.$parent.$activeHistoryId = tab.$historyId;
+ }
+
+ //Use a funny name like $tabSelected so the developer doesn't overwrite the var in a child scope
+ tab.$tabSelected = true;
+ (tab.onSelect || noop)();
+
+ if (shouldEmitEvent) {
+ $scope.$emit('$ionicHistory.change', {
+ type: 'tab',
+ tabIndex: tabIndex,
+ historyId: tab.$historyId,
+ navViewName: tab.navViewName,
+ hasNavView: !!tab.navViewName,
+ title: tab.title,
+ url: tab.href,
+ uiSref: tab.uiSref
+ });
+ }
+
+ $scope.$broadcast("tabSelected", { selectedTab: tab, selectedTabIndex: tabIndex});
+ }
+ };
+
+ self.hasActiveScope = function() {
+ for (var x = 0; x < self.tabs.length; x++) {
+ if ($ionicHistory.isActiveScope(self.tabs[x])) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ self.showBar = function(show) {
+ if (arguments.length) {
+ if (show) {
+ $element.removeClass('tabs-item-hide');
+ } else {
+ $element.addClass('tabs-item-hide');
+ }
+ isVisible = !!show;
+ }
+ return isVisible;
+ };
+}]);
+
+IonicModule
+.controller('$ionicView', [
+ '$scope',
+ '$element',
+ '$attrs',
+ '$compile',
+ '$rootScope',
+function($scope, $element, $attrs, $compile, $rootScope) {
+ var self = this;
+ var navElementHtml = {};
+ var navViewCtrl;
+ var navBarDelegateHandle;
+ var hasViewHeaderBar;
+ var deregisters = [];
+ var viewTitle;
+
+ var deregIonNavBarInit = $scope.$on('ionNavBar.init', function(ev, delegateHandle) {
+ // this view has its own ion-nav-bar, remember the navBarDelegateHandle for this view
+ ev.stopPropagation();
+ navBarDelegateHandle = delegateHandle;
+ });
+
+
+ self.init = function() {
+ deregIonNavBarInit();
+
+ var modalCtrl = $element.inheritedData('$ionModalController');
+ navViewCtrl = $element.inheritedData('$ionNavViewController');
+
+ // don't bother if inside a modal or there's no parent navView
+ if (!navViewCtrl || modalCtrl) return;
+
+ // add listeners for when this view changes
+ $scope.$on('$ionicView.beforeEnter', self.beforeEnter);
+ $scope.$on('$ionicView.afterEnter', afterEnter);
+ $scope.$on('$ionicView.beforeLeave', deregisterFns);
+ };
+
+ self.beforeEnter = function(ev, transData) {
+ // this event was emitted, starting at intial ion-view, then bubbles up
+ // only the first ion-view should do something with it, parent ion-views should ignore
+ if (transData && !transData.viewNotified) {
+ transData.viewNotified = true;
+
+ if (!$rootScope.$$phase) $scope.$digest();
+ viewTitle = isDefined($attrs.viewTitle) ? $attrs.viewTitle : $attrs.title;
+
+ var navBarItems = {};
+ for (var n in navElementHtml) {
+ navBarItems[n] = generateNavBarItem(navElementHtml[n]);
+ }
+
+ navViewCtrl.beforeEnter(extend(transData, {
+ title: viewTitle,
+ showBack: !attrTrue('hideBackButton'),
+ navBarItems: navBarItems,
+ navBarDelegate: navBarDelegateHandle || null,
+ showNavBar: !attrTrue('hideNavBar'),
+ hasHeaderBar: !!hasViewHeaderBar
+ }));
+
+ // make sure any existing observers are cleaned up
+ deregisterFns();
+ }
+ };
+
+
+ function afterEnter() {
+ // only listen for title updates after it has entered
+ // but also deregister the observe before it leaves
+ var viewTitleAttr = isDefined($attrs.viewTitle) && 'viewTitle' || isDefined($attrs.title) && 'title';
+ if (viewTitleAttr) {
+ titleUpdate($attrs[viewTitleAttr]);
+ deregisters.push($attrs.$observe(viewTitleAttr, titleUpdate));
+ }
+
+ if (isDefined($attrs.hideBackButton)) {
+ deregisters.push($scope.$watch($attrs.hideBackButton, function(val) {
+ navViewCtrl.showBackButton(!val);
+ }));
+ }
+
+ if (isDefined($attrs.hideNavBar)) {
+ deregisters.push($scope.$watch($attrs.hideNavBar, function(val) {
+ navViewCtrl.showBar(!val);
+ }));
+ }
+ }
+
+
+ function titleUpdate(newTitle) {
+ if (isDefined(newTitle) && newTitle !== viewTitle) {
+ viewTitle = newTitle;
+ navViewCtrl.title(viewTitle);
+ }
+ }
+
+
+ function deregisterFns() {
+ // remove all existing $attrs.$observe's
+ for (var x = 0; x < deregisters.length; x++) {
+ deregisters[x]();
+ }
+ deregisters = [];
+ }
+
+
+ function generateNavBarItem(html) {
+ if (html) {
+ // every time a view enters we need to recreate its view buttons if they exist
+ return $compile(html)($scope.$new());
+ }
+ }
+
+
+ function attrTrue(key) {
+ return !!$scope.$eval($attrs[key]);
+ }
+
+
+ self.navElement = function(type, html) {
+ navElementHtml[type] = html;
+ };
+
+}]);
+
+/*
+ * We don't document the ionActionSheet directive, we instead document
+ * the $ionicActionSheet service
+ */
+IonicModule
+.directive('ionActionSheet', ['$document', function($document) {
+ return {
+ restrict: 'E',
+ scope: true,
+ replace: true,
+ link: function($scope, $element) {
+
+ var keyUp = function(e) {
+ if (e.which == 27) {
+ $scope.cancel();
+ $scope.$apply();
+ }
+ };
+
+ var backdropClick = function(e) {
+ if (e.target == $element[0]) {
+ $scope.cancel();
+ $scope.$apply();
+ }
+ };
+ $scope.$on('$destroy', function() {
+ $element.remove();
+ $document.unbind('keyup', keyUp);
+ });
+
+ $document.bind('keyup', keyUp);
+ $element.bind('click', backdropClick);
+ },
+ template: '<div class="action-sheet-backdrop">' +
+ '<div class="action-sheet-wrapper">' +
+ '<div class="action-sheet" ng-class="{\'action-sheet-has-icons\': $actionSheetHasIcon}">' +
+ '<div class="action-sheet-group action-sheet-options">' +
+ '<div class="action-sheet-title" ng-if="titleText" ng-bind-html="titleText"></div>' +
+ '<button class="button action-sheet-option" ng-click="buttonClicked($index)" ng-class="b.className" ng-repeat="b in buttons" ng-bind-html="b.text"></button>' +
+ '<button class="button destructive action-sheet-destructive" ng-if="destructiveText" ng-click="destructiveButtonClicked()" ng-bind-html="destructiveText"></button>' +
+ '</div>' +
+ '<div class="action-sheet-group action-sheet-cancel" ng-if="cancelText">' +
+ '<button class="button" ng-click="cancel()" ng-bind-html="cancelText"></button>' +
+ '</div>' +
+ '</div>' +
+ '</div>' +
+ '</div>'
+ };
+}]);
+
+
+/**
+ * @ngdoc directive
+ * @name ionCheckbox
+ * @module ionic
+ * @restrict E
+ * @codepen hqcju
+ * @description
+ * The checkbox is no different than the HTML checkbox input, except it's styled differently.
+ *
+ * The checkbox behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]).
+ *
+ * @usage
+ * ```html
+ * <ion-checkbox ng-model="isChecked">Checkbox Label</ion-checkbox>
+ * ```
+ */
+
+IonicModule
+.directive('ionCheckbox', ['$ionicConfig', function($ionicConfig) {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: '?ngModel',
+ transclude: true,
+ template:
+ '<label class="item item-checkbox">' +
+ '<div class="checkbox checkbox-input-hidden disable-pointer-events">' +
+ '<input type="checkbox">' +
+ '<i class="checkbox-icon"></i>' +
+ '</div>' +
+ '<div class="item-content disable-pointer-events" ng-transclude></div>' +
+ '</label>',
+ compile: function(element, attr) {
+ var input = element.find('input');
+ forEach({
+ 'name': attr.name,
+ 'ng-value': attr.ngValue,
+ 'ng-model': attr.ngModel,
+ 'ng-checked': attr.ngChecked,
+ 'ng-disabled': attr.ngDisabled,
+ 'ng-true-value': attr.ngTrueValue,
+ 'ng-false-value': attr.ngFalseValue,
+ 'ng-change': attr.ngChange,
+ 'ng-required': attr.ngRequired,
+ 'required': attr.required
+ }, function(value, name) {
+ if (isDefined(value)) {
+ input.attr(name, value);
+ }
+ });
+ var checkboxWrapper = element[0].querySelector('.checkbox');
+ checkboxWrapper.classList.add('checkbox-' + $ionicConfig.form.checkbox());
+ }
+ };
+}]);
+
+
+/**
+ * @ngdoc directive
+ * @restrict A
+ * @name collectionRepeat
+ * @module ionic
+ * @codepen 7ec1ec58f2489ab8f359fa1a0fe89c15
+ * @description
+ * `collection-repeat` allows an app to show huge lists of items much more performantly than
+ * `ng-repeat`.
+ *
+ * It renders into the DOM only as many items as are currently visible.
+ *
+ * This means that on a phone screen that can fit eight items, only the eight items matching
+ * the current scroll position will be rendered.
+ *
+ * **The Basics**:
+ *
+ * - The data given to collection-repeat must be an array.
+ * - If the `item-height` and `item-width` attributes are not supplied, it will be assumed that
+ * every item in the list has the same dimensions as the first item.
+ * - Don't use angular one-time binding (`::`) with collection-repeat. The scope of each item is
+ * assigned new data and re-digested as you scroll. Bindings need to update, and one-time bindings
+ * won't.
+ *
+ * **Performance Tips**:
+ *
+ * - The iOS webview has a performance bottleneck when switching out `<img src>` attributes.
+ * To increase performance of images on iOS, cache your images in advance and,
+ * if possible, lower the number of unique images. We're working on [a solution](https://github.com/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="{% raw %}{{photo.src}}{% endraw %}">
+ * </div>
+ * </ion-scroll>
+ * </ion-content>
+ * ```
+ *
+ * @param {expression} collection-repeat The expression indicating how to enumerate a collection,
+ * of the format `variable in expression` – where variable is the user defined loop variable
+ * and `expression` is a scope expression giving the collection to enumerate.
+ * For example: `album in artist.albums` or `album in artist.albums | orderBy:'name'`.
+ * @param {expression=} item-width The width of the repeated element. The expression must return
+ * a number (pixels) or a percentage. Defaults to the width of the first item in the list.
+ * (previously named collection-item-width)
+ * @param {expression=} item-height The height of the repeated element. The expression must return
+ * a number (pixels) or a percentage. Defaults to the height of the first item in the list.
+ * (previously named collection-item-height)
+ * @param {number=} item-render-buffer The number of items to load before and after the visible
+ * items in the list. Default 3. Tip: set this higher if you have lots of images to preload, but
+ * don't set it too high or you'll see performance loss.
+ * @param {boolean=} force-refresh-images Force images to refresh as you scroll. This fixes a problem
+ * where, when an element is interchanged as scrolling, its image will still have the old src
+ * while the new src loads. Setting this to true comes with a small performance loss.
+ */
+
+IonicModule
+.directive('collectionRepeat', CollectionRepeatDirective)
+.factory('$ionicCollectionManager', RepeatManagerFactory);
+
+var ONE_PX_TRANSPARENT_IMG_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
+var WIDTH_HEIGHT_REGEX = /height:.*?px;\s*width:.*?px/;
+var DEFAULT_RENDER_BUFFER = 3;
+
+CollectionRepeatDirective.$inject = ['$ionicCollectionManager', '$parse', '$window', '$$rAF', '$rootScope', '$timeout'];
+function CollectionRepeatDirective($ionicCollectionManager, $parse, $window, $$rAF, $rootScope, $timeout) {
+ return {
+ restrict: 'A',
+ priority: 1000,
+ transclude: 'element',
+ $$tlb: true,
+ require: '^^$ionicScroll',
+ link: postLink
+ };
+
+ function postLink(scope, element, attr, scrollCtrl, transclude) {
+ var scrollView = scrollCtrl.scrollView;
+ var node = element[0];
+ var containerNode = angular.element('<div class="collection-repeat-container">')[0];
+ node.parentNode.replaceChild(containerNode, node);
+
+ if (scrollView.options.scrollingX && scrollView.options.scrollingY) {
+ throw new Error("collection-repeat expected a parent x or y scrollView, not " +
+ "an xy scrollView.");
+ }
+
+ var repeatExpr = attr.collectionRepeat;
+ var match = repeatExpr.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
+ if (!match) {
+ throw new Error("collection-repeat expected expression in form of '_item_ in " +
+ "_collection_[ track by _id_]' but got '" + attr.collectionRepeat + "'.");
+ }
+ var keyExpr = match[1];
+ var listExpr = match[2];
+ var listGetter = $parse(listExpr);
+ var heightData = {};
+ var widthData = {};
+ var computedStyleDimensions = {};
+ var data = [];
+ var repeatManager;
+
+ // attr.collectionBufferSize is deprecated
+ var renderBufferExpr = attr.itemRenderBuffer || attr.collectionBufferSize;
+ var renderBuffer = angular.isDefined(renderBufferExpr) ?
+ parseInt(renderBufferExpr) :
+ DEFAULT_RENDER_BUFFER;
+
+ // attr.collectionItemHeight is deprecated
+ var heightExpr = attr.itemHeight || attr.collectionItemHeight;
+ // attr.collectionItemWidth is deprecated
+ var widthExpr = attr.itemWidth || attr.collectionItemWidth;
+
+ var afterItemsContainer = initAfterItemsContainer();
+
+ var changeValidator = makeChangeValidator();
+ initDimensions();
+
+ // Dimensions are refreshed on resize or data change.
+ scrollCtrl.$element.on('scroll-resize', refreshDimensions);
+
+ angular.element($window).on('resize', onResize);
+ var unlistenToExposeAside = $rootScope.$on('$ionicExposeAside', ionic.animationFrameThrottle(function() {
+ scrollCtrl.scrollView.resize();
+ onResize();
+ }));
+ $timeout(refreshDimensions, 0, false);
+
+ function onResize() {
+ if (changeValidator.resizeRequiresRefresh(scrollView.__clientWidth, scrollView.__clientHeight)) {
+ refreshDimensions();
+ }
+ }
+
+ scope.$watchCollection(listGetter, function(newValue) {
+ data = newValue || (newValue = []);
+ if (!angular.isArray(newValue)) {
+ throw new Error("collection-repeat expected an array for '" + listExpr + "', " +
+ "but got a " + typeof value);
+ }
+ // Wait for this digest to end before refreshing everything.
+ scope.$$postDigest(function() {
+ getRepeatManager().setData(data);
+ if (changeValidator.dataChangeRequiresRefresh(data)) refreshDimensions();
+ });
+ });
+
+ scope.$on('$destroy', function() {
+ angular.element($window).off('resize', onResize);
+ unlistenToExposeAside();
+ scrollCtrl.$element && scrollCtrl.$element.off('scroll-resize', refreshDimensions);
+
+ computedStyleNode && computedStyleNode.parentNode &&
+ computedStyleNode.parentNode.removeChild(computedStyleNode);
+ computedStyleScope && computedStyleScope.$destroy();
+ computedStyleScope = computedStyleNode = null;
+
+ repeatManager && repeatManager.destroy();
+ repeatManager = null;
+ });
+
+ function makeChangeValidator() {
+ var self;
+ return (self = {
+ dataLength: 0,
+ width: 0,
+ height: 0,
+ // A resize triggers a refresh only if we have data, the scrollView has size,
+ // and the size has changed.
+ resizeRequiresRefresh: function(newWidth, newHeight) {
+ var requiresRefresh = self.dataLength && newWidth && newHeight &&
+ (newWidth !== self.width || newHeight !== self.height);
+
+ self.width = newWidth;
+ self.height = newHeight;
+
+ return !!requiresRefresh;
+ },
+ // A change in data only triggers a refresh if the data has length, or if the data's
+ // length is less than before.
+ dataChangeRequiresRefresh: function(newData) {
+ var requiresRefresh = newData.length > 0 || newData.length < self.dataLength;
+
+ self.dataLength = newData.length;
+
+ return !!requiresRefresh;
+ }
+ });
+ }
+
+ function getRepeatManager() {
+ return repeatManager || (repeatManager = new $ionicCollectionManager({
+ afterItemsNode: afterItemsContainer[0],
+ containerNode: containerNode,
+ heightData: heightData,
+ widthData: widthData,
+ forceRefreshImages: !!(isDefined(attr.forceRefreshImages) && attr.forceRefreshImages !== 'false'),
+ keyExpression: keyExpr,
+ renderBuffer: renderBuffer,
+ scope: scope,
+ scrollView: scrollCtrl.scrollView,
+ transclude: transclude
+ }));
+ }
+
+ function initAfterItemsContainer() {
+ var container = angular.element(
+ scrollView.__content.querySelector('.collection-repeat-after-container')
+ );
+ // Put everything in the view after the repeater into a container.
+ if (!container.length) {
+ var elementIsAfterRepeater = false;
+ var afterNodes = [].filter.call(scrollView.__content.childNodes, function(node) {
+ if (ionic.DomUtil.contains(node, containerNode)) {
+ elementIsAfterRepeater = true;
+ return false;
+ }
+ return elementIsAfterRepeater;
+ });
+ container = angular.element('<span class="collection-repeat-after-container">');
+ if (scrollView.options.scrollingX) {
+ container.addClass('horizontal');
+ }
+ container.append(afterNodes);
+ scrollView.__content.appendChild(container[0]);
+ }
+ return container;
+ }
+
+ function initDimensions() {
+ //Height and width have four 'modes':
+ //1) Computed Mode
+ // - Nothing is supplied, so we getComputedStyle() on one element in the list and use
+ // that width and height value for the width and height of every item. This is re-computed
+ // every resize.
+ //2) Constant Mode, Static Integer
+ // - The user provides a constant number for width or height, in pixels. We parse it,
+ // store it on the `value` field, and it never changes
+ //3) Constant Mode, Percent
+ // - The user provides a percent string for width or height. The getter for percent is
+ // stored on the `getValue()` field, and is re-evaluated once every resize. The result
+ // is stored on the `value` field.
+ //4) Dynamic Mode
+ // - The user provides a dynamic expression for the width or height. This is re-evaluated
+ // for every item, stored on the `.getValue()` field.
+ if (heightExpr) {
+ parseDimensionAttr(heightExpr, heightData);
+ } else {
+ heightData.computed = true;
+ }
+ if (widthExpr) {
+ parseDimensionAttr(widthExpr, widthData);
+ } else {
+ widthData.computed = true;
+ }
+ }
+
+ function refreshDimensions() {
+ var hasData = data.length > 0;
+
+ if (hasData && (heightData.computed || widthData.computed)) {
+ computeStyleDimensions();
+ }
+
+ if (hasData && heightData.computed) {
+ heightData.value = computedStyleDimensions.height;
+ if (!heightData.value) {
+ throw new Error('collection-repeat tried to compute the height of repeated elements "' +
+ repeatExpr + '", but was unable to. Please provide the "item-height" attribute. ' +
+ 'http://ionicframework.com/docs/api/directive/collectionRepeat/');
+ }
+ } else if (!heightData.dynamic && heightData.getValue) {
+ // If it's a constant with a getter (eg percent), we just refresh .value after resize
+ heightData.value = heightData.getValue();
+ }
+
+ if (hasData && widthData.computed) {
+ widthData.value = computedStyleDimensions.width;
+ if (!widthData.value) {
+ throw new Error('collection-repeat tried to compute the width of repeated elements "' +
+ repeatExpr + '", but was unable to. Please provide the "item-width" attribute. ' +
+ 'http://ionicframework.com/docs/api/directive/collectionRepeat/');
+ }
+ } else if (!widthData.dynamic && widthData.getValue) {
+ // If it's a constant with a getter (eg percent), we just refresh .value after resize
+ widthData.value = widthData.getValue();
+ }
+ // Dynamic dimensions aren't updated on resize. Since they're already dynamic anyway,
+ // .getValue() will be used.
+
+ getRepeatManager().refreshLayout();
+ }
+
+ function parseDimensionAttr(attrValue, dimensionData) {
+ if (!attrValue) return;
+
+ var parsedValue;
+ // Try to just parse the plain attr value
+ try {
+ parsedValue = $parse(attrValue);
+ } catch (e) {
+ // If the parse fails and the value has `px` or `%` in it, surround the attr in
+ // quotes, to attempt to let the user provide a simple `attr="100%"` or `attr="100px"`
+ if (attrValue.trim().match(/\d+(px|%)$/)) {
+ attrValue = '"' + attrValue + '"';
+ }
+ parsedValue = $parse(attrValue);
+ }
+
+ var constantAttrValue = attrValue.replace(/(\'|\"|px|%)/g, '').trim();
+ var isConstant = constantAttrValue.length && !/([a-zA-Z]|\$|:|\?)/.test(constantAttrValue);
+ dimensionData.attrValue = attrValue;
+
+ // If it's a constant, it's either a percent or just a constant pixel number.
+ if (isConstant) {
+ // For percents, store the percent getter on .getValue()
+ if (attrValue.indexOf('%') > -1) {
+ var decimalValue = parseFloat(parsedValue()) / 100;
+ dimensionData.getValue = dimensionData === heightData ?
+ function() { return Math.floor(decimalValue * scrollView.__clientHeight); } :
+ function() { return Math.floor(decimalValue * scrollView.__clientWidth); };
+ } else {
+ // For static constants, just store the static constant.
+ dimensionData.value = parseInt(parsedValue());
+ }
+
+ } else {
+ dimensionData.dynamic = true;
+ dimensionData.getValue = dimensionData === heightData ?
+ function heightGetter(scope, locals) {
+ var result = parsedValue(scope, locals);
+ if (result.charAt && result.charAt(result.length - 1) === '%') {
+ return Math.floor(parseFloat(result) / 100 * scrollView.__clientHeight);
+ }
+ return parseInt(result);
+ } :
+ function widthGetter(scope, locals) {
+ var result = parsedValue(scope, locals);
+ if (result.charAt && result.charAt(result.length - 1) === '%') {
+ return Math.floor(parseFloat(result) / 100 * scrollView.__clientWidth);
+ }
+ return parseInt(result);
+ };
+ }
+ }
+
+ var computedStyleNode;
+ var computedStyleScope;
+ function computeStyleDimensions() {
+ if (!computedStyleNode) {
+ transclude(computedStyleScope = scope.$new(), function(clone) {
+ clone[0].removeAttribute('collection-repeat'); // remove absolute position styling
+ computedStyleNode = clone[0];
+ });
+ }
+
+ computedStyleScope[keyExpr] = (listGetter(scope) || [])[0];
+ if (!$rootScope.$$phase) computedStyleScope.$digest();
+ containerNode.appendChild(computedStyleNode);
+
+ var style = $window.getComputedStyle(computedStyleNode);
+ computedStyleDimensions.width = parseInt(style.width);
+ computedStyleDimensions.height = parseInt(style.height);
+
+ containerNode.removeChild(computedStyleNode);
+ }
+
+ }
+
+}
+
+RepeatManagerFactory.$inject = ['$rootScope', '$window', '$$rAF'];
+function RepeatManagerFactory($rootScope, $window, $$rAF) {
+ var EMPTY_DIMENSION = { primaryPos: 0, secondaryPos: 0, primarySize: 0, secondarySize: 0, rowPrimarySize: 0 };
+
+ return function RepeatController(options) {
+ var afterItemsNode = options.afterItemsNode;
+ var containerNode = options.containerNode;
+ var forceRefreshImages = options.forceRefreshImages;
+ var heightData = options.heightData;
+ var widthData = options.widthData;
+ var keyExpression = options.keyExpression;
+ var renderBuffer = options.renderBuffer;
+ var scope = options.scope;
+ var scrollView = options.scrollView;
+ var transclude = options.transclude;
+
+ var data = [];
+
+ var getterLocals = {};
+ var heightFn = heightData.getValue || function() { return heightData.value; };
+ var heightGetter = function(index, value) {
+ getterLocals[keyExpression] = value;
+ getterLocals.$index = index;
+ return heightFn(scope, getterLocals);
+ };
+
+ var widthFn = widthData.getValue || function() { return widthData.value; };
+ var widthGetter = function(index, value) {
+ getterLocals[keyExpression] = value;
+ getterLocals.$index = index;
+ return widthFn(scope, getterLocals);
+ };
+
+ var isVertical = !!scrollView.options.scrollingY;
+
+ // We say it's a grid view if we're either dynamic or not 100% width
+ var isGridView = isVertical ?
+ (widthData.dynamic || widthData.value !== scrollView.__clientWidth) :
+ (heightData.dynamic || heightData.value !== scrollView.__clientHeight);
+
+ var isStaticView = !heightData.dynamic && !widthData.dynamic;
+
+ var PRIMARY = 'PRIMARY';
+ var SECONDARY = 'SECONDARY';
+ var TRANSLATE_TEMPLATE_STR = isVertical ?
+ 'translate3d(SECONDARYpx,PRIMARYpx,0)' :
+ 'translate3d(PRIMARYpx,SECONDARYpx,0)';
+ var WIDTH_HEIGHT_TEMPLATE_STR = isVertical ?
+ 'height: PRIMARYpx; width: SECONDARYpx;' :
+ 'height: SECONDARYpx; width: PRIMARYpx;';
+
+ var estimatedHeight;
+ var estimatedWidth;
+
+ var repeaterBeforeSize = 0;
+ var repeaterAfterSize = 0;
+
+ var renderStartIndex = -1;
+ var renderEndIndex = -1;
+ var renderAfterBoundary = -1;
+ var renderBeforeBoundary = -1;
+
+ var itemsPool = [];
+ var itemsLeaving = [];
+ var itemsEntering = [];
+ var itemsShownMap = {};
+ var nextItemId = 0;
+
+ var scrollViewSetDimensions = isVertical ?
+ function() { scrollView.setDimensions(null, null, null, view.getContentSize(), true); } :
+ function() { scrollView.setDimensions(null, null, view.getContentSize(), null, true); };
+
+ // view is a mix of list/grid methods + static/dynamic methods.
+ // See bottom for implementations. Available methods:
+ //
+ // getEstimatedPrimaryPos(i), getEstimatedSecondaryPos(i), getEstimatedIndex(scrollTop),
+ // calculateDimensions(toIndex), getDimensions(index),
+ // updateRenderRange(scrollTop, scrollValueEnd), onRefreshLayout(), onRefreshData()
+ var view = isVertical ? new VerticalViewType() : new HorizontalViewType();
+ (isGridView ? GridViewType : ListViewType).call(view);
+ (isStaticView ? StaticViewType : DynamicViewType).call(view);
+
+ var contentSizeStr = isVertical ? 'getContentHeight' : 'getContentWidth';
+ var originalGetContentSize = scrollView.options[contentSizeStr];
+ scrollView.options[contentSizeStr] = angular.bind(view, view.getContentSize);
+
+ scrollView.__$callback = scrollView.__callback;
+ scrollView.__callback = function(transformLeft, transformTop, zoom, wasResize) {
+ var scrollValue = view.getScrollValue();
+ if (renderStartIndex === -1 ||
+ scrollValue + view.scrollPrimarySize > renderAfterBoundary ||
+ scrollValue < renderBeforeBoundary) {
+ render();
+ }
+ scrollView.__$callback(transformLeft, transformTop, zoom, wasResize);
+ };
+
+ var isLayoutReady = false;
+ var isDataReady = false;
+ this.refreshLayout = function() {
+ if (data.length) {
+ estimatedHeight = heightGetter(0, data[0]);
+ estimatedWidth = widthGetter(0, data[0]);
+ } else {
+ // If we don't have any data in our array, just guess.
+ estimatedHeight = 100;
+ estimatedWidth = 100;
+ }
+
+ // Get the size of every element AFTER the repeater. We have to get the margin before and
+ // after the first/last element to fix a browser bug with getComputedStyle() not counting
+ // the first/last child's margins into height.
+ var style = getComputedStyle(afterItemsNode) || {};
+ var firstStyle = afterItemsNode.firstElementChild && getComputedStyle(afterItemsNode.firstElementChild) || {};
+ var lastStyle = afterItemsNode.lastElementChild && getComputedStyle(afterItemsNode.lastElementChild) || {};
+ repeaterAfterSize = (parseInt(style[isVertical ? 'height' : 'width']) || 0) +
+ (firstStyle && parseInt(firstStyle[isVertical ? 'marginTop' : 'marginLeft']) || 0) +
+ (lastStyle && parseInt(lastStyle[isVertical ? 'marginBottom' : 'marginRight']) || 0);
+
+ // Get the offsetTop of the repeater.
+ repeaterBeforeSize = 0;
+ var current = containerNode;
+ do {
+ repeaterBeforeSize += current[isVertical ? 'offsetTop' : 'offsetLeft'];
+ } while ( ionic.DomUtil.contains(scrollView.__content, current = current.offsetParent) );
+
+ var containerPrevNode = containerNode.previousElementSibling;
+ var beforeStyle = containerPrevNode ? $window.getComputedStyle(containerPrevNode) : {};
+ var beforeMargin = parseInt(beforeStyle[isVertical ? 'marginBottom' : 'marginRight'] || 0);
+
+ // Because we position the collection container with position: relative, it doesn't take
+ // into account where to position itself relative to the previous element's marginBottom.
+ // To compensate, we translate the container up by the previous element's margin.
+ containerNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
+ .replace(PRIMARY, -beforeMargin)
+ .replace(SECONDARY, 0);
+ repeaterBeforeSize -= beforeMargin;
+
+ if (!scrollView.__clientHeight || !scrollView.__clientWidth) {
+ scrollView.__clientWidth = scrollView.__container.clientWidth;
+ scrollView.__clientHeight = scrollView.__container.clientHeight;
+ }
+
+ (view.onRefreshLayout || angular.noop)();
+ view.refreshDirection();
+ scrollViewSetDimensions();
+
+ // Create the pool of items for reuse, setting the size to (estimatedItemsOnScreen) * 2,
+ // plus the size of the renderBuffer.
+ if (!isLayoutReady) {
+ var poolSize = Math.max(20, renderBuffer * 3);
+ for (var i = 0; i < poolSize; i++) {
+ itemsPool.push(new RepeatItem());
+ }
+ }
+
+ isLayoutReady = true;
+ if (isLayoutReady && isDataReady) {
+ // If the resize or latest data change caused the scrollValue to
+ // now be out of bounds, resize the scrollView.
+ if (scrollView.__scrollLeft > scrollView.__maxScrollLeft ||
+ scrollView.__scrollTop > scrollView.__maxScrollTop) {
+ scrollView.resize();
+ }
+ forceRerender(true);
+ }
+ };
+
+ this.setData = function(newData) {
+ data = newData;
+ (view.onRefreshData || angular.noop)();
+ isDataReady = true;
+ };
+
+ this.destroy = function() {
+ render.destroyed = true;
+
+ itemsPool.forEach(function(item) {
+ item.scope.$destroy();
+ item.scope = item.element = item.node = item.images = null;
+ });
+ itemsPool.length = itemsEntering.length = itemsLeaving.length = 0;
+ itemsShownMap = {};
+
+ //Restore the scrollView's normal behavior and resize it to normal size.
+ scrollView.options[contentSizeStr] = originalGetContentSize;
+ scrollView.__callback = scrollView.__$callback;
+ scrollView.resize();
+
+ (view.onDestroy || angular.noop)();
+ };
+
+ function forceRerender() {
+ return render(true);
+ }
+ function render(forceRerender) {
+ if (render.destroyed) return;
+ var i;
+ var ii;
+ var item;
+ var dim;
+ var scope;
+ var scrollValue = view.getScrollValue();
+ var scrollValueEnd = scrollValue + view.scrollPrimarySize;
+
+ view.updateRenderRange(scrollValue, scrollValueEnd);
+
+ renderStartIndex = Math.max(0, renderStartIndex - renderBuffer);
+ renderEndIndex = Math.min(data.length - 1, renderEndIndex + renderBuffer);
+
+ for (i in itemsShownMap) {
+ if (i < renderStartIndex || i > renderEndIndex) {
+ item = itemsShownMap[i];
+ delete itemsShownMap[i];
+ itemsLeaving.push(item);
+ item.isShown = false;
+ }
+ }
+
+ // Render indicies that aren't shown yet
+ //
+ // NOTE(ajoslin): this may sound crazy, but calling any other functions during this render
+ // loop will often push the render time over the edge from less than one frame to over
+ // one frame, causing visible jank.
+ // DON'T call any other functions inside this loop unless it's vital.
+ for (i = renderStartIndex; i <= renderEndIndex; i++) {
+ // We only go forward with render if the index is in data, the item isn't already shown,
+ // or forceRerender is on.
+ if (i >= data.length || (itemsShownMap[i] && !forceRerender)) continue;
+
+ item = itemsShownMap[i] || (itemsShownMap[i] = itemsLeaving.length ? itemsLeaving.pop() :
+ itemsPool.length ? itemsPool.shift() :
+ new RepeatItem());
+ itemsEntering.push(item);
+ item.isShown = true;
+
+ scope = item.scope;
+ scope.$index = i;
+ scope[keyExpression] = data[i];
+ scope.$first = (i === 0);
+ scope.$last = (i === (data.length - 1));
+ scope.$middle = !(scope.$first || scope.$last);
+ scope.$odd = !(scope.$even = (i & 1) === 0);
+
+ if (scope.$$disconnected) ionic.Utils.reconnectScope(item.scope);
+
+ dim = view.getDimensions(i);
+ if (item.secondaryPos !== dim.secondaryPos || item.primaryPos !== dim.primaryPos) {
+ item.node.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
+ .replace(PRIMARY, (item.primaryPos = dim.primaryPos))
+ .replace(SECONDARY, (item.secondaryPos = dim.secondaryPos));
+ }
+ if (item.secondarySize !== dim.secondarySize || item.primarySize !== dim.primarySize) {
+ item.node.style.cssText = item.node.style.cssText
+ .replace(WIDTH_HEIGHT_REGEX, WIDTH_HEIGHT_TEMPLATE_STR
+ //TODO fix item.primarySize + 1 hack
+ .replace(PRIMARY, (item.primarySize = dim.primarySize) + 1)
+ .replace(SECONDARY, (item.secondarySize = dim.secondarySize))
+ );
+ }
+
+ }
+
+ // If we reach the end of the list, render the afterItemsNode - this contains all the
+ // elements the developer placed after the collection-repeat
+ if (renderEndIndex === data.length - 1) {
+ dim = view.getDimensions(data.length - 1) || EMPTY_DIMENSION;
+ afterItemsNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
+ .replace(PRIMARY, dim.primaryPos + dim.primarySize)
+ .replace(SECONDARY, 0);
+ }
+
+ while (itemsLeaving.length) {
+ item = itemsLeaving.pop();
+ item.scope.$broadcast('$collectionRepeatLeave');
+ ionic.Utils.disconnectScope(item.scope);
+ itemsPool.push(item);
+ item.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
+ item.primaryPos = item.secondaryPos = null;
+ }
+
+ if (forceRefreshImages) {
+ for (i = 0, ii = itemsEntering.length; i < ii && (item = itemsEntering[i]); i++) {
+ if (!item.images) continue;
+ for (var j = 0, jj = item.images.length, img; j < jj && (img = item.images[j]); j++) {
+ var src = img.src;
+ img.src = ONE_PX_TRANSPARENT_IMG_SRC;
+ img.src = src;
+ }
+ }
+ }
+ if (forceRerender) {
+ var rootScopePhase = $rootScope.$$phase;
+ while (itemsEntering.length) {
+ item = itemsEntering.pop();
+ if (!rootScopePhase) item.scope.$digest();
+ }
+ } else {
+ digestEnteringItems();
+ }
+ }
+
+ function digestEnteringItems() {
+ var item;
+ if (digestEnteringItems.running) return;
+ digestEnteringItems.running = true;
+
+ $$rAF(function process() {
+ var rootScopePhase = $rootScope.$$phase;
+ while (itemsEntering.length) {
+ item = itemsEntering.pop();
+ if (item.isShown) {
+ if (!rootScopePhase) item.scope.$digest();
+ }
+ }
+ digestEnteringItems.running = false;
+ });
+ }
+
+ function RepeatItem() {
+ var self = this;
+ this.scope = scope.$new();
+ this.id = 'item' + (nextItemId++);
+ transclude(this.scope, function(clone) {
+ self.element = clone;
+ self.element.data('$$collectionRepeatItem', self);
+ // TODO destroy
+ self.node = clone[0];
+ // Batch style setting to lower repaints
+ self.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
+ self.node.style.cssText += ' height: 0px; width: 0px;';
+ ionic.Utils.disconnectScope(self.scope);
+ containerNode.appendChild(self.node);
+ self.images = clone[0].getElementsByTagName('img');
+ });
+ }
+
+ function VerticalViewType() {
+ this.getItemPrimarySize = heightGetter;
+ this.getItemSecondarySize = widthGetter;
+
+ this.getScrollValue = function() {
+ return Math.max(0, Math.min(scrollView.__scrollTop - repeaterBeforeSize,
+ scrollView.__maxScrollTop - repeaterBeforeSize - repeaterAfterSize));
+ };
+
+ this.refreshDirection = function() {
+ this.scrollPrimarySize = scrollView.__clientHeight;
+ this.scrollSecondarySize = scrollView.__clientWidth;
+
+ this.estimatedPrimarySize = estimatedHeight;
+ this.estimatedSecondarySize = estimatedWidth;
+ this.estimatedItemsAcross = isGridView &&
+ Math.floor(scrollView.__clientWidth / estimatedWidth) ||
+ 1;
+ };
+ }
+ function HorizontalViewType() {
+ this.getItemPrimarySize = widthGetter;
+ this.getItemSecondarySize = heightGetter;
+
+ this.getScrollValue = function() {
+ return Math.max(0, Math.min(scrollView.__scrollLeft - repeaterBeforeSize,
+ scrollView.__maxScrollLeft - repeaterBeforeSize - repeaterAfterSize));
+ };
+
+ this.refreshDirection = function() {
+ this.scrollPrimarySize = scrollView.__clientWidth;
+ this.scrollSecondarySize = scrollView.__clientHeight;
+
+ this.estimatedPrimarySize = estimatedWidth;
+ this.estimatedSecondarySize = estimatedHeight;
+ this.estimatedItemsAcross = isGridView &&
+ Math.floor(scrollView.__clientHeight / estimatedHeight) ||
+ 1;
+ };
+ }
+
+ function GridViewType() {
+ this.getEstimatedSecondaryPos = function(index) {
+ return (index % this.estimatedItemsAcross) * this.estimatedSecondarySize;
+ };
+ this.getEstimatedPrimaryPos = function(index) {
+ return Math.floor(index / this.estimatedItemsAcross) * this.estimatedPrimarySize;
+ };
+ this.getEstimatedIndex = function(scrollValue) {
+ return Math.floor(scrollValue / this.estimatedPrimarySize) *
+ this.estimatedItemsAcross;
+ };
+ }
+
+ function ListViewType() {
+ this.getEstimatedSecondaryPos = function() {
+ return 0;
+ };
+ this.getEstimatedPrimaryPos = function(index) {
+ return index * this.estimatedPrimarySize;
+ };
+ this.getEstimatedIndex = function(scrollValue) {
+ return Math.floor((scrollValue) / this.estimatedPrimarySize);
+ };
+ }
+
+ function StaticViewType() {
+ this.getContentSize = function() {
+ return this.getEstimatedPrimaryPos(data.length - 1) + this.estimatedPrimarySize +
+ repeaterBeforeSize + repeaterAfterSize;
+ };
+ // static view always returns the same object for getDimensions, to avoid memory allocation
+ // while scrolling. This could be dangerous if this was a public function, but it's not.
+ // Only we use it.
+ var dim = {};
+ this.getDimensions = function(index) {
+ dim.primaryPos = this.getEstimatedPrimaryPos(index);
+ dim.secondaryPos = this.getEstimatedSecondaryPos(index);
+ dim.primarySize = this.estimatedPrimarySize;
+ dim.secondarySize = this.estimatedSecondarySize;
+ return dim;
+ };
+ this.updateRenderRange = function(scrollValue, scrollValueEnd) {
+ renderStartIndex = Math.max(0, this.getEstimatedIndex(scrollValue));
+
+ // Make sure the renderEndIndex takes into account all the items on the row
+ renderEndIndex = Math.min(data.length - 1,
+ this.getEstimatedIndex(scrollValueEnd) + this.estimatedItemsAcross - 1);
+
+ renderBeforeBoundary = Math.max(0,
+ this.getEstimatedPrimaryPos(renderStartIndex));
+ renderAfterBoundary = this.getEstimatedPrimaryPos(renderEndIndex) +
+ this.estimatedPrimarySize;
+ };
+ }
+
+ function DynamicViewType() {
+ var self = this;
+ var debouncedScrollViewSetDimensions = ionic.debounce(scrollViewSetDimensions, 25, true);
+ var calculateDimensions = isGridView ? calculateDimensionsGrid : calculateDimensionsList;
+ var dimensionsIndex;
+ var dimensions = [];
+
+
+ // Get the dimensions at index. {width, height, left, top}.
+ // We start with no dimensions calculated, then any time dimensions are asked for at an
+ // index we calculate dimensions up to there.
+ function calculateDimensionsList(toIndex) {
+ var i, prevDimension, dim;
+ for (i = Math.max(0, dimensionsIndex); i <= toIndex && (dim = dimensions[i]); i++) {
+ prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
+ dim.primarySize = self.getItemPrimarySize(i, data[i]);
+ dim.secondarySize = self.scrollSecondarySize;
+ dim.primaryPos = prevDimension.primaryPos + prevDimension.primarySize;
+ dim.secondaryPos = 0;
+ }
+ }
+ function calculateDimensionsGrid(toIndex) {
+ var i, prevDimension, dim;
+ for (i = Math.max(dimensionsIndex, 0); i <= toIndex && (dim = dimensions[i]); i++) {
+ prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
+ dim.secondarySize = Math.min(
+ self.getItemSecondarySize(i, data[i]),
+ self.scrollSecondarySize
+ );
+ dim.secondaryPos = prevDimension.secondaryPos + prevDimension.secondarySize;
+
+ if (i === 0 || dim.secondaryPos + dim.secondarySize > self.scrollSecondarySize) {
+ dim.secondaryPos = 0;
+ dim.primarySize = self.getItemPrimarySize(i, data[i]);
+ dim.primaryPos = prevDimension.primaryPos + prevDimension.rowPrimarySize;
+
+ dim.rowStartIndex = i;
+ dim.rowPrimarySize = dim.primarySize;
+ } else {
+ dim.primarySize = self.getItemPrimarySize(i, data[i]);
+ dim.primaryPos = prevDimension.primaryPos;
+ dim.rowStartIndex = prevDimension.rowStartIndex;
+
+ dimensions[dim.rowStartIndex].rowPrimarySize = dim.rowPrimarySize = Math.max(
+ dimensions[dim.rowStartIndex].rowPrimarySize,
+ dim.primarySize
+ );
+ dim.rowPrimarySize = Math.max(dim.primarySize, dim.rowPrimarySize);
+ }
+ }
+ }
+
+ this.getContentSize = function() {
+ var dim = dimensions[dimensionsIndex] || EMPTY_DIMENSION;
+ return ((dim.primaryPos + dim.primarySize) || 0) +
+ this.getEstimatedPrimaryPos(data.length - dimensionsIndex - 1) +
+ repeaterBeforeSize + repeaterAfterSize;
+ };
+ this.onDestroy = function() {
+ dimensions.length = 0;
+ };
+
+ this.onRefreshData = function() {
+ var i;
+ var ii;
+ // Make sure dimensions has as many items as data.length.
+ // This is to be sure we don't have to allocate objects while scrolling.
+ for (i = dimensions.length, ii = data.length; i < ii; i++) {
+ dimensions.push({});
+ }
+ dimensionsIndex = -1;
+ };
+ this.onRefreshLayout = function() {
+ dimensionsIndex = -1;
+ };
+ this.getDimensions = function(index) {
+ index = Math.min(index, data.length - 1);
+
+ if (dimensionsIndex < index) {
+ // Once we start asking for dimensions near the end of the list, go ahead and calculate
+ // everything. This is to make sure when the user gets to the end of the list, the
+ // scroll height of the list is 100% accurate (not estimated anymore).
+ if (index > data.length * 0.9) {
+ calculateDimensions(data.length - 1);
+ dimensionsIndex = data.length - 1;
+ scrollViewSetDimensions();
+ } else {
+ calculateDimensions(index);
+ dimensionsIndex = index;
+ debouncedScrollViewSetDimensions();
+ }
+
+ }
+ return dimensions[index];
+ };
+
+ var oldRenderStartIndex = -1;
+ var oldScrollValue = -1;
+ this.updateRenderRange = function(scrollValue, scrollValueEnd) {
+ var i;
+ var len;
+ var dim;
+
+ // Calculate more dimensions than we estimate we'll need, to be sure.
+ this.getDimensions( this.getEstimatedIndex(scrollValueEnd) * 2 );
+
+ // -- Calculate renderStartIndex
+ // base case: start at 0
+ if (oldRenderStartIndex === -1 || scrollValue === 0) {
+ i = 0;
+ // scrolling down
+ } else if (scrollValue >= oldScrollValue) {
+ for (i = oldRenderStartIndex, len = data.length; i < len; i++) {
+ if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize >= scrollValue) {
+ break;
+ }
+ }
+ // scrolling up
+ } else {
+ for (i = oldRenderStartIndex; i >= 0; i--) {
+ if ((dim = this.getDimensions(i)) && dim.primaryPos <= scrollValue) {
+ // when grid view, make sure the render starts at the beginning of a row.
+ i = isGridView ? dim.rowStartIndex : i;
+ break;
+ }
+ }
+ }
+
+ renderStartIndex = Math.min(Math.max(0, i), data.length - 1);
+ renderBeforeBoundary = renderStartIndex !== -1 ? this.getDimensions(renderStartIndex).primaryPos : -1;
+
+ // -- Calculate renderEndIndex
+ var lastRowDim;
+ for (i = renderStartIndex + 1, len = data.length; i < len; i++) {
+ if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize > scrollValueEnd) {
+
+ // Go all the way to the end of the row if we're in a grid
+ if (isGridView) {
+ lastRowDim = dim;
+ while (i < len - 1 &&
+ (dim = this.getDimensions(i + 1)).primaryPos === lastRowDim.primaryPos) {
+ i++;
+ }
+ }
+ break;
+ }
+ }
+
+ renderEndIndex = Math.min(i, data.length - 1);
+ renderAfterBoundary = renderEndIndex !== -1 ?
+ ((dim = this.getDimensions(renderEndIndex)).primaryPos + (dim.rowPrimarySize || dim.primarySize)) :
+ -1;
+
+ oldScrollValue = scrollValue;
+ oldRenderStartIndex = renderStartIndex;
+ };
+ }
+
+
+ };
+
+}
+
+/**
+ * @ngdoc directive
+ * @name ionContent
+ * @module ionic
+ * @delegate ionic.service:$ionicScrollDelegate
+ * @restrict E
+ *
+ * @description
+ * The ionContent directive provides an easy to use content area that can be configured
+ * to use Ionic's custom Scroll View, or the built in overflow scrolling of the browser.
+ *
+ * While we recommend using the custom Scroll features in Ionic in most cases, sometimes
+ * (for performance reasons) only the browser's native overflow scrolling will suffice,
+ * and so we've made it easy to toggle between the Ionic scroll implementation and
+ * overflow scrolling.
+ *
+ * You can implement pull-to-refresh with the {@link ionic.directive:ionRefresher}
+ * directive, and infinite scrolling with the {@link ionic.directive:ionInfiniteScroll}
+ * directive.
+ *
+ * If there is any dynamic content inside the ion-content, be sure to call `.resize()` with {@link ionic.service:$ionicScrollDelegate}
+ * after the content has been added.
+ *
+ * Be aware that this directive gets its own child scope. If you do not understand why this
+ * is important, you can read [https://docs.angularjs.org/guide/scope](https://docs.angularjs.org/guide/scope).
+ *
+ * @param {string=} delegate-handle The handle used to identify this scrollView
+ * with {@link ionic.service:$ionicScrollDelegate}.
+ * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'.
+ * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true.
+ * @param {boolean=} padding Whether to add padding to the content.
+ * Defaults to true on iOS, false on Android.
+ * @param {boolean=} scroll Whether to allow scrolling of content. Defaults to true.
+ * @param {boolean=} overflow-scroll Whether to use overflow-scrolling instead of
+ * Ionic scroll. See {@link ionic.provider:$ionicConfigProvider} to set this as the global default.
+ * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true.
+ * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true.
+ * @param {string=} start-x Initial horizontal scroll position. Default 0.
+ * @param {string=} start-y Initial vertical scroll position. Default 0.
+ * @param {expression=} on-scroll Expression to evaluate when the content is scrolled.
+ * @param {expression=} on-scroll-complete Expression to evaluate when a scroll action completes. Has access to 'scrollLeft' and 'scrollTop' locals.
+ * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges
+ * of the content. Defaults to true on iOS, false on Android.
+ * @param {number=} scroll-event-interval Number of milliseconds between each firing of the 'on-scroll' expression. Default 10.
+ */
+IonicModule
+.directive('ionContent', [
+ '$timeout',
+ '$controller',
+ '$ionicBind',
+ '$ionicConfig',
+function($timeout, $controller, $ionicBind, $ionicConfig) {
+ return {
+ restrict: 'E',
+ require: '^?ionNavView',
+ scope: true,
+ priority: 800,
+ compile: function(element, attr) {
+ var innerElement;
+ var scrollCtrl;
+
+ element.addClass('scroll-content ionic-scroll');
+
+ if (attr.scroll != 'false') {
+ //We cannot use normal transclude here because it breaks element.data()
+ //inheritance on compile
+ innerElement = jqLite('<div class="scroll"></div>');
+ innerElement.append(element.contents());
+ element.append(innerElement);
+ } else {
+ element.addClass('scroll-content-false');
+ }
+
+ var nativeScrolling = attr.overflowScroll !== "false" && (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling());
+
+ // collection-repeat requires JS scrolling
+ if (nativeScrolling) {
+ nativeScrolling = !element[0].querySelector('[collection-repeat]');
+ }
+ return { pre: prelink };
+ function prelink($scope, $element, $attr) {
+ var parentScope = $scope.$parent;
+ $scope.$watch(function() {
+ return (parentScope.$hasHeader ? ' has-header' : '') +
+ (parentScope.$hasSubheader ? ' has-subheader' : '') +
+ (parentScope.$hasFooter ? ' has-footer' : '') +
+ (parentScope.$hasSubfooter ? ' has-subfooter' : '') +
+ (parentScope.$hasTabs ? ' has-tabs' : '') +
+ (parentScope.$hasTabsTop ? ' has-tabs-top' : '');
+ }, function(className, oldClassName) {
+ $element.removeClass(oldClassName);
+ $element.addClass(className);
+ });
+
+ //Only this ionContent should use these variables from parent scopes
+ $scope.$hasHeader = $scope.$hasSubheader =
+ $scope.$hasFooter = $scope.$hasSubfooter =
+ $scope.$hasTabs = $scope.$hasTabsTop =
+ false;
+ $ionicBind($scope, $attr, {
+ $onScroll: '&onScroll',
+ $onScrollComplete: '&onScrollComplete',
+ hasBouncing: '@',
+ padding: '@',
+ direction: '@',
+ scrollbarX: '@',
+ scrollbarY: '@',
+ startX: '@',
+ startY: '@',
+ scrollEventInterval: '@'
+ });
+ $scope.direction = $scope.direction || 'y';
+
+ if (isDefined($attr.padding)) {
+ $scope.$watch($attr.padding, function(newVal) {
+ (innerElement || $element).toggleClass('padding', !!newVal);
+ });
+ }
+
+ if ($attr.scroll === "false") {
+ //do nothing
+ } else {
+ var scrollViewOptions = {};
+
+ // determined in compile phase above
+ if (nativeScrolling) {
+ // use native scrolling
+ $element.addClass('overflow-scroll');
+
+ scrollViewOptions = {
+ el: $element[0],
+ delegateHandle: attr.delegateHandle,
+ startX: $scope.$eval($scope.startX) || 0,
+ startY: $scope.$eval($scope.startY) || 0,
+ nativeScrolling: true
+ };
+
+ } else {
+ // Use JS scrolling
+ scrollViewOptions = {
+ el: $element[0],
+ delegateHandle: attr.delegateHandle,
+ locking: (attr.locking || 'true') === 'true',
+ bouncing: $scope.$eval($scope.hasBouncing),
+ startX: $scope.$eval($scope.startX) || 0,
+ startY: $scope.$eval($scope.startY) || 0,
+ scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
+ scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
+ scrollingX: $scope.direction.indexOf('x') >= 0,
+ scrollingY: $scope.direction.indexOf('y') >= 0,
+ scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 10,
+ scrollingComplete: onScrollComplete
+ };
+ }
+
+ // init scroll controller with appropriate options
+ scrollCtrl = $controller('$ionicScroll', {
+ $scope: $scope,
+ scrollViewOptions: scrollViewOptions
+ });
+
+ $scope.scrollCtrl = scrollCtrl;
+
+ $scope.$on('$destroy', function() {
+ if (scrollViewOptions) {
+ scrollViewOptions.scrollingComplete = noop;
+ delete scrollViewOptions.el;
+ }
+ innerElement = null;
+ $element = null;
+ attr.$$element = null;
+ });
+ }
+
+ function onScrollComplete() {
+ $scope.$onScrollComplete({
+ scrollTop: scrollCtrl.scrollView.__scrollTop,
+ scrollLeft: scrollCtrl.scrollView.__scrollLeft
+ });
+ }
+
+ }
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name exposeAsideWhen
+ * @module ionic
+ * @restrict A
+ * @parent ionic.directive:ionSideMenus
+ *
+ * @description
+ * It is common for a tablet application to hide a menu when in portrait mode, but to show the
+ * same menu on the left side when the tablet is in landscape mode. The `exposeAsideWhen` attribute
+ * directive can be used to accomplish a similar interface.
+ *
+ * By default, side menus are hidden underneath its side menu content, and can be opened by either
+ * swiping the content left or right, or toggling a button to show the side menu. However, by adding the
+ * `exposeAsideWhen` attribute directive to an {@link ionic.directive:ionSideMenu} element directive,
+ * a side menu can be given instructions on "when" the menu should be exposed (always viewable). For
+ * example, the `expose-aside-when="large"` attribute will keep the side menu hidden when the viewport's
+ * width is less than `768px`, but when the viewport's width is `768px` or greater, the menu will then
+ * always be shown and can no longer be opened or closed like it could when it was hidden for smaller
+ * viewports.
+ *
+ * Using `large` as the attribute's value is a shortcut value to `(min-width:768px)` since it is
+ * the most common use-case. However, for added flexibility, any valid media query could be added
+ * as the value, such as `(min-width:600px)` or even multiple queries such as
+ * `(min-width:750px) and (max-width:1200px)`.
+ * @usage
+ * ```html
+ * <ion-side-menus>
+ * <!-- Center content -->
+ * <ion-side-menu-content>
+ * </ion-side-menu-content>
+ *
+ * <!-- Left menu -->
+ * <ion-side-menu expose-aside-when="large">
+ * </ion-side-menu>
+ * </ion-side-menus>
+ * ```
+ * For a complete side menu example, see the
+ * {@link ionic.directive:ionSideMenus} documentation.
+ */
+
+IonicModule.directive('exposeAsideWhen', ['$window', function($window) {
+ return {
+ restrict: 'A',
+ require: '^ionSideMenus',
+ link: function($scope, $element, $attr, sideMenuCtrl) {
+
+ var prevInnerWidth = $window.innerWidth;
+ var prevInnerHeight = $window.innerHeight;
+
+ ionic.on('resize', function() {
+ if (prevInnerWidth === $window.innerWidth && prevInnerHeight === $window.innerHeight) {
+ return;
+ }
+ prevInnerWidth = $window.innerWidth;
+ prevInnerHeight = $window.innerHeight;
+ onResize();
+ }, $window);
+
+ function checkAsideExpose() {
+ var mq = $attr.exposeAsideWhen == 'large' ? '(min-width:768px)' : $attr.exposeAsideWhen;
+ sideMenuCtrl.exposeAside($window.matchMedia(mq).matches);
+ sideMenuCtrl.activeAsideResizing(false);
+ }
+
+ function onResize() {
+ sideMenuCtrl.activeAsideResizing(true);
+ debouncedCheck();
+ }
+
+ var debouncedCheck = ionic.debounce(function() {
+ $scope.$apply(checkAsideExpose);
+ }, 300, false);
+
+ $scope.$evalAsync(checkAsideExpose);
+ }
+ };
+}]);
+
+var GESTURE_DIRECTIVES = 'onHold onTap onDoubleTap onTouch onRelease onDragStart onDrag onDragEnd onDragUp onDragRight onDragDown onDragLeft onSwipe onSwipeUp onSwipeRight onSwipeDown onSwipeLeft'.split(' ');
+
+GESTURE_DIRECTIVES.forEach(function(name) {
+ IonicModule.directive(name, gestureDirective(name));
+});
+
+
+/**
+ * @ngdoc directive
+ * @name onHold
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Touch stays at the same location for 500ms. Similar to long touch events available for AngularJS and jQuery.
+ *
+ * @usage
+ * ```html
+ * <button on-hold="onHold()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onTap
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Quick touch at a location. If the duration of the touch goes
+ * longer than 250ms it is no longer a tap gesture.
+ *
+ * @usage
+ * ```html
+ * <button on-tap="onTap()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDoubleTap
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Double tap touch at a location.
+ *
+ * @usage
+ * ```html
+ * <button on-double-tap="onDoubleTap()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onTouch
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called immediately when the user first begins a touch. This
+ * gesture does not wait for a touchend/mouseup.
+ *
+ * @usage
+ * ```html
+ * <button on-touch="onTouch()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onRelease
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the user ends a touch.
+ *
+ * @usage
+ * ```html
+ * <button on-release="onRelease()" class="button">Test</button>
+ * ```
+ */
+
+/**
+ * @ngdoc directive
+ * @name onDragStart
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a drag gesture has started.
+ *
+ * @usage
+ * ```html
+ * <button on-drag-start="onDragStart()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDrag
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Move with one touch around on the page. Blocking the scrolling when
+ * moving left and right is a good practice. When all the drag events are
+ * blocking you disable scrolling on that area.
+ *
+ * @usage
+ * ```html
+ * <button on-drag="onDrag()" class="button">Test</button>
+ * ```
+ */
+
+/**
+ * @ngdoc directive
+ * @name onDragEnd
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a drag gesture has ended.
+ *
+ * @usage
+ * ```html
+ * <button on-drag-end="onDragEnd()" class="button">Test</button>
+ * ```
+ */
+
+/**
+ * @ngdoc directive
+ * @name onDragUp
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the element is dragged up.
+ *
+ * @usage
+ * ```html
+ * <button on-drag-up="onDragUp()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDragRight
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the element is dragged to the right.
+ *
+ * @usage
+ * ```html
+ * <button on-drag-right="onDragRight()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDragDown
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the element is dragged down.
+ *
+ * @usage
+ * ```html
+ * <button on-drag-down="onDragDown()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onDragLeft
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when the element is dragged to the left.
+ *
+ * @usage
+ * ```html
+ * <button on-drag-left="onDragLeft()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipe
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity in any direction.
+ *
+ * @usage
+ * ```html
+ * <button on-swipe="onSwipe()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipeUp
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity moving up.
+ *
+ * @usage
+ * ```html
+ * <button on-swipe-up="onSwipeUp()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipeRight
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity moving to the right.
+ *
+ * @usage
+ * ```html
+ * <button on-swipe-right="onSwipeRight()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipeDown
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity moving down.
+ *
+ * @usage
+ * ```html
+ * <button on-swipe-down="onSwipeDown()" class="button">Test</button>
+ * ```
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name onSwipeLeft
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * Called when a moving touch has a high velocity moving to the left.
+ *
+ * @usage
+ * ```html
+ * <button on-swipe-left="onSwipeLeft()" class="button">Test</button>
+ * ```
+ */
+
+
+function gestureDirective(directiveName) {
+ return ['$ionicGesture', '$parse', function($ionicGesture, $parse) {
+ var eventType = directiveName.substr(2).toLowerCase();
+
+ return function(scope, element, attr) {
+ var fn = $parse( attr[directiveName] );
+
+ var listener = function(ev) {
+ scope.$apply(function() {
+ fn(scope, {
+ $event: ev
+ });
+ });
+ };
+
+ var gesture = $ionicGesture.on(eventType, listener, element);
+
+ scope.$on('$destroy', function() {
+ $ionicGesture.off(gesture, eventType, listener);
+ });
+ };
+ }];
+}
+
+
+IonicModule
+//.directive('ionHeaderBar', tapScrollToTopDirective())
+
+/**
+ * @ngdoc directive
+ * @name ionHeaderBar
+ * @module ionic
+ * @restrict E
+ *
+ * @description
+ * Adds a fixed header bar above some content.
+ *
+ * Can also be a subheader (lower down) if the 'bar-subheader' class is applied.
+ * See [the header CSS docs](/docs/components/#subheader).
+ *
+ * @param {string=} align-title How to align the title. By default the title
+ * will be aligned the same as how the platform aligns its titles (iOS centers
+ * titles, Android aligns them left).
+ * Available: 'left', 'right', or 'center'. Defaults to the same as the platform.
+ * @param {boolean=} no-tap-scroll By default, the header bar will scroll the
+ * content to the top when tapped. Set no-tap-scroll to true to disable this
+ * behavior.
+ * Available: true or false. Defaults to false.
+ *
+ * @usage
+ * ```html
+ * <ion-header-bar align-title="left" class="bar-positive">
+ * <div class="buttons">
+ * <button class="button" ng-click="doSomething()">Left Button</button>
+ * </div>
+ * <h1 class="title">Title!</h1>
+ * <div class="buttons">
+ * <button class="button">Right Button</button>
+ * </div>
+ * </ion-header-bar>
+ * <ion-content class="has-header">
+ * Some content!
+ * </ion-content>
+ * ```
+ */
+.directive('ionHeaderBar', headerFooterBarDirective(true))
+
+/**
+ * @ngdoc directive
+ * @name ionFooterBar
+ * @module ionic
+ * @restrict E
+ *
+ * @description
+ * Adds a fixed footer bar below some content.
+ *
+ * Can also be a subfooter (higher up) if the 'bar-subfooter' class is applied.
+ * See [the footer CSS docs](/docs/components/#footer).
+ *
+ * Note: If you use ionFooterBar in combination with ng-if, the surrounding content
+ * will not align correctly. This will be fixed soon.
+ *
+ * @param {string=} align-title Where to align the title.
+ * Available: 'left', 'right', or 'center'. Defaults to 'center'.
+ *
+ * @usage
+ * ```html
+ * <ion-content class="has-footer">
+ * Some content!
+ * </ion-content>
+ * <ion-footer-bar align-title="left" class="bar-assertive">
+ * <div class="buttons">
+ * <button class="button">Left Button</button>
+ * </div>
+ * <h1 class="title">Title!</h1>
+ * <div class="buttons" ng-click="doSomething()">
+ * <button class="button">Right Button</button>
+ * </div>
+ * </ion-footer-bar>
+ * ```
+ */
+.directive('ionFooterBar', headerFooterBarDirective(false));
+
+function tapScrollToTopDirective() { //eslint-disable-line no-unused-vars
+ return ['$ionicScrollDelegate', function($ionicScrollDelegate) {
+ return {
+ restrict: 'E',
+ link: function($scope, $element, $attr) {
+ if ($attr.noTapScroll == 'true') {
+ return;
+ }
+ ionic.on('tap', onTap, $element[0]);
+ $scope.$on('$destroy', function() {
+ ionic.off('tap', onTap, $element[0]);
+ });
+
+ function onTap(e) {
+ var depth = 3;
+ var current = e.target;
+ //Don't scroll to top in certain cases
+ while (depth-- && current) {
+ if (current.classList.contains('button') ||
+ current.tagName.match(/input|textarea|select/i) ||
+ current.isContentEditable) {
+ return;
+ }
+ current = current.parentNode;
+ }
+ var touch = e.gesture && e.gesture.touches[0] || e.detail.touches[0];
+ var bounds = $element[0].getBoundingClientRect();
+ if (ionic.DomUtil.rectContains(
+ touch.pageX, touch.pageY,
+ bounds.left, bounds.top - 20,
+ bounds.left + bounds.width, bounds.top + bounds.height
+ )) {
+ $ionicScrollDelegate.scrollTop(true);
+ }
+ }
+ }
+ };
+ }];
+}
+
+function headerFooterBarDirective(isHeader) {
+ return ['$document', '$timeout', function($document, $timeout) {
+ return {
+ restrict: 'E',
+ controller: '$ionicHeaderBar',
+ compile: function(tElement) {
+ tElement.addClass(isHeader ? 'bar bar-header' : 'bar bar-footer');
+ // top style tabs? if so, remove bottom border for seamless display
+ $timeout(function() {
+ if (isHeader && $document[0].getElementsByClassName('tabs-top').length) tElement.addClass('has-tabs-top');
+ });
+
+ return { pre: prelink };
+ function prelink($scope, $element, $attr, ctrl) {
+ if (isHeader) {
+ $scope.$watch(function() { return $element[0].className; }, function(value) {
+ var isShown = value.indexOf('ng-hide') === -1;
+ var isSubheader = value.indexOf('bar-subheader') !== -1;
+ $scope.$hasHeader = isShown && !isSubheader;
+ $scope.$hasSubheader = isShown && isSubheader;
+ $scope.$emit('$ionicSubheader', $scope.$hasSubheader);
+ });
+ $scope.$on('$destroy', function() {
+ delete $scope.$hasHeader;
+ delete $scope.$hasSubheader;
+ });
+ ctrl.align();
+ $scope.$on('$ionicHeader.align', function() {
+ ionic.requestAnimationFrame(function() {
+ ctrl.align();
+ });
+ });
+
+ } else {
+ $scope.$watch(function() { return $element[0].className; }, function(value) {
+ var isShown = value.indexOf('ng-hide') === -1;
+ var isSubfooter = value.indexOf('bar-subfooter') !== -1;
+ $scope.$hasFooter = isShown && !isSubfooter;
+ $scope.$hasSubfooter = isShown && isSubfooter;
+ });
+ $scope.$on('$destroy', function() {
+ delete $scope.$hasFooter;
+ delete $scope.$hasSubfooter;
+ });
+ $scope.$watch('$hasTabs', function(val) {
+ $element.toggleClass('has-tabs', !!val);
+ });
+ ctrl.align();
+ $scope.$on('$ionicFooter.align', function() {
+ ionic.requestAnimationFrame(function() {
+ ctrl.align();
+ });
+ });
+ }
+ }
+ }
+ };
+ }];
+}
+
+/**
+ * @ngdoc directive
+ * @name ionInfiniteScroll
+ * @module ionic
+ * @parent ionic.directive:ionContent, ionic.directive:ionScroll
+ * @restrict E
+ *
+ * @description
+ * The ionInfiniteScroll directive allows you to call a function whenever
+ * the user gets to the bottom of the page or near the bottom of the page.
+ *
+ * The expression you pass in for `on-infinite` is called when the user scrolls
+ * greater than `distance` away from the bottom of the content. Once `on-infinite`
+ * is done loading new data, it should broadcast the `scroll.infiniteScrollComplete`
+ * event from your controller (see below example).
+ *
+ * @param {expression} on-infinite What to call when the scroller reaches the
+ * bottom.
+ * @param {string=} distance The distance from the bottom that the scroll must
+ * reach to trigger the on-infinite expression. Default: 1%.
+ * @param {string=} spinner The {@link ionic.directive:ionSpinner} to show while loading. The SVG
+ * {@link ionic.directive:ionSpinner} is now the default, replacing rotating font icons.
+ * @param {string=} icon The icon to show while loading. Default: 'ion-load-d'. This is depreciated
+ * in favor of the SVG {@link ionic.directive:ionSpinner}.
+ * @param {boolean=} immediate-check Whether to check the infinite scroll bounds immediately on load.
+ *
+ * @usage
+ * ```html
+ * <ion-content ng-controller="MyController">
+ * <ion-list>
+ * ....
+ * ....
+ * </ion-list>
+ *
+ * <ion-infinite-scroll
+ * on-infinite="loadMore()"
+ * distance="1%">
+ * </ion-infinite-scroll>
+ * </ion-content>
+ * ```
+ * ```js
+ * function MyController($scope, $http) {
+ * $scope.items = [];
+ * $scope.loadMore = function() {
+ * $http.get('/more-items').success(function(items) {
+ * useItems(items);
+ * $scope.$broadcast('scroll.infiniteScrollComplete');
+ * });
+ * };
+ *
+ * $scope.$on('$stateChangeSuccess', function() {
+ * $scope.loadMore();
+ * });
+ * }
+ * ```
+ *
+ * An easy to way to stop infinite scroll once there is no more data to load
+ * is to use angular's `ng-if` directive:
+ *
+ * ```html
+ * <ion-infinite-scroll
+ * ng-if="moreDataCanBeLoaded()"
+ * icon="ion-loading-c"
+ * on-infinite="loadMoreData()">
+ * </ion-infinite-scroll>
+ * ```
+ */
+IonicModule
+.directive('ionInfiniteScroll', ['$timeout', function($timeout) {
+ return {
+ restrict: 'E',
+ require: ['?^$ionicScroll', 'ionInfiniteScroll'],
+ template: function($element, $attrs) {
+ if ($attrs.icon) return '<i class="icon {{icon()}} icon-refreshing {{scrollingType}}"></i>';
+ return '<ion-spinner icon="{{spinner()}}"></ion-spinner>';
+ },
+ scope: true,
+ controller: '$ionInfiniteScroll',
+ link: function($scope, $element, $attrs, ctrls) {
+ var infiniteScrollCtrl = ctrls[1];
+ var scrollCtrl = infiniteScrollCtrl.scrollCtrl = ctrls[0];
+ var jsScrolling = infiniteScrollCtrl.jsScrolling = !scrollCtrl.isNative();
+
+ // if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling
+ if (jsScrolling) {
+ infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
+ $scope.scrollingType = 'js-scrolling';
+ //bind to JS scroll events
+ scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds);
+ } else {
+ // grabbing the scrollable element, to determine dimensions, and current scroll pos
+ var scrollEl = ionic.DomUtil.getParentOrSelfWithClass($element[0].parentNode, 'overflow-scroll');
+ infiniteScrollCtrl.scrollEl = scrollEl;
+ // if there's no scroll controller, and no overflow scroll div, infinite scroll wont work
+ if (!scrollEl) {
+ throw 'Infinite scroll must be used inside a scrollable div';
+ }
+ //bind to native scroll events
+ infiniteScrollCtrl.scrollEl.addEventListener('scroll', infiniteScrollCtrl.checkBounds);
+ }
+
+ // Optionally check bounds on start after scrollView is fully rendered
+ var doImmediateCheck = isDefined($attrs.immediateCheck) ? $scope.$eval($attrs.immediateCheck) : true;
+ if (doImmediateCheck) {
+ $timeout(function() { infiniteScrollCtrl.checkBounds(); });
+ }
+ }
+ };
+}]);
+
+/**
+* @ngdoc directive
+* @name ionInput
+* @parent ionic.directive:ionList
+* @module ionic
+* @restrict E
+* Creates a text input group that can easily be focused
+*
+* @usage
+*
+* ```html
+* <ion-list>
+* <ion-input>
+* <input type="text" placeholder="First Name">
+* </ion-input>
+*
+* <ion-input>
+* <ion-label>Username</ion-label>
+* <input type="text">
+* </ion-input>
+* </ion-list>
+* ```
+*/
+
+var labelIds = -1;
+
+IonicModule
+.directive('ionInput', [function() {
+ return {
+ restrict: 'E',
+ controller: ['$scope', '$element', function($scope, $element) {
+ this.$scope = $scope;
+ this.$element = $element;
+
+ this.setInputAriaLabeledBy = function(id) {
+ var inputs = $element[0].querySelectorAll('input,textarea');
+ inputs.length && inputs[0].setAttribute('aria-labelledby', id);
+ };
+
+ this.focus = function() {
+ var inputs = $element[0].querySelectorAll('input,textarea');
+ inputs.length && inputs[0].focus();
+ };
+ }]
+ };
+}]);
+
+/**
+* @ngdoc directive
+* @name ionLabel
+* @parent ionic.directive:ionList
+* @module ionic
+* @restrict E
+*
+* New in Ionic 1.2. It is strongly recommended that you use `<ion-label>` in place
+* of any `<label>` elements for maximum cross-browser support and performance.
+*
+* Creates a label for a form input.
+*
+* @usage
+*
+* ```html
+* <ion-list>
+* <ion-input>
+* <ion-label>Username</ion-label>
+* <input type="text">
+* </ion-input>
+* </ion-list>
+* ```
+*/
+IonicModule
+.directive('ionLabel', [function() {
+ return {
+ restrict: 'E',
+ require: '?^ionInput',
+ compile: function() {
+
+ return function link($scope, $element, $attrs, ionInputCtrl) {
+ var element = $element[0];
+
+ $element.addClass('input-label');
+
+ $element.attr('aria-label', $element.text());
+ var id = element.id || '_label-' + ++labelIds;
+
+ if (!element.id) {
+ $element.attr('id', id);
+ }
+
+ if (ionInputCtrl) {
+
+ ionInputCtrl.setInputAriaLabeledBy(id);
+
+ $element.on('click', function() {
+ ionInputCtrl.focus();
+ });
+ }
+ };
+ }
+ };
+}]);
+
+/**
+ * Input label adds accessibility to <span class="input-label">.
+ */
+IonicModule
+.directive('inputLabel', [function() {
+ return {
+ restrict: 'C',
+ require: '?^ionInput',
+ compile: function() {
+
+ return function link($scope, $element, $attrs, ionInputCtrl) {
+ var element = $element[0];
+
+ $element.attr('aria-label', $element.text());
+ var id = element.id || '_label-' + ++labelIds;
+
+ if (!element.id) {
+ $element.attr('id', id);
+ }
+
+ if (ionInputCtrl) {
+ ionInputCtrl.setInputAriaLabeledBy(id);
+ }
+
+ };
+ }
+ };
+}]);
+
+/**
+* @ngdoc directive
+* @name ionItem
+* @parent ionic.directive:ionList
+* @module ionic
+* @restrict E
+* Creates a list-item that can easily be swiped,
+* deleted, reordered, edited, and more.
+*
+* See {@link ionic.directive:ionList} for a complete example & explanation.
+*
+* Can be assigned any item class name. See the
+* [list CSS documentation](/docs/components/#list).
+*
+* @usage
+*
+* ```html
+* <ion-list>
+* <ion-item>Hello!</ion-item>
+* <ion-item href="#/detail">
+* Link to detail page
+* </ion-item>
+* </ion-list>
+* ```
+*/
+IonicModule
+.directive('ionItem', ['$$rAF', function($$rAF) {
+ return {
+ restrict: 'E',
+ controller: ['$scope', '$element', function($scope, $element) {
+ this.$scope = $scope;
+ this.$element = $element;
+ }],
+ scope: true,
+ compile: function($element, $attrs) {
+ var isAnchor = isDefined($attrs.href) ||
+ isDefined($attrs.ngHref) ||
+ isDefined($attrs.uiSref);
+ var isComplexItem = isAnchor ||
+ //Lame way of testing, but we have to know at compile what to do with the element
+ /ion-(delete|option|reorder)-button/i.test($element.html());
+
+ if (isComplexItem) {
+ var innerElement = jqLite(isAnchor ? '<a></a>' : '<div></div>');
+ innerElement.addClass('item-content');
+
+ if (isDefined($attrs.href) || isDefined($attrs.ngHref)) {
+ innerElement.attr('ng-href', '{{$href()}}');
+ if (isDefined($attrs.target)) {
+ innerElement.attr('target', '{{$target()}}');
+ }
+ }
+
+ innerElement.append($element.contents());
+
+ $element.addClass('item item-complex')
+ .append(innerElement);
+ } else {
+ $element.addClass('item');
+ }
+
+ return function link($scope, $element, $attrs) {
+ $scope.$href = function() {
+ return $attrs.href || $attrs.ngHref;
+ };
+ $scope.$target = function() {
+ return $attrs.target;
+ };
+
+ var content = $element[0].querySelector('.item-content');
+ if (content) {
+ $scope.$on('$collectionRepeatLeave', function() {
+ if (content && content.$$ionicOptionsOpen) {
+ content.style[ionic.CSS.TRANSFORM] = '';
+ content.style[ionic.CSS.TRANSITION] = 'none';
+ $$rAF(function() {
+ content.style[ionic.CSS.TRANSITION] = '';
+ });
+ content.$$ionicOptionsOpen = false;
+ }
+ });
+ }
+ };
+
+ }
+ };
+}]);
+
+var ITEM_TPL_DELETE_BUTTON =
+ '<div class="item-left-edit item-delete enable-pointer-events">' +
+ '</div>';
+/**
+* @ngdoc directive
+* @name ionDeleteButton
+* @parent ionic.directive:ionItem
+* @module ionic
+* @restrict E
+* Creates a delete button inside a list item, that is visible when the
+* {@link ionic.directive:ionList ionList parent's} `show-delete` evaluates to true or
+* `$ionicListDelegate.showDelete(true)` is called.
+*
+* Takes any ionicon as a class.
+*
+* See {@link ionic.directive:ionList} for a complete example & explanation.
+*
+* @usage
+*
+* ```html
+* <ion-list show-delete="shouldShowDelete">
+* <ion-item>
+* <ion-delete-button class="ion-minus-circled"></ion-delete-button>
+* Hello, list item!
+* </ion-item>
+* </ion-list>
+* <ion-toggle ng-model="shouldShowDelete">
+* Show Delete?
+* </ion-toggle>
+* ```
+*/
+IonicModule
+.directive('ionDeleteButton', function() {
+
+ function stopPropagation(ev) {
+ ev.stopPropagation();
+ }
+
+ return {
+ restrict: 'E',
+ require: ['^^ionItem', '^?ionList'],
+ //Run before anything else, so we can move it before other directives process
+ //its location (eg ngIf relies on the location of the directive in the dom)
+ priority: Number.MAX_VALUE,
+ compile: function($element, $attr) {
+ //Add the classes we need during the compile phase, so that they stay
+ //even if something else like ngIf removes the element and re-addss it
+ $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true);
+ return function($scope, $element, $attr, ctrls) {
+ var itemCtrl = ctrls[0];
+ var listCtrl = ctrls[1];
+ var container = jqLite(ITEM_TPL_DELETE_BUTTON);
+ container.append($element);
+ itemCtrl.$element.append(container).addClass('item-left-editable');
+
+ //Don't bubble click up to main .item
+ $element.on('click', stopPropagation);
+
+ init();
+ $scope.$on('$ionic.reconnectScope', init);
+ function init() {
+ listCtrl = listCtrl || $element.controller('ionList');
+ if (listCtrl && listCtrl.showDelete()) {
+ container.addClass('visible active');
+ }
+ }
+ };
+ }
+ };
+});
+
+
+IonicModule
+.directive('itemFloatingLabel', function() {
+ return {
+ restrict: 'C',
+ link: function(scope, element) {
+ var el = element[0];
+ var input = el.querySelector('input, textarea');
+ var inputLabel = el.querySelector('.input-label');
+
+ if (!input || !inputLabel) return;
+
+ var onInput = function() {
+ if (input.value) {
+ inputLabel.classList.add('has-input');
+ } else {
+ inputLabel.classList.remove('has-input');
+ }
+ };
+
+ input.addEventListener('input', onInput);
+
+ var ngModelCtrl = jqLite(input).controller('ngModel');
+ if (ngModelCtrl) {
+ ngModelCtrl.$render = function() {
+ input.value = ngModelCtrl.$viewValue || '';
+ onInput();
+ };
+ }
+
+ scope.$on('$destroy', function() {
+ input.removeEventListener('input', onInput);
+ });
+ }
+ };
+});
+
+var ITEM_TPL_OPTION_BUTTONS =
+ '<div class="item-options invisible">' +
+ '</div>';
+/**
+* @ngdoc directive
+* @name ionOptionButton
+* @parent ionic.directive:ionItem
+* @module ionic
+* @restrict E
+* @description
+* Creates an option button inside a list item, that is visible when the item is swiped
+* to the left by the user. Swiped open option buttons can be hidden with
+* {@link ionic.service:$ionicListDelegate#closeOptionButtons $ionicListDelegate.closeOptionButtons}.
+*
+* Can be assigned any button class.
+*
+* See {@link ionic.directive:ionList} for a complete example & explanation.
+*
+* @usage
+*
+* ```html
+* <ion-list>
+* <ion-item>
+* I love kittens!
+* <ion-option-button class="button-positive">Share</ion-option-button>
+* <ion-option-button class="button-assertive">Edit</ion-option-button>
+* </ion-item>
+* </ion-list>
+* ```
+*/
+IonicModule.directive('ionOptionButton', [function() {
+ function stopPropagation(e) {
+ e.stopPropagation();
+ }
+ return {
+ restrict: 'E',
+ require: '^ionItem',
+ priority: Number.MAX_VALUE,
+ compile: function($element, $attr) {
+ $attr.$set('class', ($attr['class'] || '') + ' button', true);
+ return function($scope, $element, $attr, itemCtrl) {
+ if (!itemCtrl.optionsContainer) {
+ itemCtrl.optionsContainer = jqLite(ITEM_TPL_OPTION_BUTTONS);
+ itemCtrl.$element.prepend(itemCtrl.optionsContainer);
+ }
+ itemCtrl.optionsContainer.prepend($element);
+
+ itemCtrl.$element.addClass('item-right-editable');
+
+ //Don't bubble click up to main .item
+ $element.on('click', stopPropagation);
+ };
+ }
+ };
+}]);
+
+var ITEM_TPL_REORDER_BUTTON =
+ '<div data-prevent-scroll="true" class="item-right-edit item-reorder enable-pointer-events">' +
+ '</div>';
+
+/**
+* @ngdoc directive
+* @name ionReorderButton
+* @parent ionic.directive:ionItem
+* @module ionic
+* @restrict E
+* Creates a reorder button inside a list item, that is visible when the
+* {@link ionic.directive:ionList ionList parent's} `show-reorder` evaluates to true or
+* `$ionicListDelegate.showReorder(true)` is called.
+*
+* Can be dragged to reorder items in the list. Takes any ionicon class.
+*
+* Note: Reordering works best when used with `ng-repeat`. Be sure that all `ion-item` children of an `ion-list` are part of the same `ng-repeat` expression.
+*
+* When an item reorder is complete, the expression given in the `on-reorder` attribute is called. The `on-reorder` expression is given two locals that can be used: `$fromIndex` and `$toIndex`. See below for an example.
+*
+* Look at {@link ionic.directive:ionList} for more examples.
+*
+* @usage
+*
+* ```html
+* <ion-list ng-controller="MyCtrl" show-reorder="true">
+* <ion-item ng-repeat="item in items">
+* Item {{item}}
+* <ion-reorder-button class="ion-navicon"
+* on-reorder="moveItem(item, $fromIndex, $toIndex)">
+* </ion-reorder-button>
+* </ion-item>
+* </ion-list>
+* ```
+* ```js
+* function MyCtrl($scope) {
+* $scope.items = [1, 2, 3, 4];
+* $scope.moveItem = function(item, fromIndex, toIndex) {
+* //Move the item in the array
+* $scope.items.splice(fromIndex, 1);
+* $scope.items.splice(toIndex, 0, item);
+* };
+* }
+* ```
+*
+* @param {expression=} on-reorder Expression to call when an item is reordered.
+* Parameters given: $fromIndex, $toIndex.
+*/
+IonicModule
+.directive('ionReorderButton', ['$parse', function($parse) {
+ return {
+ restrict: 'E',
+ require: ['^ionItem', '^?ionList'],
+ priority: Number.MAX_VALUE,
+ compile: function($element, $attr) {
+ $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true);
+ $element[0].setAttribute('data-prevent-scroll', true);
+ return function($scope, $element, $attr, ctrls) {
+ var itemCtrl = ctrls[0];
+ var listCtrl = ctrls[1];
+ var onReorderFn = $parse($attr.onReorder);
+
+ $scope.$onReorder = function(oldIndex, newIndex) {
+ onReorderFn($scope, {
+ $fromIndex: oldIndex,
+ $toIndex: newIndex
+ });
+ };
+
+ // prevent clicks from bubbling up to the item
+ if (!$attr.ngClick && !$attr.onClick && !$attr.onclick) {
+ $element[0].onclick = function(e) {
+ e.stopPropagation();
+ return false;
+ };
+ }
+
+ var container = jqLite(ITEM_TPL_REORDER_BUTTON);
+ container.append($element);
+ itemCtrl.$element.append(container).addClass('item-right-editable');
+
+ if (listCtrl && listCtrl.showReorder()) {
+ container.addClass('visible active');
+ }
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name keyboardAttach
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * keyboard-attach is an attribute directive which will cause an element to float above
+ * the keyboard when the keyboard shows. Currently only supports the
+ * [ion-footer-bar]({{ page.versionHref }}/api/directive/ionFooterBar/) directive.
+ *
+ * ### Notes
+ * - This directive requires the
+ * [Ionic Keyboard Plugin](https://github.com/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) {
+ ionic.on('native.keyboardshow', onShow, window);
+ ionic.on('native.keyboardhide', onHide, window);
+
+ //deprecated
+ ionic.on('native.showkeyboard', onShow, window);
+ ionic.on('native.hidekeyboard', onHide, window);
+
+
+ var scrollCtrl;
+
+ function onShow(e) {
+ if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
+ return;
+ }
+
+ //for testing
+ var keyboardHeight = e.keyboardHeight || (e.detail && e.detail.keyboardHeight);
+ element.css('bottom', keyboardHeight + "px");
+ scrollCtrl = element.controller('$ionicScroll');
+ if (scrollCtrl) {
+ scrollCtrl.scrollView.__container.style.bottom = keyboardHeight + keyboardAttachGetClientHeight(element[0]) + "px";
+ }
+ }
+
+ function onHide() {
+ if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
+ return;
+ }
+
+ element.css('bottom', '');
+ if (scrollCtrl) {
+ scrollCtrl.scrollView.__container.style.bottom = '';
+ }
+ }
+
+ scope.$on('$destroy', function() {
+ ionic.off('native.keyboardshow', onShow, window);
+ ionic.off('native.keyboardhide', onHide, window);
+
+ //deprecated
+ ionic.off('native.showkeyboard', onShow, window);
+ ionic.off('native.hidekeyboard', onHide, window);
+ });
+ };
+});
+
+function keyboardAttachGetClientHeight(element) {
+ return element.clientHeight;
+}
+
+/**
+* @ngdoc directive
+* @name ionList
+* @module ionic
+* @delegate ionic.service:$ionicListDelegate
+* @codepen JsHjf
+* @restrict E
+* @description
+* The List is a widely used interface element in almost any mobile app, and can include
+* content ranging from basic text all the way to buttons, toggles, icons, and thumbnails.
+*
+* Both the list, which contains items, and the list items themselves can be any HTML
+* element. The containing element requires the `list` class and each list item requires
+* the `item` class.
+*
+* However, using the ionList and ionItem directives make it easy to support various
+* interaction modes such as swipe to edit, drag to reorder, and removing items.
+*
+* Related: {@link ionic.directive:ionItem}, {@link ionic.directive:ionOptionButton}
+* {@link ionic.directive:ionReorderButton}, {@link ionic.directive:ionDeleteButton}, [`list CSS documentation`](/docs/components/#list).
+*
+* @usage
+*
+* Basic Usage:
+*
+* ```html
+* <ion-list>
+* <ion-item ng-repeat="item in items">
+* {% raw %}Hello, {{item}}!{% endraw %}
+* </ion-item>
+* </ion-list>
+* ```
+*
+* Advanced Usage: Thumbnails, Delete buttons, Reordering, Swiping
+*
+* ```html
+* <ion-list ng-controller="MyCtrl"
+* show-delete="shouldShowDelete"
+* show-reorder="shouldShowReorder"
+* can-swipe="listCanSwipe">
+* <ion-item ng-repeat="item in items"
+* class="item-thumbnail-left">
+*
+* {% raw %}<img ng-src="{{item.img}}">
+* <h2>{{item.title}}</h2>
+* <p>{{item.description}}</p>{% endraw %}
+* <ion-option-button class="button-positive"
+* ng-click="share(item)">
+* Share
+* </ion-option-button>
+* <ion-option-button class="button-info"
+* ng-click="edit(item)">
+* Edit
+* </ion-option-button>
+* <ion-delete-button class="ion-minus-circled"
+* ng-click="items.splice($index, 1)">
+* </ion-delete-button>
+* <ion-reorder-button class="ion-navicon"
+* on-reorder="reorderItem(item, $fromIndex, $toIndex)">
+* </ion-reorder-button>
+*
+* </ion-item>
+* </ion-list>
+* ```
+*
+*```javascript
+* app.controller('MyCtrl', function($scope) {
+* $scope.shouldShowDelete = false;
+* $scope.shouldShowReorder = false;
+* $scope.listCanSwipe = true
+* });
+*```
+*
+* @param {string=} delegate-handle The handle used to identify this list with
+* {@link ionic.service:$ionicListDelegate}.
+* @param type {string=} The type of list to use (list-inset or card)
+* @param show-delete {boolean=} Whether the delete buttons for the items in the list are
+* currently shown or hidden.
+* @param show-reorder {boolean=} Whether the reorder buttons for the items in the list are
+* currently shown or hidden.
+* @param can-swipe {boolean=} Whether the items in the list are allowed to be swiped to reveal
+* option buttons. Default: true.
+*/
+IonicModule
+.directive('ionList', [
+ '$timeout',
+function($timeout) {
+ return {
+ restrict: 'E',
+ require: ['ionList', '^?$ionicScroll'],
+ controller: '$ionicList',
+ compile: function($element, $attr) {
+ var listEl = jqLite('<div class="list">')
+ .append($element.contents())
+ .addClass($attr.type);
+
+ $element.append(listEl);
+
+ return function($scope, $element, $attrs, ctrls) {
+ var listCtrl = ctrls[0];
+ var scrollCtrl = ctrls[1];
+
+ // Wait for child elements to render...
+ $timeout(init);
+
+ function init() {
+ var listView = listCtrl.listView = new ionic.views.ListView({
+ el: $element[0],
+ listEl: $element.children()[0],
+ scrollEl: scrollCtrl && scrollCtrl.element,
+ scrollView: scrollCtrl && scrollCtrl.scrollView,
+ onReorder: function(el, oldIndex, newIndex) {
+ var itemScope = jqLite(el).scope();
+ if (itemScope && itemScope.$onReorder) {
+ // Make sure onReorder is called in apply cycle,
+ // but also make sure it has no conflicts by doing
+ // $evalAsync
+ $timeout(function() {
+ itemScope.$onReorder(oldIndex, newIndex);
+ });
+ }
+ },
+ canSwipe: function() {
+ return listCtrl.canSwipeItems();
+ }
+ });
+
+ $scope.$on('$destroy', function() {
+ if (listView) {
+ listView.deregister && listView.deregister();
+ listView = null;
+ }
+ });
+
+ if (isDefined($attr.canSwipe)) {
+ $scope.$watch('!!(' + $attr.canSwipe + ')', function(value) {
+ listCtrl.canSwipeItems(value);
+ });
+ }
+ if (isDefined($attr.showDelete)) {
+ $scope.$watch('!!(' + $attr.showDelete + ')', function(value) {
+ listCtrl.showDelete(value);
+ });
+ }
+ if (isDefined($attr.showReorder)) {
+ $scope.$watch('!!(' + $attr.showReorder + ')', function(value) {
+ listCtrl.showReorder(value);
+ });
+ }
+
+ $scope.$watch(function() {
+ return listCtrl.showDelete();
+ }, function(isShown, wasShown) {
+ //Only use isShown=false if it was already shown
+ if (!isShown && !wasShown) { return; }
+
+ if (isShown) listCtrl.closeOptionButtons();
+ listCtrl.canSwipeItems(!isShown);
+
+ $element.children().toggleClass('list-left-editing', isShown);
+ $element.toggleClass('disable-pointer-events', isShown);
+
+ var deleteButton = jqLite($element[0].getElementsByClassName('item-delete'));
+ setButtonShown(deleteButton, listCtrl.showDelete);
+ });
+
+ $scope.$watch(function() {
+ return listCtrl.showReorder();
+ }, function(isShown, wasShown) {
+ //Only use isShown=false if it was already shown
+ if (!isShown && !wasShown) { return; }
+
+ if (isShown) listCtrl.closeOptionButtons();
+ listCtrl.canSwipeItems(!isShown);
+
+ $element.children().toggleClass('list-right-editing', isShown);
+ $element.toggleClass('disable-pointer-events', isShown);
+
+ var reorderButton = jqLite($element[0].getElementsByClassName('item-reorder'));
+ setButtonShown(reorderButton, listCtrl.showReorder);
+ });
+
+ function setButtonShown(el, shown) {
+ shown() && el.addClass('visible') || el.removeClass('active');
+ ionic.requestAnimationFrame(function() {
+ shown() && el.addClass('active') || el.removeClass('visible');
+ });
+ }
+ }
+
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name menuClose
+ * @module ionic
+ * @restrict AC
+ *
+ * @description
+ * `menu-close` is an attribute directive that closes a currently opened side menu.
+ * Note that by default, navigation transitions will not animate between views when
+ * the menu is open. Additionally, this directive will reset the entering view's
+ * history stack, making the new page the root of the history stack. This is done
+ * to replicate the user experience seen in most side menu implementations, which is
+ * to not show the back button at the root of the stack and show only the
+ * menu button. We recommend that you also use the `enable-menu-with-back-views="false"`
+ * {@link ionic.directive:ionSideMenus} attribute when using the menuClose directive.
+ *
+ * @usage
+ * Below is an example of a link within a side menu. Tapping this link would
+ * automatically close the currently opened menu.
+ *
+ * ```html
+ * <a menu-close href="#/home" class="item">Home</a>
+ * ```
+ *
+ * Note that if your destination state uses a resolve and that resolve asynchronously
+ * takes longer than a standard transition (300ms), you'll need to set the
+ * `nextViewOptions` manually as your resolve completes.
+ *
+ * ```js
+ * $ionicHistory.nextViewOptions({
+ * historyRoot: true,
+ * disableAnimate: true,
+ * expire: 300
+ * });
+ * ```
+ */
+IonicModule
+.directive('menuClose', ['$ionicHistory', '$timeout', function($ionicHistory, $timeout) {
+ return {
+ restrict: 'AC',
+ link: function($scope, $element) {
+ $element.bind('click', function() {
+ var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
+ if (sideMenuCtrl) {
+ $ionicHistory.nextViewOptions({
+ historyRoot: true,
+ disableAnimate: true,
+ expire: 300
+ });
+ // if no transition in 300ms, reset nextViewOptions
+ // the expire should take care of it, but will be cancelled in some
+ // cases. This directive is an exception to the rules of history.js
+ $timeout( function() {
+ $ionicHistory.nextViewOptions({
+ historyRoot: false,
+ disableAnimate: false
+ });
+ }, 300);
+ sideMenuCtrl.close();
+ }
+ });
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name menuToggle
+ * @module ionic
+ * @restrict AC
+ *
+ * @description
+ * Toggle a side menu on the given side.
+ *
+ * @usage
+ * Below is an example of a link within a nav bar. Tapping this button
+ * would open the given side menu, and tapping it again would close it.
+ *
+ * ```html
+ * <ion-nav-bar>
+ * <ion-nav-buttons side="left">
+ * <!-- Toggle left side menu -->
+ * <button menu-toggle="left" class="button button-icon icon ion-navicon"></button>
+ * </ion-nav-buttons>
+ * <ion-nav-buttons side="right">
+ * <!-- Toggle right side menu -->
+ * <button menu-toggle="right" class="button button-icon icon ion-navicon"></button>
+ * </ion-nav-buttons>
+ * </ion-nav-bar>
+ * ```
+ *
+ * ### Button Hidden On Child Views
+ * By default, the menu toggle button will only appear on a root
+ * level side-menu page. Navigating in to child views will hide the menu-
+ * toggle button. They can be made visible on child pages by setting the
+ * enable-menu-with-back-views attribute of the {@link ionic.directive:ionSideMenus}
+ * directive to true.
+ *
+ * ```html
+ * <ion-side-menus enable-menu-with-back-views="true">
+ * ```
+ */
+IonicModule
+.directive('menuToggle', function() {
+ return {
+ restrict: 'AC',
+ link: function($scope, $element, $attr) {
+ $scope.$on('$ionicView.beforeEnter', function(ev, viewData) {
+ if (viewData.enableBack) {
+ var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
+ if (!sideMenuCtrl.enableMenuWithBackViews()) {
+ $element.addClass('hide');
+ }
+ } else {
+ $element.removeClass('hide');
+ }
+ });
+
+ $element.bind('click', function() {
+ var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
+ sideMenuCtrl && sideMenuCtrl.toggle($attr.menuToggle);
+ });
+ }
+ };
+});
+
+/*
+ * We don't document the ionModal directive, we instead document
+ * the $ionicModal service
+ */
+IonicModule
+.directive('ionModal', [function() {
+ return {
+ restrict: 'E',
+ transclude: true,
+ replace: true,
+ controller: [function() {}],
+ template: '<div class="modal-backdrop">' +
+ '<div class="modal-backdrop-bg"></div>' +
+ '<div class="modal-wrapper" ng-transclude></div>' +
+ '</div>'
+ };
+}]);
+
+IonicModule
+.directive('ionModalView', function() {
+ return {
+ restrict: 'E',
+ compile: function(element) {
+ element.addClass('modal');
+ }
+ };
+});
+
+/**
+ * @ngdoc directive
+ * @name ionNavBackButton
+ * @module ionic
+ * @restrict E
+ * @parent ionNavBar
+ * @description
+ * Creates a back button inside an {@link ionic.directive:ionNavBar}.
+ *
+ * The back button will appear when the user is able to go back in the current navigation stack. By
+ * default, the markup of the back button is automatically built using platform-appropriate defaults
+ * (iOS back button icon on iOS and Android icon on Android).
+ *
+ * Additionally, the button is automatically set to `$ionicGoBack()` on click/tap. By default, the
+ * app will navigate back one view when the back button is clicked. More advanced behavior is also
+ * possible, as outlined below.
+ *
+ * @usage
+ *
+ * Recommended markup for default settings:
+ *
+ * ```html
+ * <ion-nav-bar>
+ * <ion-nav-back-button>
+ * </ion-nav-back-button>
+ * </ion-nav-bar>
+ * ```
+ *
+ * With custom inner markup, and automatically adds a default click action:
+ *
+ * ```html
+ * <ion-nav-bar>
+ * <ion-nav-back-button class="button-clear">
+ * <i class="ion-arrow-left-c"></i> Back
+ * </ion-nav-back-button>
+ * </ion-nav-bar>
+ * ```
+ *
+ * With custom inner markup and custom click action, using {@link ionic.service:$ionicHistory}:
+ *
+ * ```html
+ * <ion-nav-bar ng-controller="MyCtrl">
+ * <ion-nav-back-button class="button-clear"
+ * ng-click="myGoBack()">
+ * <i class="ion-arrow-left-c"></i> Back
+ * </ion-nav-back-button>
+ * </ion-nav-bar>
+ * ```
+ * ```js
+ * function MyCtrl($scope, $ionicHistory) {
+ * $scope.myGoBack = function() {
+ * $ionicHistory.goBack();
+ * };
+ * }
+ * ```
+ */
+IonicModule
+.directive('ionNavBackButton', ['$ionicConfig', '$document', function($ionicConfig, $document) {
+ return {
+ restrict: 'E',
+ require: '^ionNavBar',
+ compile: function(tElement, tAttrs) {
+
+ // clone the back button, but as a <div>
+ var buttonEle = $document[0].createElement('button');
+ for (var n in tAttrs.$attr) {
+ buttonEle.setAttribute(tAttrs.$attr[n], tAttrs[n]);
+ }
+
+ if (!tAttrs.ngClick) {
+ buttonEle.setAttribute('ng-click', '$ionicGoBack()');
+ }
+
+ buttonEle.className = 'button back-button hide buttons ' + (tElement.attr('class') || '');
+ buttonEle.innerHTML = tElement.html() || '';
+
+ var childNode;
+ var hasIcon = hasIconClass(tElement[0]);
+ var hasInnerText;
+ var hasButtonText;
+ var hasPreviousTitle;
+
+ for (var x = 0; x < tElement[0].childNodes.length; x++) {
+ childNode = tElement[0].childNodes[x];
+ if (childNode.nodeType === 1) {
+ if (hasIconClass(childNode)) {
+ hasIcon = true;
+ } else if (childNode.classList.contains('default-title')) {
+ hasButtonText = true;
+ } else if (childNode.classList.contains('previous-title')) {
+ hasPreviousTitle = true;
+ }
+ } else if (!hasInnerText && childNode.nodeType === 3) {
+ hasInnerText = !!childNode.nodeValue.trim();
+ }
+ }
+
+ function hasIconClass(ele) {
+ return /ion-|icon/.test(ele.className);
+ }
+
+ var defaultIcon = $ionicConfig.backButton.icon();
+ if (!hasIcon && defaultIcon && defaultIcon !== 'none') {
+ buttonEle.innerHTML = '<i class="icon ' + defaultIcon + '"></i> ' + buttonEle.innerHTML;
+ buttonEle.className += ' button-clear';
+ }
+
+ if (!hasInnerText) {
+ var buttonTextEle = $document[0].createElement('span');
+ buttonTextEle.className = 'back-text';
+
+ if (!hasButtonText && $ionicConfig.backButton.text()) {
+ buttonTextEle.innerHTML += '<span class="default-title">' + $ionicConfig.backButton.text() + '</span>';
+ }
+ if (!hasPreviousTitle && $ionicConfig.backButton.previousTitleText()) {
+ buttonTextEle.innerHTML += '<span class="previous-title"></span>';
+ }
+ buttonEle.appendChild(buttonTextEle);
+
+ }
+
+ tElement.attr('class', 'hide');
+ tElement.empty();
+
+ return {
+ pre: function($scope, $element, $attr, navBarCtrl) {
+ // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
+ navBarCtrl.navElement('backButton', buttonEle.outerHTML);
+ buttonEle = null;
+ }
+ };
+ }
+ };
+}]);
+
+
+/**
+ * @ngdoc directive
+ * @name ionNavBar
+ * @module ionic
+ * @delegate ionic.service:$ionicNavBarDelegate
+ * @restrict E
+ *
+ * @description
+ * If we have an {@link ionic.directive:ionNavView} directive, we can also create an
+ * `<ion-nav-bar>`, which will create a topbar that updates as the application state changes.
+ *
+ * We can add a back button by putting an {@link ionic.directive:ionNavBackButton} inside.
+ *
+ * We can add buttons depending on the currently visible view using
+ * {@link ionic.directive:ionNavButtons}.
+ *
+ * Note that the ion-nav-bar element will only work correctly if your content has an
+ * ionView around it.
+ *
+ * @usage
+ *
+ * ```html
+ * <body ng-app="starter">
+ * <!-- The nav bar that will be updated as we navigate -->
+ * <ion-nav-bar class="bar-positive">
+ * </ion-nav-bar>
+ *
+ * <!-- where the initial view template will be rendered -->
+ * <ion-nav-view>
+ * <ion-view>
+ * <ion-content>Hello!</ion-content>
+ * </ion-view>
+ * </ion-nav-view>
+ * </body>
+ * ```
+ *
+ * @param {string=} delegate-handle The handle used to identify this navBar
+ * with {@link ionic.service:$ionicNavBarDelegate}.
+ * @param align-title {string=} Where to align the title of the navbar.
+ * Available: 'left', 'right', 'center'. Defaults to 'center'.
+ * @param {boolean=} no-tap-scroll By default, the navbar will scroll the content
+ * to the top when tapped. Set no-tap-scroll to true to disable this behavior.
+ *
+ */
+IonicModule
+.directive('ionNavBar', function() {
+ return {
+ restrict: 'E',
+ controller: '$ionicNavBar',
+ scope: true,
+ link: function($scope, $element, $attr, ctrl) {
+ ctrl.init();
+ }
+ };
+});
+
+
+/**
+ * @ngdoc directive
+ * @name ionNavButtons
+ * @module ionic
+ * @restrict E
+ * @parent ionNavView
+ *
+ * @description
+ * Use nav buttons to set the buttons on your {@link ionic.directive:ionNavBar}
+ * from within an {@link ionic.directive:ionView}. This gives each
+ * view template the ability to specify which buttons should show in the nav bar,
+ * overriding any default buttons already placed in the nav bar.
+ *
+ * Any buttons you declare will be positioned on the navbar's corresponding side. Primary
+ * buttons generally map to the left side of the header, and secondary buttons are
+ * generally on the right side. However, their exact locations are platform-specific.
+ * For example, in iOS, the primary buttons are on the far left of the header, and
+ * secondary buttons are on the far right, with the header title centered between them.
+ * For Android, however, both groups of buttons are on the far right of the header,
+ * with the header title aligned left.
+ *
+ * We recommend always using `primary` and `secondary`, so the buttons correctly map
+ * to the side familiar to users of each platform. However, in cases where buttons should
+ * always be on an exact side, both `left` and `right` sides are still available. For
+ * example, a toggle button for a left side menu should be on the left side; in this case,
+ * we'd recommend using `side="left"`, so it's always on the left, no matter the platform.
+ *
+ * ***Note*** that `ion-nav-buttons` must be immediate descendants of the `ion-view` or
+ * `ion-nav-bar` element (basically, don't wrap it in another div).
+ *
+ * @usage
+ * ```html
+ * <ion-nav-bar>
+ * </ion-nav-bar>
+ * <ion-nav-view>
+ * <ion-view>
+ * <ion-nav-buttons side="primary">
+ * <button class="button" ng-click="doSomething()">
+ * I'm a button on the primary of the navbar!
+ * </button>
+ * </ion-nav-buttons>
+ * <ion-content>
+ * Some super content here!
+ * </ion-content>
+ * </ion-view>
+ * </ion-nav-view>
+ * ```
+ *
+ * @param {string} side The side to place the buttons in the
+ * {@link ionic.directive:ionNavBar}. Available sides: `primary`, `secondary`, `left`, and `right`.
+ */
+IonicModule
+.directive('ionNavButtons', ['$document', function($document) {
+ return {
+ require: '^ionNavBar',
+ restrict: 'E',
+ compile: function(tElement, tAttrs) {
+ var side = 'left';
+
+ if (/^primary|secondary|right$/i.test(tAttrs.side || '')) {
+ side = tAttrs.side.toLowerCase();
+ }
+
+ var spanEle = $document[0].createElement('span');
+ spanEle.className = side + '-buttons';
+ spanEle.innerHTML = tElement.html();
+
+ var navElementType = side + 'Buttons';
+
+ tElement.attr('class', 'hide');
+ tElement.empty();
+
+ return {
+ pre: function($scope, $element, $attrs, navBarCtrl) {
+ // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
+
+ var parentViewCtrl = $element.parent().data('$ionViewController');
+ if (parentViewCtrl) {
+ // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
+ parentViewCtrl.navElement(navElementType, spanEle.outerHTML);
+
+ } else {
+ // these are buttons for all views that do not have their own ion-nav-buttons
+ navBarCtrl.navElement(navElementType, spanEle.outerHTML);
+ }
+
+ spanEle = null;
+ }
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name navDirection
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * The direction which the nav view transition should animate. Available options
+ * are: `forward`, `back`, `enter`, `exit`, `swap`.
+ *
+ * @usage
+ *
+ * ```html
+ * <a nav-direction="forward" href="#/home">Home</a>
+ * ```
+ */
+IonicModule
+.directive('navDirection', ['$ionicViewSwitcher', function($ionicViewSwitcher) {
+ return {
+ restrict: 'A',
+ priority: 1000,
+ link: function($scope, $element, $attr) {
+ $element.bind('click', function() {
+ $ionicViewSwitcher.nextDirection($attr.navDirection);
+ });
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionNavTitle
+ * @module ionic
+ * @restrict E
+ * @parent ionNavView
+ *
+ * @description
+ *
+ * The nav title directive replaces an {@link ionic.directive:ionNavBar} title text with
+ * custom HTML from within an {@link ionic.directive:ionView} template. This gives each
+ * view the ability to specify its own custom title element, such as an image or any HTML,
+ * rather than being text-only. Alternatively, text-only titles can be updated using the
+ * `view-title` {@link ionic.directive:ionView} attribute.
+ *
+ * Note that `ion-nav-title` must be an immediate descendant of the `ion-view` or
+ * `ion-nav-bar` element (basically don't wrap it in another div).
+ *
+ * @usage
+ * ```html
+ * <ion-nav-bar>
+ * </ion-nav-bar>
+ * <ion-nav-view>
+ * <ion-view>
+ * <ion-nav-title>
+ * <img src="logo.svg">
+ * </ion-nav-title>
+ * <ion-content>
+ * Some super content here!
+ * </ion-content>
+ * </ion-view>
+ * </ion-nav-view>
+ * ```
+ *
+ */
+IonicModule
+.directive('ionNavTitle', ['$document', function($document) {
+ return {
+ require: '^ionNavBar',
+ restrict: 'E',
+ compile: function(tElement, tAttrs) {
+ var navElementType = 'title';
+ var spanEle = $document[0].createElement('span');
+ for (var n in tAttrs.$attr) {
+ spanEle.setAttribute(tAttrs.$attr[n], tAttrs[n]);
+ }
+ spanEle.classList.add('nav-bar-title');
+ spanEle.innerHTML = tElement.html();
+
+ tElement.attr('class', 'hide');
+ tElement.empty();
+
+ return {
+ pre: function($scope, $element, $attrs, navBarCtrl) {
+ // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
+
+ var parentViewCtrl = $element.parent().data('$ionViewController');
+ if (parentViewCtrl) {
+ // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
+ parentViewCtrl.navElement(navElementType, spanEle.outerHTML);
+
+ } else {
+ // these are buttons for all views that do not have their own ion-nav-buttons
+ navBarCtrl.navElement(navElementType, spanEle.outerHTML);
+ }
+
+ spanEle = null;
+ }
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name navTransition
+ * @module ionic
+ * @restrict A
+ *
+ * @description
+ * The transition type which the nav view transition should use when it animates.
+ * Current, options are `ios`, `android`, and `none`. More options coming soon.
+ *
+ * @usage
+ *
+ * ```html
+ * <a nav-transition="none" href="#/home">Home</a>
+ * ```
+ */
+IonicModule
+.directive('navTransition', ['$ionicViewSwitcher', function($ionicViewSwitcher) {
+ return {
+ restrict: 'A',
+ priority: 1000,
+ link: function($scope, $element, $attr) {
+ $element.bind('click', function() {
+ $ionicViewSwitcher.nextTransition($attr.navTransition);
+ });
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionNavView
+ * @module ionic
+ * @restrict E
+ * @codepen odqCz
+ *
+ * @description
+ * As a user navigates throughout your app, Ionic is able to keep track of their
+ * navigation history. By knowing their history, transitions between views
+ * correctly enter and exit using the platform's transition style. An additional
+ * benefit to Ionic's navigation system is its ability to manage multiple
+ * histories. For example, each tab can have it's own navigation history stack.
+ *
+ * Ionic uses the AngularUI Router module so app interfaces can be organized
+ * into various "states". Like Angular's core $route service, URLs can be used
+ * to control the views. However, the AngularUI Router provides a more powerful
+ * state manager in that states are bound to named, nested, and parallel views,
+ * allowing more than one template to be rendered on the same page.
+ * Additionally, each state is not required to be bound to a URL, and data can
+ * be pushed to each state which allows much flexibility.
+ *
+ * The ionNavView directive is used to render templates in your application. Each template
+ * is part of a state. States are usually mapped to a url, and are defined programatically
+ * using angular-ui-router (see [their docs](https://github.com/angular-ui/ui-router/wiki),
+ * and remember to replace ui-view with ion-nav-view in examples).
+ *
+ * @usage
+ * In this example, we will create a navigation view that contains our different states for the app.
+ *
+ * To do this, in our markup we use ionNavView top level directive. To display a header bar we use
+ * the {@link ionic.directive:ionNavBar} directive that updates as we navigate through the
+ * navigation stack.
+ *
+ * Next, we need to setup our states that will be rendered.
+ *
+ * ```js
+ * var app = angular.module('myApp', ['ionic']);
+ * app.config(function($stateProvider) {
+ * $stateProvider
+ * .state('index', {
+ * url: '/',
+ * templateUrl: 'home.html'
+ * })
+ * .state('music', {
+ * url: '/music',
+ * templateUrl: 'music.html'
+ * });
+ * });
+ * ```
+ * Then on app start, $stateProvider will look at the url, see if it matches the index state,
+ * and then try to load home.html into the `<ion-nav-view>`.
+ *
+ * Pages are loaded by the URLs given. One simple way to create templates in Angular is to put
+ * them directly into your HTML file and use the `<script type="text/ng-template">` syntax.
+ * So here is one way to put home.html into our app:
+ *
+ * ```html
+ * <script id="home" type="text/ng-template">
+ * <!-- The title of the ion-view will be shown on the navbar -->
+ * <ion-view view-title="Home">
+ * <ion-content ng-controller="HomeCtrl">
+ * <!-- The content of the page -->
+ * <a href="#/music">Go to music page!</a>
+ * </ion-content>
+ * </ion-view>
+ * </script>
+ * ```
+ *
+ * This is good to do because the template will be cached for very fast loading, instead of
+ * having to fetch them from the network.
+ *
+ * ## Caching
+ *
+ * By default, views are cached to improve performance. When a view is navigated away from, its
+ * element is left in the DOM, and its scope is disconnected from the `$watch` cycle. When
+ * navigating to a view that is already cached, its scope is then reconnected, and the existing
+ * element that was left in the DOM becomes the active view. This also allows for the scroll
+ * position of previous views to be maintained.
+ *
+ * Caching can be disabled and enabled in multiple ways. By default, Ionic will cache a maximum of
+ * 10 views, and not only can this be configured, but apps can also explicitly state which views
+ * should and should not be cached.
+ *
+ * Note that because we are caching these views, *we aren’t destroying scopes*. Instead, scopes
+ * are being disconnected from the watch cycle. Because scopes are not being destroyed and
+ * recreated, controllers are not loading again on a subsequent viewing. If the app/controller
+ * needs to know when a view has entered or has left, then view events emitted from the
+ * {@link ionic.directive:ionView} scope, such as `$ionicView.enter`, may be useful.
+ *
+ * By default, when navigating back in the history, the "forward" views are removed from the cache.
+ * If you navigate forward to the same view again, it'll create a new DOM element and controller
+ * instance. Basically, any forward views are reset each time. This can be configured using the
+ * {@link ionic.provider:$ionicConfigProvider}:
+ *
+ * ```js
+ * $ionicConfigProvider.views.forwardCache(true);
+ * ```
+ *
+ * #### Disable cache globally
+ *
+ * The {@link ionic.provider:$ionicConfigProvider} can be used to set the maximum allowable views
+ * which can be cached, but this can also be use to disable all caching by setting it to 0.
+ *
+ * ```js
+ * $ionicConfigProvider.views.maxCache(0);
+ * ```
+ *
+ * #### Disable cache within state provider
+ *
+ * ```js
+ * $stateProvider.state('myState', {
+ * cache: false,
+ * url : '/myUrl',
+ * templateUrl : 'my-template.html'
+ * })
+ * ```
+ *
+ * #### Disable cache with an attribute
+ *
+ * ```html
+ * <ion-view cache-view="false" view-title="My Title!">
+ * ...
+ * </ion-view>
+ * ```
+ *
+ *
+ * ## AngularUI Router
+ *
+ * Please visit [AngularUI Router's docs](https://github.com/angular-ui/ui-router/wiki) for
+ * more info. Below is a great video by the AngularUI Router team that may help to explain
+ * how it all works:
+ *
+ * <iframe width="560" height="315" src="//www.youtube.com/embed/dqJRoh8MnBo"
+ * frameborder="0" allowfullscreen></iframe>
+ *
+ * Note: We do not recommend using [resolve](https://github.com/angular-ui/ui-router/wiki#resolve)
+ * of AngularUI Router. The recommended approach is to execute any logic needed before beginning the state transition.
+ *
+ * @param {string=} name A view name. The name should be unique amongst the other views in the
+ * same state. You can have views of the same name that live in different states. For more
+ * information, see ui-router's
+ * [ui-view documentation](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.directive:ui-view).
+ */
+IonicModule
+.directive('ionNavView', [
+ '$state',
+ '$ionicConfig',
+function($state, $ionicConfig) {
+ // IONIC's fork of Angular UI Router, v0.2.10
+ // the navView handles registering views in the history and how to transition between them
+ return {
+ restrict: 'E',
+ terminal: true,
+ priority: 2000,
+ transclude: true,
+ controller: '$ionicNavView',
+ compile: function(tElement, tAttrs, transclude) {
+
+ // a nav view element is a container for numerous views
+ tElement.addClass('view-container');
+ ionic.DomUtil.cachedAttr(tElement, 'nav-view-transition', $ionicConfig.views.transition());
+
+ return function($scope, $element, $attr, navViewCtrl) {
+ var latestLocals;
+
+ // Put in the compiled initial view
+ transclude($scope, function(clone) {
+ $element.append(clone);
+ });
+
+ var viewData = navViewCtrl.init();
+
+ // listen for $stateChangeSuccess
+ $scope.$on('$stateChangeSuccess', function() {
+ updateView(false);
+ });
+ $scope.$on('$viewContentLoading', function() {
+ updateView(false);
+ });
+
+ // initial load, ready go
+ updateView(true);
+
+
+ function updateView(firstTime) {
+ // get the current local according to the $state
+ var viewLocals = $state.$current && $state.$current.locals[viewData.name];
+
+ // do not update THIS nav-view if its is not the container for the given state
+ // if the viewLocals are the same as THIS latestLocals, then nothing to do
+ if (!viewLocals || (!firstTime && viewLocals === latestLocals)) return;
+
+ // update the latestLocals
+ latestLocals = viewLocals;
+ viewData.state = viewLocals.$$state;
+
+ // register, update and transition to the new view
+ navViewCtrl.register(viewLocals);
+ }
+
+ };
+ }
+ };
+}]);
+
+IonicModule
+
+.config(['$provide', function($provide) {
+ $provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
+ // drop the default ngClick directive
+ $delegate.shift();
+ return $delegate;
+ }]);
+}])
+
+/**
+ * @private
+ */
+.factory('$ionicNgClick', ['$parse', function($parse) {
+ return function(scope, element, clickExpr) {
+ var clickHandler = angular.isFunction(clickExpr) ?
+ clickExpr :
+ $parse(clickExpr);
+
+ element.on('click', function(event) {
+ scope.$apply(function() {
+ clickHandler(scope, {$event: (event)});
+ });
+ });
+
+ // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
+ // something else nearby.
+ element.onclick = noop;
+ };
+}])
+
+.directive('ngClick', ['$ionicNgClick', function($ionicNgClick) {
+ return function(scope, element, attr) {
+ $ionicNgClick(scope, element, attr.ngClick);
+ };
+}])
+
+.directive('ionStopEvent', function() {
+ return {
+ restrict: 'A',
+ link: function(scope, element, attr) {
+ element.bind(attr.ionStopEvent, eventStopPropagation);
+ }
+ };
+});
+function eventStopPropagation(e) {
+ e.stopPropagation();
+}
+
+
+/**
+ * @ngdoc directive
+ * @name ionPane
+ * @module ionic
+ * @restrict E
+ *
+ * @description A simple container that fits content, with no side effects. Adds the 'pane' class to the element.
+ */
+IonicModule
+.directive('ionPane', function() {
+ return {
+ restrict: 'E',
+ link: function(scope, element) {
+ element.addClass('pane');
+ }
+ };
+});
+
+/*
+ * We don't document the ionPopover directive, we instead document
+ * the $ionicPopover service
+ */
+IonicModule
+.directive('ionPopover', [function() {
+ return {
+ restrict: 'E',
+ transclude: true,
+ replace: true,
+ controller: [function() {}],
+ template: '<div class="popover-backdrop">' +
+ '<div class="popover-wrapper" ng-transclude></div>' +
+ '</div>'
+ };
+}]);
+
+IonicModule
+.directive('ionPopoverView', function() {
+ return {
+ restrict: 'E',
+ compile: function(element) {
+ element.append(jqLite('<div class="popover-arrow">'));
+ element.addClass('popover');
+ }
+ };
+});
+
+/**
+ * @ngdoc directive
+ * @name ionRadio
+ * @module ionic
+ * @restrict E
+ * @codepen saoBG
+ * @description
+ * The radio directive is no different than the HTML radio input, except it's styled differently.
+ *
+ * Radio behaves like [AngularJS radio](http://docs.angularjs.org/api/ng/input/input[radio]).
+ *
+ * @usage
+ * ```html
+ * <ion-radio ng-model="choice" ng-value="'A'">Choose A</ion-radio>
+ * <ion-radio ng-model="choice" ng-value="'B'">Choose B</ion-radio>
+ * <ion-radio ng-model="choice" ng-value="'C'">Choose C</ion-radio>
+ * ```
+ *
+ * @param {string=} name The name of the radio input.
+ * @param {expression=} value The value of the radio input.
+ * @param {boolean=} disabled The state of the radio input.
+ * @param {string=} icon The icon to use when the radio input is selected.
+ * @param {expression=} ng-value Angular equivalent of the value attribute.
+ * @param {expression=} ng-model The angular model for the radio input.
+ * @param {boolean=} ng-disabled Angular equivalent of the disabled attribute.
+ * @param {expression=} ng-change Triggers given expression when radio input's model changes
+ */
+IonicModule
+.directive('ionRadio', function() {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: '?ngModel',
+ transclude: true,
+ template:
+ '<label class="item item-radio">' +
+ '<input type="radio" name="radio-group">' +
+ '<div class="radio-content">' +
+ '<div class="item-content disable-pointer-events" ng-transclude></div>' +
+ '<i class="radio-icon disable-pointer-events icon ion-checkmark"></i>' +
+ '</div>' +
+ '</label>',
+
+ compile: function(element, attr) {
+ if (attr.icon) {
+ var iconElm = element.find('i');
+ iconElm.removeClass('ion-checkmark').addClass(attr.icon);
+ }
+
+ var input = element.find('input');
+ forEach({
+ 'name': attr.name,
+ 'value': attr.value,
+ 'disabled': attr.disabled,
+ 'ng-value': attr.ngValue,
+ 'ng-model': attr.ngModel,
+ 'ng-disabled': attr.ngDisabled,
+ 'ng-change': attr.ngChange,
+ 'ng-required': attr.ngRequired,
+ 'required': attr.required
+ }, function(value, name) {
+ if (isDefined(value)) {
+ input.attr(name, value);
+ }
+ });
+
+ return function(scope, element, attr) {
+ scope.getValue = function() {
+ return scope.ngValue || attr.value;
+ };
+ };
+ }
+ };
+});
+
+
+/**
+ * @ngdoc directive
+ * @name ionRefresher
+ * @module ionic
+ * @restrict E
+ * @parent ionic.directive:ionContent, ionic.directive:ionScroll
+ * @description
+ * Allows you to add pull-to-refresh to a scrollView.
+ *
+ * Place it as the first child of your {@link ionic.directive:ionContent} or
+ * {@link ionic.directive:ionScroll} element.
+ *
+ * When refreshing is complete, $broadcast the 'scroll.refreshComplete' event
+ * from your controller.
+ *
+ * @usage
+ *
+ * ```html
+ * <ion-content ng-controller="MyController">
+ * <ion-refresher
+ * pulling-text="Pull to refresh..."
+ * on-refresh="doRefresh()">
+ * </ion-refresher>
+ * <ion-list>
+ * <ion-item ng-repeat="item in items"></ion-item>
+ * </ion-list>
+ * </ion-content>
+ * ```
+ * ```js
+ * angular.module('testApp', ['ionic'])
+ * .controller('MyController', function($scope, $http) {
+ * $scope.items = [1,2,3];
+ * $scope.doRefresh = function() {
+ * $http.get('/new-items')
+ * .success(function(newItems) {
+ * $scope.items = newItems;
+ * })
+ * .finally(function() {
+ * // Stop the ion-refresher from spinning
+ * $scope.$broadcast('scroll.refreshComplete');
+ * });
+ * };
+ * });
+ * ```
+ *
+ * @param {expression=} on-refresh Called when the user pulls down enough and lets go
+ * of the refresher.
+ * @param {expression=} on-pulling Called when the user starts to pull down
+ * on the refresher.
+ * @param {string=} pulling-text The text to display while the user is pulling down.
+ * @param {string=} pulling-icon The icon to display while the user is pulling down.
+ * Default: 'ion-android-arrow-down'.
+ * @param {string=} spinner The {@link ionic.directive:ionSpinner} icon to display
+ * after user lets go of the refresher. The SVG {@link ionic.directive:ionSpinner}
+ * is now the default, replacing rotating font icons. Set to `none` to disable both the
+ * spinner and the icon.
+ * @param {string=} refreshing-icon The font icon to display after user lets go of the
+ * refresher. This is deprecated in favor of the SVG {@link ionic.directive:ionSpinner}.
+ * @param {boolean=} disable-pulling-rotation Disables the rotation animation of the pulling
+ * icon when it reaches its activated threshold. To be used with a custom `pulling-icon`.
+ *
+ */
+IonicModule
+.directive('ionRefresher', [function() {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: ['?^$ionicScroll', 'ionRefresher'],
+ controller: '$ionicRefresher',
+ template:
+ '<div class="scroll-refresher invisible" collection-repeat-ignore>' +
+ '<div class="ionic-refresher-content" ' +
+ 'ng-class="{\'ionic-refresher-with-text\': pullingText || refreshingText}">' +
+ '<div class="icon-pulling" ng-class="{\'pulling-rotation-disabled\':disablePullingRotation}">' +
+ '<i class="icon {{pullingIcon}}"></i>' +
+ '</div>' +
+ '<div class="text-pulling" ng-bind-html="pullingText"></div>' +
+ '<div class="icon-refreshing">' +
+ '<ion-spinner ng-if="showSpinner" icon="{{spinner}}"></ion-spinner>' +
+ '<i ng-if="showIcon" class="icon {{refreshingIcon}}"></i>' +
+ '</div>' +
+ '<div class="text-refreshing" ng-bind-html="refreshingText"></div>' +
+ '</div>' +
+ '</div>',
+ link: function($scope, $element, $attrs, ctrls) {
+
+ // JS Scrolling uses the scroll controller
+ var scrollCtrl = ctrls[0],
+ refresherCtrl = ctrls[1];
+ if (!scrollCtrl || scrollCtrl.isNative()) {
+ // Kick off native scrolling
+ refresherCtrl.init();
+ } else {
+ $element[0].classList.add('js-scrolling');
+ scrollCtrl._setRefresher(
+ $scope,
+ $element[0],
+ refresherCtrl.getRefresherDomMethods()
+ );
+
+ $scope.$on('scroll.refreshComplete', function() {
+ $scope.$evalAsync(function() {
+ if(scrollCtrl.scrollView){
+ scrollCtrl.scrollView.finishPullToRefresh();
+ }
+ });
+ });
+ }
+
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionScroll
+ * @module ionic
+ * @delegate ionic.service:$ionicScrollDelegate
+ * @codepen mwFuh
+ * @restrict E
+ *
+ * @description
+ * Creates a scrollable container for all content inside.
+ *
+ * @usage
+ *
+ * Basic usage:
+ *
+ * ```html
+ * <ion-scroll zooming="true" direction="xy" style="width: 500px; height: 500px">
+ * <div style="width: 5000px; height: 5000px; background: url('https://upload.wikimedia.org/wikipedia/commons/a/ad/Europe_geological_map-en.jpg') repeat"></div>
+ * </ion-scroll>
+ * ```
+ *
+ * Note that it's important to set the height of the scroll box as well as the height of the inner
+ * content to enable scrolling. This makes it possible to have full control over scrollable areas.
+ *
+ * If you'd just like to have a center content scrolling area, use {@link ionic.directive:ionContent} instead.
+ *
+ * @param {string=} delegate-handle The handle used to identify this scrollView
+ * with {@link ionic.service:$ionicScrollDelegate}.
+ * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'.
+ * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true.
+ * @param {boolean=} paging Whether to scroll with paging.
+ * @param {expression=} on-refresh Called on pull-to-refresh, triggered by an {@link ionic.directive:ionRefresher}.
+ * @param {expression=} on-scroll Called whenever the user scrolls.
+ * @param {expression=} on-scroll-complete Called whenever the scrolling paging is completed.
+ * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true.
+ * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true.
+ * @param {boolean=} zooming Whether to support pinch-to-zoom
+ * @param {integer=} min-zoom The smallest zoom amount allowed (default is 0.5)
+ * @param {integer=} max-zoom The largest zoom amount allowed (default is 3)
+ * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges
+ * of the content. Defaults to true on iOS, false on Android.
+ */
+IonicModule
+.directive('ionScroll', [
+ '$timeout',
+ '$controller',
+ '$ionicBind',
+ '$ionicConfig',
+function($timeout, $controller, $ionicBind, $ionicConfig) {
+ return {
+ restrict: 'E',
+ scope: true,
+ controller: function() {},
+ compile: function(element, attr) {
+ element.addClass('scroll-view ionic-scroll');
+
+ //We cannot transclude here because it breaks element.data() inheritance on compile
+ var innerElement = jqLite('<div class="scroll"></div>');
+ innerElement.append(element.contents());
+ element.append(innerElement);
+
+ var nativeScrolling = attr.overflowScroll !== "false" && (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling());
+
+ return { pre: prelink };
+ function prelink($scope, $element, $attr) {
+ $ionicBind($scope, $attr, {
+ direction: '@',
+ paging: '@',
+ $onScroll: '&onScroll',
+ $onScrollComplete: '&onScrollComplete',
+ scroll: '@',
+ scrollbarX: '@',
+ scrollbarY: '@',
+ zooming: '@',
+ minZoom: '@',
+ maxZoom: '@'
+ });
+ $scope.direction = $scope.direction || 'y';
+
+ if (isDefined($attr.padding)) {
+ $scope.$watch($attr.padding, function(newVal) {
+ innerElement.toggleClass('padding', !!newVal);
+ });
+ }
+ if ($scope.$eval($scope.paging) === true) {
+ innerElement.addClass('scroll-paging');
+ }
+
+ if (!$scope.direction) { $scope.direction = 'y'; }
+ var isPaging = $scope.$eval($scope.paging) === true;
+
+ if (nativeScrolling) {
+ $element.addClass('overflow-scroll');
+ }
+
+ $element.addClass('scroll-' + $scope.direction);
+
+ var scrollViewOptions = {
+ el: $element[0],
+ delegateHandle: $attr.delegateHandle,
+ locking: ($attr.locking || 'true') === 'true',
+ bouncing: $scope.$eval($attr.hasBouncing),
+ paging: isPaging,
+ scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
+ scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
+ scrollingX: $scope.direction.indexOf('x') >= 0,
+ scrollingY: $scope.direction.indexOf('y') >= 0,
+ zooming: $scope.$eval($scope.zooming) === true,
+ maxZoom: $scope.$eval($scope.maxZoom) || 3,
+ minZoom: $scope.$eval($scope.minZoom) || 0.5,
+ preventDefault: true,
+ nativeScrolling: nativeScrolling,
+ scrollingComplete: onScrollComplete
+ };
+
+ if (isPaging) {
+ scrollViewOptions.speedMultiplier = 0.8;
+ scrollViewOptions.bouncing = false;
+ }
+
+ var scrollCtrl = $controller('$ionicScroll', {
+ $scope: $scope,
+ scrollViewOptions: scrollViewOptions
+ });
+
+ function onScrollComplete() {
+ $scope.$onScrollComplete && $scope.$onScrollComplete({
+ scrollTop: scrollCtrl.scrollView.__scrollTop,
+ scrollLeft: scrollCtrl.scrollView.__scrollLeft
+ });
+ }
+ }
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionSideMenu
+ * @module ionic
+ * @restrict E
+ * @parent ionic.directive:ionSideMenus
+ *
+ * @description
+ * A container for a side menu, sibling to an {@link ionic.directive:ionSideMenuContent} directive.
+ *
+ * @usage
+ * ```html
+ * <ion-side-menu
+ * side="left"
+ * width="myWidthValue + 20"
+ * is-enabled="shouldLeftSideMenuBeEnabled()">
+ * </ion-side-menu>
+ * ```
+ * For a complete side menu example, see the
+ * {@link ionic.directive:ionSideMenus} documentation.
+ *
+ * @param {string} side Which side the side menu is currently on. Allowed values: 'left' or 'right'.
+ * @param {boolean=} is-enabled Whether this side menu is enabled.
+ * @param {number=} width How many pixels wide the side menu should be. Defaults to 275.
+ */
+IonicModule
+.directive('ionSideMenu', function() {
+ return {
+ restrict: 'E',
+ require: '^ionSideMenus',
+ scope: true,
+ compile: function(element, attr) {
+ angular.isUndefined(attr.isEnabled) && attr.$set('isEnabled', 'true');
+ angular.isUndefined(attr.width) && attr.$set('width', '275');
+
+ element.addClass('menu menu-' + attr.side);
+
+ return function($scope, $element, $attr, sideMenuCtrl) {
+ $scope.side = $attr.side || 'left';
+
+ var sideMenu = sideMenuCtrl[$scope.side] = new ionic.views.SideMenu({
+ width: attr.width,
+ el: $element[0],
+ isEnabled: true
+ });
+
+ $scope.$watch($attr.width, function(val) {
+ var numberVal = +val;
+ if (numberVal && numberVal == val) {
+ sideMenu.setWidth(+val);
+ }
+ });
+ $scope.$watch($attr.isEnabled, function(val) {
+ sideMenu.setIsEnabled(!!val);
+ });
+ };
+ }
+ };
+});
+
+
+/**
+ * @ngdoc directive
+ * @name ionSideMenuContent
+ * @module ionic
+ * @restrict E
+ * @parent ionic.directive:ionSideMenus
+ *
+ * @description
+ * A container for the main visible content, sibling to one or more
+ * {@link ionic.directive:ionSideMenu} directives.
+ *
+ * @usage
+ * ```html
+ * <ion-side-menu-content
+ * edge-drag-threshold="true"
+ * drag-content="true">
+ * </ion-side-menu-content>
+ * ```
+ * For a complete side menu example, see the
+ * {@link ionic.directive:ionSideMenus} documentation.
+ *
+ * @param {boolean=} drag-content Whether the content can be dragged. Default true.
+ * @param {boolean|number=} edge-drag-threshold Whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Default false. Accepts three types of values:
+ * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu.
+ * - If true is given, the default number of pixels (25) is used as the maximum allowed distance.
+ * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed.
+ *
+ */
+IonicModule
+.directive('ionSideMenuContent', [
+ '$timeout',
+ '$ionicGesture',
+ '$window',
+function($timeout, $ionicGesture, $window) {
+
+ return {
+ restrict: 'EA', //DEPRECATED 'A'
+ require: '^ionSideMenus',
+ scope: true,
+ compile: function(element, attr) {
+ element.addClass('menu-content pane');
+
+ return { pre: prelink };
+ function prelink($scope, $element, $attr, sideMenuCtrl) {
+ var startCoord = null;
+ var primaryScrollAxis = null;
+
+ if (isDefined(attr.dragContent)) {
+ $scope.$watch(attr.dragContent, function(value) {
+ sideMenuCtrl.canDragContent(value);
+ });
+ } else {
+ sideMenuCtrl.canDragContent(true);
+ }
+
+ if (isDefined(attr.edgeDragThreshold)) {
+ $scope.$watch(attr.edgeDragThreshold, function(value) {
+ sideMenuCtrl.edgeDragThreshold(value);
+ });
+ }
+
+ // Listen for taps on the content to close the menu
+ function onContentTap(gestureEvt) {
+ if (sideMenuCtrl.getOpenAmount() !== 0) {
+ sideMenuCtrl.close();
+ gestureEvt.gesture.srcEvent.preventDefault();
+ startCoord = null;
+ primaryScrollAxis = null;
+ } else if (!startCoord) {
+ startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
+ }
+ }
+
+ function onDragX(e) {
+ if (!sideMenuCtrl.isDraggableTarget(e)) return;
+
+ if (getPrimaryScrollAxis(e) == 'x') {
+ sideMenuCtrl._handleDrag(e);
+ e.gesture.srcEvent.preventDefault();
+ }
+ }
+
+ function onDragY(e) {
+ if (getPrimaryScrollAxis(e) == 'x') {
+ e.gesture.srcEvent.preventDefault();
+ }
+ }
+
+ function onDragRelease(e) {
+ sideMenuCtrl._endDrag(e);
+ startCoord = null;
+ primaryScrollAxis = null;
+ }
+
+ function getPrimaryScrollAxis(gestureEvt) {
+ // gets whether the user is primarily scrolling on the X or Y
+ // If a majority of the drag has been on the Y since the start of
+ // the drag, but the X has moved a little bit, it's still a Y drag
+
+ if (primaryScrollAxis) {
+ // we already figured out which way they're scrolling
+ return primaryScrollAxis;
+ }
+
+ if (gestureEvt && gestureEvt.gesture) {
+
+ if (!startCoord) {
+ // get the starting point
+ startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
+
+ } else {
+ // we already have a starting point, figure out which direction they're going
+ var endCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
+
+ var xDistance = Math.abs(endCoord.x - startCoord.x);
+ var yDistance = Math.abs(endCoord.y - startCoord.y);
+
+ var scrollAxis = (xDistance < yDistance ? 'y' : 'x');
+
+ if (Math.max(xDistance, yDistance) > 30) {
+ // ok, we pretty much know which way they're going
+ // let's lock it in
+ primaryScrollAxis = scrollAxis;
+ }
+
+ return scrollAxis;
+ }
+ }
+ return 'y';
+ }
+
+ var content = {
+ element: element[0],
+ onDrag: function() {},
+ endDrag: function() {},
+ setCanScroll: function(canScroll) {
+ var c = $element[0].querySelector('.scroll');
+
+ if (!c) {
+ return;
+ }
+
+ var content = angular.element(c.parentElement);
+ if (!content) {
+ return;
+ }
+
+ // freeze our scroll container if we have one
+ var scrollScope = content.scope();
+ scrollScope.scrollCtrl && scrollScope.scrollCtrl.freezeScrollShut(!canScroll);
+ },
+ getTranslateX: function() {
+ return $scope.sideMenuContentTranslateX || 0;
+ },
+ setTranslateX: ionic.animationFrameThrottle(function(amount) {
+ var xTransform = content.offsetX + amount;
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + xTransform + 'px,0,0)';
+ $timeout(function() {
+ $scope.sideMenuContentTranslateX = amount;
+ });
+ }),
+ setMarginLeft: ionic.animationFrameThrottle(function(amount) {
+ if (amount) {
+ amount = parseInt(amount, 10);
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amount + 'px,0,0)';
+ $element[0].style.width = ($window.innerWidth - amount) + 'px';
+ content.offsetX = amount;
+ } else {
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
+ $element[0].style.width = '';
+ content.offsetX = 0;
+ }
+ }),
+ setMarginRight: ionic.animationFrameThrottle(function(amount) {
+ if (amount) {
+ amount = parseInt(amount, 10);
+ $element[0].style.width = ($window.innerWidth - amount) + 'px';
+ content.offsetX = amount;
+ } else {
+ $element[0].style.width = '';
+ content.offsetX = 0;
+ }
+ // reset incase left gets grabby
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
+ }),
+ setMarginLeftAndRight: ionic.animationFrameThrottle(function(amountLeft, amountRight) {
+ amountLeft = amountLeft && parseInt(amountLeft, 10) || 0;
+ amountRight = amountRight && parseInt(amountRight, 10) || 0;
+
+ var amount = amountLeft + amountRight;
+
+ if (amount > 0) {
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amountLeft + 'px,0,0)';
+ $element[0].style.width = ($window.innerWidth - amount) + 'px';
+ content.offsetX = amountLeft;
+ } else {
+ $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
+ $element[0].style.width = '';
+ content.offsetX = 0;
+ }
+ // reset incase left gets grabby
+ //$element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
+ }),
+ enableAnimation: function() {
+ $scope.animationEnabled = true;
+ $element[0].classList.add('menu-animated');
+ },
+ disableAnimation: function() {
+ $scope.animationEnabled = false;
+ $element[0].classList.remove('menu-animated');
+ },
+ offsetX: 0
+ };
+
+ sideMenuCtrl.setContent(content);
+
+ // add gesture handlers
+ var gestureOpts = { stop_browser_behavior: false };
+ gestureOpts.prevent_default_directions = ['left', 'right'];
+ var contentTapGesture = $ionicGesture.on('tap', onContentTap, $element, gestureOpts);
+ var dragRightGesture = $ionicGesture.on('dragright', onDragX, $element, gestureOpts);
+ var dragLeftGesture = $ionicGesture.on('dragleft', onDragX, $element, gestureOpts);
+ var dragUpGesture = $ionicGesture.on('dragup', onDragY, $element, gestureOpts);
+ var dragDownGesture = $ionicGesture.on('dragdown', onDragY, $element, gestureOpts);
+ var releaseGesture = $ionicGesture.on('release', onDragRelease, $element, gestureOpts);
+
+ // Cleanup
+ $scope.$on('$destroy', function() {
+ if (content) {
+ content.element = null;
+ content = null;
+ }
+ $ionicGesture.off(dragLeftGesture, 'dragleft', onDragX);
+ $ionicGesture.off(dragRightGesture, 'dragright', onDragX);
+ $ionicGesture.off(dragUpGesture, 'dragup', onDragY);
+ $ionicGesture.off(dragDownGesture, 'dragdown', onDragY);
+ $ionicGesture.off(releaseGesture, 'release', onDragRelease);
+ $ionicGesture.off(contentTapGesture, 'tap', onContentTap);
+ });
+ }
+ }
+ };
+}]);
+
+IonicModule
+
+/**
+ * @ngdoc directive
+ * @name ionSideMenus
+ * @module ionic
+ * @delegate ionic.service:$ionicSideMenuDelegate
+ * @restrict E
+ *
+ * @description
+ * A container element for side menu(s) and the main content. Allows the left and/or right side menu
+ * to be toggled by dragging the main content area side to side.
+ *
+ * To automatically close an opened menu, you can add the {@link ionic.directive:menuClose} attribute
+ * directive. The `menu-close` attribute is usually added to links and buttons within
+ * `ion-side-menu-content`, so that when the element is clicked, the opened side menu will
+ * automatically close.
+ *
+ * "Burger Icon" toggles can be added to the header with the {@link ionic.directive:menuToggle}
+ * attribute directive. Clicking the toggle will open and close the side menu like the `menu-close`
+ * directive. The side menu will automatically hide on child pages, but can be overridden with the
+ * enable-menu-with-back-views attribute mentioned below.
+ *
+ * By default, side menus are hidden underneath their side menu content and can be opened by swiping
+ * the content left or right or by toggling a button to show the side menu. Additionally, by adding the
+ * {@link ionic.directive:exposeAsideWhen} attribute directive to an
+ * {@link ionic.directive:ionSideMenu} element directive, a side menu can be given instructions about
+ * "when" the menu should be exposed (always viewable).
+ *
+ * ![Side Menu](http://ionicframework.com.s3.amazonaws.com/docs/controllers/sidemenu.gif)
+ *
+ * For more information on side menus, check out:
+ *
+ * - {@link ionic.directive:ionSideMenuContent}
+ * - {@link ionic.directive:ionSideMenu}
+ * - {@link ionic.directive:menuToggle}
+ * - {@link ionic.directive:menuClose}
+ * - {@link ionic.directive:exposeAsideWhen}
+ *
+ * @usage
+ * To use side menus, add an `<ion-side-menus>` parent element. This will encompass all pages that have a
+ * side menu, and have at least 2 child elements: 1 `<ion-side-menu-content>` for the center content,
+ * and one or more `<ion-side-menu>` directives for each side menu(left/right) that you wish to place.
+ *
+ * ```html
+ * <ion-side-menus>
+ * <!-- Left menu -->
+ * <ion-side-menu side="left">
+ * </ion-side-menu>
+ *
+ * <ion-side-menu-content>
+ * <!-- Main content, usually <ion-nav-view> -->
+ * </ion-side-menu-content>
+ *
+ * <!-- Right menu -->
+ * <ion-side-menu side="right">
+ * </ion-side-menu>
+ *
+ * </ion-side-menus>
+ * ```
+ * ```js
+ * function ContentController($scope, $ionicSideMenuDelegate) {
+ * $scope.toggleLeft = function() {
+ * $ionicSideMenuDelegate.toggleLeft();
+ * };
+ * }
+ * ```
+ *
+ * @param {bool=} enable-menu-with-back-views Determines whether the side menu is enabled when the
+ * back button is showing. When set to `false`, any {@link ionic.directive:menuToggle} will be hidden,
+ * and the user cannot swipe to open the menu. When going back to the root page of the side menu (the
+ * page without a back button visible), then any menuToggle buttons will show again, and menus will be
+ * enabled again.
+ * @param {string=} delegate-handle The handle used to identify this side menu
+ * with {@link ionic.service:$ionicSideMenuDelegate}.
+ *
+ */
+.directive('ionSideMenus', ['$ionicBody', function($ionicBody) {
+ return {
+ restrict: 'ECA',
+ controller: '$ionicSideMenus',
+ compile: function(element, attr) {
+ attr.$set('class', (attr['class'] || '') + ' view');
+
+ return { pre: prelink };
+ function prelink($scope, $element, $attrs, ctrl) {
+
+ ctrl.enableMenuWithBackViews($scope.$eval($attrs.enableMenuWithBackViews));
+
+ $scope.$on('$ionicExposeAside', function(evt, isAsideExposed) {
+ if (!$scope.$exposeAside) $scope.$exposeAside = {};
+ $scope.$exposeAside.active = isAsideExposed;
+ $ionicBody.enableClass(isAsideExposed, 'aside-open');
+ });
+
+ $scope.$on('$ionicView.beforeEnter', function(ev, d) {
+ if (d.historyId) {
+ $scope.$activeHistoryId = d.historyId;
+ }
+ });
+
+ $scope.$on('$destroy', function() {
+ $ionicBody.removeClass('menu-open', 'aside-open');
+ });
+
+ }
+ }
+ };
+}]);
+
+
+/**
+ * @ngdoc directive
+ * @name ionSlideBox
+ * @module ionic
+ * @codepen AjgEB
+ * @deprecated will be removed in the next Ionic release in favor of the new ion-slides component.
+ * Don't depend on the internal behavior of this widget.
+ * @delegate ionic.service:$ionicSlideBoxDelegate
+ * @restrict E
+ * @description
+ * The Slide Box is a multi-page container where each page can be swiped or dragged between:
+ *
+ *
+ * @usage
+ * ```html
+ * <ion-slide-box on-slide-changed="slideHasChanged($index)">
+ * <ion-slide>
+ * <div class="box blue"><h1>BLUE</h1></div>
+ * </ion-slide>
+ * <ion-slide>
+ * <div class="box yellow"><h1>YELLOW</h1></div>
+ * </ion-slide>
+ * <ion-slide>
+ * <div class="box pink"><h1>PINK</h1></div>
+ * </ion-slide>
+ * </ion-slide-box>
+ * ```
+ *
+ * @param {string=} delegate-handle The handle used to identify this slideBox
+ * with {@link ionic.service:$ionicSlideBoxDelegate}.
+ * @param {boolean=} does-continue Whether the slide box should loop.
+ * @param {boolean=} auto-play Whether the slide box should automatically slide. Default true if does-continue is true.
+ * @param {number=} slide-interval How many milliseconds to wait to change slides (if does-continue is true). Defaults to 4000.
+ * @param {boolean=} show-pager Whether a pager should be shown for this slide box. Accepts expressions via `show-pager="{{shouldShow()}}"`. Defaults to true.
+ * @param {expression=} pager-click Expression to call when a pager is clicked (if show-pager is true). Is passed the 'index' variable.
+ * @param {expression=} on-slide-changed Expression called whenever the slide is changed. Is passed an '$index' variable.
+ * @param {expression=} active-slide Model to bind the current slide index to.
+ */
+IonicModule
+.directive('ionSlideBox', [
+ '$animate',
+ '$timeout',
+ '$compile',
+ '$ionicSlideBoxDelegate',
+ '$ionicHistory',
+ '$ionicScrollDelegate',
+function($animate, $timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory, $ionicScrollDelegate) {
+ return {
+ restrict: 'E',
+ replace: true,
+ transclude: true,
+ scope: {
+ autoPlay: '=',
+ doesContinue: '@',
+ slideInterval: '@',
+ showPager: '@',
+ pagerClick: '&',
+ disableScroll: '@',
+ onSlideChanged: '&',
+ activeSlide: '=?',
+ bounce: '@'
+ },
+ controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
+ var _this = this;
+
+ var continuous = $scope.$eval($scope.doesContinue) === true;
+ var bouncing = ($scope.$eval($scope.bounce) !== false); //Default to true
+ var shouldAutoPlay = isDefined($attrs.autoPlay) ? !!$scope.autoPlay : false;
+ var slideInterval = shouldAutoPlay ? $scope.$eval($scope.slideInterval) || 4000 : 0;
+
+ var slider = new ionic.views.Slider({
+ el: $element[0],
+ auto: slideInterval,
+ continuous: continuous,
+ startSlide: $scope.activeSlide,
+ bouncing: bouncing,
+ slidesChanged: function() {
+ $scope.currentSlide = slider.currentIndex();
+
+ // Try to trigger a digest
+ $timeout(function() {});
+ },
+ callback: function(slideIndex) {
+ $scope.currentSlide = slideIndex;
+ $scope.onSlideChanged({ index: $scope.currentSlide, $index: $scope.currentSlide});
+ $scope.$parent.$broadcast('slideBox.slideChanged', slideIndex);
+ $scope.activeSlide = slideIndex;
+ // Try to trigger a digest
+ $timeout(function() {});
+ },
+ onDrag: function() {
+ freezeAllScrolls(true);
+ },
+ onDragEnd: function() {
+ freezeAllScrolls(false);
+ }
+ });
+
+ function freezeAllScrolls(shouldFreeze) {
+ if (shouldFreeze && !_this.isScrollFreeze) {
+ $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);
+
+ } else if (!shouldFreeze && _this.isScrollFreeze) {
+ $ionicScrollDelegate.freezeAllScrolls(false);
+ }
+ _this.isScrollFreeze = shouldFreeze;
+ }
+
+ slider.enableSlide($scope.$eval($attrs.disableScroll) !== true);
+
+ $scope.$watch('activeSlide', function(nv) {
+ if (isDefined(nv)) {
+ slider.slide(nv);
+ }
+ });
+
+ $scope.$on('slideBox.nextSlide', function() {
+ slider.next();
+ });
+
+ $scope.$on('slideBox.prevSlide', function() {
+ slider.prev();
+ });
+
+ $scope.$on('slideBox.setSlide', function(e, index) {
+ slider.slide(index);
+ });
+
+ //Exposed for testing
+ this.__slider = slider;
+
+ var deregisterInstance = $ionicSlideBoxDelegate._registerInstance(
+ slider, $attrs.delegateHandle, function() {
+ return $ionicHistory.isActiveScope($scope);
+ }
+ );
+ $scope.$on('$destroy', function() {
+ deregisterInstance();
+ slider.kill();
+ });
+
+ this.slidesCount = function() {
+ return slider.slidesCount();
+ };
+
+ this.onPagerClick = function(index) {
+ $scope.pagerClick({index: index});
+ };
+
+ $timeout(function() {
+ slider.load();
+ });
+ }],
+ template: '<div class="slider">' +
+ '<div class="slider-slides" ng-transclude>' +
+ '</div>' +
+ '</div>',
+
+ link: function($scope, $element, $attr) {
+ // Disable ngAnimate for slidebox and its children
+ $animate.enabled($element, false);
+
+ // if showPager is undefined, show the pager
+ if (!isDefined($attr.showPager)) {
+ $scope.showPager = true;
+ getPager().toggleClass('hide', !true);
+ }
+
+ $attr.$observe('showPager', function(show) {
+ if (show === undefined) return;
+ show = $scope.$eval(show);
+ getPager().toggleClass('hide', !show);
+ });
+
+ var pager;
+ function getPager() {
+ if (!pager) {
+ var childScope = $scope.$new();
+ pager = jqLite('<ion-pager></ion-pager>');
+ $element.append(pager);
+ pager = $compile(pager)(childScope);
+ }
+ return pager;
+ }
+ }
+ };
+}])
+.directive('ionSlide', function() {
+ return {
+ restrict: 'E',
+ require: '?^ionSlideBox',
+ compile: function(element) {
+ element.addClass('slider-slide');
+ }
+ };
+})
+
+.directive('ionPager', function() {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: '^ionSlideBox',
+ template: '<div class="slider-pager"><span class="slider-pager-page" ng-repeat="slide in numSlides() track by $index" ng-class="{active: $index == currentSlide}" ng-click="pagerClick($index)"><i class="icon ion-record"></i></span></div>',
+ link: function($scope, $element, $attr, slideBox) {
+ var selectPage = function(index) {
+ var children = $element[0].children;
+ var length = children.length;
+ for (var i = 0; i < length; i++) {
+ if (i == index) {
+ children[i].classList.add('active');
+ } else {
+ children[i].classList.remove('active');
+ }
+ }
+ };
+
+ $scope.pagerClick = function(index) {
+ slideBox.onPagerClick(index);
+ };
+
+ $scope.numSlides = function() {
+ return new Array(slideBox.slidesCount());
+ };
+
+ $scope.$watch('currentSlide', function(v) {
+ selectPage(v);
+ });
+ }
+ };
+
+});
+
+
+/**
+ * @ngdoc directive
+ * @name ionSlides
+ * @module ionic
+ * @restrict E
+ * @description
+ * The Slides component is a powerful multi-page container where each page can be swiped or dragged between.
+ *
+ * Note: this is a new version of the Ionic Slide Box based on the [Swiper](http://www.idangero.us/swiper/#.Vmc1J-ODFBc) widget from
+ * [idangerous](http://www.idangero.us/).
+ *
+ * ![SlideBox](http://ionicframework.com.s3.amazonaws.com/docs/controllers/slideBox.gif)
+ *
+ * @usage
+ * ```html
+ * <ion-content scroll="false">
+ * <ion-slides options="options" slider="data.slider">
+ * <ion-slide-page>
+ * <div class="box blue"><h1>BLUE</h1></div>
+ * </ion-slide-page>
+ * <ion-slide-page>
+ * <div class="box yellow"><h1>YELLOW</h1></div>
+ * </ion-slide-page>
+ * <ion-slide-page>
+ * <div class="box pink"><h1>PINK</h1></div>
+ * </ion-slide-page>
+ * </ion-slides>
+ * </ion-content>
+ * ```
+ *
+ * ```js
+ * $scope.options = {
+ * loop: false,
+ * effect: 'fade',
+ * speed: 500,
+ * }
+ *
+ * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){
+ * // data.slider is the instance of Swiper
+ * $scope.slider = data.slider;
+ * });
+ *
+ * $scope.$on("$ionicSlides.slideChangeStart", function(event, data){
+ * console.log('Slide change is beginning');
+ * });
+ *
+ * $scope.$on("$ionicSlides.slideChangeEnd", function(event, data){
+ * // note: the indexes are 0-based
+ * $scope.activeIndex = data.slider.activeIndex;
+ * $scope.previousIndex = data.slider.previousIndex;
+ * });
+ *
+ * ```
+ *
+ * ## Slide Events
+ *
+ * The slides component dispatches events when the active slide changes
+ *
+ * <table class="table">
+ * <tr>
+ * <td><code>$ionicSlides.slideChangeStart</code></td>
+ * <td>This event is emitted when a slide change begins</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicSlides.slideChangeEnd</code></td>
+ * <td>This event is emitted when a slide change completes</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicSlides.sliderInitialized</code></td>
+ * <td>This event is emitted when the slider is initialized. It provides access to an instance of the slider.</td>
+ * </tr>
+ * </table>
+ *
+ *
+ * ## Updating Slides Dynamically
+ * When applying data to the slider at runtime, typically everything will work as expected.
+ *
+ * In the event that the slides are looped, use the `updateLoop` method on the slider to ensure the slides update correctly.
+ *
+ * ```
+ * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){
+ * // grab an instance of the slider
+ * $scope.slider = data.slider;
+ * });
+ *
+ * function dataChangeHandler(){
+ * // call this function when data changes, such as an HTTP request, etc
+ * if ( $scope.slider ){
+ * $scope.slider.updateLoop();
+ * }
+ * }
+ * ```
+ *
+ */
+IonicModule
+.directive('ionSlides', [
+ '$animate',
+ '$timeout',
+ '$compile',
+function($animate, $timeout, $compile) {
+ return {
+ restrict: 'E',
+ transclude: true,
+ scope: {
+ options: '=',
+ slider: '='
+ },
+ template: '<div class="swiper-container">' +
+ '<div class="swiper-wrapper" ng-transclude>' +
+ '</div>' +
+ '<div ng-hide="!showPager" class="swiper-pagination"></div>' +
+ '</div>',
+ controller: ['$scope', '$element', function($scope, $element) {
+ var _this = this;
+
+ this.update = function() {
+ $timeout(function() {
+ if (!_this.__slider) {
+ return;
+ }
+
+ _this.__slider.update();
+ if (_this._options.loop) {
+ _this.__slider.createLoop();
+ }
+
+ var slidesLength = _this.__slider.slides.length;
+
+ // Don't allow pager to show with > 10 slides
+ if (slidesLength > 10) {
+ $scope.showPager = false;
+ }
+
+ // When slide index is greater than total then slide to last index
+ if (_this.__slider.activeIndex > slidesLength - 1) {
+ _this.__slider.slideTo(slidesLength - 1);
+ }
+ });
+ };
+
+ this.rapidUpdate = ionic.debounce(function() {
+ _this.update();
+ }, 50);
+
+ this.getSlider = function() {
+ return _this.__slider;
+ };
+
+ var options = $scope.options || {};
+
+ var newOptions = angular.extend({
+ pagination: $element.children().children()[1],
+ paginationClickable: true,
+ lazyLoading: true,
+ preloadImages: false
+ }, options);
+
+ this._options = newOptions;
+
+ $timeout(function() {
+ var slider = new ionic.views.Swiper($element.children()[0], newOptions, $scope, $compile);
+
+ $scope.$emit("$ionicSlides.sliderInitialized", { slider: slider });
+
+ _this.__slider = slider;
+ $scope.slider = _this.__slider;
+
+ $scope.$on('$destroy', function() {
+ slider.destroy();
+ _this.__slider = null;
+ });
+ });
+
+ $timeout(function() {
+ // if it's a loop, render the slides again just incase
+ _this.rapidUpdate();
+ }, 200);
+
+ }],
+
+ link: function($scope) {
+ $scope.showPager = true;
+ // Disable ngAnimate for slidebox and its children
+ //$animate.enabled(false, $element);
+ }
+ };
+}])
+.directive('ionSlidePage', [function() {
+ return {
+ restrict: 'E',
+ require: '?^ionSlides',
+ transclude: true,
+ replace: true,
+ template: '<div class="swiper-slide" ng-transclude></div>',
+ link: function($scope, $element, $attr, ionSlidesCtrl) {
+ ionSlidesCtrl.rapidUpdate();
+
+ $scope.$on('$destroy', function() {
+ ionSlidesCtrl.rapidUpdate();
+ });
+ }
+ };
+}]);
+
+/**
+* @ngdoc directive
+* @name ionSpinner
+* @module ionic
+* @restrict E
+ *
+ * @description
+ * The `ionSpinner` directive provides a variety of animated spinners.
+ * Spinners enables you to give your users feedback that the app is
+ * processing/thinking/waiting/chillin' out, or whatever you'd like it to indicate.
+ * By default, the {@link ionic.directive:ionRefresher} feature uses this spinner, rather
+ * than rotating font icons (previously included in [ionicons](http://ionicons.com/)).
+ * While font icons are great for simple or stationary graphics, they're not suited to
+ * provide great animations, which is why Ionic uses SVG instead.
+ *
+ * Ionic offers ten spinners out of the box, and by default, it will use the appropriate spinner
+ * for the platform on which it's running. Under the hood, the `ionSpinner` directive dynamically
+ * builds the required SVG element, which allows Ionic to provide all ten of the animated SVGs
+ * within 3KB.
+ *
+ * <style>
+ * .spinner-table {
+ * max-width: 280px;
+ * }
+ * .spinner-table tbody > tr > th, .spinner-table tbody > tr > td {
+ * vertical-align: middle;
+ * width: 42px;
+ * height: 42px;
+ * }
+ * .spinner {
+ * stroke: #444;
+ * fill: #444; }
+ * .spinner svg {
+ * width: 28px;
+ * height: 28px; }
+ * .spinner.spinner-inverse {
+ * stroke: #fff;
+ * fill: #fff; }
+ *
+ * .spinner-android {
+ * stroke: #4b8bf4; }
+ *
+ * .spinner-ios, .spinner-ios-small {
+ * stroke: #69717d; }
+ *
+ * .spinner-spiral .stop1 {
+ * stop-color: #fff;
+ * stop-opacity: 0; }
+ * .spinner-spiral.spinner-inverse .stop1 {
+ * stop-color: #000; }
+ * .spinner-spiral.spinner-inverse .stop2 {
+ * stop-color: #fff; }
+ * </style>
+ *
+ * <script src="http://code.ionicframework.com/nightly/js/ionic.bundle.min.js"></script>
+ * <table class="table spinner-table" ng-app="ionic">
+ * <tr>
+ * <th>
+ * <code>android</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="android"></ion-spinner>
+ * </td>
+ * </tr>
+ * <tr>
+ * <th>
+ * <code>ios</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="ios"></ion-spinner>
+ * </td>
+ * </tr>
+ * <tr>
+ * <th>
+ * <code>ios-small</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="ios-small"></ion-spinner>
+ * </td>
+ * </tr>
+ * <tr>
+ * <th>
+ * <code>bubbles</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="bubbles"></ion-spinner>
+ * </td>
+ * </tr>
+ * <tr>
+ * <th>
+ * <code>circles</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="circles"></ion-spinner>
+ * </td>
+ * </tr>
+ * <tr>
+ * <th>
+ * <code>crescent</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="crescent"></ion-spinner>
+ * </td>
+ * </tr>
+ * <tr>
+ * <th>
+ * <code>dots</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="dots"></ion-spinner>
+ * </td>
+ * </tr>
+ * <tr>
+ * <th>
+ * <code>lines</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="lines"></ion-spinner>
+ * </td>
+ * </tr>
+ * <tr>
+ * <th>
+ * <code>ripple</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="ripple"></ion-spinner>
+ * </td>
+ * </tr>
+ * <tr>
+ * <th>
+ * <code>spiral</code>
+ * </th>
+ * <td>
+ * <ion-spinner icon="spiral"></ion-spinner>
+ * </td>
+ * </tr>
+ * </table>
+ *
+ * Each spinner uses SVG with SMIL animations, however, the Android spinner also uses JavaScript
+ * so it also works on Android 4.0-4.3. Additionally, each spinner can be styled with CSS,
+ * and scaled to any size.
+ *
+ *
+ * @usage
+ * The following code would use the default spinner for the platform it's running from. If it's neither
+ * iOS or Android, it'll default to use `ios`.
+ *
+ * ```html
+ * <ion-spinner></ion-spinner>
+ * ```
+ *
+ * By setting the `icon` attribute, you can specify which spinner to use, no matter what
+ * the platform is.
+ *
+ * ```html
+ * <ion-spinner icon="spiral"></ion-spinner>
+ * ```
+ *
+ * ## Spinner Colors
+ * Like with most of Ionic's other components, spinners can also be styled using
+ * Ionic's standard color naming convention. For example:
+ *
+ * ```html
+ * <ion-spinner class="spinner-energized"></ion-spinner>
+ * ```
+ *
+ *
+ * ## Styling SVG with CSS
+ * One cool thing about SVG is its ability to be styled with CSS! Some of the properties
+ * have different names, for example, SVG uses the term `stroke` instead of `border`, and
+ * `fill` instead of `background-color`.
+ *
+ * ```css
+ * .spinner svg {
+ * width: 28px;
+ * height: 28px;
+ * stroke: #444;
+ * fill: #444;
+ * }
+ * ```
+ *
+*/
+IonicModule
+.directive('ionSpinner', function() {
+ return {
+ restrict: 'E',
+ controller: '$ionicSpinner',
+ link: function($scope, $element, $attrs, ctrl) {
+ var spinnerName = ctrl.init();
+ $element.addClass('spinner spinner-' + spinnerName);
+
+ $element.on('$destroy', function onDestroy() {
+ ctrl.stop();
+ });
+ }
+ };
+});
+
+/**
+ * @ngdoc directive
+ * @name ionTab
+ * @module ionic
+ * @restrict E
+ * @parent ionic.directive:ionTabs
+ *
+ * @description
+ * Contains a tab's content. The content only exists while the given tab is selected.
+ *
+ * Each ionTab has its own view history.
+ *
+ * @usage
+ * ```html
+ * <ion-tab
+ * title="Tab!"
+ * icon="my-icon"
+ * href="#/tab/tab-link"
+ * on-select="onTabSelected()"
+ * on-deselect="onTabDeselected()">
+ * </ion-tab>
+ * ```
+ * For a complete, working tab bar example, see the {@link ionic.directive:ionTabs} documentation.
+ *
+ * @param {string} title The title of the tab.
+ * @param {string=} href The link that this tab will navigate to when tapped.
+ * @param {string=} icon The icon of the tab. If given, this will become the default for icon-on and icon-off.
+ * @param {string=} icon-on The icon of the tab while it is selected.
+ * @param {string=} icon-off The icon of the tab while it is not selected.
+ * @param {expression=} badge The badge to put on this tab (usually a number).
+ * @param {expression=} badge-style The style of badge to put on this tab (eg: badge-positive).
+ * @param {expression=} on-select Called when this tab is selected.
+ * @param {expression=} on-deselect Called when this tab is deselected.
+ * @param {expression=} ng-click By default, the tab will be selected on click. If ngClick is set, it will not. You can explicitly switch tabs using {@link ionic.service:$ionicTabsDelegate#select $ionicTabsDelegate.select()}.
+ * @param {expression=} hidden Whether the tab is to be hidden or not.
+ * @param {expression=} disabled Whether the tab is to be disabled or not.
+ */
+IonicModule
+.directive('ionTab', [
+ '$compile',
+ '$ionicConfig',
+ '$ionicBind',
+ '$ionicViewSwitcher',
+function($compile, $ionicConfig, $ionicBind, $ionicViewSwitcher) {
+
+ //Returns ' key="value"' if value exists
+ function attrStr(k, v) {
+ return isDefined(v) ? ' ' + k + '="' + v + '"' : '';
+ }
+ return {
+ restrict: 'E',
+ require: ['^ionTabs', 'ionTab'],
+ controller: '$ionicTab',
+ scope: true,
+ compile: function(element, attr) {
+
+ //We create the tabNavTemplate in the compile phase so that the
+ //attributes we pass down won't be interpolated yet - we want
+ //to pass down the 'raw' versions of the attributes
+ var tabNavTemplate = '<ion-tab-nav' +
+ attrStr('ng-click', attr.ngClick) +
+ attrStr('title', attr.title) +
+ attrStr('icon', attr.icon) +
+ attrStr('icon-on', attr.iconOn) +
+ attrStr('icon-off', attr.iconOff) +
+ attrStr('badge', attr.badge) +
+ attrStr('badge-style', attr.badgeStyle) +
+ attrStr('hidden', attr.hidden) +
+ attrStr('disabled', attr.disabled) +
+ attrStr('class', attr['class']) +
+ '></ion-tab-nav>';
+
+ //Remove the contents of the element so we can compile them later, if tab is selected
+ var tabContentEle = document.createElement('div');
+ for (var x = 0; x < element[0].children.length; x++) {
+ tabContentEle.appendChild(element[0].children[x].cloneNode(true));
+ }
+ var childElementCount = tabContentEle.childElementCount;
+ element.empty();
+
+ var navViewName, isNavView;
+ if (childElementCount) {
+ if (tabContentEle.children[0].tagName === 'ION-NAV-VIEW') {
+ // get the name if it's a nav-view
+ navViewName = tabContentEle.children[0].getAttribute('name');
+ tabContentEle.children[0].classList.add('view-container');
+ isNavView = true;
+ }
+ if (childElementCount === 1) {
+ // make the 1 child element the primary tab content container
+ tabContentEle = tabContentEle.children[0];
+ }
+ if (!isNavView) tabContentEle.classList.add('pane');
+ tabContentEle.classList.add('tab-content');
+ }
+
+ return function link($scope, $element, $attr, ctrls) {
+ var childScope;
+ var childElement;
+ var tabsCtrl = ctrls[0];
+ var tabCtrl = ctrls[1];
+ var isTabContentAttached = false;
+ $scope.$tabSelected = false;
+
+ $ionicBind($scope, $attr, {
+ onSelect: '&',
+ onDeselect: '&',
+ title: '@',
+ uiSref: '@',
+ href: '@'
+ });
+
+ tabsCtrl.add($scope);
+ $scope.$on('$destroy', function() {
+ if (!$scope.$tabsDestroy) {
+ // if the containing ionTabs directive is being destroyed
+ // then don't bother going through the controllers remove
+ // method, since remove will reset the active tab as each tab
+ // is being destroyed, causing unnecessary view loads and transitions
+ tabsCtrl.remove($scope);
+ }
+ tabNavElement.isolateScope().$destroy();
+ tabNavElement.remove();
+ tabNavElement = tabContentEle = childElement = null;
+ });
+
+ //Remove title attribute so browser-tooltip does not apear
+ $element[0].removeAttribute('title');
+
+ if (navViewName) {
+ tabCtrl.navViewName = $scope.navViewName = navViewName;
+ }
+ $scope.$on('$stateChangeSuccess', selectIfMatchesState);
+ selectIfMatchesState();
+ function selectIfMatchesState() {
+ if (tabCtrl.tabMatchesState()) {
+ tabsCtrl.select($scope, false);
+ }
+ }
+
+ var tabNavElement = jqLite(tabNavTemplate);
+ tabNavElement.data('$ionTabsController', tabsCtrl);
+ tabNavElement.data('$ionTabController', tabCtrl);
+ tabsCtrl.$tabsElement.append($compile(tabNavElement)($scope));
+
+
+ function tabSelected(isSelected) {
+ if (isSelected && childElementCount) {
+ // this tab is being selected
+
+ // check if the tab is already in the DOM
+ // only do this if the tab has child elements
+ if (!isTabContentAttached) {
+ // tab should be selected and is NOT in the DOM
+ // create a new scope and append it
+ childScope = $scope.$new();
+ childElement = jqLite(tabContentEle);
+ $ionicViewSwitcher.viewEleIsActive(childElement, true);
+ tabsCtrl.$element.append(childElement);
+ $compile(childElement)(childScope);
+ isTabContentAttached = true;
+ }
+
+ // remove the hide class so the tabs content shows up
+ $ionicViewSwitcher.viewEleIsActive(childElement, true);
+
+ } else if (isTabContentAttached && childElement) {
+ // this tab should NOT be selected, and it is already in the DOM
+
+ if ($ionicConfig.views.maxCache() > 0) {
+ // keep the tabs in the DOM, only css hide it
+ $ionicViewSwitcher.viewEleIsActive(childElement, false);
+
+ } else {
+ // do not keep tabs in the DOM
+ destroyTab();
+ }
+
+ }
+ }
+
+ function destroyTab() {
+ childScope && childScope.$destroy();
+ isTabContentAttached && childElement && childElement.remove();
+ tabContentEle.innerHTML = '';
+ isTabContentAttached = childScope = childElement = null;
+ }
+
+ $scope.$watch('$tabSelected', tabSelected);
+
+ $scope.$on('$ionicView.afterEnter', function() {
+ $ionicViewSwitcher.viewEleIsActive(childElement, $scope.$tabSelected);
+ });
+
+ $scope.$on('$ionicView.clearCache', function() {
+ if (!$scope.$tabSelected) {
+ destroyTab();
+ }
+ });
+
+ };
+ }
+ };
+}]);
+
+IonicModule
+.directive('ionTabNav', [function() {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: ['^ionTabs', '^ionTab'],
+ template:
+ '<a ng-class="{\'has-badge\':badge, \'tab-hidden\':isHidden(), \'tab-item-active\': isTabActive()}" ' +
+ ' ng-disabled="disabled()" class="tab-item">' +
+ '<span class="badge {{badgeStyle}}" ng-if="badge">{{badge}}</span>' +
+ '<i class="icon {{getIcon()}}" ng-if="getIcon()"></i>' +
+ '<span class="tab-title" ng-bind-html="title"></span>' +
+ '</a>',
+ scope: {
+ title: '@',
+ icon: '@',
+ iconOn: '@',
+ iconOff: '@',
+ badge: '=',
+ hidden: '@',
+ disabled: '&',
+ badgeStyle: '@',
+ 'class': '@'
+ },
+ link: function($scope, $element, $attrs, ctrls) {
+ var tabsCtrl = ctrls[0],
+ tabCtrl = ctrls[1];
+
+ //Remove title attribute so browser-tooltip does not apear
+ $element[0].removeAttribute('title');
+
+ $scope.selectTab = function(e) {
+ e.preventDefault();
+ tabsCtrl.select(tabCtrl.$scope, true);
+ };
+ if (!$attrs.ngClick) {
+ $element.on('click', function(event) {
+ $scope.$apply(function() {
+ $scope.selectTab(event);
+ });
+ });
+ }
+
+ $scope.isHidden = function() {
+ if ($attrs.hidden === 'true' || $attrs.hidden === true) return true;
+ return false;
+ };
+
+ $scope.getIconOn = function() {
+ return $scope.iconOn || $scope.icon;
+ };
+ $scope.getIconOff = function() {
+ return $scope.iconOff || $scope.icon;
+ };
+
+ $scope.isTabActive = function() {
+ return tabsCtrl.selectedTab() === tabCtrl.$scope;
+ };
+
+ $scope.getIcon = function() {
+ if ( tabsCtrl.selectedTab() === tabCtrl.$scope ) {
+ // active
+ return $scope.iconOn || $scope.icon;
+ }
+ else {
+ // inactive
+ return $scope.iconOff || $scope.icon;
+ }
+ };
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionTabs
+ * @module ionic
+ * @delegate ionic.service:$ionicTabsDelegate
+ * @restrict E
+ * @codepen odqCz
+ *
+ * @description
+ * Powers a multi-tabbed interface with a Tab Bar and a set of "pages" that can be tabbed
+ * through.
+ *
+ * Assign any [tabs class](/docs/components#tabs) to the element to define
+ * its look and feel.
+ *
+ * For iOS, tabs will appear at the bottom of the screen. For Android, tabs will be at the top
+ * of the screen, below the nav-bar. This follows each OS's design specification, but can be
+ * configured with the {@link ionic.provider:$ionicConfigProvider}.
+ *
+ * See the {@link ionic.directive:ionTab} directive's documentation for more details on
+ * individual tabs.
+ *
+ * Note: do not place ion-tabs inside of an ion-content element; it has been known to cause a
+ * certain CSS bug.
+ *
+ * @usage
+ * ```html
+ * <ion-tabs class="tabs-positive tabs-icon-top">
+ *
+ * <ion-tab title="Home" icon-on="ion-ios-filing" icon-off="ion-ios-filing-outline">
+ * <!-- Tab 1 content -->
+ * </ion-tab>
+ *
+ * <ion-tab title="About" icon-on="ion-ios-clock" icon-off="ion-ios-clock-outline">
+ * <!-- Tab 2 content -->
+ * </ion-tab>
+ *
+ * <ion-tab title="Settings" icon-on="ion-ios-gear" icon-off="ion-ios-gear-outline">
+ * <!-- Tab 3 content -->
+ * </ion-tab>
+ *
+ * </ion-tabs>
+ * ```
+ *
+ * @param {string=} delegate-handle The handle used to identify these tabs
+ * with {@link ionic.service:$ionicTabsDelegate}.
+ */
+
+IonicModule
+.directive('ionTabs', [
+ '$ionicTabsDelegate',
+ '$ionicConfig',
+function($ionicTabsDelegate, $ionicConfig) {
+ return {
+ restrict: 'E',
+ scope: true,
+ controller: '$ionicTabs',
+ compile: function(tElement) {
+ //We cannot use regular transclude here because it breaks element.data()
+ //inheritance on compile
+ var innerElement = jqLite('<div class="tab-nav tabs">');
+ innerElement.append(tElement.contents());
+
+ tElement.append(innerElement)
+ .addClass('tabs-' + $ionicConfig.tabs.position() + ' tabs-' + $ionicConfig.tabs.style());
+
+ return { pre: prelink, post: postLink };
+ function prelink($scope, $element, $attr, tabsCtrl) {
+ var deregisterInstance = $ionicTabsDelegate._registerInstance(
+ tabsCtrl, $attr.delegateHandle, tabsCtrl.hasActiveScope
+ );
+
+ tabsCtrl.$scope = $scope;
+ tabsCtrl.$element = $element;
+ tabsCtrl.$tabsElement = jqLite($element[0].querySelector('.tabs'));
+
+ $scope.$watch(function() { return $element[0].className; }, function(value) {
+ var isTabsTop = value.indexOf('tabs-top') !== -1;
+ var isHidden = value.indexOf('tabs-item-hide') !== -1;
+ $scope.$hasTabs = !isTabsTop && !isHidden;
+ $scope.$hasTabsTop = isTabsTop && !isHidden;
+ $scope.$emit('$ionicTabs.top', $scope.$hasTabsTop);
+ });
+
+ function emitLifecycleEvent(ev, data) {
+ ev.stopPropagation();
+ var previousSelectedTab = tabsCtrl.previousSelectedTab();
+ if (previousSelectedTab) {
+ previousSelectedTab.$broadcast(ev.name.replace('NavView', 'Tabs'), data);
+ }
+ }
+
+ $scope.$on('$ionicNavView.beforeLeave', emitLifecycleEvent);
+ $scope.$on('$ionicNavView.afterLeave', emitLifecycleEvent);
+ $scope.$on('$ionicNavView.leave', emitLifecycleEvent);
+
+ $scope.$on('$destroy', function() {
+ // variable to inform child tabs that they're all being blown away
+ // used so that while destorying an individual tab, each one
+ // doesn't select the next tab as the active one, which causes unnecessary
+ // loading of tab views when each will eventually all go away anyway
+ $scope.$tabsDestroy = true;
+ deregisterInstance();
+ tabsCtrl.$tabsElement = tabsCtrl.$element = tabsCtrl.$scope = innerElement = null;
+ delete $scope.$hasTabs;
+ delete $scope.$hasTabsTop;
+ });
+ }
+
+ function postLink($scope, $element, $attr, tabsCtrl) {
+ if (!tabsCtrl.selectedTab()) {
+ // all the tabs have been added
+ // but one hasn't been selected yet
+ tabsCtrl.select(0);
+ }
+ }
+ }
+ };
+}]);
+
+/**
+* @ngdoc directive
+* @name ionTitle
+* @module ionic
+* @restrict E
+*
+* Used for titles in header and nav bars. New in 1.2
+*
+* Identical to <div class="title"> but with future compatibility for Ionic 2
+*
+* @usage
+*
+* ```html
+* <ion-nav-bar>
+* <ion-title>Hello</ion-title>
+* <ion-nav-bar>
+* ```
+*/
+IonicModule
+.directive('ionTitle', [function() {
+ return {
+ restrict: 'E',
+ compile: function(element) {
+ element.addClass('title');
+ }
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionToggle
+ * @module ionic
+ * @codepen tfAzj
+ * @restrict E
+ *
+ * @description
+ * A toggle is an animated switch which binds a given model to a boolean.
+ *
+ * Allows dragging of the switch's nub.
+ *
+ * The toggle behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]) otherwise.
+ *
+ * @param toggle-class {string=} Sets the CSS class on the inner `label.toggle` element created by the directive.
+ *
+ * @usage
+ * Below is an example of a toggle directive which is wired up to the `airplaneMode` model
+ * and has the `toggle-calm` CSS class assigned to the inner element.
+ *
+ * ```html
+ * <ion-toggle ng-model="airplaneMode" toggle-class="toggle-calm">Airplane Mode</ion-toggle>
+ * ```
+ */
+IonicModule
+.directive('ionToggle', [
+ '$timeout',
+ '$ionicConfig',
+function($timeout, $ionicConfig) {
+
+ return {
+ restrict: 'E',
+ replace: true,
+ require: '?ngModel',
+ transclude: true,
+ template:
+ '<div class="item item-toggle">' +
+ '<div ng-transclude></div>' +
+ '<label class="toggle">' +
+ '<input type="checkbox">' +
+ '<div class="track">' +
+ '<div class="handle"></div>' +
+ '</div>' +
+ '</label>' +
+ '</div>',
+
+ compile: function(element, attr) {
+ var input = element.find('input');
+ forEach({
+ 'name': attr.name,
+ 'ng-value': attr.ngValue,
+ 'ng-model': attr.ngModel,
+ 'ng-checked': attr.ngChecked,
+ 'ng-disabled': attr.ngDisabled,
+ 'ng-true-value': attr.ngTrueValue,
+ 'ng-false-value': attr.ngFalseValue,
+ 'ng-change': attr.ngChange,
+ 'ng-required': attr.ngRequired,
+ 'required': attr.required
+ }, function(value, name) {
+ if (isDefined(value)) {
+ input.attr(name, value);
+ }
+ });
+
+ if (attr.toggleClass) {
+ element[0].getElementsByTagName('label')[0].classList.add(attr.toggleClass);
+ }
+
+ element.addClass('toggle-' + $ionicConfig.form.toggle());
+
+ return function($scope, $element) {
+ var el = $element[0].getElementsByTagName('label')[0];
+ var checkbox = el.children[0];
+ var track = el.children[1];
+ var handle = track.children[0];
+
+ var ngModelController = jqLite(checkbox).controller('ngModel');
+
+ $scope.toggle = new ionic.views.Toggle({
+ el: el,
+ track: track,
+ checkbox: checkbox,
+ handle: handle,
+ onChange: function() {
+ if (ngModelController) {
+ ngModelController.$setViewValue(checkbox.checked);
+ $scope.$apply();
+ }
+ }
+ });
+
+ $scope.$on('$destroy', function() {
+ $scope.toggle.destroy();
+ });
+ };
+ }
+
+ };
+}]);
+
+/**
+ * @ngdoc directive
+ * @name ionView
+ * @module ionic
+ * @restrict E
+ * @parent ionNavView
+ *
+ * @description
+ * A container for view content and any navigational and header bar information. When a view
+ * enters and exits its parent {@link ionic.directive:ionNavView}, the view also emits view
+ * information, such as its title, whether the back button should be displayed or not, whether the
+ * corresponding {@link ionic.directive:ionNavBar} should be displayed or not, which transition the view
+ * should use to animate, and which direction to animate.
+ *
+ * *Views are cached to improve performance.* When a view is navigated away from, its element is
+ * left in the DOM, and its scope is disconnected from the `$watch` cycle. When navigating to a
+ * view that is already cached, its scope is reconnected, and the existing element, which was
+ * left in the DOM, becomes active again. This can be disabled, or the maximum number of cached
+ * views changed in {@link ionic.provider:$ionicConfigProvider}, in the view's `$state` configuration, or
+ * as an attribute on the view itself (see below).
+ *
+ * @usage
+ * Below is an example where our page will load with a {@link ionic.directive:ionNavBar} containing
+ * "My Page" as the title.
+ *
+ * ```html
+ * <ion-nav-bar></ion-nav-bar>
+ * <ion-nav-view>
+ * <ion-view view-title="My Page">
+ * <ion-content>
+ * Hello!
+ * </ion-content>
+ * </ion-view>
+ * </ion-nav-view>
+ * ```
+ *
+ * ## View LifeCycle and Events
+ *
+ * Views can be cached, which means ***controllers normally only load once***, which may
+ * affect your controller logic. To know when a view has entered or left, events
+ * have been added that are emitted from the view's scope. These events also
+ * contain data about the view, such as the title and whether the back button should
+ * show. Also contained is transition data, such as the transition type and
+ * direction that will be or was used.
+ *
+ * Life cycle events are emitted upwards from the transitioning view's scope. In some cases, it is
+ * desirable for a child/nested view to be notified of the event.
+ * For this use case, `$ionicParentView` life cycle events are broadcast downwards.
+ *
+ * <table class="table">
+ * <tr>
+ * <td><code>$ionicView.loaded</code></td>
+ * <td>The view has loaded. This event only happens once per
+ * view being created and added to the DOM. If a view leaves but is cached,
+ * then this event will not fire again on a subsequent viewing. The loaded event
+ * is good place to put your setup code for the view; however, it is not the
+ * recommended event to listen to when a view becomes active.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicView.enter</code></td>
+ * <td>The view has fully entered and is now the active view.
+ * This event will fire, whether it was the first load or a cached view.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicView.leave</code></td>
+ * <td>The view has finished leaving and is no longer the
+ * active view. This event will fire, whether it is cached or destroyed.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicView.beforeEnter</code></td>
+ * <td>The view is about to enter and become the active view.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicView.beforeLeave</code></td>
+ * <td>The view is about to leave and no longer be the active view.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicView.afterEnter</code></td>
+ * <td>The view has fully entered and is now the active view.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicView.afterLeave</code></td>
+ * <td>The view has finished leaving and is no longer the active view.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicView.unloaded</code></td>
+ * <td>The view's controller has been destroyed and its element has been
+ * removed from the DOM.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicParentView.enter</code></td>
+ * <td>The parent view has fully entered and is now the active view.
+ * This event will fire, whether it was the first load or a cached view.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicParentView.leave</code></td>
+ * <td>The parent view has finished leaving and is no longer the
+ * active view. This event will fire, whether it is cached or destroyed.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicParentView.beforeEnter</code></td>
+ * <td>The parent view is about to enter and become the active view.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicParentView.beforeLeave</code></td>
+ * <td>The parent view is about to leave and no longer be the active view.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicParentView.afterEnter</code></td>
+ * <td>The parent view has fully entered and is now the active view.</td>
+ * </tr>
+ * <tr>
+ * <td><code>$ionicParentView.afterLeave</code></td>
+ * <td>The parent view has finished leaving and is no longer the active view.</td>
+ * </tr>
+ * </table>
+ *
+ * ## LifeCycle Event Usage
+ *
+ * Below is an example of how to listen to life cycle events and
+ * access state parameter data
+ *
+ * ```js
+ * $scope.$on("$ionicView.beforeEnter", function(event, data){
+ * // handle event
+ * console.log("State Params: ", data.stateParams);
+ * });
+ *
+ * $scope.$on("$ionicView.enter", function(event, data){
+ * // handle event
+ * console.log("State Params: ", data.stateParams);
+ * });
+ *
+ * $scope.$on("$ionicView.afterEnter", function(event, data){
+ * // handle event
+ * console.log("State Params: ", data.stateParams);
+ * });
+ * ```
+ *
+ * ## Caching
+ *
+ * Caching can be disabled and enabled in multiple ways. By default, Ionic will
+ * cache a maximum of 10 views. You can optionally choose to disable caching at
+ * either an individual view basis, or by global configuration. Please see the
+ * _Caching_ section in {@link ionic.directive:ionNavView} for more info.
+ *
+ * @param {string=} view-title A text-only title to display on the parent {@link ionic.directive:ionNavBar}.
+ * For an HTML title, such as an image, see {@link ionic.directive:ionNavTitle} instead.
+ * @param {boolean=} cache-view If this view should be allowed to be cached or not.
+ * Please see the _Caching_ section in {@link ionic.directive:ionNavView} for
+ * more info. Default `true`
+ * @param {boolean=} can-swipe-back If this view should be allowed to use the swipe to go back gesture or not.
+ * This does not enable the swipe to go back feature if it is not available for the platform it's running
+ * from, or there isn't a previous view. Default `true`
+ * @param {boolean=} hide-back-button Whether to hide the back button on the parent
+ * {@link ionic.directive:ionNavBar} by default.
+ * @param {boolean=} hide-nav-bar Whether to hide the parent
+ * {@link ionic.directive:ionNavBar} by default.
+ */
+IonicModule
+.directive('ionView', function() {
+ return {
+ restrict: 'EA',
+ priority: 1000,
+ controller: '$ionicView',
+ compile: function(tElement) {
+ tElement.addClass('pane');
+ tElement[0].removeAttribute('title');
+ return function link($scope, $element, $attrs, viewCtrl) {
+ viewCtrl.init();
+ };
+ }
+ };
+});
+
+})(); \ No newline at end of file