/*! * Copyright 2015 Drifty Co. * http://drifty.com/ * * Ionic, v1.3.1 * 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: 'Share 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('')(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('
'); 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.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 it's own config within the `$ionicConfigProvider.platform` * property. The config below would only apply to Android devices. * * ```js * $ionicConfigProvider.platform.android.views.maxCache(5); * ``` * * @usage * ```js * var myApp = angular.module('reallyCoolApp', ['ionic']); * * myApp.config(function($ionicConfigProvider) { * $ionicConfigProvider.views.maxCache(5); * * // note that you can also chain configs * $ionicConfigProvider.backButton.text('Go Back').icon('ion-chevron-left'); * }); * ``` */ /** * @ngdoc method * @name $ionicConfigProvider#views.transition * @description Animation style when transitioning between views. Default `platform`. * * @param {string} transition Which style of view transitioning to use. * * * `platform`: Dynamically choose the correct transition style depending on the platform * the app is running from. If the platform is not `ios` or `android` then it will default * to `ios`. * * `ios`: iOS style transition. * * `android`: Android style transition. * * `none`: Do not 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 = '
' + '
' + '
' + '
'; /** * @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...' * }).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: '' }) .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 `` element. * * **Notes:** * - A modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating * scope, passing in itself as an event argument. Both the modal.removed and modal.hidden events are * called when the modal is removed. * * - This example assumes your modal is in your main index file or another template file. If it is in its own * template file, remove the script tags and call it by file name. * * @usage * ```html * * ``` * ```js * angular.module('testApp', ['ionic']) * .controller('MyController', function($scope, $ionicModal) { * $ionicModal.fromTemplateUrl('my-modal.html', { * scope: $scope, * animation: 'slide-in-up' * }).then(function(modal) { * $scope.modal = modal; * }); * $scope.openModal = function() { * $scope.modal.show(); * }; * $scope.closeModal = function() { * $scope.modal.hide(); * }; * // Cleanup the modal when we're done with it! * $scope.$on('$destroy', function() { * $scope.modal.remove(); * }); * // Execute action on hide modal * $scope.$on('modal.hidden', function() { * // Execute action * }); * // Execute action on remove modal * $scope.$on('modal.removed', function() { * // Execute action * }); * }); * ``` */ IonicModule .factory('$ionicModal', [ '$rootScope', '$ionicBody', '$compile', '$timeout', '$ionicPlatform', '$ionicTemplateLoader', '$$q', '$log', '$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() { $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('' + templateString + '')(scope); options.$el = element; options.el = element[0]; options.modalEl = options.el.querySelector('.' + options.viewType); var modal = new ModalView(options); modal.scope = scope; // If this wasn't a defined scope, we can assign the viewType to the isolated scope // we created if (!options.scope) { scope[ options.viewType ] = modal; } return modal; }; 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 * * * * * * ``` * ```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/edge/cordova_events_events.md.html#Events). * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/edge/cordova_events_events.md.html#Events). * @param {function} callback Called when the Cordova event is fired. * @returns {function} Returns a deregistration function to remove the event listener. */ on: function(type, cb) { ionic.Platform.ready(function() { document.addEventListener(type, cb, false); }); return function() { ionic.Platform.ready(function() { document.removeEventListener(type, cb); }); }; }, /** * @ngdoc method * @name $ionicPlatform#ready * @description * Trigger a callback once the device is ready, * or immediately if the device is already ready. * @param {function=} callback The function to call. * @returns {promise} A promise which is resolved when the device is ready. */ ready: function(cb) { var q = $q.defer(); ionic.Platform.ready(function() { 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 `` element. * * @usage * ```html *

* *

* * * ``` * ```js * angular.module('testApp', ['ionic']) * .controller('MyController', function($scope, $ionicPopover) { * * // .fromTemplate() method * var template = '

My Popover Title

Hello!
'; * * $scope.popover = $ionicPopover.fromTemplate(template, { * scope: $scope * }); * * // .fromTemplateUrl() method * $ionicPopover.fromTemplateUrl('my-popover.html', { * scope: $scope * }).then(function(popover) { * $scope.popover = popover; * }); * * * $scope.openPopover = function($event) { * $scope.popover.show($event); * }; * $scope.closePopover = function() { * $scope.popover.hide(); * }; * //Cleanup the popover when we're done with it! * $scope.$on('$destroy', function() { * $scope.popover.remove(); * }); * // Execute action on hide popover * $scope.$on('popover.hidden', function() { * // Execute action * }); * // Execute action on remove popover * $scope.$on('popover.removed', function() { * // Execute action * }); * }); * ``` */ IonicModule .factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', '$window', function($ionicModal, $ionicPosition, $document, $window) { var POPOVER_BODY_PADDING = 6; var POPOVER_OPTIONS = { viewType: 'popover', hideDelay: 1, animation: 'none', positionView: positionView }; function positionView(target, popoverEle) { var targetEle = 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 = ''; /** * @ngdoc service * @name $ionicPopup * @module ionic * @restrict E * @codepen zkmhJ * @description * * The Ionic Popup service allows programmatically creating and showing popup * windows that require the user to respond in order to continue. * * The popup system has support for more flexible versions of the built in `alert()`, `prompt()`, * and `confirm()` functions that users are used to, in addition to allowing popups with completely * custom content and look. * * An input can be given an `autofocus` attribute so it automatically receives focus when * the popup first shows. However, depending on certain use-cases this can cause issues with * the tap/click system, which is why Ionic prefers using the `autofocus` attribute as * an opt-in feature and not the default. * * @usage * A few basic examples, see below for details about all of the options available. * * ```js *angular.module('mySuperApp', ['ionic']) *.controller('PopupCtrl',function($scope, $ionicPopup, $timeout) { * * // Triggered on a button click, or some other target * $scope.showPopup = function() { * $scope.data = {}; * * // An elaborate, custom popup * var myPopup = $ionicPopup.show({ * template: '', * title: 'Enter Wi-Fi Password', * subTitle: 'Please use normal things', * scope: $scope, * buttons: [ * { text: 'Cancel' }, * { * text: 'Save', * type: 'button-positive', * onTap: function(e) { * if (!$scope.data.wifi) { * //don't allow the user to close unless he enters wifi password * e.preventDefault(); * } else { * return $scope.data.wifi; * } * } * } * ] * }); * * myPopup.then(function(res) { * console.log('Tapped!', res); * }); * * $timeout(function() { * myPopup.close(); //close the popup after 3 seconds for some reason * }, 3000); * }; * * // A confirm dialog * $scope.showConfirm = function() { * var confirmPopup = $ionicPopup.confirm({ * title: 'Consume Ice Cream', * template: 'Are you sure you want to eat this ice cream?' * }); * * confirmPopup.then(function(res) { * if(res) { * console.log('You are sure'); * } else { * console.log('You are not sure'); * } * }); * }; * * // An alert dialog * $scope.showAlert = function() { * var alertPopup = $ionicPopup.alert({ * title: 'Don\'t eat that!', * template: 'It might taste good' * }); * * alertPopup.then(function(res) { * console.log('Thank you for not eating my delicious ice cream cone'); * }); * }; *}); *``` */ IonicModule .factory('$ionicPopup', [ '$ionicTemplateLoader', '$ionicBackdrop', '$q', '$timeout', '$rootScope', '$ionicBody', '$compile', '$ionicPlatform', '$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 = '' + opts.template + ''; delete opts.template; } return showPopup(extend({ template: text + '', 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 * * * * * * ``` * ```js * function MainCtrl($scope, $ionicScrollDelegate) { * $scope.scrollTop = function() { * $ionicScrollDelegate.scrollTop(); * }; * } * ``` * * Example of advanced usage, with two scroll areas using `delegate-handle` * for fine control. * * ```html * * * * * * * * * ``` * ```js * function MainCtrl($scope, $ionicScrollDelegate) { * $scope.scrollMainToTop = function() { * $ionicScrollDelegate.$getByHandle('mainScroll').scrollTop(); * }; * $scope.scrollSmallToTop = function() { * $ionicScrollDelegate.$getByHandle('small').scrollTop(); * }; * } * ``` */ IonicModule .service('$ionicScrollDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicScrollDelegate#resize * @description Tell the scrollView to recalculate the size of its container. */ 'resize', /** * @ngdoc method * @name $ionicScrollDelegate#scrollTop * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollTop', /** * @ngdoc method * @name $ionicScrollDelegate#scrollBottom * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollBottom', /** * @ngdoc method * @name $ionicScrollDelegate#scrollTo * @param {number} left The x-value to scroll to. * @param {number} top The y-value to scroll to. * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollTo', /** * @ngdoc method * @name $ionicScrollDelegate#scrollBy * @param {number} left The x-offset to scroll by. * @param {number} top The y-offset to scroll by. * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollBy', /** * @ngdoc method * @name $ionicScrollDelegate#zoomTo * @param {number} level Level to zoom to. * @param {boolean=} animate Whether to animate the zoom. * @param {number=} originLeft Zoom in at given left coordinate. * @param {number=} originTop Zoom in at given top coordinate. */ 'zoomTo', /** * @ngdoc method * @name $ionicScrollDelegate#zoomBy * @param {number} factor The factor to zoom by. * @param {boolean=} animate Whether to animate the zoom. * @param {number=} originLeft Zoom in at given left coordinate. * @param {number=} originTop Zoom in at given top coordinate. */ 'zoomBy', /** * @ngdoc method * @name $ionicScrollDelegate#getScrollPosition * @returns {object} The scroll position of this view, with the following properties: * - `{number}` `left` The distance the user has scrolled from the left (starts at 0). * - `{number}` `top` The distance the user has scrolled from the top (starts at 0). * - `{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 * * * * Content! * * * * Left Menu! * * * * ``` * ```js * function MainCtrl($scope, $ionicSideMenuDelegate) { * $scope.toggleLeftSideMenu = function() { * $ionicSideMenuDelegate.toggleLeft(); * }; * } * ``` */ IonicModule .service('$ionicSideMenuDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicSideMenuDelegate#toggleLeft * @description Toggle the left side menu (if it exists). * @param {boolean=} isOpen Whether to open or close the menu. * Default: Toggles the menu. */ 'toggleLeft', /** * @ngdoc method * @name $ionicSideMenuDelegate#toggleRight * @description Toggle the right side menu (if it exists). * @param {boolean=} isOpen Whether to open or close the menu. * Default: Toggles the menu. */ 'toggleRight', /** * @ngdoc method * @name $ionicSideMenuDelegate#getOpenRatio * @description Gets the ratio of open amount over menu width. For example, a * menu of width 100 that is opened by 50 pixels is 50% opened, and would return * a ratio of 0.5. * * @returns {float} 0 if nothing is open, between 0 and 1 if left menu is * opened/opening, and between 0 and -1 if right menu is opened/opening. */ 'getOpenRatio', /** * @ngdoc method * @name $ionicSideMenuDelegate#isOpen * @returns {boolean} Whether either the left or right menu is currently opened. */ 'isOpen', /** * @ngdoc method * @name $ionicSideMenuDelegate#isOpenLeft * @returns {boolean} Whether the left menu is currently opened. */ 'isOpenLeft', /** * @ngdoc method * @name $ionicSideMenuDelegate#isOpenRight * @returns {boolean} Whether the right menu is currently opened. */ 'isOpenRight', /** * @ngdoc method * @name $ionicSideMenuDelegate#canDragContent * @param {boolean=} canDrag Set whether the content can or cannot be dragged to open * side menus. * @returns {boolean} Whether the content can be dragged to open side menus. */ 'canDragContent', /** * @ngdoc method * @name $ionicSideMenuDelegate#edgeDragThreshold * @param {boolean|number=} value Set whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Accepts three different values: * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu. * - If true is given, the default number of pixels (25) is used as the maximum allowed distance. * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed. * @returns {boolean} Whether the drag can start only from within the edge of screen threshold. */ 'edgeDragThreshold' /** * @ngdoc method * @name $ionicSideMenuDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionSideMenus} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicSideMenuDelegate.$getByHandle('my-handle').toggleLeft();` */ ])); /** * @ngdoc service * @name $ionicSlideBoxDelegate * @module ionic * @description * Delegate that controls the {@link ionic.directive:ionSlideBox} directive. * * Methods called directly on the $ionicSlideBoxDelegate service will control all slide boxes. Use the {@link ionic.service:$ionicSlideBoxDelegate#$getByHandle $getByHandle} * method to control specific slide box instances. * * @usage * * ```html * * * *
* *
*
* *
* Slide 2! *
*
*
*
* ``` * ```js * function MyCtrl($scope, $ionicSlideBoxDelegate) { * $scope.nextSlide = function() { * $ionicSlideBoxDelegate.next(); * } * } * ``` */ IonicModule .service('$ionicSlideBoxDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicSlideBoxDelegate#update * @description * Update the slidebox (for example if using Angular with ng-repeat, * resize it for the elements inside). */ 'update', /** * @ngdoc method * @name $ionicSlideBoxDelegate#slide * @param {number} to The index to slide to. * @param {number=} speed The number of milliseconds 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 * * * * * Hello tab 1! * * * Hello tab 2! * * * * ``` * ```js * function MyCtrl($scope, $ionicTabsDelegate) { * $scope.selectTabWithIndex = function(index) { * $ionicTabsDelegate.select(index); * } * } * ``` */ IonicModule .service('$ionicTabsDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicTabsDelegate#select * @description Select the tab matching the given index. * * @param {number} index Index of the tab to select. */ 'select', /** * @ngdoc method * @name $ionicTabsDelegate#selectedIndex * @returns `number` The index of the selected tab, or -1. */ 'selectedIndex', /** * @ngdoc method * @name $ionicTabsDelegate#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('
').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 %} * * * * * Hello, {{i}}! * * * * * {% endraw %} * ``` * ```js * function MyCtrl($scope, $ionicListDelegate) { * $scope.showDeleteButtons = function() { * $ionicListDelegate.showDelete(true); * }; * } * ``` */ IonicModule.service('$ionicListDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicListDelegate#showReorder * @param {boolean=} showReorder Set whether or not this list is showing its reorder buttons. * @returns {boolean} Whether the reorder buttons are shown. */ 'showReorder', /** * @ngdoc method * @name $ionicListDelegate#showDelete * @param {boolean=} showDelete Set whether or not this list is showing its delete buttons. * @returns {boolean} Whether the delete buttons are shown. */ 'showDelete', /** * @ngdoc method * @name $ionicListDelegate#canSwipeItems * @param {boolean=} canSwipeItems Set whether or not this list is able to swipe to show * option buttons. * @returns {boolean} Whether the list is able to swipe to show option buttons. */ 'canSwipeItems', /** * @ngdoc method * @name $ionicListDelegate#closeOptionButtons * @description Closes any option buttons on the list that are swiped open. */ 'closeOptionButtons' /** * @ngdoc method * @name $ionicListDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionList} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicListDelegate.$getByHandle('my-handle').showReorder(true);` */ ])) .controller('$ionicList', [ '$scope', '$attrs', '$ionicListDelegate', '$ionicHistory', function($scope, $attrs, $ionicListDelegate, $ionicHistory) { var self = this; var isSwipeable = true; var isReorderShown = false; var isDeleteShown = false; var deregisterInstance = $ionicListDelegate._registerInstance( self, $attrs.delegateHandle, function() { return $ionicHistory.isActiveScope($scope); } ); $scope.$on('$destroy', deregisterInstance); self.showReorder = function(show) { if (arguments.length) { isReorderShown = !!show; } return isReorderShown; }; self.showDelete = function(show) { if (arguments.length) { isDeleteShown = !!show; } return isDeleteShown; }; self.canSwipeItems = function(can) { if (arguments.length) { isSwipeable = !!can; } return isSwipeable; }; self.closeOptionButtons = function() { self.listView && self.listView.clearDragEffects(); }; }]); IonicModule .controller('$ionicNavBar', [ '$scope', '$element', '$attrs', '$compile', '$timeout', '$ionicNavBarDelegate', '$ionicConfig', '$ionicHistory', function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $ionicConfig, $ionicHistory) { var CSS_HIDE = 'hide'; var DATA_NAV_BAR_CTRL = '$ionNavBarController'; var PRIMARY_BUTTONS = 'primaryButtons'; var SECONDARY_BUTTONS = 'secondaryButtons'; var BACK_BUTTON = 'backButton'; var ITEM_TYPES = 'primaryButtons secondaryButtons leftButtons rightButtons title'.split(' '); var self = this; var headerBars = []; var navElementHtml = {}; var isVisible = true; var queuedTransitionStart, queuedTransitionEnd, latestTransitionId; $element.parent().data(DATA_NAV_BAR_CTRL, self); var delegateHandle = $attrs.delegateHandle || 'navBar' + ionic.Utils.nextUid(); var deregisterInstance = $ionicNavBarDelegate._registerInstance(self, delegateHandle); self.init = function() { $element.addClass('nav-bar-container'); ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', $ionicConfig.views.transition()); // create two nav bar blocks which will trade out which one is shown self.createHeaderBar(false); self.createHeaderBar(true); $scope.$emit('ionNavBar.init', delegateHandle); }; self.createHeaderBar = function(isActive) { var containerEle = jqLite('