|
|
/*! * 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: '<b>Share</b> This' }, * { text: 'Move' } * ], * destructiveText: 'Delete', * titleText: 'Modify your album', * cancelText: 'Cancel', * cancel: function() { // add cancel code..
}, * buttonClicked: function(index) { * return true; * } * }); * * // For example's sake, hide the sheet after two seconds
* $timeout(function() { * hideSheet(); * }, 2000); * * }; * }); * ```
* */ IonicModule .factory('$ionicActionSheet', [ '$rootScope', '$compile', '$animate', '$timeout', '$ionicTemplateLoader', '$ionicPlatform', '$ionicBody', 'IONIC_BACK_PRIORITY', function($rootScope, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform, $ionicBody, IONIC_BACK_PRIORITY) {
return { show: actionSheet };
/** * @ngdoc method * @name $ionicActionSheet#show * @description * Load and return a new action sheet. * * A new isolated scope will be created for the * action sheet and the new element will be appended into the body. * * @param {object} options The options for this ActionSheet. Properties: * * - `[Object]` `buttons` Which buttons to show. Each button is an object with a `text` field. * - `{string}` `titleText` The title to show on the action sheet. * - `{string=}` `cancelText` the text for a 'cancel' button on the action sheet. * - `{string=}` `destructiveText` The text for a 'danger' on the action sheet. * - `{function=}` `cancel` Called if the cancel button is pressed, the backdrop is tapped or * the hardware back button is pressed. * - `{function=}` `buttonClicked` Called when one of the non-destructive buttons is clicked, * with the index of the button that was clicked and the button object. Return true to close * the action sheet, or false to keep it opened. * - `{function=}` `destructiveButtonClicked` Called when the destructive button is clicked. * Return true to close the action sheet, or false to keep it opened. * - `{boolean=}` `cancelOnStateChange` Whether to cancel the actionSheet when navigating * to a new state. Default true. * - `{string}` `cssClass` The custom CSS class name. * * @returns {function} `hideSheet` A function which, when called, hides & cancels the action sheet. */ function actionSheet(opts) { var scope = $rootScope.$new(true);
extend(scope, { cancel: noop, destructiveButtonClicked: noop, buttonClicked: noop, $deregisterBackButton: noop, buttons: [], cancelOnStateChange: true }, opts || {});
function textForIcon(text) { if (text && /icon/.test(text)) { scope.$actionSheetHasIcon = true; } }
for (var x = 0; x < scope.buttons.length; x++) { textForIcon(scope.buttons[x].text); } textForIcon(scope.cancelText); textForIcon(scope.destructiveText);
// Compile the template
var element = scope.element = $compile('<ion-action-sheet ng-class="cssClass" buttons="buttons"></ion-action-sheet>')(scope);
// Grab the sheet element for animation
var sheetEl = jqLite(element[0].querySelector('.action-sheet-wrapper'));
var stateChangeListenDone = scope.cancelOnStateChange ? $rootScope.$on('$stateChangeSuccess', function() { scope.cancel(); }) : noop;
// removes the actionSheet from the screen
scope.removeSheet = function(done) { if (scope.removed) return;
scope.removed = true; sheetEl.removeClass('action-sheet-up'); $timeout(function() { // wait to remove this due to a 300ms delay native
// click which would trigging whatever was underneath this
$ionicBody.removeClass('action-sheet-open'); }, 400); scope.$deregisterBackButton(); stateChangeListenDone();
$animate.removeClass(element, 'active').then(function() { scope.$destroy(); element.remove(); // scope.cancel.$scope is defined near the bottom
scope.cancel.$scope = sheetEl = null; (done || noop)(opts.buttons); }); };
scope.showSheet = function(done) { if (scope.removed) return;
$ionicBody.append(element) .addClass('action-sheet-open');
$animate.addClass(element, 'active').then(function() { if (scope.removed) return; (done || noop)(); }); $timeout(function() { if (scope.removed) return; sheetEl.addClass('action-sheet-up'); }, 20, false); };
// registerBackButtonAction returns a callback to deregister the action
scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction( function() { $timeout(scope.cancel); }, IONIC_BACK_PRIORITY.actionSheet );
// called when the user presses the cancel button
scope.cancel = function() { // after the animation is out, call the cancel callback
scope.removeSheet(opts.cancel); };
scope.buttonClicked = function(index) { // Check if the button click event returned true, which means
// we can close the action sheet
if (opts.buttonClicked(index, opts.buttons[index]) === true) { scope.removeSheet(); } };
scope.destructiveButtonClicked = function() { // Check if the destructive button click event returned true, which means
// we can close the action sheet
if (opts.destructiveButtonClicked() === true) { scope.removeSheet(); } };
scope.showSheet();
// Expose the scope on $ionicActionSheet's return value for the sake
// of testing it.
scope.cancel.$scope = scope;
return scope.cancel; } }]);
jqLite.prototype.addClass = function(cssClasses) { var x, y, cssClass, el, splitClasses, existingClasses; if (cssClasses && cssClasses != 'ng-scope' && cssClasses != 'ng-isolate-scope') { for (x = 0; x < this.length; x++) { el = this[x]; if (el.setAttribute) {
if (cssClasses.indexOf(' ') < 0 && el.classList.add) { el.classList.add(cssClasses); } else { existingClasses = (' ' + (el.getAttribute('class') || '') + ' ') .replace(/[\n\t]/g, " "); splitClasses = cssClasses.split(' ');
for (y = 0; y < splitClasses.length; y++) { cssClass = splitClasses[y].trim(); if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { existingClasses += cssClass + ' '; } } el.setAttribute('class', existingClasses.trim()); } } } } return this; };
jqLite.prototype.removeClass = function(cssClasses) { var x, y, splitClasses, cssClass, el; if (cssClasses) { for (x = 0; x < this.length; x++) { el = this[x]; if (el.getAttribute) { if (cssClasses.indexOf(' ') < 0 && el.classList.remove) { el.classList.remove(cssClasses); } else { splitClasses = cssClasses.split(' ');
for (y = 0; y < splitClasses.length; y++) { cssClass = splitClasses[y]; el.setAttribute('class', ( (" " + (el.getAttribute('class') || '') + " ") .replace(/[\n\t]/g, " ") .replace(" " + cssClass.trim() + " ", " ")).trim() ); } } } } } return this; };
/** * @ngdoc service * @name $ionicBackdrop * @module ionic * @description * Shows and hides a backdrop over the UI. Appears behind popups, loading, * and other overlays. * * Often, multiple UI components require a backdrop, but only one backdrop is * ever needed in the DOM at a time. * * Therefore, each component that requires the backdrop to be shown calls * `$ionicBackdrop.retain()` when it wants the backdrop, then `$ionicBackdrop.release()` * when it is done with the backdrop. * * For each time `retain` is called, the backdrop will be shown until `release` is called. * * For example, if `retain` is called three times, the backdrop will be shown until `release` * is called three times. * * **Notes:** * - The backdrop service will broadcast 'backdrop.shown' and 'backdrop.hidden' events from the root scope, * this is useful for alerting native components not in html. * * @usage * * ```js
* function MyController($scope, $ionicBackdrop, $timeout, $rootScope) { * //Show a backdrop for one second
* $scope.action = function() { * $ionicBackdrop.retain(); * $timeout(function() { * $ionicBackdrop.release(); * }, 1000); * }; * * // Execute action on backdrop disappearing
* $scope.$on('backdrop.hidden', function() { * // Execute action
* }); * * // Execute action on backdrop appearing
* $scope.$on('backdrop.shown', function() { * // Execute action
* }); * * } * ```
*/ IonicModule .factory('$ionicBackdrop', [ '$document', '$timeout', '$$rAF', '$rootScope', function($document, $timeout, $$rAF, $rootScope) {
var el = jqLite('<div class="backdrop">'); var backdropHolds = 0;
$document[0].body.appendChild(el[0]);
return { /** * @ngdoc method * @name $ionicBackdrop#retain * @description Retains the backdrop. */ retain: retain, /** * @ngdoc method * @name $ionicBackdrop#release * @description * Releases the backdrop. */ release: release,
getElement: getElement,
// exposed for testing
_element: el };
function retain() { backdropHolds++; if (backdropHolds === 1) { el.addClass('visible'); $rootScope.$broadcast('backdrop.shown'); $$rAF(function() { // If we're still at >0 backdropHolds after async...
if (backdropHolds >= 1) el.addClass('active'); }); } } function release() { if (backdropHolds === 1) { el.removeClass('active'); $rootScope.$broadcast('backdrop.hidden'); $timeout(function() { // If we're still at 0 backdropHolds after async...
if (backdropHolds === 0) el.removeClass('visible'); }, 400, false); } backdropHolds = Math.max(0, backdropHolds - 1); }
function getElement() { return el; }
}]);
/** * @private */ IonicModule .factory('$ionicBind', ['$parse', '$interpolate', function($parse, $interpolate) { var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; return function(scope, attrs, bindDefinition) { forEach(bindDefinition || {}, function(definition, scopeName) { //Adapted from angular.js $compile
var match = definition.match(LOCAL_REGEXP) || [], attrName = match[3] || scopeName, mode = match[1], // @, =, or &
parentGet, unwatch;
switch (mode) { case '@': if (!attrs[attrName]) { return; } attrs.$observe(attrName, function(value) { scope[scopeName] = value; }); // we trigger an interpolation to ensure
// the value is there for use immediately
if (attrs[attrName]) { scope[scopeName] = $interpolate(attrs[attrName])(scope); } break;
case '=': if (!attrs[attrName]) { return; } unwatch = scope.$watch(attrs[attrName], function(value) { scope[scopeName] = value; }); //Destroy parent scope watcher when this scope is destroyed
scope.$on('$destroy', unwatch); break;
case '&': /* jshint -W044 */ if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) { throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' + attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.'); } parentGet = $parse(attrs[attrName]); scope[scopeName] = function(locals) { return parentGet(scope, locals); }; break; } }); }; }]);
/** * @ngdoc service * @name $ionicBody * @module ionic * @description An angular utility service to easily and efficiently * add and remove CSS classes from the document's body element. */ IonicModule .factory('$ionicBody', ['$document', function($document) { return { /** * @ngdoc method * @name $ionicBody#addClass * @description Add a class to the document's body element. * @param {string} class Each argument will be added to the body element. * @returns {$ionicBody} The $ionicBody service so methods can be chained. */ addClass: function() { for (var x = 0; x < arguments.length; x++) { $document[0].body.classList.add(arguments[x]); } return this; }, /** * @ngdoc method * @name $ionicBody#removeClass * @description Remove a class from the document's body element. * @param {string} class Each argument will be removed from the body element. * @returns {$ionicBody} The $ionicBody service so methods can be chained. */ removeClass: function() { for (var x = 0; x < arguments.length; x++) { $document[0].body.classList.remove(arguments[x]); } return this; }, /** * @ngdoc method * @name $ionicBody#enableClass * @description Similar to the `add` method, except the first parameter accepts a boolean * value determining if the class should be added or removed. Rather than writing user code, * such as "if true then add the class, else then remove the class", this method can be * given a true or false value which reduces redundant code. * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed. * @param {string} class Each remaining argument would be added or removed depending on * the first argument. * @returns {$ionicBody} The $ionicBody service so methods can be chained. */ enableClass: function(shouldEnableClass) { var args = Array.prototype.slice.call(arguments).slice(1); if (shouldEnableClass) { this.addClass.apply(this, args); } else { this.removeClass.apply(this, args); } return this; }, /** * @ngdoc method * @name $ionicBody#append * @description Append a child to the document's body. * @param {element} element The element to be appended to the body. The passed in element * can be either a jqLite element, or a DOM element. * @returns {$ionicBody} The $ionicBody service so methods can be chained. */ append: function(ele) { $document[0].body.appendChild(ele.length ? ele[0] : ele); return this; }, /** * @ngdoc method * @name $ionicBody#get * @description Get the document's body element. * @returns {element} Returns the document's body element. */ get: function() { return $document[0].body; } }; }]);
IonicModule .factory('$ionicClickBlock', [ '$document', '$ionicBody', '$timeout', function($document, $ionicBody, $timeout) { var CSS_HIDE = 'click-block-hide'; var cbEle, fallbackTimer, pendingShow;
function preventClick(ev) { ev.preventDefault(); ev.stopPropagation(); }
function addClickBlock() { if (pendingShow) { if (cbEle) { cbEle.classList.remove(CSS_HIDE); } else { cbEle = $document[0].createElement('div'); cbEle.className = 'click-block'; $ionicBody.append(cbEle); cbEle.addEventListener('touchstart', preventClick); cbEle.addEventListener('mousedown', preventClick); } pendingShow = false; } }
function removeClickBlock() { cbEle && cbEle.classList.add(CSS_HIDE); }
return { show: function(autoExpire) { pendingShow = true; $timeout.cancel(fallbackTimer); fallbackTimer = $timeout(this.hide, autoExpire || 310, false); addClickBlock(); }, hide: function() { pendingShow = false; $timeout.cancel(fallbackTimer); removeClickBlock(); } }; }]);
/** * @ngdoc service * @name $ionicGesture * @module ionic * @description An angular service exposing ionic * {@link ionic.utility:ionic.EventController}'s gestures. */ IonicModule .factory('$ionicGesture', [function() { return { /** * @ngdoc method * @name $ionicGesture#on * @description Add an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#onGesture}. * @param {string} eventType The gesture event to listen for. * @param {function(e)} callback The function to call when the gesture * happens. * @param {element} $element The angular element to listen for the event on. * @param {object} options object. * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on). */ on: function(eventType, cb, $element, options) { return window.ionic.onGesture(eventType, cb, $element[0], options); }, /** * @ngdoc method * @name $ionicGesture#off * @description Remove an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#offGesture}. * @param {ionic.Gesture} gesture The gesture that should be removed. * @param {string} eventType The gesture event to remove the listener for. * @param {function(e)} callback The listener to remove. */ off: function(gesture, eventType, cb) { return window.ionic.offGesture(gesture, eventType, cb); } }; }]);
/** * @ngdoc service * @name $ionicHistory * @module ionic * @description * $ionicHistory keeps track of views as the user navigates through an app. Similar to the way a * browser behaves, an Ionic app is able to keep track of the previous view, the current view, and * the forward view (if there is one). However, a typical web browser only keeps track of one * history stack in a linear fashion. * * Unlike a traditional browser environment, apps and webapps have parallel independent histories, * such as with tabs. Should a user navigate few pages deep on one tab, and then switch to a new * tab and back, the back button relates not to the previous tab, but to the previous pages * visited within _that_ tab. * * `$ionicHistory` facilitates this parallel history architecture. */
IonicModule .factory('$ionicHistory', [ '$rootScope', '$state', '$location', '$window', '$timeout', '$ionicViewSwitcher', '$ionicNavViewDelegate', function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ionicNavViewDelegate) {
// history actions while navigating views
var ACTION_INITIAL_VIEW = 'initialView'; var ACTION_NEW_VIEW = 'newView'; var ACTION_MOVE_BACK = 'moveBack'; var ACTION_MOVE_FORWARD = 'moveForward';
// direction of navigation
var DIRECTION_BACK = 'back'; var DIRECTION_FORWARD = 'forward'; var DIRECTION_ENTER = 'enter'; var DIRECTION_EXIT = 'exit'; var DIRECTION_SWAP = 'swap'; var DIRECTION_NONE = 'none';
var stateChangeCounter = 0; var lastStateId, nextViewOptions, deregisterStateChangeListener, nextViewExpireTimer, forcedNav;
var viewHistory = { histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1 } }, views: {}, backView: null, forwardView: null, currentView: null };
var View = function() {}; View.prototype.initialize = function(data) { if (data) { for (var name in data) this[name] = data[name]; return this; } return null; }; View.prototype.go = function() {
if (this.stateName) { return $state.go(this.stateName, this.stateParams); }
if (this.url && this.url !== $location.url()) {
if (viewHistory.backView === this) { return $window.history.go(-1); } else if (viewHistory.forwardView === this) { return $window.history.go(1); }
$location.url(this.url); }
return null; }; View.prototype.destroy = function() { if (this.scope) { this.scope.$destroy && this.scope.$destroy(); this.scope = null; } };
function getViewById(viewId) { return (viewId ? viewHistory.views[ viewId ] : null); }
function getBackView(view) { return (view ? getViewById(view.backViewId) : null); }
function getForwardView(view) { return (view ? getViewById(view.forwardViewId) : null); }
function getHistoryById(historyId) { return (historyId ? viewHistory.histories[ historyId ] : null); }
function getHistory(scope) { var histObj = getParentHistoryObj(scope);
if (!viewHistory.histories[ histObj.historyId ]) { // this history object exists in parent scope, but doesn't
// exist in the history data yet
viewHistory.histories[ histObj.historyId ] = { historyId: histObj.historyId, parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId, stack: [], cursor: -1 }; } return getHistoryById(histObj.historyId); }
function getParentHistoryObj(scope) { var parentScope = scope; while (parentScope) { if (parentScope.hasOwnProperty('$historyId')) { // this parent scope has a historyId
return { historyId: parentScope.$historyId, scope: parentScope }; } // nothing found keep climbing up
parentScope = parentScope.$parent; } // no history for the parent, use the root
return { historyId: 'root', scope: $rootScope }; }
function setNavViews(viewId) { viewHistory.currentView = getViewById(viewId); viewHistory.backView = getBackView(viewHistory.currentView); viewHistory.forwardView = getForwardView(viewHistory.currentView); }
function getCurrentStateId() { var id; if ($state && $state.current && $state.current.name) { id = $state.current.name; if ($state.params) { for (var key in $state.params) { if ($state.params.hasOwnProperty(key) && $state.params[key]) { id += "_" + key + "=" + $state.params[key]; } } } return id; } // if something goes wrong make sure its got a unique stateId
return ionic.Utils.nextUid(); }
function getCurrentStateParams() { var rtn; if ($state && $state.params) { for (var key in $state.params) { if ($state.params.hasOwnProperty(key)) { rtn = rtn || {}; rtn[key] = $state.params[key]; } } } return rtn; }
return {
register: function(parentScope, viewLocals) {
var currentStateId = getCurrentStateId(), hist = getHistory(parentScope), currentView = viewHistory.currentView, backView = viewHistory.backView, forwardView = viewHistory.forwardView, viewId = null, action = null, direction = DIRECTION_NONE, historyId = hist.historyId, url = $location.url(), tmp, x, ele;
if (lastStateId !== currentStateId) { lastStateId = currentStateId; stateChangeCounter++; }
if (forcedNav) { // we've previously set exactly what to do
viewId = forcedNav.viewId; action = forcedNav.action; direction = forcedNav.direction; forcedNav = null;
} else if (backView && backView.stateId === currentStateId) { // they went back one, set the old current view as a forward view
viewId = backView.viewId; historyId = backView.historyId; action = ACTION_MOVE_BACK; if (backView.historyId === currentView.historyId) { // went back in the same history
direction = DIRECTION_BACK;
} else if (currentView) { direction = DIRECTION_EXIT;
tmp = getHistoryById(backView.historyId); if (tmp && tmp.parentHistoryId === currentView.historyId) { direction = DIRECTION_ENTER;
} else { tmp = getHistoryById(currentView.historyId); if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { direction = DIRECTION_SWAP; } } }
} else if (forwardView && forwardView.stateId === currentStateId) { // they went to the forward one, set the forward view to no longer a forward view
viewId = forwardView.viewId; historyId = forwardView.historyId; action = ACTION_MOVE_FORWARD; if (forwardView.historyId === currentView.historyId) { direction = DIRECTION_FORWARD;
} else if (currentView) { direction = DIRECTION_EXIT;
if (currentView.historyId === hist.parentHistoryId) { direction = DIRECTION_ENTER;
} else { tmp = getHistoryById(currentView.historyId); if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { direction = DIRECTION_SWAP; } } }
tmp = getParentHistoryObj(parentScope); if (forwardView.historyId && tmp.scope) { // if a history has already been created by the forward view then make sure it stays the same
tmp.scope.$historyId = forwardView.historyId; historyId = forwardView.historyId; }
} else if (currentView && currentView.historyId !== historyId && hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length && hist.stack[hist.cursor].stateId === currentStateId) { // they just changed to a different history and the history already has views in it
var switchToView = hist.stack[hist.cursor]; viewId = switchToView.viewId; historyId = switchToView.historyId; action = ACTION_MOVE_BACK; direction = DIRECTION_SWAP;
tmp = getHistoryById(currentView.historyId); if (tmp && tmp.parentHistoryId === historyId) { direction = DIRECTION_EXIT;
} else { tmp = getHistoryById(historyId); if (tmp && tmp.parentHistoryId === currentView.historyId) { direction = DIRECTION_ENTER; } }
// if switching to a different history, and the history of the view we're switching
// to has an existing back view from a different history than itself, then
// it's back view would be better represented using the current view as its back view
tmp = getViewById(switchToView.backViewId); if (tmp && switchToView.historyId !== tmp.historyId) { // the new view is being removed from it's old position in the history and being placed at the top,
// so we need to update any views that reference it as a backview, otherwise there will be infinitely loops
var viewIds = Object.keys(viewHistory.views); viewIds.forEach(function(viewId) { var view = viewHistory.views[viewId]; if ( view.backViewId === switchToView.viewId ) { view.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 = '<div class="loading-container">' + '<div class="loading">' + '</div>' + '</div>';
/** * @ngdoc service * @name $ionicLoading * @module ionic * @description * An overlay that can be used to indicate activity while blocking user * interaction. * * @usage * ```js
* angular.module('LoadingApp', ['ionic']) * .controller('LoadingCtrl', function($scope, $ionicLoading) { * $scope.show = function() { * $ionicLoading.show({ * template: 'Loading...' * }).then(function(){ * console.log("The loading indicator is now displayed"); * }); * }; * $scope.hide = function(){ * $ionicLoading.hide().then(function(){ * console.log("The loading indicator is now hidden"); * }); * }; * }); * ```
*/ /** * @ngdoc object * @name $ionicLoadingConfig * @module ionic * @description * Set the default options to be passed to the {@link ionic.service:$ionicLoading} service. * * @usage * ```js
* var app = angular.module('myApp', ['ionic']) * app.constant('$ionicLoadingConfig', { * template: 'Default Loading Template...' * }); * app.controller('AppCtrl', function($scope, $ionicLoading) { * $scope.showLoading = function() { * //options default to values in $ionicLoadingConfig
* $ionicLoading.show().then(function(){ * console.log("The loading indicator is now displayed"); * }); * }; * }); * ```
*/ IonicModule .constant('$ionicLoadingConfig', { template: '<ion-spinner></ion-spinner>' }) .factory('$ionicLoading', [ '$ionicLoadingConfig', '$ionicBody', '$ionicTemplateLoader', '$ionicBackdrop', '$timeout', '$q', '$log', '$compile', '$ionicPlatform', '$rootScope', 'IONIC_BACK_PRIORITY', function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform, $rootScope, IONIC_BACK_PRIORITY) {
var loaderInstance; //default values
var deregisterBackAction = noop; var deregisterStateListener1 = noop; var deregisterStateListener2 = noop; var loadingShowDelay = $q.when();
return { /** * @ngdoc method * @name $ionicLoading#show * @description Shows a loading indicator. If the indicator is already shown, * it will set the options given and keep the indicator shown. * @returns {promise} A promise which is resolved when the loading indicator is presented. * @param {object} opts The options for the loading indicator. Available properties: * - `{string=}` `template` The html content of the indicator. * - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator. * - `{object=}` `scope` The scope to be a child of. Default: creates a child of $rootScope. * - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown. * - `{boolean=}` `hideOnStateChange` Whether to hide the loading spinner when navigating * to a new state. Default false. * - `{number=}` `delay` How many milliseconds to delay showing the indicator. By default there is no delay. * - `{number=}` `duration` How many milliseconds to wait until automatically * hiding the indicator. By default, the indicator will be shown until `.hide()` is called. */ show: showLoader, /** * @ngdoc method * @name $ionicLoading#hide * @description Hides the loading indicator, if shown. * @returns {promise} A promise which is resolved when the loading indicator is hidden. */ hide: hideLoader, /** * @private for testing */ _getLoader: getLoader };
function getLoader() { if (!loaderInstance) { loaderInstance = $ionicTemplateLoader.compile({ template: LOADING_TPL, appendTo: $ionicBody.get() }) .then(function(self) { self.show = function(options) { var templatePromise = options.templateUrl ? $ionicTemplateLoader.load(options.templateUrl) : //options.content: deprecated
$q.when(options.template || options.content || '');
self.scope = options.scope || self.scope;
if (!self.isShown) { //options.showBackdrop: deprecated
self.hasBackdrop = !options.noBackdrop && options.showBackdrop !== false; if (self.hasBackdrop) { $ionicBackdrop.retain(); $ionicBackdrop.getElement().addClass('backdrop-loading'); } }
if (options.duration) { $timeout.cancel(self.durationTimeout); self.durationTimeout = $timeout( angular.bind(self, self.hide), +options.duration ); }
deregisterBackAction(); //Disable hardware back button while loading
deregisterBackAction = $ionicPlatform.registerBackButtonAction( noop, IONIC_BACK_PRIORITY.loading );
templatePromise.then(function(html) { if (html) { var loading = self.element.children(); loading.html(html); $compile(loading.contents())(self.scope); }
//Don't show until template changes
if (self.isShown) { self.element.addClass('visible'); ionic.requestAnimationFrame(function() { if (self.isShown) { self.element.addClass('active'); $ionicBody.addClass('loading-active'); } }); } });
self.isShown = true; }; self.hide = function() {
deregisterBackAction(); if (self.isShown) { if (self.hasBackdrop) { $ionicBackdrop.release(); $ionicBackdrop.getElement().removeClass('backdrop-loading'); } self.element.removeClass('active'); $ionicBody.removeClass('loading-active'); self.element.removeClass('visible'); ionic.requestAnimationFrame(function() { !self.isShown && self.element.removeClass('visible'); }); } $timeout.cancel(self.durationTimeout); self.isShown = false; var loading = self.element.children(); loading.html(""); };
return self; }); } return loaderInstance; }
function showLoader(options) { options = extend({}, $ionicLoadingConfig || {}, options || {}); // use a default delay of 100 to avoid some issues reported on github
// https://github.com/driftyco/ionic/issues/3717
var delay = options.delay || options.showDelay || 0;
deregisterStateListener1(); deregisterStateListener2(); if (options.hideOnStateChange) { deregisterStateListener1 = $rootScope.$on('$stateChangeSuccess', hideLoader); deregisterStateListener2 = $rootScope.$on('$stateChangeError', hideLoader); }
//If loading.show() was called previously, cancel it and show with our new options
$timeout.cancel(loadingShowDelay); loadingShowDelay = $timeout(noop, delay); return loadingShowDelay.then(getLoader).then(function(loader) { return loader.show(options); }); }
function hideLoader() { deregisterStateListener1(); deregisterStateListener2(); $timeout.cancel(loadingShowDelay); return getLoader().then(function(loader) { return loader.hide(); }); } }]);
/** * @ngdoc service * @name $ionicModal * @module ionic * @codepen gblny * @description * * Related: {@link ionic.controller:ionicModal ionicModal controller}. * * The Modal is a content pane that can go over the user's main view * temporarily. Usually used for making a choice or editing an item. * * Put the content of the modal inside of an `<ion-modal-view>` element. * * **Notes:** * - A modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating * scope, passing in itself as an event argument. Both the modal.removed and modal.hidden events are * called when the modal is removed. * * - This example assumes your modal is in your main index file or another template file. If it is in its own * template file, remove the script tags and call it by file name. * * @usage * ```html
* <script id="my-modal.html" type="text/ng-template"> * <ion-modal-view> * <ion-header-bar> * <h1 class="title">My Modal title</h1> * </ion-header-bar> * <ion-content> * Hello! * </ion-content> * </ion-modal-view> * </script> * ```
* ```js
* angular.module('testApp', ['ionic']) * .controller('MyController', function($scope, $ionicModal) { * $ionicModal.fromTemplateUrl('my-modal.html', { * scope: $scope, * animation: 'slide-in-up' * }).then(function(modal) { * $scope.modal = modal; * }); * $scope.openModal = function() { * $scope.modal.show(); * }; * $scope.closeModal = function() { * $scope.modal.hide(); * }; * // Cleanup the modal when we're done with it!
* $scope.$on('$destroy', function() { * $scope.modal.remove(); * }); * // Execute action on hide modal
* $scope.$on('modal.hidden', function() { * // Execute action
* }); * // Execute action on remove modal
* $scope.$on('modal.removed', function() { * // Execute action
* }); * }); * ```
*/ IonicModule .factory('$ionicModal', [ '$rootScope', '$ionicBody', '$compile', '$timeout', '$ionicPlatform', '$ionicTemplateLoader', '$$q', '$log', '$ionicClickBlock', '$window', 'IONIC_BACK_PRIORITY', function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $$q, $log, $ionicClickBlock, $window, IONIC_BACK_PRIORITY) {
/** * @ngdoc controller * @name ionicModal * @module ionic * @description * Instantiated by the {@link ionic.service:$ionicModal} service. * * Be sure to call [remove()](#remove) when you are done with each modal * to clean it up and avoid memory leaks. * * Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating * scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are * called when the modal is removed. */ var ModalView = ionic.views.Modal.inherit({ /** * @ngdoc method * @name ionicModal#initialize * @description Creates a new modal controller instance. * @param {object} options An options object with the following properties: * - `{object=}` `scope` The scope to be a child of. * Default: creates a child of $rootScope. * - `{string=}` `animation` The animation to show & hide with. * Default: 'slide-in-up' * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of * the modal when shown. Will only show the keyboard on iOS, to force the keyboard to show * on Android, please use the [Ionic keyboard plugin](https://github.com/driftyco/ionic-plugin-keyboard#keyboardshow).
* Default: false. * - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop. * Default: true. * - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware * back button on Android and similar devices. Default: true. */ initialize: function(opts) { ionic.views.Modal.prototype.initialize.call(this, opts); this.animation = opts.animation || 'slide-in-up'; },
/** * @ngdoc method * @name ionicModal#show * @description Show this modal instance. * @returns {promise} A promise which is resolved when the modal is finished animating in. */ show: function(target) { var self = this;
if (self.scope.$$destroyed) { $log.error('Cannot call ' + self.viewType + '.show() after remove(). Please create a new ' + self.viewType + ' instance.'); return $$q.when(); }
// on iOS, clicks will sometimes bleed through/ghost click on underlying
// elements
$ionicClickBlock.show(600); stack.add(self);
var modalEl = jqLite(self.modalEl);
self.el.classList.remove('hide'); $timeout(function() { if (!self._isShown) return; $ionicBody.addClass(self.viewType + '-open'); }, 400, false);
if (!self.el.parentElement) { modalEl.addClass(self.animation); $ionicBody.append(self.el); }
// if modal was closed while the keyboard was up, reset scroll view on
// next show since we can only resize it once it's visible
var scrollCtrl = modalEl.data('$$ionicScrollController'); scrollCtrl && scrollCtrl.resize();
if (target && self.positionView) { self.positionView(target, modalEl); // set up a listener for in case the window size changes
self._onWindowResize = function() { if (self._isShown) self.positionView(target, modalEl); }; ionic.on('resize', self._onWindowResize, window); }
modalEl.addClass('ng-enter active') .removeClass('ng-leave ng-leave-active');
self._isShown = true; self._deregisterBackButton = $ionicPlatform.registerBackButtonAction( self.hardwareBackButtonClose ? angular.bind(self, self.hide) : noop, IONIC_BACK_PRIORITY.modal );
ionic.views.Modal.prototype.show.call(self);
$timeout(function() { if (!self._isShown) return; modalEl.addClass('ng-enter-active'); ionic.trigger('resize'); self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self); self.el.classList.add('active'); self.scope.$broadcast('$ionicHeader.align'); self.scope.$broadcast('$ionicFooter.align'); self.scope.$broadcast('$ionic.modalPresented'); }, 20);
return $timeout(function() { if (!self._isShown) return; self.$el.on('touchmove', function(e) { //Don't allow scrolling while open by dragging on backdrop
var isInScroll = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'scroll'); if (!isInScroll) { e.preventDefault(); } }); //After animating in, allow hide on backdrop click
self.$el.on('click', function(e) { if (self.backdropClickToClose && e.target === self.el && stack.isHighest(self)) { self.hide(); } }); }, 400); },
/** * @ngdoc method * @name ionicModal#hide * @description Hide this modal instance. * @returns {promise} A promise which is resolved when the modal is finished animating out. */ hide: function() { var self = this; var modalEl = jqLite(self.modalEl);
// on iOS, clicks will sometimes bleed through/ghost click on underlying
// elements
$ionicClickBlock.show(600); stack.remove(self);
self.el.classList.remove('active'); modalEl.addClass('ng-leave');
$timeout(function() { if (self._isShown) return; modalEl.addClass('ng-leave-active') .removeClass('ng-enter ng-enter-active active');
self.scope.$broadcast('$ionic.modalRemoved'); }, 20, false);
self.$el.off('click'); self._isShown = false; self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self); self._deregisterBackButton && self._deregisterBackButton();
ionic.views.Modal.prototype.hide.call(self);
// clean up event listeners
if (self.positionView) { ionic.off('resize', self._onWindowResize, window); }
return $timeout(function() { $ionicBody.removeClass(self.viewType + '-open'); self.el.classList.add('hide'); }, self.hideDelay || 320); },
/** * @ngdoc method * @name ionicModal#remove * @description Remove this modal instance from the DOM and clean up. * @returns {promise} A promise which is resolved when the modal is finished animating out. */ remove: function() { var self = this, deferred, promise; self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self);
// Only hide modal, when it is actually shown!
// The hide function shows a click-block-div for a split second, because on iOS,
// clicks will sometimes bleed through/ghost click on underlying elements.
// However, this will make the app unresponsive for short amount of time.
// We don't want that, if the modal window is already hidden.
if (self._isShown) { promise = self.hide(); } else { deferred = $$q.defer(); deferred.resolve(); promise = deferred.promise; }
return promise.then(function() { self.scope.$destroy(); self.$el.remove(); }); },
/** * @ngdoc method * @name ionicModal#isShown * @returns boolean Whether this modal is currently shown. */ isShown: function() { return !!this._isShown; } });
var createModal = function(templateString, options) { // Create a new scope for the modal
var scope = options.scope && options.scope.$new() || $rootScope.$new(true);
options.viewType = options.viewType || 'modal';
extend(scope, { $hasHeader: false, $hasSubheader: false, $hasFooter: false, $hasSubfooter: false, $hasTabs: false, $hasTabsTop: false });
// Compile the template
var element = $compile('<ion-' + options.viewType + '>' + templateString + '</ion-' + options.viewType + '>')(scope);
options.$el = element; options.el = element[0]; options.modalEl = options.el.querySelector('.' + options.viewType); var modal = new ModalView(options);
modal.scope = scope;
// If this wasn't a defined scope, we can assign the viewType to the isolated scope
// we created
if (!options.scope) { scope[ options.viewType ] = modal; }
return modal; };
var modalStack = []; var stack = { add: function(modal) { modalStack.push(modal); }, remove: function(modal) { var index = modalStack.indexOf(modal); if (index > -1 && index < modalStack.length) { modalStack.splice(index, 1); } }, isHighest: function(modal) { var index = modalStack.indexOf(modal); return (index > -1 && index === modalStack.length - 1); } };
return { /** * @ngdoc method * @name $ionicModal#fromTemplate * @param {string} templateString The template string to use as the modal's * content. * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method. * @returns {object} An instance of an {@link ionic.controller:ionicModal} * controller. */ fromTemplate: function(templateString, options) { var modal = createModal(templateString, options || {}); return modal; }, /** * @ngdoc method * @name $ionicModal#fromTemplateUrl * @param {string} templateUrl The url to load the template from. * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method. * options object. * @returns {promise} A promise that will be resolved with an instance of * an {@link ionic.controller:ionicModal} controller. */ fromTemplateUrl: function(url, options, _) { var cb; //Deprecated: allow a callback as second parameter. Now we return a promise.
if (angular.isFunction(options)) { cb = options; options = _; } return $ionicTemplateLoader.load(url).then(function(templateString) { var modal = createModal(templateString, options || {}); cb && cb(modal); return modal; }); },
stack: stack }; }]);
/** * @ngdoc service * @name $ionicNavBarDelegate * @module ionic * @description * Delegate for controlling the {@link ionic.directive:ionNavBar} directive. * * @usage * * ```html
* <body ng-controller="MyCtrl"> * <ion-nav-bar> * <button ng-click="setNavTitle('banana')"> * Set title to banana! * </button> * </ion-nav-bar> * </body> * ```
* ```js
* function MyCtrl($scope, $ionicNavBarDelegate) { * $scope.setNavTitle = function(title) { * $ionicNavBarDelegate.title(title); * } * } * ```
*/ IonicModule .service('$ionicNavBarDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicNavBarDelegate#align * @description Aligns the title with the buttons in a given direction. * @param {string=} direction The direction to the align the title text towards. * Available: 'left', 'right', 'center'. Default: 'center'. */ 'align', /** * @ngdoc method * @name $ionicNavBarDelegate#showBackButton * @description * Set/get whether the {@link ionic.directive:ionNavBackButton} is shown * (if it exists and there is a previous view that can be navigated to). * @param {boolean=} show Whether to show the back button. * @returns {boolean} Whether the back button is shown. */ 'showBackButton', /** * @ngdoc method * @name $ionicNavBarDelegate#showBar * @description * Set/get whether the {@link ionic.directive:ionNavBar} is shown. * @param {boolean} show Whether to show the bar. * @returns {boolean} Whether the bar is shown. */ 'showBar', /** * @ngdoc method * @name $ionicNavBarDelegate#title * @description * Set the title for the {@link ionic.directive:ionNavBar}. * @param {string} title The new title to show. */ 'title',
// DEPRECATED, as of v1.0.0-beta14 -------
'changeTitle', 'setTitle', 'getTitle', 'back', 'getPreviousTitle' // END DEPRECATED -------
]));
IonicModule .service('$ionicNavViewDelegate', ionic.DelegateService([ 'clearCache' ]));
/** * @ngdoc service * @name $ionicPlatform * @module ionic * @description * An angular abstraction of {@link ionic.utility:ionic.Platform}. * * Used to detect the current platform, as well as do things like override the * Android back button in PhoneGap/Cordova. */ IonicModule .constant('IONIC_BACK_PRIORITY', { view: 100, sideMenu: 150, modal: 200, actionSheet: 300, popup: 400, loading: 500 }) .provider('$ionicPlatform', function() { return { $get: ['$q', '$ionicScrollDelegate', function($q, $ionicScrollDelegate) { var self = {
/** * @ngdoc method * @name $ionicPlatform#onHardwareBackButton * @description * Some platforms have a hardware back button, so this is one way to * bind to it. * @param {function} callback the callback to trigger when this event occurs */ onHardwareBackButton: function(cb) { ionic.Platform.ready(function() { document.addEventListener('backbutton', cb, false); }); },
/** * @ngdoc method * @name $ionicPlatform#offHardwareBackButton * @description * Remove an event listener for the backbutton. * @param {function} callback The listener function that was * originally bound. */ offHardwareBackButton: function(fn) { ionic.Platform.ready(function() { document.removeEventListener('backbutton', fn); }); },
/** * @ngdoc method * @name $ionicPlatform#registerBackButtonAction * @description * Register a hardware back button action. Only one action will execute * when the back button is clicked, so this method decides which of * the registered back button actions has the highest priority. * * For example, if an actionsheet is showing, the back button should * close the actionsheet, but it should not also go back a page view * or close a modal which may be open. * * The priorities for the existing back button hooks are as follows: * Return to previous view = 100 * Close side menu = 150 * Dismiss modal = 200 * Close action sheet = 300 * Dismiss popup = 400 * Dismiss loading overlay = 500 * * Your back button action will override each of the above actions * whose priority is less than the priority you provide. For example, * an action assigned a priority of 101 will override the 'return to * previous view' action, but not any of the other actions. * * @param {function} callback Called when the back button is pressed, * if this listener is the highest priority. * @param {number} priority Only the highest priority will execute. * @param {*=} actionId The id to assign this action. Default: a * random unique id. * @returns {function} A function that, when called, will deregister * this backButtonAction. */ $backButtonActions: {}, registerBackButtonAction: function(fn, priority, actionId) {
if (!self._hasBackButtonHandler) { // add a back button listener if one hasn't been setup yet
self.$backButtonActions = {}; self.onHardwareBackButton(self.hardwareBackButtonClick); self._hasBackButtonHandler = true; }
var action = { id: (actionId ? actionId : ionic.Utils.nextUid()), priority: (priority ? priority : 0), fn: fn }; self.$backButtonActions[action.id] = action;
// return a function to de-register this back button action
return function() { delete self.$backButtonActions[action.id]; }; },
/** * @private */ hardwareBackButtonClick: function(e) { // loop through all the registered back button actions
// and only run the last one of the highest priority
var priorityAction, actionId; for (actionId in self.$backButtonActions) { if (!priorityAction || self.$backButtonActions[actionId].priority >= priorityAction.priority) { priorityAction = self.$backButtonActions[actionId]; } } if (priorityAction) { priorityAction.fn(e); return priorityAction; } },
is: function(type) { return ionic.Platform.is(type); },
/** * @ngdoc method * @name $ionicPlatform#on * @description * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`, * `offline`, etc. More information about available event types can be found in * [Cordova's event documentation](https://cordova.apache.org/docs/en/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 `<ion-popover-view>` element. * * @usage * ```html
* <p> * <button ng-click="openPopover($event)">Open Popover</button> * </p> * * <script id="my-popover.html" type="text/ng-template"> * <ion-popover-view> * <ion-header-bar> * <h1 class="title">My Popover Title</h1> * </ion-header-bar> * <ion-content> * Hello! * </ion-content> * </ion-popover-view> * </script> * ```
* ```js
* angular.module('testApp', ['ionic']) * .controller('MyController', function($scope, $ionicPopover) { * * // .fromTemplate() method
* var template = '<ion-popover-view><ion-header-bar> <h1 class="title">My Popover Title</h1> </ion-header-bar> <ion-content> Hello! </ion-content></ion-popover-view>'; * * $scope.popover = $ionicPopover.fromTemplate(template, { * scope: $scope * }); * * // .fromTemplateUrl() method
* $ionicPopover.fromTemplateUrl('my-popover.html', { * scope: $scope * }).then(function(popover) { * $scope.popover = popover; * }); * * * $scope.openPopover = function($event) { * $scope.popover.show($event); * }; * $scope.closePopover = function() { * $scope.popover.hide(); * }; * //Cleanup the popover when we're done with it!
* $scope.$on('$destroy', function() { * $scope.popover.remove(); * }); * // Execute action on hide popover
* $scope.$on('popover.hidden', function() { * // Execute action
* }); * // Execute action on remove popover
* $scope.$on('popover.removed', function() { * // Execute action
* }); * }); * ```
*/
IonicModule .factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', '$window', function($ionicModal, $ionicPosition, $document, $window) {
var POPOVER_BODY_PADDING = 6;
var POPOVER_OPTIONS = { viewType: 'popover', hideDelay: 1, animation: 'none', positionView: positionView };
function positionView(target, popoverEle) { var targetEle = jqLite(target.target || target); var buttonOffset = $ionicPosition.offset(targetEle); var popoverWidth = popoverEle.prop('offsetWidth'); var popoverHeight = popoverEle.prop('offsetHeight'); // Use innerWidth and innerHeight, because clientWidth and clientHeight
// doesn't work consistently for body on all platforms
var bodyWidth = $window.innerWidth; var bodyHeight = $window.innerHeight;
var popoverCSS = { left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2 }; var arrowEle = jqLite(popoverEle[0].querySelector('.popover-arrow'));
if (popoverCSS.left < POPOVER_BODY_PADDING) { popoverCSS.left = POPOVER_BODY_PADDING; } else if (popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) { popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING; }
// If the popover when popped down stretches past bottom of screen,
// make it pop up if there's room above
if (buttonOffset.top + buttonOffset.height + popoverHeight > bodyHeight && buttonOffset.top - popoverHeight > 0) { popoverCSS.top = buttonOffset.top - popoverHeight; popoverEle.addClass('popover-bottom'); } else { popoverCSS.top = buttonOffset.top + buttonOffset.height; popoverEle.removeClass('popover-bottom'); }
arrowEle.css({ left: buttonOffset.left + buttonOffset.width / 2 - arrowEle.prop('offsetWidth') / 2 - popoverCSS.left + 'px' });
popoverEle.css({ top: popoverCSS.top + 'px', left: popoverCSS.left + 'px', marginLeft: '0', opacity: '1' });
}
/** * @ngdoc controller * @name ionicPopover * @module ionic * @description * Instantiated by the {@link ionic.service:$ionicPopover} service. * * Be sure to call [remove()](#remove) when you are done with each popover * to clean it up and avoid memory leaks. * * Note: a popover will broadcast 'popover.shown', 'popover.hidden', and 'popover.removed' events from its originating * scope, passing in itself as an event argument. Both the popover.removed and popover.hidden events are * called when the popover is removed. */
/** * @ngdoc method * @name ionicPopover#initialize * @description Creates a new popover controller instance. * @param {object} options An options object with the following properties: * - `{object=}` `scope` The scope to be a child of. * Default: creates a child of $rootScope. * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of * the popover when shown. Default: false. * - `{boolean=}` `backdropClickToClose` Whether to close the popover on clicking the backdrop. * Default: true. * - `{boolean=}` `hardwareBackButtonClose` Whether the popover can be closed using the hardware * back button on Android and similar devices. Default: true. */
/** * @ngdoc method * @name ionicPopover#show * @description Show this popover instance. * @param {$event} $event The $event or target element which the popover should align * itself next to. * @returns {promise} A promise which is resolved when the popover is finished animating in. */
/** * @ngdoc method * @name ionicPopover#hide * @description Hide this popover instance. * @returns {promise} A promise which is resolved when the popover is finished animating out. */
/** * @ngdoc method * @name ionicPopover#remove * @description Remove this popover instance from the DOM and clean up. * @returns {promise} A promise which is resolved when the popover is finished animating out. */
/** * @ngdoc method * @name ionicPopover#isShown * @returns boolean Whether this popover is currently shown. */
return { /** * @ngdoc method * @name $ionicPopover#fromTemplate * @param {string} templateString The template string to use as the popovers's * content. * @param {object} options Options to be passed to the initialize method. * @returns {object} An instance of an {@link ionic.controller:ionicPopover} * controller (ionicPopover is built on top of $ionicPopover). */ fromTemplate: function(templateString, options) { return $ionicModal.fromTemplate(templateString, ionic.Utils.extend({}, POPOVER_OPTIONS, options)); }, /** * @ngdoc method * @name $ionicPopover#fromTemplateUrl * @param {string} templateUrl The url to load the template from. * @param {object} options Options to be passed to the initialize method. * @returns {promise} A promise that will be resolved with an instance of * an {@link ionic.controller:ionicPopover} controller (ionicPopover is built on top of $ionicPopover). */ fromTemplateUrl: function(url, options) { return $ionicModal.fromTemplateUrl(url, ionic.Utils.extend({}, POPOVER_OPTIONS, options)); } };
}]);
var POPUP_TPL = '<div class="popup-container" ng-class="cssClass">' + '<div class="popup">' + '<div class="popup-head">' + '<h3 class="popup-title" ng-bind-html="title"></h3>' + '<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' + '</div>' + '<div class="popup-body">' + '</div>' + '<div class="popup-buttons" ng-show="buttons.length">' + '<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' + '</div>' + '</div>' + '</div>';
/** * @ngdoc service * @name $ionicPopup * @module ionic * @restrict E * @codepen zkmhJ * @description * * The Ionic Popup service allows programmatically creating and showing popup * windows that require the user to respond in order to continue. * * The popup system has support for more flexible versions of the built in `alert()`, `prompt()`, * and `confirm()` functions that users are used to, in addition to allowing popups with completely * custom content and look. * * An input can be given an `autofocus` attribute so it automatically receives focus when * the popup first shows. However, depending on certain use-cases this can cause issues with * the tap/click system, which is why Ionic prefers using the `autofocus` attribute as * an opt-in feature and not the default. * * @usage * A few basic examples, see below for details about all of the options available. * * ```js
*angular.module('mySuperApp', ['ionic']) *.controller('PopupCtrl',function($scope, $ionicPopup, $timeout) { * * // Triggered on a button click, or some other target
* $scope.showPopup = function() { * $scope.data = {}; * * // An elaborate, custom popup
* var myPopup = $ionicPopup.show({ * template: '<input type="password" ng-model="data.wifi">', * title: 'Enter Wi-Fi Password', * subTitle: 'Please use normal things', * scope: $scope, * buttons: [ * { text: 'Cancel' }, * { * text: '<b>Save</b>', * type: 'button-positive', * onTap: function(e) { * if (!$scope.data.wifi) { * //don't allow the user to close unless he enters wifi password
* e.preventDefault(); * } else { * return $scope.data.wifi; * } * } * } * ] * }); * * myPopup.then(function(res) { * console.log('Tapped!', res); * }); * * $timeout(function() { * myPopup.close(); //close the popup after 3 seconds for some reason
* }, 3000); * }; * * // A confirm dialog
* $scope.showConfirm = function() { * var confirmPopup = $ionicPopup.confirm({ * title: 'Consume Ice Cream', * template: 'Are you sure you want to eat this ice cream?' * }); * * confirmPopup.then(function(res) { * if(res) { * console.log('You are sure'); * } else { * console.log('You are not sure'); * } * }); * }; * * // An alert dialog
* $scope.showAlert = function() { * var alertPopup = $ionicPopup.alert({ * title: 'Don\'t eat that!', * template: 'It might taste good' * }); * * alertPopup.then(function(res) { * console.log('Thank you for not eating my delicious ice cream cone'); * }); * }; *}); *```
*/
IonicModule .factory('$ionicPopup', [ '$ionicTemplateLoader', '$ionicBackdrop', '$q', '$timeout', '$rootScope', '$ionicBody', '$compile', '$ionicPlatform', '$ionicModal', 'IONIC_BACK_PRIORITY', function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicBody, $compile, $ionicPlatform, $ionicModal, IONIC_BACK_PRIORITY) { //TODO allow this to be configured
var config = { stackPushDelay: 75 }; var popupStack = [];
var $ionicPopup = { /** * @ngdoc method * @description * Show a complex popup. This is the master show function for all popups. * * A complex popup has a `buttons` array, with each button having a `text` and `type` * field, in addition to an `onTap` function. The `onTap` function, called when * the corresponding button on the popup is tapped, will by default close the popup * and resolve the popup promise with its return value. If you wish to prevent the * default and keep the popup open on button tap, call `event.preventDefault()` on the * passed in tap event. Details below. * * @name $ionicPopup#show * @param {object} options The options for the new popup, of the form: * * ```
* { * title: '', // String. The title of the popup.
* cssClass: '', // String, The custom CSS class name
* subTitle: '', // String (optional). The sub-title of the popup.
* template: '', // String (optional). The html template to place in the popup body.
* templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
* scope: null, // Scope (optional). A scope to link to the popup content.
* buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer.
* text: 'Cancel', * type: 'button-default', * onTap: function(e) { * // e.preventDefault() will stop the popup from closing when tapped.
* e.preventDefault(); * } * }, { * text: 'OK', * type: 'button-positive', * onTap: function(e) { * // Returning a value will cause the promise to resolve with the given value.
* return scope.data.response; * } * }] * } * ```
* * @returns {object} A promise which is resolved when the popup is closed. Has an additional * `close` function, which can be used to programmatically close the popup. */ show: showPopup,
/** * @ngdoc method * @name $ionicPopup#alert * @description Show a simple alert popup with a message and one button that the user can * tap to close the popup. * * @param {object} options The options for showing the alert, of the form: * * ```
* { * title: '', // String. The title of the popup.
* cssClass: '', // String, The custom CSS class name
* subTitle: '', // String (optional). The sub-title of the popup.
* template: '', // String (optional). The html template to place in the popup body.
* templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
* okText: '', // String (default: 'OK'). The text of the OK button.
* okType: '', // String (default: 'button-positive'). The type of the OK button.
* } * ```
* * @returns {object} A promise which is resolved when the popup is closed. Has one additional * function `close`, which can be called with any value to programmatically close the popup * with the given value. */ alert: showAlert,
/** * @ngdoc method * @name $ionicPopup#confirm * @description * Show a simple confirm popup with a Cancel and OK button. * * Resolves the promise with true if the user presses the OK button, and false if the * user presses the Cancel button. * * @param {object} options The options for showing the confirm popup, of the form: * * ```
* { * title: '', // String. The title of the popup.
* cssClass: '', // String, The custom CSS class name
* subTitle: '', // String (optional). The sub-title of the popup.
* template: '', // String (optional). The html template to place in the popup body.
* templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
* cancelText: '', // String (default: 'Cancel'). The text of the Cancel button.
* cancelType: '', // String (default: 'button-default'). The type of the Cancel button.
* okText: '', // String (default: 'OK'). The text of the OK button.
* okType: '', // String (default: 'button-positive'). The type of the OK button.
* } * ```
* * @returns {object} A promise which is resolved when the popup is closed. Has one additional * function `close`, which can be called with any value to programmatically close the popup * with the given value. */ confirm: showConfirm,
/** * @ngdoc method * @name $ionicPopup#prompt * @description Show a simple prompt popup, which has an input, OK button, and Cancel button. * Resolves the promise with the value of the input if the user presses OK, and with undefined * if the user presses Cancel. * * ```javascript
* $ionicPopup.prompt({ * title: 'Password Check', * template: 'Enter your secret password', * inputType: 'password', * inputPlaceholder: 'Your password' * }).then(function(res) { * console.log('Your password is', res); * }); * ```
* @param {object} options The options for showing the prompt popup, of the form: * * ```
* { * title: '', // String. The title of the popup.
* cssClass: '', // String, The custom CSS class name
* subTitle: '', // String (optional). The sub-title of the popup.
* template: '', // String (optional). The html template to place in the popup body.
* templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
* inputType: // String (default: 'text'). The type of input to use
* defaultText: // String (default: ''). The initial value placed into the input.
* maxLength: // Integer (default: null). Specify a maxlength attribute for the input.
* inputPlaceholder: // String (default: ''). A placeholder to use for the input.
* cancelText: // String (default: 'Cancel'. The text of the Cancel button.
* cancelType: // String (default: 'button-default'). The type of the Cancel button.
* okText: // String (default: 'OK'). The text of the OK button.
* okType: // String (default: 'button-positive'). The type of the OK button.
* } * ```
* * @returns {object} A promise which is resolved when the popup is closed. Has one additional * function `close`, which can be called with any value to programmatically close the popup * with the given value. */ prompt: showPrompt, /** * @private for testing */ _createPopup: createPopup, _popupStack: popupStack };
return $ionicPopup;
function createPopup(options) { options = extend({ scope: null, title: '', buttons: [] }, options || {});
var self = {}; self.scope = (options.scope || $rootScope).$new(); self.element = jqLite(POPUP_TPL); self.responseDeferred = $q.defer();
$ionicBody.get().appendChild(self.element[0]); $compile(self.element)(self.scope);
extend(self.scope, { title: options.title, buttons: options.buttons, subTitle: options.subTitle, cssClass: options.cssClass, $buttonTapped: function(button, event) { var result = (button.onTap || noop).apply(self, [event]); event = event.originalEvent || event; //jquery events
if (!event.defaultPrevented) { self.responseDeferred.resolve(result); } } });
$q.when( options.templateUrl ? $ionicTemplateLoader.load(options.templateUrl) : (options.template || options.content || '') ).then(function(template) { var popupBody = jqLite(self.element[0].querySelector('.popup-body')); if (template) { popupBody.html(template); $compile(popupBody.contents())(self.scope); } else { popupBody.remove(); } });
self.show = function() { if (self.isShown || self.removed) return;
$ionicModal.stack.add(self); self.isShown = true; ionic.requestAnimationFrame(function() { //if hidden while waiting for raf, don't show
if (!self.isShown) return;
self.element.removeClass('popup-hidden'); self.element.addClass('popup-showing active'); focusInput(self.element); }); };
self.hide = function(callback) { callback = callback || noop; if (!self.isShown) return callback();
$ionicModal.stack.remove(self); self.isShown = false; self.element.removeClass('active'); self.element.addClass('popup-hidden'); $timeout(callback, 250, false); };
self.remove = function() { if (self.removed) return;
self.hide(function() { self.element.remove(); self.scope.$destroy(); });
self.removed = true; };
return self; }
function onHardwareBackButton() { var last = popupStack[popupStack.length - 1]; last && last.responseDeferred.resolve(); }
function showPopup(options) { var popup = $ionicPopup._createPopup(options); var showDelay = 0;
if (popupStack.length > 0) { showDelay = config.stackPushDelay; $timeout(popupStack[popupStack.length - 1].hide, showDelay, false); } else { //Add popup-open & backdrop if this is first popup
$ionicBody.addClass('popup-open'); $ionicBackdrop.retain(); //only show the backdrop on the first popup
$ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction( onHardwareBackButton, IONIC_BACK_PRIORITY.popup ); }
// Expose a 'close' method on the returned promise
popup.responseDeferred.promise.close = function popupClose(result) { if (!popup.removed) popup.responseDeferred.resolve(result); }; //DEPRECATED: notify the promise with an object with a close method
popup.responseDeferred.notify({ close: popup.responseDeferred.close });
doShow();
return popup.responseDeferred.promise;
function doShow() { popupStack.push(popup); $timeout(popup.show, showDelay, false);
popup.responseDeferred.promise.then(function(result) { var index = popupStack.indexOf(popup); if (index !== -1) { popupStack.splice(index, 1); }
popup.remove();
if (popupStack.length > 0) { popupStack[popupStack.length - 1].show(); } else { $ionicBackdrop.release(); //Remove popup-open & backdrop if this is last popup
$timeout(function() { // wait to remove this due to a 300ms delay native
// click which would trigging whatever was underneath this
if (!popupStack.length) { $ionicBody.removeClass('popup-open'); } }, 400, false); ($ionicPopup._backButtonActionDone || noop)(); }
return result; });
}
}
function focusInput(element) { var focusOn = element[0].querySelector('[autofocus]'); if (focusOn) { focusOn.focus(); } }
function showAlert(opts) { return showPopup(extend({ buttons: [{ text: opts.okText || 'OK', type: opts.okType || 'button-positive', onTap: function() { return true; } }] }, opts || {})); }
function showConfirm(opts) { return showPopup(extend({ buttons: [{ text: opts.cancelText || 'Cancel', type: opts.cancelType || 'button-default', onTap: function() { return false; } }, { text: opts.okText || 'OK', type: opts.okType || 'button-positive', onTap: function() { return true; } }] }, opts || {})); }
function showPrompt(opts) { var scope = $rootScope.$new(true); scope.data = {}; scope.data.fieldtype = opts.inputType ? opts.inputType : 'text'; scope.data.response = opts.defaultText ? opts.defaultText : ''; scope.data.placeholder = opts.inputPlaceholder ? opts.inputPlaceholder : ''; scope.data.maxlength = opts.maxLength ? parseInt(opts.maxLength) : ''; var text = ''; if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) { text = '<span>' + opts.template + '</span>'; delete opts.template; } return showPopup(extend({ template: text + '<input ng-model="data.response" ' + 'type="{{ data.fieldtype }}"' + 'maxlength="{{ data.maxlength }}"' + 'placeholder="{{ data.placeholder }}"' + '>', scope: scope, buttons: [{ text: opts.cancelText || 'Cancel', type: opts.cancelType || 'button-default', onTap: function() {} }, { text: opts.okText || 'OK', type: opts.okType || 'button-positive', onTap: function() { return scope.data.response || ''; } }] }, opts || {})); } }]);
/** * @ngdoc service * @name $ionicPosition * @module ionic * @description * A set of utility methods that can be use to retrieve position of DOM elements. * It is meant to be used where we need to absolute-position DOM elements in * relation to other, existing elements (this is the case for tooltips, popovers, etc.). * * Adapted from [AngularUI Bootstrap](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js),
* ([license](https://github.com/angular-ui/bootstrap/blob/master/LICENSE))
*/ IonicModule .factory('$ionicPosition', ['$document', '$window', function($document, $window) {
function getStyle(el, cssprop) { if (el.currentStyle) { //IE
return el.currentStyle[cssprop]; } else if ($window.getComputedStyle) { return $window.getComputedStyle(el)[cssprop]; } // finally try and get inline style
return el.style[cssprop]; }
/** * Checks if a given element is statically positioned * @param element - raw DOM element */ function isStaticPositioned(element) { return (getStyle(element, 'position') || 'static') === 'static'; }
/** * returns the closest, non-statically positioned parentOffset of a given element * @param element */ var parentOffsetEl = function(element) { var docDomEl = $document[0]; var offsetParent = element.offsetParent || docDomEl; while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) { offsetParent = offsetParent.offsetParent; } return offsetParent || docDomEl; };
return { /** * @ngdoc method * @name $ionicPosition#position * @description Get the current coordinates of the element, relative to the offset parent. * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/).
* @param {element} element The element to get the position of. * @returns {object} Returns an object containing the properties top, left, width and height. */ position: function(element) { var elBCR = this.offset(element); var offsetParentBCR = { top: 0, left: 0 }; var offsetParentEl = parentOffsetEl(element[0]); if (offsetParentEl != $document[0]) { offsetParentBCR = this.offset(jqLite(offsetParentEl)); offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; }
var boundingClientRect = element[0].getBoundingClientRect(); return { width: boundingClientRect.width || element.prop('offsetWidth'), height: boundingClientRect.height || element.prop('offsetHeight'), top: elBCR.top - offsetParentBCR.top, left: elBCR.left - offsetParentBCR.left }; },
/** * @ngdoc method * @name $ionicPosition#offset * @description Get the current coordinates of the element, relative to the document. * Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/).
* @param {element} element The element to get the offset of. * @returns {object} Returns an object containing the properties top, left, width and height. */ offset: function(element) { var boundingClientRect = element[0].getBoundingClientRect(); return { width: boundingClientRect.width || element.prop('offsetWidth'), height: boundingClientRect.height || element.prop('offsetHeight'), top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) }; }
}; }]);
/** * @ngdoc service * @name $ionicScrollDelegate * @module ionic * @description * Delegate for controlling scrollViews (created by * {@link ionic.directive:ionContent} and * {@link ionic.directive:ionScroll} directives). * * Methods called directly on the $ionicScrollDelegate service will control all scroll * views. Use the {@link ionic.service:$ionicScrollDelegate#$getByHandle $getByHandle} * method to control specific scrollViews. * * @usage * * ```html
* <body ng-controller="MainCtrl"> * <ion-content> * <button ng-click="scrollTop()">Scroll to Top!</button> * </ion-content> * </body> * ```
* ```js
* function MainCtrl($scope, $ionicScrollDelegate) { * $scope.scrollTop = function() { * $ionicScrollDelegate.scrollTop(); * }; * } * ```
* * Example of advanced usage, with two scroll areas using `delegate-handle` * for fine control. * * ```html
* <body ng-controller="MainCtrl"> * <ion-content delegate-handle="mainScroll"> * <button ng-click="scrollMainToTop()"> * Scroll content to top! * </button> * <ion-scroll delegate-handle="small" style="height: 100px;"> * <button ng-click="scrollSmallToTop()"> * Scroll small area to top! * </button> * </ion-scroll> * </ion-content> * </body> * ```
* ```js
* function MainCtrl($scope, $ionicScrollDelegate) { * $scope.scrollMainToTop = function() { * $ionicScrollDelegate.$getByHandle('mainScroll').scrollTop(); * }; * $scope.scrollSmallToTop = function() { * $ionicScrollDelegate.$getByHandle('small').scrollTop(); * }; * } * ```
*/ IonicModule .service('$ionicScrollDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicScrollDelegate#resize * @description Tell the scrollView to recalculate the size of its container. */ 'resize', /** * @ngdoc method * @name $ionicScrollDelegate#scrollTop * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollTop', /** * @ngdoc method * @name $ionicScrollDelegate#scrollBottom * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollBottom', /** * @ngdoc method * @name $ionicScrollDelegate#scrollTo * @param {number} left The x-value to scroll to. * @param {number} top The y-value to scroll to. * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollTo', /** * @ngdoc method * @name $ionicScrollDelegate#scrollBy * @param {number} left The x-offset to scroll by. * @param {number} top The y-offset to scroll by. * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollBy', /** * @ngdoc method * @name $ionicScrollDelegate#zoomTo * @param {number} level Level to zoom to. * @param {boolean=} animate Whether to animate the zoom. * @param {number=} originLeft Zoom in at given left coordinate. * @param {number=} originTop Zoom in at given top coordinate. */ 'zoomTo', /** * @ngdoc method * @name $ionicScrollDelegate#zoomBy * @param {number} factor The factor to zoom by. * @param {boolean=} animate Whether to animate the zoom. * @param {number=} originLeft Zoom in at given left coordinate. * @param {number=} originTop Zoom in at given top coordinate. */ 'zoomBy', /** * @ngdoc method * @name $ionicScrollDelegate#getScrollPosition * @returns {object} The scroll position of this view, with the following properties: * - `{number}` `left` The distance the user has scrolled from the left (starts at 0). * - `{number}` `top` The distance the user has scrolled from the top (starts at 0). * - `{number}` `zoom` The current zoom level. */ 'getScrollPosition', /** * @ngdoc method * @name $ionicScrollDelegate#anchorScroll * @description Tell the scrollView to scroll to the element with an id * matching window.location.hash. * * If no matching element is found, it will scroll to top. * * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'anchorScroll', /** * @ngdoc method * @name $ionicScrollDelegate#freezeScroll * @description Does not allow this scroll view to scroll either x or y. * @param {boolean=} shouldFreeze Should this scroll view be prevented from scrolling or not. * @returns {boolean} If the scroll view is being prevented from scrolling or not. */ 'freezeScroll', /** * @ngdoc method * @name $ionicScrollDelegate#freezeAllScrolls * @description Does not allow any of the app's scroll views to scroll either x or y. * @param {boolean=} shouldFreeze Should all app scrolls be prevented from scrolling or not. */ 'freezeAllScrolls', /** * @ngdoc method * @name $ionicScrollDelegate#getScrollView * @returns {object} The scrollView associated with this delegate. */ 'getScrollView' /** * @ngdoc method * @name $ionicScrollDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * scrollViews with `delegate-handle` matching the given handle. * * Example: `$ionicScrollDelegate.$getByHandle('my-handle').scrollTop();` */ ]));
/** * @ngdoc service * @name $ionicSideMenuDelegate * @module ionic * * @description * Delegate for controlling the {@link ionic.directive:ionSideMenus} directive. * * Methods called directly on the $ionicSideMenuDelegate service will control all side * menus. Use the {@link ionic.service:$ionicSideMenuDelegate#$getByHandle $getByHandle} * method to control specific ionSideMenus instances. * * @usage * * ```html
* <body ng-controller="MainCtrl"> * <ion-side-menus> * <ion-side-menu-content> * Content! * <button ng-click="toggleLeftSideMenu()"> * Toggle Left Side Menu * </button> * </ion-side-menu-content> * <ion-side-menu side="left"> * Left Menu! * <ion-side-menu> * </ion-side-menus> * </body> * ```
* ```js
* function MainCtrl($scope, $ionicSideMenuDelegate) { * $scope.toggleLeftSideMenu = function() { * $ionicSideMenuDelegate.toggleLeft(); * }; * } * ```
*/ IonicModule .service('$ionicSideMenuDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicSideMenuDelegate#toggleLeft * @description Toggle the left side menu (if it exists). * @param {boolean=} isOpen Whether to open or close the menu. * Default: Toggles the menu. */ 'toggleLeft', /** * @ngdoc method * @name $ionicSideMenuDelegate#toggleRight * @description Toggle the right side menu (if it exists). * @param {boolean=} isOpen Whether to open or close the menu. * Default: Toggles the menu. */ 'toggleRight', /** * @ngdoc method * @name $ionicSideMenuDelegate#getOpenRatio * @description Gets the ratio of open amount over menu width. For example, a * menu of width 100 that is opened by 50 pixels is 50% opened, and would return * a ratio of 0.5. * * @returns {float} 0 if nothing is open, between 0 and 1 if left menu is * opened/opening, and between 0 and -1 if right menu is opened/opening. */ 'getOpenRatio', /** * @ngdoc method * @name $ionicSideMenuDelegate#isOpen * @returns {boolean} Whether either the left or right menu is currently opened. */ 'isOpen', /** * @ngdoc method * @name $ionicSideMenuDelegate#isOpenLeft * @returns {boolean} Whether the left menu is currently opened. */ 'isOpenLeft', /** * @ngdoc method * @name $ionicSideMenuDelegate#isOpenRight * @returns {boolean} Whether the right menu is currently opened. */ 'isOpenRight', /** * @ngdoc method * @name $ionicSideMenuDelegate#canDragContent * @param {boolean=} canDrag Set whether the content can or cannot be dragged to open * side menus. * @returns {boolean} Whether the content can be dragged to open side menus. */ 'canDragContent', /** * @ngdoc method * @name $ionicSideMenuDelegate#edgeDragThreshold * @param {boolean|number=} value Set whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Accepts three different values: * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu. * - If true is given, the default number of pixels (25) is used as the maximum allowed distance. * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed. * @returns {boolean} Whether the drag can start only from within the edge of screen threshold. */ 'edgeDragThreshold' /** * @ngdoc method * @name $ionicSideMenuDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionSideMenus} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicSideMenuDelegate.$getByHandle('my-handle').toggleLeft();` */ ]));
/** * @ngdoc service * @name $ionicSlideBoxDelegate * @module ionic * @description * Delegate that controls the {@link ionic.directive:ionSlideBox} directive. * * Methods called directly on the $ionicSlideBoxDelegate service will control all slide boxes. Use the {@link ionic.service:$ionicSlideBoxDelegate#$getByHandle $getByHandle} * method to control specific slide box instances. * * @usage * * ```html
* <ion-view> * <ion-slide-box> * <ion-slide> * <div class="box blue"> * <button ng-click="nextSlide()">Next slide!</button> * </div> * </ion-slide> * <ion-slide> * <div class="box red"> * Slide 2! * </div> * </ion-slide> * </ion-slide-box> * </ion-view> * ```
* ```js
* function MyCtrl($scope, $ionicSlideBoxDelegate) { * $scope.nextSlide = function() { * $ionicSlideBoxDelegate.next(); * } * } * ```
*/ IonicModule .service('$ionicSlideBoxDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicSlideBoxDelegate#update * @description * Update the slidebox (for example if using Angular with ng-repeat, * resize it for the elements inside). */ 'update', /** * @ngdoc method * @name $ionicSlideBoxDelegate#slide * @param {number} to The index to slide to. * @param {number=} speed The number of milliseconds the change should take. */ 'slide', 'select', /** * @ngdoc method * @name $ionicSlideBoxDelegate#enableSlide * @param {boolean=} shouldEnable Whether to enable sliding the slidebox. * @returns {boolean} Whether sliding is enabled. */ 'enableSlide', /** * @ngdoc method * @name $ionicSlideBoxDelegate#previous * @param {number=} speed The number of milliseconds the change should take. * @description Go to the previous slide. Wraps around if at the beginning. */ 'previous', /** * @ngdoc method * @name $ionicSlideBoxDelegate#next * @param {number=} speed The number of milliseconds the change should take. * @description Go to the next slide. Wraps around if at the end. */ 'next', /** * @ngdoc method * @name $ionicSlideBoxDelegate#stop * @description Stop sliding. The slideBox will not move again until * explicitly told to do so. */ 'stop', 'autoPlay', /** * @ngdoc method * @name $ionicSlideBoxDelegate#start * @description Start sliding again if the slideBox was stopped. */ 'start', /** * @ngdoc method * @name $ionicSlideBoxDelegate#currentIndex * @returns number The index of the current slide. */ 'currentIndex', 'selected', /** * @ngdoc method * @name $ionicSlideBoxDelegate#slidesCount * @returns number The number of slides there are currently. */ 'slidesCount', 'count', 'loop' /** * @ngdoc method * @name $ionicSlideBoxDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionSlideBox} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicSlideBoxDelegate.$getByHandle('my-handle').stop();` */ ]));
/** * @ngdoc service * @name $ionicTabsDelegate * @module ionic * * @description * Delegate for controlling the {@link ionic.directive:ionTabs} directive. * * Methods called directly on the $ionicTabsDelegate service will control all ionTabs * directives. Use the {@link ionic.service:$ionicTabsDelegate#$getByHandle $getByHandle} * method to control specific ionTabs instances. * * @usage * * ```html
* <body ng-controller="MyCtrl"> * <ion-tabs> * * <ion-tab title="Tab 1"> * Hello tab 1! * <button ng-click="selectTabWithIndex(1)">Select tab 2!</button> * </ion-tab> * <ion-tab title="Tab 2">Hello tab 2!</ion-tab> * * </ion-tabs> * </body> * ```
* ```js
* function MyCtrl($scope, $ionicTabsDelegate) { * $scope.selectTabWithIndex = function(index) { * $ionicTabsDelegate.select(index); * } * } * ```
*/ IonicModule .service('$ionicTabsDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicTabsDelegate#select * @description Select the tab matching the given index. * * @param {number} index Index of the tab to select. */ 'select', /** * @ngdoc method * @name $ionicTabsDelegate#selectedIndex * @returns `number` The index of the selected tab, or -1. */ 'selectedIndex', /** * @ngdoc method * @name $ionicTabsDelegate#showBar * @description * Set/get whether the {@link ionic.directive:ionTabs} is shown * @param {boolean} show Whether to show the bar. * @returns {boolean} Whether the bar is shown. */ 'showBar' /** * @ngdoc method * @name $ionicTabsDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionTabs} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicTabsDelegate.$getByHandle('my-handle').select(0);` */ ]));
// closure to keep things neat
(function() { var templatesToCache = [];
/** * @ngdoc service * @name $ionicTemplateCache * @module ionic * @description A service that preemptively caches template files to eliminate transition flicker and boost performance. * @usage * State templates are cached automatically, but you can optionally cache other templates. * * ```js
* $ionicTemplateCache('myNgIncludeTemplate.html'); * ```
* * Optionally disable all preemptive caching with the `$ionicConfigProvider` or individual states by setting `prefetchTemplate` * in the `$state` definition * * ```js
* angular.module('myApp', ['ionic']) * .config(function($stateProvider, $ionicConfigProvider) { * * // disable preemptive template caching globally
* $ionicConfigProvider.templates.prefetch(false); * * // disable individual states
* $stateProvider * .state('tabs', { * url: "/tab", * abstract: true, * prefetchTemplate: false, * templateUrl: "tabs-templates/tabs.html" * }) * .state('tabs.home', { * url: "/home", * views: { * 'home-tab': { * prefetchTemplate: false, * templateUrl: "tabs-templates/home.html", * controller: 'HomeTabCtrl' * } * } * }); * }); * ```
*/ IonicModule .factory('$ionicTemplateCache', [ '$http', '$templateCache', '$timeout', function($http, $templateCache, $timeout) { var toCache = templatesToCache, hasRun;
function $ionicTemplateCache(templates) { if (typeof templates === 'undefined') { return run(); } if (isString(templates)) { templates = [templates]; } forEach(templates, function(template) { toCache.push(template); }); if (hasRun) { run(); } }
// run through methods - internal method
function run() { var template; $ionicTemplateCache._runCount++;
hasRun = true; // ignore if race condition already zeroed out array
if (toCache.length === 0) return;
var i = 0; while (i < 4 && (template = toCache.pop())) { // note that inline templates are ignored by this request
if (isString(template)) $http.get(template, { cache: $templateCache }); i++; } // only preload 3 templates a second
if (toCache.length) { $timeout(run, 1000); } }
// exposing for testing
$ionicTemplateCache._runCount = 0; // default method
return $ionicTemplateCache; }])
// Intercepts the $stateprovider.state() command to look for templateUrls that can be cached
.config([ '$stateProvider', '$ionicConfigProvider', function($stateProvider, $ionicConfigProvider) { var stateProviderState = $stateProvider.state; $stateProvider.state = function(stateName, definition) { // don't even bother if it's disabled. note, another config may run after this, so it's not a catch-all
if (typeof definition === 'object') { var enabled = definition.prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch(); if (enabled && isString(definition.templateUrl)) templatesToCache.push(definition.templateUrl); if (angular.isObject(definition.views)) { for (var key in definition.views) { enabled = definition.views[key].prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch(); if (enabled && isString(definition.views[key].templateUrl)) templatesToCache.push(definition.views[key].templateUrl); } } } return stateProviderState.call($stateProvider, stateName, definition); }; }])
// process the templateUrls collected by the $stateProvider, adding them to the cache
.run(['$ionicTemplateCache', function($ionicTemplateCache) { $ionicTemplateCache(); }]);
})();
IonicModule .factory('$ionicTemplateLoader', [ '$compile', '$controller', '$http', '$q', '$rootScope', '$templateCache', function($compile, $controller, $http, $q, $rootScope, $templateCache) {
return { load: fetchTemplate, compile: loadAndCompile };
function fetchTemplate(url) { return $http.get(url, {cache: $templateCache}) .then(function(response) { return response.data && response.data.trim(); }); }
function loadAndCompile(options) { options = extend({ template: '', templateUrl: '', scope: null, controller: null, locals: {}, appendTo: null }, options || {});
var templatePromise = options.templateUrl ? this.load(options.templateUrl) : $q.when(options.template);
return templatePromise.then(function(template) { var controller; var scope = options.scope || $rootScope.$new();
//Incase template doesn't have just one root element, do this
var element = jqLite('<div>').html(template).contents();
if (options.controller) { controller = $controller( options.controller, extend(options.locals, { $scope: scope }) ); element.children().data('$ngControllerController', controller); } if (options.appendTo) { jqLite(options.appendTo).append(element); }
$compile(element)(scope);
return { element: element, scope: scope }; }); }
}]);
/** * @private * DEPRECATED, as of v1.0.0-beta14 ------- */ IonicModule .factory('$ionicViewService', ['$ionicHistory', '$log', function($ionicHistory, $log) {
function warn(oldMethod, newMethod) { $log.warn('$ionicViewService' + oldMethod + ' is deprecated, please use $ionicHistory' + newMethod + ' instead: http://ionicframework.com/docs/nightly/api/service/$ionicHistory/'); }
warn('', '');
var methodsMap = { getCurrentView: 'currentView', getBackView: 'backView', getForwardView: 'forwardView', getCurrentStateName: 'currentStateName', nextViewOptions: 'nextViewOptions', clearHistory: 'clearHistory' };
forEach(methodsMap, function(newMethod, oldMethod) { methodsMap[oldMethod] = function() { warn('.' + oldMethod, '.' + newMethod); return $ionicHistory[newMethod].apply(this, arguments); }; });
return methodsMap;
}]);
/** * @private * TODO document */
IonicModule.factory('$ionicViewSwitcher', [ '$timeout', '$document', '$q', '$ionicClickBlock', '$ionicConfig', '$ionicNavBarDelegate', function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDelegate) {
var TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; var DATA_NO_CACHE = '$noCache'; var DATA_DESTROY_ELE = '$destroyEle'; var DATA_ELE_IDENTIFIER = '$eleId'; var DATA_VIEW_ACCESSED = '$accessed'; var DATA_FALLBACK_TIMER = '$fallbackTimer'; var DATA_VIEW = '$viewData'; var NAV_VIEW_ATTR = 'nav-view'; var VIEW_STATUS_ACTIVE = 'active'; var VIEW_STATUS_CACHED = 'cached'; var VIEW_STATUS_STAGED = 'stage';
var transitionCounter = 0; var nextTransition, nextDirection; ionic.transition = ionic.transition || {}; ionic.transition.isActive = false; var isActiveTimer; var cachedAttr = ionic.DomUtil.cachedAttr; var transitionPromises = []; var defaultTimeout = 1100;
var ionicViewSwitcher = {
create: function(navViewCtrl, viewLocals, enteringView, leavingView, renderStart, renderEnd) { // get a reference to an entering/leaving element if they exist
// loop through to see if the view is already in the navViewElement
var enteringEle, leavingEle; var transitionId = ++transitionCounter; var alreadyInDom;
var switcher = {
init: function(registerData, callback) { ionicViewSwitcher.isTransitioning(true);
switcher.loadViewElements(registerData);
switcher.render(registerData, function() { callback && callback(); }); },
loadViewElements: function(registerData) { var x, l, viewEle; var viewElements = navViewCtrl.getViewElements(); var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView); var navViewActiveEleId = navViewCtrl.activeEleId();
for (x = 0, l = viewElements.length; x < l; x++) { viewEle = viewElements.eq(x);
if (viewEle.data(DATA_ELE_IDENTIFIER) === enteringEleIdentifier) { // we found an existing element in the DOM that should be entering the view
if (viewEle.data(DATA_NO_CACHE)) { // the existing element should not be cached, don't use it
viewEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier + ionic.Utils.nextUid()); viewEle.data(DATA_DESTROY_ELE, true);
} else { enteringEle = viewEle; }
} else if (isDefined(navViewActiveEleId) && viewEle.data(DATA_ELE_IDENTIFIER) === navViewActiveEleId) { leavingEle = viewEle; }
if (enteringEle && leavingEle) break; }
alreadyInDom = !!enteringEle;
if (!alreadyInDom) { // still no existing element to use
// create it using existing template/scope/locals
enteringEle = registerData.ele || ionicViewSwitcher.createViewEle(viewLocals);
// existing elements in the DOM are looked up by their state name and state id
enteringEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier); }
if (renderEnd) { navViewCtrl.activeEleId(enteringEleIdentifier); }
registerData.ele = null; },
render: function(registerData, callback) { if (alreadyInDom) { // it was already found in the DOM, just reconnect the scope
ionic.Utils.reconnectScope(enteringEle.scope());
} else { // the entering element is not already in the DOM
// set that the entering element should be "staged" and its
// styles of where this element will go before it hits the DOM
navViewAttr(enteringEle, VIEW_STATUS_STAGED);
var enteringData = getTransitionData(viewLocals, enteringEle, registerData.direction, enteringView); var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none; transitionFn(enteringEle, null, enteringData.direction, true).run(0);
enteringEle.data(DATA_VIEW, { viewId: enteringData.viewId, historyId: enteringData.historyId, stateName: enteringData.stateName, stateParams: enteringData.stateParams });
// if the current state has cache:false
// or the element has cache-view="false" attribute
if (viewState(viewLocals).cache === false || viewState(viewLocals).cache === 'false' || enteringEle.attr('cache-view') == 'false' || $ionicConfig.views.maxCache() === 0) { enteringEle.data(DATA_NO_CACHE, true); }
// append the entering element to the DOM, create a new scope and run link
var viewScope = navViewCtrl.appendViewElement(enteringEle, viewLocals);
delete enteringData.direction; delete enteringData.transition; viewScope.$emit('$ionicView.loaded', enteringData); }
// update that this view was just accessed
enteringEle.data(DATA_VIEW_ACCESSED, Date.now());
callback && callback(); },
transition: function(direction, enableBack, allowAnimate) { var deferred; var enteringData = getTransitionData(viewLocals, enteringEle, direction, enteringView); var leavingData = extend(extend({}, enteringData), getViewData(leavingView)); enteringData.transitionId = leavingData.transitionId = transitionId; enteringData.fromCache = !!alreadyInDom; enteringData.enableBack = !!enableBack; enteringData.renderStart = renderStart; enteringData.renderEnd = renderEnd;
cachedAttr(enteringEle.parent(), 'nav-view-transition', enteringData.transition); cachedAttr(enteringEle.parent(), 'nav-view-direction', enteringData.direction);
// cancel any previous transition complete fallbacks
$timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
// get the transition ready and see if it'll animate
var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none; var viewTransition = transitionFn(enteringEle, leavingEle, enteringData.direction, enteringData.shouldAnimate && allowAnimate && renderEnd);
if (viewTransition.shouldAnimate) { // attach transitionend events (and fallback timer)
enteringEle.on(TRANSITIONEND_EVENT, completeOnTransitionEnd); enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, defaultTimeout)); $ionicClickBlock.show(defaultTimeout); }
if (renderStart) { // notify the views "before" the transition starts
switcher.emit('before', enteringData, leavingData);
// stage entering element, opacity 0, no transition duration
navViewAttr(enteringEle, VIEW_STATUS_STAGED);
// render the elements in the correct location for their starting point
viewTransition.run(0); }
if (renderEnd) { // create a promise so we can keep track of when all transitions finish
// only required if this transition should complete
deferred = $q.defer(); transitionPromises.push(deferred.promise); }
if (renderStart && renderEnd) { // CSS "auto" transitioned, not manually transitioned
// wait a frame so the styles apply before auto transitioning
$timeout(function() { ionic.requestAnimationFrame(onReflow); }); } else if (!renderEnd) { // just the start of a manual transition
// but it will not render the end of the transition
navViewAttr(enteringEle, 'entering'); navViewAttr(leavingEle, 'leaving');
// return the transition run method so each step can be ran manually
return { run: viewTransition.run, cancel: function(shouldAnimate) { if (shouldAnimate) { enteringEle.on(TRANSITIONEND_EVENT, cancelOnTransitionEnd); enteringEle.data(DATA_FALLBACK_TIMER, $timeout(cancelTransition, defaultTimeout)); $ionicClickBlock.show(defaultTimeout); } else { cancelTransition(); } viewTransition.shouldAnimate = shouldAnimate; viewTransition.run(0); viewTransition = null; } };
} else if (renderEnd) { // just the end of a manual transition
// happens after the manual transition has completed
// and a full history change has happened
onReflow(); }
function onReflow() { // remove that we're staging the entering element so it can auto transition
navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE); navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED);
// start the auto transition and let the CSS take over
viewTransition.run(1);
// trigger auto transitions on the associated nav bars
$ionicNavBarDelegate._instances.forEach(function(instance) { instance.triggerTransitionStart(transitionId); });
if (!viewTransition.shouldAnimate) { // no animated auto transition
transitionComplete(); } }
// Make sure that transitionend events bubbling up from children won't fire
// transitionComplete. Will only go forward if ev.target == the element listening.
function completeOnTransitionEnd(ev) { if (ev.target !== this) return; transitionComplete(); } function transitionComplete() { if (transitionComplete.x) return; transitionComplete.x = true;
enteringEle.off(TRANSITIONEND_EVENT, completeOnTransitionEnd); $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER));
// resolve that this one transition (there could be many w/ nested views)
deferred && deferred.resolve(navViewCtrl);
// the most recent transition added has completed and all the active
// transition promises should be added to the services array of promises
if (transitionId === transitionCounter) { $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd);
// emit that the views have finished transitioning
// each parent nav-view will update which views are active and cached
switcher.emit('after', enteringData, leavingData); switcher.cleanup(enteringData); }
// tell the nav bars that the transition has ended
$ionicNavBarDelegate._instances.forEach(function(instance) { instance.triggerTransitionEnd(); });
// remove any references that could cause memory issues
nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null; }
// Make sure that transitionend events bubbling up from children won't fire
// transitionComplete. Will only go forward if ev.target == the element listening.
function cancelOnTransitionEnd(ev) { if (ev.target !== this) return; cancelTransition(); } function cancelTransition() { navViewAttr(enteringEle, VIEW_STATUS_CACHED); navViewAttr(leavingEle, VIEW_STATUS_ACTIVE); enteringEle.off(TRANSITIONEND_EVENT, cancelOnTransitionEnd); $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); ionicViewSwitcher.transitionEnd([navViewCtrl]); }
},
emit: function(step, enteringData, leavingData) { var enteringScope = getScopeForElement(enteringEle, enteringData); var leavingScope = getScopeForElement(leavingEle, leavingData);
var prefixesAreEqual;
if ( !enteringData.viewId || enteringData.abstractView ) { // it's an abstract view, so treat it accordingly
// we only get access to the leaving scope once in the transition,
// so dispatch all events right away if it exists
if ( leavingScope ) { leavingScope.$emit('$ionicView.beforeLeave', leavingData); leavingScope.$emit('$ionicView.leave', leavingData); leavingScope.$emit('$ionicView.afterLeave', leavingData); leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData); leavingScope.$broadcast('$ionicParentView.leave', leavingData); leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData); } } else { // it's a regular view, so do the normal process
if (step == 'after') { if (enteringScope) { enteringScope.$emit('$ionicView.enter', enteringData); enteringScope.$broadcast('$ionicParentView.enter', enteringData); }
if (leavingScope) { leavingScope.$emit('$ionicView.leave', leavingData); leavingScope.$broadcast('$ionicParentView.leave', leavingData); } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) { // we only want to dispatch this when we are doing a single-tier
// state change such as changing a tab, so compare the state
// for the same state-prefix but different suffix
prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName); if ( prefixesAreEqual ) { enteringScope.$emit('$ionicNavView.leave', leavingData); } } }
if (enteringScope) { enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData); enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData); }
if (leavingScope) { leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData); leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData);
} else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) { // we only want to dispatch this when we are doing a single-tier
// state change such as changing a tab, so compare the state
// for the same state-prefix but different suffix
prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName); if ( prefixesAreEqual ) { enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData); } } } },
cleanup: function(transData) { // check if any views should be removed
if (leavingEle && transData.direction == 'back' && !$ionicConfig.views.forwardCache()) { // if they just navigated back we can destroy the forward view
// do not remove forward views if cacheForwardViews config is true
destroyViewEle(leavingEle); }
var viewElements = navViewCtrl.getViewElements(); var viewElementsLength = viewElements.length; var x, viewElement; var removeOldestAccess = (viewElementsLength - 1) > $ionicConfig.views.maxCache(); var removableEle; var oldestAccess = Date.now();
for (x = 0; x < viewElementsLength; x++) { viewElement = viewElements.eq(x);
if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) { // remember what was the oldest element to be accessed so it can be destroyed
oldestAccess = viewElement.data(DATA_VIEW_ACCESSED); removableEle = viewElements.eq(x);
} else if (viewElement.data(DATA_DESTROY_ELE) && navViewAttr(viewElement) != VIEW_STATUS_ACTIVE) { destroyViewEle(viewElement); } }
destroyViewEle(removableEle);
if (enteringEle.data(DATA_NO_CACHE)) { enteringEle.data(DATA_DESTROY_ELE, true); } },
enteringEle: function() { return enteringEle; }, leavingEle: function() { return leavingEle; }
};
return switcher; },
transitionEnd: function(navViewCtrls) { forEach(navViewCtrls, function(navViewCtrl) { navViewCtrl.transitionEnd(); });
ionicViewSwitcher.isTransitioning(false); $ionicClickBlock.hide(); transitionPromises = []; },
nextTransition: function(val) { nextTransition = val; },
nextDirection: function(val) { nextDirection = val; },
isTransitioning: function(val) { if (arguments.length) { ionic.transition.isActive = !!val; $timeout.cancel(isActiveTimer); if (val) { isActiveTimer = $timeout(function() { ionicViewSwitcher.isTransitioning(false); }, 999); } } return ionic.transition.isActive; },
createViewEle: function(viewLocals) { var containerEle = $document[0].createElement('div'); if (viewLocals && viewLocals.$template) { containerEle.innerHTML = viewLocals.$template; if (containerEle.children.length === 1) { containerEle.children[0].classList.add('pane'); if ( viewLocals.$$state && viewLocals.$$state.self && viewLocals.$$state.self['abstract'] ) { angular.element(containerEle.children[0]).attr("abstract", "true"); } else { if ( viewLocals.$$state && viewLocals.$$state.self ) { angular.element(containerEle.children[0]).attr("state", viewLocals.$$state.self.name); }
} return jqLite(containerEle.children[0]); } } containerEle.className = "pane"; return jqLite(containerEle); },
viewEleIsActive: function(viewEle, isActiveAttr) { navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED); },
getTransitionData: getTransitionData, navViewAttr: navViewAttr, destroyViewEle: destroyViewEle
};
return ionicViewSwitcher;
function getViewElementIdentifier(locals, view) { if (viewState(locals)['abstract']) return viewState(locals).name; if (view) return view.stateId || view.viewId; return ionic.Utils.nextUid(); }
function viewState(locals) { return locals && locals.$$state && locals.$$state.self || {}; }
function getTransitionData(viewLocals, enteringEle, direction, view) { // Priority
// 1) attribute directive on the button/link to this view
// 2) entering element's attribute
// 3) entering view's $state config property
// 4) view registration data
// 5) global config
// 6) fallback value
var state = viewState(viewLocals); var viewTransition = nextTransition || cachedAttr(enteringEle, 'view-transition') || state.viewTransition || $ionicConfig.views.transition() || 'ios'; var navBarTransition = $ionicConfig.navBar.transition(); direction = nextDirection || cachedAttr(enteringEle, 'view-direction') || state.viewDirection || direction || 'none';
return extend(getViewData(view), { transition: viewTransition, navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition, direction: direction, shouldAnimate: (viewTransition !== 'none' && direction !== 'none') }); }
function getViewData(view) { view = view || {}; return { viewId: view.viewId, historyId: view.historyId, stateId: view.stateId, stateName: view.stateName, stateParams: view.stateParams }; }
function navViewAttr(ele, value) { if (arguments.length > 1) { cachedAttr(ele, NAV_VIEW_ATTR, value); } else { return cachedAttr(ele, NAV_VIEW_ATTR); } }
function destroyViewEle(ele) { // we found an element that should be removed
// destroy its scope, then remove the element
if (ele && ele.length) { var viewScope = ele.scope(); if (viewScope) { viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW)); viewScope.$destroy(); } ele.remove(); } }
function compareStatePrefixes(enteringStateName, exitingStateName) { var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.'); var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.');
// if either of the prefixes are empty, just return false
if ( enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0 ) { return false; }
var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex); var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex);
return enteringPrefix === exitingPrefix; }
function getScopeForElement(element, stateData) { if ( !element ) { return null; } // check if it's abstract
var attributeValue = angular.element(element).attr("abstract"); var stateValue = angular.element(element).attr("state");
if ( attributeValue !== "true" ) { // it's not an abstract view, so make sure the element
// matches the state. Due to abstract view weirdness,
// sometimes it doesn't. If it doesn't, don't dispatch events
// so leave the scope undefined
if ( stateValue === stateData.stateName ) { return angular.element(element).scope(); } return null; } else { // it is an abstract element, so look for element with the "state" attributeValue
// set to the name of the stateData state
var elements = aggregateNavViewChildren(element); for ( var i = 0; i < elements.length; i++ ) { var state = angular.element(elements[i]).attr("state"); if ( state === stateData.stateName ) { stateData.abstractView = true; return angular.element(elements[i]).scope(); } } // we didn't find a match, so return null
return null; } }
function aggregateNavViewChildren(element) { var aggregate = []; var navViews = angular.element(element).find("ion-nav-view"); for ( var i = 0; i < navViews.length; i++ ) { var children = angular.element(navViews[i]).children(); var childrenAggregated = []; for ( var j = 0; j < children.length; j++ ) { childrenAggregated = childrenAggregated.concat(children[j]); } aggregate = aggregate.concat(childrenAggregated); } return aggregate; }
}]);
/** * ================== angular-ios9-uiwebview.patch.js v1.1.1 ================== * * This patch works around iOS9 UIWebView regression that causes infinite digest * errors in Angular. * * The patch can be applied to Angular 1.2.0 – 1.4.5. Newer versions of Angular * have the workaround baked in. * * To apply this patch load/bundle this file with your application and add a * dependency on the "ngIOS9UIWebViewPatch" module to your main app module. * * For example: * * ```
* angular.module('myApp', ['ngRoute'])`
* ```
* * becomes * * ```
* angular.module('myApp', ['ngRoute', 'ngIOS9UIWebViewPatch']) * ```
* * * More info: * - https://openradar.appspot.com/22186109
* - https://github.com/angular/angular.js/issues/12241
* - https://github.com/driftyco/ionic/issues/4082
* * * @license AngularJS * (c) 2010-2015 Google, Inc. http://angularjs.org
* License: MIT */
angular.module('ngIOS9UIWebViewPatch', ['ng']).config(['$provide', function($provide) { 'use strict';
$provide.decorator('$browser', ['$delegate', '$window', function($delegate, $window) {
if (isIOS9UIWebView($window.navigator.userAgent)) { return applyIOS9Shim($delegate); }
return $delegate;
function isIOS9UIWebView(userAgent) { return /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent); }
function applyIOS9Shim(browser) { var pendingLocationUrl = null; var originalUrlFn = browser.url;
browser.url = function() { if (arguments.length) { pendingLocationUrl = arguments[0]; return originalUrlFn.apply(browser, arguments); }
return pendingLocationUrl || originalUrlFn.apply(browser, arguments); };
window.addEventListener('popstate', clearPendingLocationUrl, false); window.addEventListener('hashchange', clearPendingLocationUrl, false);
function clearPendingLocationUrl() { pendingLocationUrl = null; }
return browser; } }]); }]);
/** * @private * Parts of Ionic requires that $scope data is attached to the element. * We do not want to disable adding $scope data to the $element when * $compileProvider.debugInfoEnabled(false) is used. */ IonicModule.config(['$provide', function($provide) { $provide.decorator('$compile', ['$delegate', function($compile) { $compile.$$addScopeInfo = function $$addScopeInfo($element, scope, isolated, noTemplate) { var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; $element.data(dataName, scope); }; return $compile; }]); }]);
/** * @private */ IonicModule.config([ '$provide', function($provide) { function $LocationDecorator($location, $timeout) {
$location.__hash = $location.hash; //Fix: when window.location.hash is set, the scrollable area
//found nearest to body's scrollTop is set to scroll to an element
//with that ID.
$location.hash = function(value) { if (isDefined(value) && value.length > 0) { $timeout(function() { var scroll = document.querySelector('.scroll-content'); if (scroll) { scroll.scrollTop = 0; } }, 0, false); } return $location.__hash(value); };
return $location; }
$provide.decorator('$location', ['$delegate', '$timeout', $LocationDecorator]); }]);
IonicModule
.controller('$ionicHeaderBar', [ '$scope', '$element', '$attrs', '$q', '$ionicConfig', '$ionicHistory', function($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) { var TITLE = 'title'; var BACK_TEXT = 'back-text'; var BACK_BUTTON = 'back-button'; var DEFAULT_TITLE = 'default-title'; var PREVIOUS_TITLE = 'previous-title'; var HIDE = 'hide';
var self = this; var titleText = ''; var previousTitleText = ''; var titleLeft = 0; var titleRight = 0; var titleCss = ''; var isBackEnabled = false; var isBackShown = true; var isNavBackShown = true; var isBackElementShown = false; var titleTextWidth = 0;
self.beforeEnter = function(viewData) { $scope.$broadcast('$ionicView.beforeEnter', viewData); };
self.title = function(newTitleText) { if (arguments.length && newTitleText !== titleText) { getEle(TITLE).innerHTML = newTitleText; titleText = newTitleText; titleTextWidth = 0; } return titleText; };
self.enableBack = function(shouldEnable, disableReset) { // whether or not the back button show be visible, according
// to the navigation and history
if (arguments.length) { isBackEnabled = shouldEnable; if (!disableReset) self.updateBackButton(); } return isBackEnabled; };
self.showBack = function(shouldShow, disableReset) { // different from enableBack() because this will always have the back
// visually hidden if false, even if the history says it should show
if (arguments.length) { isBackShown = shouldShow; if (!disableReset) self.updateBackButton(); } return isBackShown; };
self.showNavBack = function(shouldShow) { // different from showBack() because this is for the entire nav bar's
// setting for all of it's child headers. For internal use.
isNavBackShown = shouldShow; self.updateBackButton(); };
self.updateBackButton = function() { var ele; if ((isBackShown && isNavBackShown && isBackEnabled) !== isBackElementShown) { isBackElementShown = isBackShown && isNavBackShown && isBackEnabled; ele = getEle(BACK_BUTTON); ele && ele.classList[ isBackElementShown ? 'remove' : 'add' ](HIDE); }
if (isBackEnabled) { ele = ele || getEle(BACK_BUTTON); if (ele) { if (self.backButtonIcon !== $ionicConfig.backButton.icon()) { ele = getEle(BACK_BUTTON + ' .icon'); if (ele) { self.backButtonIcon = $ionicConfig.backButton.icon(); ele.className = 'icon ' + self.backButtonIcon; } }
if (self.backButtonText !== $ionicConfig.backButton.text()) { ele = getEle(BACK_BUTTON + ' .back-text'); if (ele) { ele.textContent = self.backButtonText = $ionicConfig.backButton.text(); } } } } };
self.titleTextWidth = function() { var element = getEle(TITLE); if ( element ) { // If the element has a nav-bar-title, use that instead
// to calculate the width of the title
var children = angular.element(element).children(); for ( var i = 0; i < children.length; i++ ) { if ( angular.element(children[i]).hasClass('nav-bar-title') ) { element = children[i]; break; } } } var bounds = ionic.DomUtil.getTextBounds(element); titleTextWidth = Math.min(bounds && bounds.width || 30); return titleTextWidth; };
self.titleWidth = function() { var titleWidth = self.titleTextWidth(); var offsetWidth = getEle(TITLE).offsetWidth; if (offsetWidth < titleWidth) { titleWidth = offsetWidth + (titleLeft - titleRight - 5); } return titleWidth; };
self.titleTextX = function() { return ($element[0].offsetWidth / 2) - (self.titleWidth() / 2); };
self.titleLeftRight = function() { return titleLeft - titleRight; };
self.backButtonTextLeft = function() { var offsetLeft = 0; var ele = getEle(BACK_TEXT); while (ele) { offsetLeft += ele.offsetLeft; ele = ele.parentElement; } return offsetLeft; };
self.resetBackButton = function(viewData) { if ($ionicConfig.backButton.previousTitleText()) { var previousTitleEle = getEle(PREVIOUS_TITLE); if (previousTitleEle) { previousTitleEle.classList.remove(HIDE);
var view = (viewData && $ionicHistory.getViewById(viewData.viewId)); var newPreviousTitleText = $ionicHistory.backTitle(view);
if (newPreviousTitleText !== previousTitleText) { previousTitleText = previousTitleEle.innerHTML = newPreviousTitleText; } } var defaultTitleEle = getEle(DEFAULT_TITLE); if (defaultTitleEle) { defaultTitleEle.classList.remove(HIDE); } } };
self.align = function(textAlign) { var titleEle = getEle(TITLE);
textAlign = textAlign || $attrs.alignTitle || $ionicConfig.navBar.alignTitle();
var widths = self.calcWidths(textAlign, false);
if (isBackShown && previousTitleText && $ionicConfig.backButton.previousTitleText()) { var previousTitleWidths = self.calcWidths(textAlign, true);
var availableTitleWidth = $element[0].offsetWidth - previousTitleWidths.titleLeft - previousTitleWidths.titleRight;
if (self.titleTextWidth() <= availableTitleWidth) { widths = previousTitleWidths; } }
return self.updatePositions(titleEle, widths.titleLeft, widths.titleRight, widths.buttonsLeft, widths.buttonsRight, widths.css, widths.showPrevTitle); };
self.calcWidths = function(textAlign, isPreviousTitle) { var titleEle = getEle(TITLE); var backBtnEle = getEle(BACK_BUTTON); var x, y, z, b, c, d, childSize, bounds; var childNodes = $element[0].childNodes; var buttonsLeft = 0; var buttonsRight = 0; var isCountRightOfTitle; var updateTitleLeft = 0; var updateTitleRight = 0; var updateCss = ''; var backButtonWidth = 0;
// Compute how wide the left children are
// Skip all titles (there may still be two titles, one leaving the dom)
// Once we encounter a titleEle, realize we are now counting the right-buttons, not left
for (x = 0; x < childNodes.length; x++) { c = childNodes[x];
childSize = 0; if (c.nodeType == 1) { // element node
if (c === titleEle) { isCountRightOfTitle = true; continue; }
if (c.classList.contains(HIDE)) { continue; }
if (isBackShown && c === backBtnEle) {
for (y = 0; y < c.childNodes.length; y++) { b = c.childNodes[y];
if (b.nodeType == 1) {
if (b.classList.contains(BACK_TEXT)) { for (z = 0; z < b.children.length; z++) { d = b.children[z];
if (isPreviousTitle) { if (d.classList.contains(DEFAULT_TITLE)) continue; backButtonWidth += d.offsetWidth; } else { if (d.classList.contains(PREVIOUS_TITLE)) continue; backButtonWidth += d.offsetWidth; } }
} else { backButtonWidth += b.offsetWidth; }
} else if (b.nodeType == 3 && b.nodeValue.trim()) { bounds = ionic.DomUtil.getTextBounds(b); backButtonWidth += bounds && bounds.width || 0; }
} childSize = backButtonWidth || c.offsetWidth;
} else { // not the title, not the back button, not a hidden element
childSize = c.offsetWidth; }
} else if (c.nodeType == 3 && c.nodeValue.trim()) { // text node
bounds = ionic.DomUtil.getTextBounds(c); childSize = bounds && bounds.width || 0; }
if (isCountRightOfTitle) { buttonsRight += childSize; } else { buttonsLeft += childSize; } }
// Size and align the header titleEle based on the sizes of the left and
// right children, and the desired alignment mode
if (textAlign == 'left') { updateCss = 'title-left'; if (buttonsLeft) { updateTitleLeft = buttonsLeft + 15; } if (buttonsRight) { updateTitleRight = buttonsRight + 15; }
} else if (textAlign == 'right') { updateCss = 'title-right'; if (buttonsLeft) { updateTitleLeft = buttonsLeft + 15; } if (buttonsRight) { updateTitleRight = buttonsRight + 15; }
} else { // center the default
var margin = Math.max(buttonsLeft, buttonsRight) + 10; if (margin > 10) { updateTitleLeft = updateTitleRight = margin; } }
return { backButtonWidth: backButtonWidth, buttonsLeft: buttonsLeft, buttonsRight: buttonsRight, titleLeft: updateTitleLeft, titleRight: updateTitleRight, showPrevTitle: isPreviousTitle, css: updateCss }; };
self.updatePositions = function(titleEle, updateTitleLeft, updateTitleRight, buttonsLeft, buttonsRight, updateCss, showPreviousTitle) { var deferred = $q.defer();
// only make DOM updates when there are actual changes
if (titleEle) { if (updateTitleLeft !== titleLeft) { titleEle.style.left = updateTitleLeft ? updateTitleLeft + 'px' : ''; titleLeft = updateTitleLeft; } if (updateTitleRight !== titleRight) { titleEle.style.right = updateTitleRight ? updateTitleRight + 'px' : ''; titleRight = updateTitleRight; }
if (updateCss !== titleCss) { updateCss && titleEle.classList.add(updateCss); titleCss && titleEle.classList.remove(titleCss); titleCss = updateCss; } }
if ($ionicConfig.backButton.previousTitleText()) { var prevTitle = getEle(PREVIOUS_TITLE); var defaultTitle = getEle(DEFAULT_TITLE);
prevTitle && prevTitle.classList[ showPreviousTitle ? 'remove' : 'add'](HIDE); defaultTitle && defaultTitle.classList[ showPreviousTitle ? 'add' : 'remove'](HIDE); }
ionic.requestAnimationFrame(function() { if (titleEle && titleEle.offsetWidth + 10 < titleEle.scrollWidth) { var minRight = buttonsRight + 5; var testRight = $element[0].offsetWidth - titleLeft - self.titleTextWidth() - 20; updateTitleRight = testRight < minRight ? minRight : testRight; if (updateTitleRight !== titleRight) { titleEle.style.right = updateTitleRight + 'px'; titleRight = updateTitleRight; } } deferred.resolve(); });
return deferred.promise; };
self.setCss = function(elementClassname, css) { ionic.DomUtil.cachedStyles(getEle(elementClassname), css); };
var eleCache = {}; function getEle(className) { if (!eleCache[className]) { eleCache[className] = $element[0].querySelector('.' + className); } return eleCache[className]; }
$scope.$on('$destroy', function() { for (var n in eleCache) eleCache[n] = null; });
}]);
IonicModule .controller('$ionInfiniteScroll', [ '$scope', '$attrs', '$element', '$timeout', function($scope, $attrs, $element, $timeout) { var self = this; self.isLoading = false;
$scope.icon = function() { return isDefined($attrs.icon) ? $attrs.icon : 'ion-load-d'; };
$scope.spinner = function() { return isDefined($attrs.spinner) ? $attrs.spinner : ''; };
$scope.$on('scroll.infiniteScrollComplete', function() { finishInfiniteScroll(); });
$scope.$on('$destroy', function() { if (self.scrollCtrl && self.scrollCtrl.$element) self.scrollCtrl.$element.off('scroll', self.checkBounds); if (self.scrollEl && self.scrollEl.removeEventListener) { self.scrollEl.removeEventListener('scroll', self.checkBounds); } });
// debounce checking infinite scroll events
self.checkBounds = ionic.Utils.throttle(checkInfiniteBounds, 300);
function onInfinite() { ionic.requestAnimationFrame(function() { $element[0].classList.add('active'); }); self.isLoading = true; $scope.$parent && $scope.$parent.$apply($attrs.onInfinite || ''); }
function finishInfiniteScroll() { ionic.requestAnimationFrame(function() { $element[0].classList.remove('active'); }); $timeout(function() { if (self.jsScrolling) self.scrollView.resize(); // only check bounds again immediately if the page isn't cached (scroll el has height)
if ((self.jsScrolling && self.scrollView.__container && self.scrollView.__container.offsetHeight > 0) || !self.jsScrolling) { self.checkBounds(); } }, 30, false); self.isLoading = false; }
// check if we've scrolled far enough to trigger an infinite scroll
function checkInfiniteBounds() { if (self.isLoading) return; var maxScroll = {};
if (self.jsScrolling) { maxScroll = self.getJSMaxScroll(); var scrollValues = self.scrollView.getValues(); if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) || (maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) { onInfinite(); } } else { maxScroll = self.getNativeMaxScroll(); if (( maxScroll.left !== -1 && self.scrollEl.scrollLeft >= maxScroll.left - self.scrollEl.clientWidth ) || ( maxScroll.top !== -1 && self.scrollEl.scrollTop >= maxScroll.top - self.scrollEl.clientHeight )) { onInfinite(); } } }
// determine the threshold at which we should fire an infinite scroll
// note: this gets processed every scroll event, can it be cached?
self.getJSMaxScroll = function() { var maxValues = self.scrollView.getScrollMax(); return { left: self.scrollView.options.scrollingX ? calculateMaxValue(maxValues.left) : -1, top: self.scrollView.options.scrollingY ? calculateMaxValue(maxValues.top) : -1 }; };
self.getNativeMaxScroll = function() { var maxValues = { left: self.scrollEl.scrollWidth, top: self.scrollEl.scrollHeight }; var computedStyle = window.getComputedStyle(self.scrollEl) || {}; return { left: maxValues.left && (computedStyle.overflowX === 'scroll' || computedStyle.overflowX === 'auto' || self.scrollEl.style['overflow-x'] === 'scroll') ? calculateMaxValue(maxValues.left) : -1, top: maxValues.top && (computedStyle.overflowY === 'scroll' || computedStyle.overflowY === 'auto' || self.scrollEl.style['overflow-y'] === 'scroll' ) ? calculateMaxValue(maxValues.top) : -1 }; };
// determine pixel refresh distance based on % or value
function calculateMaxValue(maximum) { var distance = ($attrs.distance || '2.5%').trim(); var isPercent = distance.indexOf('%') !== -1; return isPercent ? maximum * (1 - parseFloat(distance) / 100) : maximum - parseFloat(distance); }
//for testing
self.__finishInfiniteScroll = finishInfiniteScroll;
}]);
/** * @ngdoc service * @name $ionicListDelegate * @module ionic * * @description * Delegate for controlling the {@link ionic.directive:ionList} directive. * * Methods called directly on the $ionicListDelegate service will control all lists. * Use the {@link ionic.service:$ionicListDelegate#$getByHandle $getByHandle} * method to control specific ionList instances. * * @usage * ```html
* {% raw %} * <ion-content ng-controller="MyCtrl"> * <button class="button" ng-click="showDeleteButtons()"></button> * <ion-list> * <ion-item ng-repeat="i in items"> * Hello, {{i}}! * <ion-delete-button class="ion-minus-circled"></ion-delete-button> * </ion-item> * </ion-list> * </ion-content> * {% endraw %} * ```
* ```js
* function MyCtrl($scope, $ionicListDelegate) { * $scope.showDeleteButtons = function() { * $ionicListDelegate.showDelete(true); * }; * } * ```
*/ IonicModule.service('$ionicListDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicListDelegate#showReorder * @param {boolean=} showReorder Set whether or not this list is showing its reorder buttons. * @returns {boolean} Whether the reorder buttons are shown. */ 'showReorder', /** * @ngdoc method * @name $ionicListDelegate#showDelete * @param {boolean=} showDelete Set whether or not this list is showing its delete buttons. * @returns {boolean} Whether the delete buttons are shown. */ 'showDelete', /** * @ngdoc method * @name $ionicListDelegate#canSwipeItems * @param {boolean=} canSwipeItems Set whether or not this list is able to swipe to show * option buttons. * @returns {boolean} Whether the list is able to swipe to show option buttons. */ 'canSwipeItems', /** * @ngdoc method * @name $ionicListDelegate#closeOptionButtons * @description Closes any option buttons on the list that are swiped open. */ 'closeOptionButtons' /** * @ngdoc method * @name $ionicListDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionList} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicListDelegate.$getByHandle('my-handle').showReorder(true);` */ ]))
.controller('$ionicList', [ '$scope', '$attrs', '$ionicListDelegate', '$ionicHistory', function($scope, $attrs, $ionicListDelegate, $ionicHistory) { var self = this; var isSwipeable = true; var isReorderShown = false; var isDeleteShown = false;
var deregisterInstance = $ionicListDelegate._registerInstance( self, $attrs.delegateHandle, function() { return $ionicHistory.isActiveScope($scope); } ); $scope.$on('$destroy', deregisterInstance);
self.showReorder = function(show) { if (arguments.length) { isReorderShown = !!show; } return isReorderShown; };
self.showDelete = function(show) { if (arguments.length) { isDeleteShown = !!show; } return isDeleteShown; };
self.canSwipeItems = function(can) { if (arguments.length) { isSwipeable = !!can; } return isSwipeable; };
self.closeOptionButtons = function() { self.listView && self.listView.clearDragEffects(); }; }]);
IonicModule
.controller('$ionicNavBar', [ '$scope', '$element', '$attrs', '$compile', '$timeout', '$ionicNavBarDelegate', '$ionicConfig', '$ionicHistory', function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $ionicConfig, $ionicHistory) {
var CSS_HIDE = 'hide'; var DATA_NAV_BAR_CTRL = '$ionNavBarController'; var PRIMARY_BUTTONS = 'primaryButtons'; var SECONDARY_BUTTONS = 'secondaryButtons'; var BACK_BUTTON = 'backButton'; var ITEM_TYPES = 'primaryButtons secondaryButtons leftButtons rightButtons title'.split(' ');
var self = this; var headerBars = []; var navElementHtml = {}; var isVisible = true; var queuedTransitionStart, queuedTransitionEnd, latestTransitionId;
$element.parent().data(DATA_NAV_BAR_CTRL, self);
var delegateHandle = $attrs.delegateHandle || 'navBar' + ionic.Utils.nextUid();
var deregisterInstance = $ionicNavBarDelegate._registerInstance(self, delegateHandle);
self.init = function() { $element.addClass('nav-bar-container'); ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', $ionicConfig.views.transition());
// create two nav bar blocks which will trade out which one is shown
self.createHeaderBar(false); self.createHeaderBar(true);
$scope.$emit('ionNavBar.init', delegateHandle); };
self.createHeaderBar = function(isActive) { var containerEle = jqLite('<div class="nav-bar-block">'); ionic.DomUtil.cachedAttr(containerEle, 'nav-bar', isActive ? 'active' : 'cached');
var alignTitle = $attrs.alignTitle || $ionicConfig.navBar.alignTitle(); var headerBarEle = jqLite('<ion-header-bar>').addClass($attrs['class']).attr('align-title', alignTitle); if (isDefined($attrs.noTapScroll)) headerBarEle.attr('no-tap-scroll', $attrs.noTapScroll); var titleEle = jqLite('<div class="title title-' + alignTitle + '">'); var navEle = {}; var lastViewItemEle = {}; var leftButtonsEle, rightButtonsEle;
navEle[BACK_BUTTON] = createNavElement(BACK_BUTTON); navEle[BACK_BUTTON] && headerBarEle.append(navEle[BACK_BUTTON]);
// append title in the header, this is the rock to where buttons append
headerBarEle.append(titleEle);
forEach(ITEM_TYPES, function(itemType) { // create default button elements
navEle[itemType] = createNavElement(itemType); // append and position buttons
positionItem(navEle[itemType], itemType); });
// add header-item to the root children
for (var x = 0; x < headerBarEle[0].children.length; x++) { headerBarEle[0].children[x].classList.add('header-item'); }
// compile header and append to the DOM
containerEle.append(headerBarEle); $element.append($compile(containerEle)($scope.$new()));
var headerBarCtrl = headerBarEle.data('$ionHeaderBarController'); headerBarCtrl.backButtonIcon = $ionicConfig.backButton.icon(); headerBarCtrl.backButtonText = $ionicConfig.backButton.text();
var headerBarInstance = { isActive: isActive, title: function(newTitleText) { headerBarCtrl.title(newTitleText); }, setItem: function(navBarItemEle, itemType) { // first make sure any exiting nav bar item has been removed
headerBarInstance.removeItem(itemType);
if (navBarItemEle) { if (itemType === 'title') { // clear out the text based title
headerBarInstance.title(""); }
// there's a custom nav bar item
positionItem(navBarItemEle, itemType);
if (navEle[itemType]) { // make sure the default on this itemType is hidden
navEle[itemType].addClass(CSS_HIDE); } lastViewItemEle[itemType] = navBarItemEle;
} else if (navEle[itemType]) { // there's a default button for this side and no view button
navEle[itemType].removeClass(CSS_HIDE); } }, removeItem: function(itemType) { if (lastViewItemEle[itemType]) { lastViewItemEle[itemType].scope().$destroy(); lastViewItemEle[itemType].remove(); lastViewItemEle[itemType] = null; } }, containerEle: function() { return containerEle; }, headerBarEle: function() { return headerBarEle; }, afterLeave: function() { forEach(ITEM_TYPES, function(itemType) { headerBarInstance.removeItem(itemType); }); headerBarCtrl.resetBackButton(); }, controller: function() { return headerBarCtrl; }, destroy: function() { forEach(ITEM_TYPES, function(itemType) { headerBarInstance.removeItem(itemType); }); containerEle.scope().$destroy(); for (var n in navEle) { if (navEle[n]) { navEle[n].removeData(); navEle[n] = null; } } leftButtonsEle && leftButtonsEle.removeData(); rightButtonsEle && rightButtonsEle.removeData(); titleEle.removeData(); headerBarEle.removeData(); containerEle.remove(); containerEle = headerBarEle = titleEle = leftButtonsEle = rightButtonsEle = null; } };
function positionItem(ele, itemType) { if (!ele) return;
if (itemType === 'title') { // title element
titleEle.append(ele);
} else if (itemType == 'rightButtons' || (itemType == SECONDARY_BUTTONS && $ionicConfig.navBar.positionSecondaryButtons() != 'left') || (itemType == PRIMARY_BUTTONS && $ionicConfig.navBar.positionPrimaryButtons() == 'right')) { // right side
if (!rightButtonsEle) { rightButtonsEle = jqLite('<div class="buttons buttons-right">'); headerBarEle.append(rightButtonsEle); } if (itemType == SECONDARY_BUTTONS) { rightButtonsEle.append(ele); } else { rightButtonsEle.prepend(ele); }
} else { // left side
if (!leftButtonsEle) { leftButtonsEle = jqLite('<div class="buttons buttons-left">'); if (navEle[BACK_BUTTON]) { navEle[BACK_BUTTON].after(leftButtonsEle); } else { headerBarEle.prepend(leftButtonsEle); } } if (itemType == SECONDARY_BUTTONS) { leftButtonsEle.append(ele); } else { leftButtonsEle.prepend(ele); } }
}
headerBars.push(headerBarInstance);
return headerBarInstance; };
self.navElement = function(type, html) { if (isDefined(html)) { navElementHtml[type] = html; } return navElementHtml[type]; };
self.update = function(viewData) { var showNavBar = !viewData.hasHeaderBar && viewData.showNavBar; viewData.transition = $ionicConfig.views.transition();
if (!showNavBar) { viewData.direction = 'none'; }
self.enable(showNavBar); var enteringHeaderBar = self.isInitialized ? getOffScreenHeaderBar() : getOnScreenHeaderBar(); var leavingHeaderBar = self.isInitialized ? getOnScreenHeaderBar() : null; var enteringHeaderCtrl = enteringHeaderBar.controller();
// update if the entering header should show the back button or not
enteringHeaderCtrl.enableBack(viewData.enableBack, true); enteringHeaderCtrl.showBack(viewData.showBack, true); enteringHeaderCtrl.updateBackButton();
// update the entering header bar's title
self.title(viewData.title, enteringHeaderBar);
self.showBar(showNavBar);
// update the nav bar items, depending if the view has their own or not
if (viewData.navBarItems) { forEach(ITEM_TYPES, function(itemType) { enteringHeaderBar.setItem(viewData.navBarItems[itemType], itemType); }); }
// begin transition of entering and leaving header bars
self.transition(enteringHeaderBar, leavingHeaderBar, viewData);
self.isInitialized = true; navSwipeAttr(''); };
self.transition = function(enteringHeaderBar, leavingHeaderBar, viewData) { var enteringHeaderBarCtrl = enteringHeaderBar.controller(); var transitionFn = $ionicConfig.transitions.navBar[viewData.navBarTransition] || $ionicConfig.transitions.navBar.none; var transitionId = viewData.transitionId;
enteringHeaderBarCtrl.beforeEnter(viewData);
var navBarTransition = transitionFn(enteringHeaderBar, leavingHeaderBar, viewData.direction, viewData.shouldAnimate && self.isInitialized);
ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', viewData.navBarTransition); ionic.DomUtil.cachedAttr($element, 'nav-bar-direction', viewData.direction);
if (navBarTransition.shouldAnimate && viewData.renderEnd) { navBarAttr(enteringHeaderBar, 'stage'); } else { navBarAttr(enteringHeaderBar, 'entering'); navBarAttr(leavingHeaderBar, 'leaving'); }
enteringHeaderBarCtrl.resetBackButton(viewData);
navBarTransition.run(0);
self.activeTransition = { run: function(step) { navBarTransition.shouldAnimate = false; navBarTransition.direction = 'back'; navBarTransition.run(step); }, cancel: function(shouldAnimate, speed, cancelData) { navSwipeAttr(speed); navBarAttr(leavingHeaderBar, 'active'); navBarAttr(enteringHeaderBar, 'cached'); navBarTransition.shouldAnimate = shouldAnimate; navBarTransition.run(0); self.activeTransition = navBarTransition = null;
var runApply; if (cancelData.showBar !== self.showBar()) { self.showBar(cancelData.showBar); } if (cancelData.showBackButton !== self.showBackButton()) { self.showBackButton(cancelData.showBackButton); } if (runApply) { $scope.$apply(); } }, complete: function(shouldAnimate, speed) { navSwipeAttr(speed); navBarTransition.shouldAnimate = shouldAnimate; navBarTransition.run(1); queuedTransitionEnd = transitionEnd; } };
$timeout(enteringHeaderBarCtrl.align, 16);
queuedTransitionStart = function() { if (latestTransitionId !== transitionId) return;
navBarAttr(enteringHeaderBar, 'entering'); navBarAttr(leavingHeaderBar, 'leaving');
navBarTransition.run(1);
queuedTransitionEnd = function() { if (latestTransitionId == transitionId || !navBarTransition.shouldAnimate) { transitionEnd(); } };
queuedTransitionStart = null; };
function transitionEnd() { for (var x = 0; x < headerBars.length; x++) { headerBars[x].isActive = false; } enteringHeaderBar.isActive = true;
navBarAttr(enteringHeaderBar, 'active'); navBarAttr(leavingHeaderBar, 'cached');
self.activeTransition = navBarTransition = queuedTransitionEnd = null; }
queuedTransitionStart(); };
self.triggerTransitionStart = function(triggerTransitionId) { latestTransitionId = triggerTransitionId; queuedTransitionStart && queuedTransitionStart(); };
self.triggerTransitionEnd = function() { queuedTransitionEnd && queuedTransitionEnd(); };
self.showBar = function(shouldShow) { if (arguments.length) { self.visibleBar(shouldShow); $scope.$parent.$hasHeader = !!shouldShow; } return !!$scope.$parent.$hasHeader; };
self.visibleBar = function(shouldShow) { if (shouldShow && !isVisible) { $element.removeClass(CSS_HIDE); self.align(); } else if (!shouldShow && isVisible) { $element.addClass(CSS_HIDE); } isVisible = shouldShow; };
self.enable = function(val) { // set primary to show first
self.visibleBar(val);
// set non primary to hide second
for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) { if ($ionicNavBarDelegate._instances[x] !== self) $ionicNavBarDelegate._instances[x].visibleBar(false); } };
/** * @ngdoc method * @name $ionicNavBar#showBackButton * @description Show/hide the nav bar back button when there is a * back view. If the back button is not possible, for example, the * first view in the stack, then this will not force the back button * to show. */ self.showBackButton = function(shouldShow) { if (arguments.length) { for (var x = 0; x < headerBars.length; x++) { headerBars[x].controller().showNavBack(!!shouldShow); } $scope.$isBackButtonShown = !!shouldShow; } return $scope.$isBackButtonShown; };
/** * @ngdoc method * @name $ionicNavBar#showActiveBackButton * @description Show/hide only the active header bar's back button. */ self.showActiveBackButton = function(shouldShow) { var headerBar = getOnScreenHeaderBar(); if (headerBar) { if (arguments.length) { return headerBar.controller().showBack(shouldShow); } return headerBar.controller().showBack(); } };
self.title = function(newTitleText, headerBar) { if (isDefined(newTitleText)) { newTitleText = newTitleText || ''; headerBar = headerBar || getOnScreenHeaderBar(); headerBar && headerBar.title(newTitleText); $scope.$title = newTitleText; $ionicHistory.currentTitle(newTitleText); } return $scope.$title; };
self.align = function(val, headerBar) { headerBar = headerBar || getOnScreenHeaderBar(); headerBar && headerBar.controller().align(val); };
self.hasTabsTop = function(isTabsTop) { $element[isTabsTop ? 'addClass' : 'removeClass']('nav-bar-tabs-top'); };
self.hasBarSubheader = function(isBarSubheader) { $element[isBarSubheader ? 'addClass' : 'removeClass']('nav-bar-has-subheader'); };
// DEPRECATED, as of v1.0.0-beta14 -------
self.changeTitle = function(val) { deprecatedWarning('changeTitle(val)', 'title(val)'); self.title(val); }; self.setTitle = function(val) { deprecatedWarning('setTitle(val)', 'title(val)'); self.title(val); }; self.getTitle = function() { deprecatedWarning('getTitle()', 'title()'); return self.title(); }; self.back = function() { deprecatedWarning('back()', '$ionicHistory.goBack()'); $ionicHistory.goBack(); }; self.getPreviousTitle = function() { deprecatedWarning('getPreviousTitle()', '$ionicHistory.backTitle()'); $ionicHistory.goBack(); }; function deprecatedWarning(oldMethod, newMethod) { var warn = console.warn || console.log; warn && warn.call(console, 'navBarController.' + oldMethod + ' is deprecated, please use ' + newMethod + ' instead'); } // END DEPRECATED -------
function createNavElement(type) { if (navElementHtml[type]) { return jqLite(navElementHtml[type]); } }
function getOnScreenHeaderBar() { for (var x = 0; x < headerBars.length; x++) { if (headerBars[x].isActive) return headerBars[x]; } }
function getOffScreenHeaderBar() { for (var x = 0; x < headerBars.length; x++) { if (!headerBars[x].isActive) return headerBars[x]; } }
function navBarAttr(ctrl, val) { ctrl && ionic.DomUtil.cachedAttr(ctrl.containerEle(), 'nav-bar', val); }
function navSwipeAttr(val) { ionic.DomUtil.cachedAttr($element, 'nav-swipe', val); }
$scope.$on('$destroy', function() { $scope.$parent.$hasHeader = false; $element.parent().removeData(DATA_NAV_BAR_CTRL); for (var x = 0; x < headerBars.length; x++) { headerBars[x].destroy(); } $element.remove(); $element = headerBars = null; deregisterInstance(); });
}]);
IonicModule .controller('$ionicNavView', [ '$scope', '$element', '$attrs', '$compile', '$controller', '$ionicNavBarDelegate', '$ionicNavViewDelegate', '$ionicHistory', '$ionicViewSwitcher', '$ionicConfig', '$ionicScrollDelegate', '$ionicSideMenuDelegate', function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher, $ionicConfig, $ionicScrollDelegate, $ionicSideMenuDelegate) {
var DATA_ELE_IDENTIFIER = '$eleId'; var DATA_DESTROY_ELE = '$destroyEle'; var DATA_NO_CACHE = '$noCache'; var VIEW_STATUS_ACTIVE = 'active'; var VIEW_STATUS_CACHED = 'cached';
var self = this; var direction; var isPrimary = false; var navBarDelegate; var activeEleId; var navViewAttr = $ionicViewSwitcher.navViewAttr; var disableRenderStartViewId, disableAnimation;
self.scope = $scope; self.element = $element;
self.init = function() { var navViewName = $attrs.name || '';
// Find the details of the parent view directive (if any) and use it
// to derive our own qualified view name, then hang our own details
// off the DOM so child directives can find it.
var parent = $element.parent().inheritedData('$uiView'); var parentViewName = ((parent && parent.state) ? parent.state.name : ''); if (navViewName.indexOf('@') < 0) navViewName = navViewName + '@' + parentViewName;
var viewData = { name: navViewName, state: null }; $element.data('$uiView', viewData);
var deregisterInstance = $ionicNavViewDelegate._registerInstance(self, $attrs.delegateHandle); $scope.$on('$destroy', function() { deregisterInstance();
// ensure no scrolls have been left frozen
if (self.isSwipeFreeze) { $ionicScrollDelegate.freezeAllScrolls(false); } });
$scope.$on('$ionicHistory.deselect', self.cacheCleanup); $scope.$on('$ionicTabs.top', onTabsTop); $scope.$on('$ionicSubheader', onBarSubheader);
$scope.$on('$ionicTabs.beforeLeave', onTabsLeave); $scope.$on('$ionicTabs.afterLeave', onTabsLeave); $scope.$on('$ionicTabs.leave', onTabsLeave);
ionic.Platform.ready(function() { if ( ionic.Platform.isWebView() && ionic.Platform.isIOS() ) { self.initSwipeBack(); } });
return viewData; };
self.register = function(viewLocals) { var leavingView = extend({}, $ionicHistory.currentView());
// register that a view is coming in and get info on how it should transition
var registerData = $ionicHistory.register($scope, viewLocals);
// update which direction
self.update(registerData);
// begin rendering and transitioning
var enteringView = $ionicHistory.getViewById(registerData.viewId) || {};
var renderStart = (disableRenderStartViewId !== registerData.viewId); self.render(registerData, viewLocals, enteringView, leavingView, renderStart, true); };
self.update = function(registerData) { // always reset that this is the primary navView
isPrimary = true;
// remember what direction this navView should use
// this may get updated later by a child navView
direction = registerData.direction;
var parentNavViewCtrl = $element.parent().inheritedData('$ionNavViewController'); if (parentNavViewCtrl) { // this navView is nested inside another one
// update the parent to use this direction and not
// the other it originally was set to
// inform the parent navView that it is not the primary navView
parentNavViewCtrl.isPrimary(false);
if (direction === 'enter' || direction === 'exit') { // they're entering/exiting a history
// find parent navViewController
parentNavViewCtrl.direction(direction);
if (direction === 'enter') { // reset the direction so this navView doesn't animate
// because it's parent will
direction = 'none'; } } } };
self.render = function(registerData, viewLocals, enteringView, leavingView, renderStart, renderEnd) { // register the view and figure out where it lives in the various
// histories and nav stacks, along with how views should enter/leave
var switcher = $ionicViewSwitcher.create(self, viewLocals, enteringView, leavingView, renderStart, renderEnd);
// init the rendering of views for this navView directive
switcher.init(registerData, function() { // the view is now compiled, in the dom and linked, now lets transition the views.
// this uses a callback incase THIS nav-view has a nested nav-view, and after the NESTED
// nav-view links, the NESTED nav-view would update which direction THIS nav-view should use
// kick off the transition of views
switcher.transition(self.direction(), registerData.enableBack, !disableAnimation);
// reset private vars for next time
disableRenderStartViewId = disableAnimation = null; });
};
self.beforeEnter = function(transitionData) { if (isPrimary) { // only update this nav-view's nav-bar if this is the primary nav-view
navBarDelegate = transitionData.navBarDelegate; var associatedNavBarCtrl = getAssociatedNavBarCtrl(); associatedNavBarCtrl && associatedNavBarCtrl.update(transitionData); navSwipeAttr(''); } };
self.activeEleId = function(eleId) { if (arguments.length) { activeEleId = eleId; } return activeEleId; };
self.transitionEnd = function() { var viewElements = $element.children(); var x, l, viewElement;
for (x = 0, l = viewElements.length; x < l; x++) { viewElement = viewElements.eq(x);
if (viewElement.data(DATA_ELE_IDENTIFIER) === activeEleId) { // this is the active element
navViewAttr(viewElement, VIEW_STATUS_ACTIVE);
} else if (navViewAttr(viewElement) === 'leaving' || navViewAttr(viewElement) === VIEW_STATUS_ACTIVE || navViewAttr(viewElement) === VIEW_STATUS_CACHED) { // this is a leaving element or was the former active element, or is an cached element
if (viewElement.data(DATA_DESTROY_ELE) || viewElement.data(DATA_NO_CACHE)) { // this element shouldn't stay cached
$ionicViewSwitcher.destroyViewEle(viewElement);
} else { // keep in the DOM, mark as cached
navViewAttr(viewElement, VIEW_STATUS_CACHED);
// disconnect the leaving scope
ionic.Utils.disconnectScope(viewElement.scope()); } } }
navSwipeAttr('');
// ensure no scrolls have been left frozen
if (self.isSwipeFreeze) { $ionicScrollDelegate.freezeAllScrolls(false); } };
function onTabsLeave(ev, data) { var viewElements = $element.children(); var viewElement, viewScope;
for (var x = 0, l = viewElements.length; x < l; x++) { viewElement = viewElements.eq(x); if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) { viewScope = viewElement.scope(); viewScope && viewScope.$emit(ev.name.replace('Tabs', 'View'), data); viewScope && viewScope.$broadcast(ev.name.replace('Tabs', 'ParentView'), data); break; } } }
self.cacheCleanup = function() { var viewElements = $element.children(); for (var x = 0, l = viewElements.length; x < l; x++) { if (viewElements.eq(x).data(DATA_DESTROY_ELE)) { $ionicViewSwitcher.destroyViewEle(viewElements.eq(x)); } } };
self.clearCache = function(stateIds) { var viewElements = $element.children(); var viewElement, viewScope, x, l, y, eleIdentifier;
for (x = 0, l = viewElements.length; x < l; x++) { viewElement = viewElements.eq(x);
if (stateIds) { eleIdentifier = viewElement.data(DATA_ELE_IDENTIFIER);
for (y = 0; y < stateIds.length; y++) { if (eleIdentifier === stateIds[y]) { $ionicViewSwitcher.destroyViewEle(viewElement); } } continue; }
if (navViewAttr(viewElement) == VIEW_STATUS_CACHED) { $ionicViewSwitcher.destroyViewEle(viewElement);
} else if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) { viewScope = viewElement.scope(); viewScope && viewScope.$broadcast('$ionicView.clearCache'); }
} };
self.getViewElements = function() { return $element.children(); };
self.appendViewElement = function(viewEle, viewLocals) { // compile the entering element and get the link function
var linkFn = $compile(viewEle);
$element.append(viewEle);
var viewScope = $scope.$new();
if (viewLocals && viewLocals.$$controller) { viewLocals.$scope = viewScope; var controller = $controller(viewLocals.$$controller, viewLocals); if (viewLocals.$$controllerAs) { viewScope[viewLocals.$$controllerAs] = controller; } $element.children().data('$ngControllerController', controller); }
linkFn(viewScope);
return viewScope; };
self.title = function(val) { var associatedNavBarCtrl = getAssociatedNavBarCtrl(); associatedNavBarCtrl && associatedNavBarCtrl.title(val); };
/** * @ngdoc method * @name $ionicNavView#enableBackButton * @description Enable/disable if the back button can be shown or not. For * example, the very first view in the navigation stack would not have a * back view, so the back button would be disabled. */ self.enableBackButton = function(shouldEnable) { var associatedNavBarCtrl = getAssociatedNavBarCtrl(); associatedNavBarCtrl && associatedNavBarCtrl.enableBackButton(shouldEnable); };
/** * @ngdoc method * @name $ionicNavView#showBackButton * @description Show/hide the nav bar active back button. If the back button * is not possible this will not force the back button to show. The * `enableBackButton()` method handles if a back button is even possible or not. */ self.showBackButton = function(shouldShow) { var associatedNavBarCtrl = getAssociatedNavBarCtrl(); if (associatedNavBarCtrl) { if (arguments.length) { return associatedNavBarCtrl.showActiveBackButton(shouldShow); } return associatedNavBarCtrl.showActiveBackButton(); } return true; };
self.showBar = function(val) { var associatedNavBarCtrl = getAssociatedNavBarCtrl(); if (associatedNavBarCtrl) { if (arguments.length) { return associatedNavBarCtrl.showBar(val); } return associatedNavBarCtrl.showBar(); } return true; };
self.isPrimary = function(val) { if (arguments.length) { isPrimary = val; } return isPrimary; };
self.direction = function(val) { if (arguments.length) { direction = val; } return direction; };
self.initSwipeBack = function() { var swipeBackHitWidth = $ionicConfig.views.swipeBackHitWidth(); var viewTransition, associatedNavBarCtrl, backView; var deregDragStart, deregDrag, deregRelease; var windowWidth, startDragX, dragPoints; var cancelData = {};
function onDragStart(ev) { if (!isPrimary || !$ionicConfig.views.swipeBackEnabled() || $ionicSideMenuDelegate.isOpenRight() ) return;
startDragX = getDragX(ev); if (startDragX > swipeBackHitWidth) return;
backView = $ionicHistory.backView();
var currentView = $ionicHistory.currentView();
if (!backView || backView.historyId !== currentView.historyId || currentView.canSwipeBack === false) return;
if (!windowWidth) windowWidth = window.innerWidth;
self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(true);
var registerData = { direction: 'back' };
dragPoints = [];
cancelData = { showBar: self.showBar(), showBackButton: self.showBackButton() };
var switcher = $ionicViewSwitcher.create(self, registerData, backView, currentView, true, false); switcher.loadViewElements(registerData); switcher.render(registerData);
viewTransition = switcher.transition('back', $ionicHistory.enabledBack(backView), true);
associatedNavBarCtrl = getAssociatedNavBarCtrl();
deregDrag = ionic.onGesture('drag', onDrag, $element[0]); deregRelease = ionic.onGesture('release', onRelease, $element[0]); }
function onDrag(ev) { if (isPrimary && viewTransition) { var dragX = getDragX(ev);
dragPoints.push({ t: Date.now(), x: dragX });
if (dragX >= windowWidth - 15) { onRelease(ev);
} else { var step = Math.min(Math.max(getSwipeCompletion(dragX), 0), 1); viewTransition.run(step); associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.run(step); }
} }
function onRelease(ev) { if (isPrimary && viewTransition && dragPoints && dragPoints.length > 1) {
var now = Date.now(); var releaseX = getDragX(ev); var startDrag = dragPoints[dragPoints.length - 1];
for (var x = dragPoints.length - 2; x >= 0; x--) { if (now - startDrag.t > 200) { break; } startDrag = dragPoints[x]; }
var isSwipingRight = (releaseX >= dragPoints[dragPoints.length - 2].x); var releaseSwipeCompletion = getSwipeCompletion(releaseX); var velocity = Math.abs(startDrag.x - releaseX) / (now - startDrag.t);
// private variables because ui-router has no way to pass custom data using $state.go
disableRenderStartViewId = backView.viewId; disableAnimation = (releaseSwipeCompletion < 0.03 || releaseSwipeCompletion > 0.97);
if (isSwipingRight && (releaseSwipeCompletion > 0.5 || velocity > 0.1)) { // complete view transition on release
var speed = (velocity > 0.5 || velocity < 0.05 || releaseX > windowWidth - 45) ? 'fast' : 'slow'; navSwipeAttr(disableAnimation ? '' : speed); backView.go(); associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.complete(!disableAnimation, speed);
} else { // cancel view transition on release
navSwipeAttr(disableAnimation ? '' : 'fast'); disableRenderStartViewId = null; viewTransition.cancel(!disableAnimation); associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.cancel(!disableAnimation, 'fast', cancelData); disableAnimation = null; }
}
ionic.offGesture(deregDrag, 'drag', onDrag); ionic.offGesture(deregRelease, 'release', onRelease);
windowWidth = viewTransition = dragPoints = null;
self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(false); }
function getDragX(ev) { return ionic.tap.pointerCoord(ev.gesture.srcEvent).x; }
function getSwipeCompletion(dragX) { return (dragX - startDragX) / windowWidth; }
deregDragStart = ionic.onGesture('dragstart', onDragStart, $element[0]);
$scope.$on('$destroy', function() { ionic.offGesture(deregDragStart, 'dragstart', onDragStart); ionic.offGesture(deregDrag, 'drag', onDrag); ionic.offGesture(deregRelease, 'release', onRelease); self.element = viewTransition = associatedNavBarCtrl = null; }); };
function navSwipeAttr(val) { ionic.DomUtil.cachedAttr($element, 'nav-swipe', val); }
function onTabsTop(ev, isTabsTop) { var associatedNavBarCtrl = getAssociatedNavBarCtrl(); associatedNavBarCtrl && associatedNavBarCtrl.hasTabsTop(isTabsTop); }
function onBarSubheader(ev, isBarSubheader) { var associatedNavBarCtrl = getAssociatedNavBarCtrl(); associatedNavBarCtrl && associatedNavBarCtrl.hasBarSubheader(isBarSubheader); }
function getAssociatedNavBarCtrl() { if (navBarDelegate) { for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) { if ($ionicNavBarDelegate._instances[x].$$delegateHandle == navBarDelegate) { return $ionicNavBarDelegate._instances[x]; } } } return $element.inheritedData('$ionNavBarController'); }
}]);
IonicModule .controller('$ionicRefresher', [ '$scope', '$attrs', '$element', '$ionicBind', '$timeout', function($scope, $attrs, $element, $ionicBind, $timeout) { var self = this, isDragging = false, isOverscrolling = false, dragOffset = 0, lastOverscroll = 0, ptrThreshold = 60, activated = false, scrollTime = 500, startY = null, deltaY = null, canOverscroll = true, scrollParent, scrollChild;
if (!isDefined($attrs.pullingIcon)) { $attrs.$set('pullingIcon', 'ion-android-arrow-down'); }
$scope.showSpinner = !isDefined($attrs.refreshingIcon) && $attrs.spinner != 'none';
$scope.showIcon = isDefined($attrs.refreshingIcon);
$ionicBind($scope, $attrs, { pullingIcon: '@', pullingText: '@', refreshingIcon: '@', refreshingText: '@', spinner: '@', disablePullingRotation: '@', $onRefresh: '&onRefresh', $onPulling: '&onPulling' });
function handleMousedown(e) { e.touches = e.touches || [{ screenX: e.screenX, screenY: e.screenY }]; // Mouse needs this
startY = Math.floor(e.touches[0].screenY); }
function handleTouchstart(e) { e.touches = e.touches || [{ screenX: e.screenX, screenY: e.screenY }];
startY = e.touches[0].screenY; }
function handleTouchend() { // reset Y
startY = null; // if this wasn't an overscroll, get out immediately
if (!canOverscroll && !isDragging) { return; } // the user has overscrolled but went back to native scrolling
if (!isDragging) { dragOffset = 0; isOverscrolling = false; setScrollLock(false); } else { isDragging = false; dragOffset = 0;
// the user has scroll far enough to trigger a refresh
if (lastOverscroll > ptrThreshold) { start(); scrollTo(ptrThreshold, scrollTime);
// the user has overscrolled but not far enough to trigger a refresh
} else { scrollTo(0, scrollTime, deactivate); isOverscrolling = false; } } }
function handleTouchmove(e) { e.touches = e.touches || [{ screenX: e.screenX, screenY: e.screenY }];
// Force mouse events to have had a down event first
if (!startY && e.type == 'mousemove') { return; }
// if multitouch or regular scroll event, get out immediately
if (!canOverscroll || e.touches.length > 1) { return; } //if this is a new drag, keep track of where we start
if (startY === null) { startY = e.touches[0].screenY; }
deltaY = e.touches[0].screenY - startY;
// how far have we dragged so far?
// kitkat fix for touchcancel events http://updates.html5rocks.com/2014/05/A-More-Compatible-Smoother-Touch
// Only do this if we're not on crosswalk
if (ionic.Platform.isAndroid() && ionic.Platform.version() === 4.4 && !ionic.Platform.isCrosswalk() && scrollParent.scrollTop === 0 && deltaY > 0) { isDragging = true; e.preventDefault(); }
// if we've dragged up and back down in to native scroll territory
if (deltaY - dragOffset <= 0 || scrollParent.scrollTop !== 0) {
if (isOverscrolling) { isOverscrolling = false; setScrollLock(false); }
if (isDragging) { nativescroll(scrollParent, deltaY - dragOffset * -1); }
// if we're not at overscroll 0 yet, 0 out
if (lastOverscroll !== 0) { overscroll(0); } return;
} else if (deltaY > 0 && scrollParent.scrollTop === 0 && !isOverscrolling) { // starting overscroll, but drag started below scrollTop 0, so we need to offset the position
dragOffset = deltaY; }
// prevent native scroll events while overscrolling
e.preventDefault();
// if not overscrolling yet, initiate overscrolling
if (!isOverscrolling) { isOverscrolling = true; setScrollLock(true); }
isDragging = true; // overscroll according to the user's drag so far
overscroll((deltaY - dragOffset) / 3);
// update the icon accordingly
if (!activated && lastOverscroll > ptrThreshold) { activated = true; ionic.requestAnimationFrame(activate);
} else if (activated && lastOverscroll < ptrThreshold) { activated = false; ionic.requestAnimationFrame(deactivate); } }
function handleScroll(e) { // canOverscrol is used to greatly simplify the drag handler during normal scrolling
canOverscroll = (e.target.scrollTop === 0) || isDragging; }
function overscroll(val) { scrollChild.style[ionic.CSS.TRANSFORM] = 'translate3d(0px, ' + val + 'px, 0px)'; lastOverscroll = val; }
function nativescroll(target, newScrollTop) { // creates a scroll event that bubbles, can be cancelled, and with its view
// and detail property initialized to window and 1, respectively
target.scrollTop = newScrollTop; var e = document.createEvent("UIEvents"); e.initUIEvent("scroll", true, true, window, 1); target.dispatchEvent(e); }
function setScrollLock(enabled) { // set the scrollbar to be position:fixed in preparation to overscroll
// or remove it so the app can be natively scrolled
if (enabled) { ionic.requestAnimationFrame(function() { scrollChild.classList.add('overscroll'); show(); });
} else { ionic.requestAnimationFrame(function() { scrollChild.classList.remove('overscroll'); hide(); deactivate(); }); } }
$scope.$on('scroll.refreshComplete', function() { // prevent the complete from firing before the scroll has started
$timeout(function() {
ionic.requestAnimationFrame(tail);
// scroll back to home during tail animation
scrollTo(0, scrollTime, deactivate);
// return to native scrolling after tail animation has time to finish
$timeout(function() {
if (isOverscrolling) { isOverscrolling = false; setScrollLock(false); }
}, scrollTime);
}, scrollTime); });
function scrollTo(Y, duration, callback) { // scroll animation loop w/ easing
// credit https://gist.github.com/dezinezync/5487119
var start = Date.now(), from = lastOverscroll;
if (from === Y) { callback(); return; /* Prevent scrolling to the Y point if already there */ }
// decelerating to zero velocity
function easeOutCubic(t) { return (--t) * t * t + 1; }
// scroll loop
function scroll() { var currentTime = Date.now(), time = Math.min(1, ((currentTime - start) / duration)), // where .5 would be 50% of time on a linear scale easedT gives a
// fraction based on the easing method
easedT = easeOutCubic(time);
overscroll(Math.floor((easedT * (Y - from)) + from));
if (time < 1) { ionic.requestAnimationFrame(scroll);
} else {
if (Y < 5 && Y > -5) { isOverscrolling = false; setScrollLock(false); }
callback && callback(); } }
// start scroll loop
ionic.requestAnimationFrame(scroll); }
var touchStartEvent, touchMoveEvent, touchEndEvent; if (window.navigator.pointerEnabled) { touchStartEvent = 'pointerdown'; touchMoveEvent = 'pointermove'; touchEndEvent = 'pointerup'; } else if (window.navigator.msPointerEnabled) { touchStartEvent = 'MSPointerDown'; touchMoveEvent = 'MSPointerMove'; touchEndEvent = 'MSPointerUp'; } else { touchStartEvent = 'touchstart'; touchMoveEvent = 'touchmove'; touchEndEvent = 'touchend'; }
self.init = function() { scrollParent = $element.parent().parent()[0]; scrollChild = $element.parent()[0];
if (!scrollParent || !scrollParent.classList.contains('ionic-scroll') || !scrollChild || !scrollChild.classList.contains('scroll')) { throw new Error('Refresher must be immediate child of ion-content or ion-scroll'); }
ionic.on(touchStartEvent, handleTouchstart, scrollChild); ionic.on(touchMoveEvent, handleTouchmove, scrollChild); ionic.on(touchEndEvent, handleTouchend, scrollChild); ionic.on('mousedown', handleMousedown, scrollChild); ionic.on('mousemove', handleTouchmove, scrollChild); ionic.on('mouseup', handleTouchend, scrollChild); ionic.on('scroll', handleScroll, scrollParent);
// cleanup when done
$scope.$on('$destroy', destroy); };
function destroy() { if ( scrollChild ) { ionic.off(touchStartEvent, handleTouchstart, scrollChild); ionic.off(touchMoveEvent, handleTouchmove, scrollChild); ionic.off(touchEndEvent, handleTouchend, scrollChild); ionic.off('mousedown', handleMousedown, scrollChild); ionic.off('mousemove', handleTouchmove, scrollChild); ionic.off('mouseup', handleTouchend, scrollChild); } if ( scrollParent ) { ionic.off('scroll', handleScroll, scrollParent); } scrollParent = null; scrollChild = null; }
// DOM manipulation and broadcast methods shared by JS and Native Scrolling
// getter used by JS Scrolling
self.getRefresherDomMethods = function() { return { activate: activate, deactivate: deactivate, start: start, show: show, hide: hide, tail: tail }; };
function activate() { $element[0].classList.add('active'); $scope.$onPulling(); }
function deactivate() { // give tail 150ms to finish
$timeout(function() { // deactivateCallback
$element.removeClass('active refreshing refreshing-tail'); if (activated) activated = false; }, 150); }
function start() { // startCallback
$element[0].classList.add('refreshing'); var q = $scope.$onRefresh();
if (q && q.then) { q['finally'](function() { $scope.$broadcast('scroll.refreshComplete'); }); } }
function show() { // showCallback
$element[0].classList.remove('invisible'); }
function hide() { // showCallback
$element[0].classList.add('invisible'); }
function tail() { // tailCallback
$element[0].classList.add('refreshing-tail'); }
// for testing
self.__handleTouchmove = handleTouchmove; self.__getScrollChild = function() { return scrollChild; }; self.__getScrollParent = function() { return scrollParent; }; } ]);
/** * @private */ IonicModule
.controller('$ionicScroll', [ '$scope', 'scrollViewOptions', '$timeout', '$window', '$location', '$document', '$ionicScrollDelegate', '$ionicHistory', function($scope, scrollViewOptions, $timeout, $window, $location, $document, $ionicScrollDelegate, $ionicHistory) {
var self = this; // for testing
self.__timeout = $timeout;
self._scrollViewOptions = scrollViewOptions; //for testing
self.isNative = function() { return !!scrollViewOptions.nativeScrolling; };
var element = self.element = scrollViewOptions.el; var $element = self.$element = jqLite(element); var scrollView; if (self.isNative()) { scrollView = self.scrollView = new ionic.views.ScrollNative(scrollViewOptions); } else { scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions); }
//Attach self to element as a controller so other directives can require this controller
//through `require: '$ionicScroll'
//Also attach to parent so that sibling elements can require this
($element.parent().length ? $element.parent() : $element) .data('$$ionicScrollController', self);
var deregisterInstance = $ionicScrollDelegate._registerInstance( self, scrollViewOptions.delegateHandle, function() { return $ionicHistory.isActiveScope($scope); } );
if (!isDefined(scrollViewOptions.bouncing)) { ionic.Platform.ready(function() { if (scrollView && scrollView.options) { scrollView.options.bouncing = true; if (ionic.Platform.isAndroid()) { // No bouncing by default on Android
scrollView.options.bouncing = false; // Faster scroll decel
scrollView.options.deceleration = 0.95; } } }); }
var resize = angular.bind(scrollView, scrollView.resize); angular.element($window).on('resize', resize);
var scrollFunc = function(e) { var detail = (e.originalEvent || e).detail || {}; $scope.$onScroll && $scope.$onScroll({ event: e, scrollTop: detail.scrollTop || 0, scrollLeft: detail.scrollLeft || 0 }); };
$element.on('scroll', scrollFunc);
$scope.$on('$destroy', function() { deregisterInstance(); scrollView && scrollView.__cleanup && scrollView.__cleanup(); angular.element($window).off('resize', resize); if ( $element ) { $element.off('scroll', scrollFunc); } if ( self._scrollViewOptions ) { self._scrollViewOptions.el = null; } if ( scrollViewOptions ) { scrollViewOptions.el = null; }
scrollView = self.scrollView = scrollViewOptions = self._scrollViewOptions = element = self.$element = $element = null; });
$timeout(function() { scrollView && scrollView.run && scrollView.run(); });
self.getScrollView = function() { return scrollView; };
self.getScrollPosition = function() { return scrollView.getValues(); };
self.resize = function() { return $timeout(resize, 0, false).then(function() { $element && $element.triggerHandler('scroll-resize'); }); };
self.scrollTop = function(shouldAnimate) { self.resize().then(function() { if (!scrollView) { return; } scrollView.scrollTo(0, 0, !!shouldAnimate); }); };
self.scrollBottom = function(shouldAnimate) { self.resize().then(function() { if (!scrollView) { return; } var max = scrollView.getScrollMax(); scrollView.scrollTo(max.left, max.top, !!shouldAnimate); }); };
self.scrollTo = function(left, top, shouldAnimate) { self.resize().then(function() { if (!scrollView) { return; } scrollView.scrollTo(left, top, !!shouldAnimate); }); };
self.zoomTo = function(zoom, shouldAnimate, originLeft, originTop) { self.resize().then(function() { if (!scrollView) { return; } scrollView.zoomTo(zoom, !!shouldAnimate, originLeft, originTop); }); };
self.zoomBy = function(zoom, shouldAnimate, originLeft, originTop) { self.resize().then(function() { if (!scrollView) { return; } scrollView.zoomBy(zoom, !!shouldAnimate, originLeft, originTop); }); };
self.scrollBy = function(left, top, shouldAnimate) { self.resize().then(function() { if (!scrollView) { return; } scrollView.scrollBy(left, top, !!shouldAnimate); }); };
self.anchorScroll = function(shouldAnimate) { self.resize().then(function() { if (!scrollView) { return; } var hash = $location.hash(); var elm = hash && $document[0].getElementById(hash); if (!(hash && elm)) { scrollView.scrollTo(0, 0, !!shouldAnimate); return; } var curElm = elm; var scrollLeft = 0, scrollTop = 0; do { if (curElm !== null) scrollLeft += curElm.offsetLeft; if (curElm !== null) scrollTop += curElm.offsetTop; curElm = curElm.offsetParent; } while (curElm.attributes != self.element.attributes && curElm.offsetParent); scrollView.scrollTo(scrollLeft, scrollTop, !!shouldAnimate); }); };
self.freezeScroll = scrollView.freeze; self.freezeScrollShut = scrollView.freezeShut;
self.freezeAllScrolls = function(shouldFreeze) { for (var i = 0; i < $ionicScrollDelegate._instances.length; i++) { $ionicScrollDelegate._instances[i].freezeScroll(shouldFreeze); } };
/** * @private */ self._setRefresher = function(refresherScope, refresherElement, refresherMethods) { self.refresher = refresherElement; var refresherHeight = self.refresher.clientHeight || 60; scrollView.activatePullToRefresh( refresherHeight, refresherMethods ); };
}]);
IonicModule .controller('$ionicSideMenus', [ '$scope', '$attrs', '$ionicSideMenuDelegate', '$ionicPlatform', '$ionicBody', '$ionicHistory', '$ionicScrollDelegate', 'IONIC_BACK_PRIORITY', '$rootScope', function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $ionicBody, $ionicHistory, $ionicScrollDelegate, IONIC_BACK_PRIORITY, $rootScope) { var self = this; var rightShowing, leftShowing, isDragging; var startX, lastX, offsetX, isAsideExposed; var enableMenuWithBackViews = true;
self.$scope = $scope;
self.initialize = function(options) { self.left = options.left; self.right = options.right; self.setContent(options.content); self.dragThresholdX = options.dragThresholdX || 10; $ionicHistory.registerHistory(self.$scope); };
/** * Set the content view controller if not passed in the constructor options. * * @param {object} content */ self.setContent = function(content) { if (content) { self.content = content;
self.content.onDrag = function(e) { self._handleDrag(e); };
self.content.endDrag = function(e) { self._endDrag(e); }; } };
self.isOpenLeft = function() { return self.getOpenAmount() > 0; };
self.isOpenRight = function() { return self.getOpenAmount() < 0; };
/** * Toggle the left menu to open 100% */ self.toggleLeft = function(shouldOpen) { if (isAsideExposed || !self.left.isEnabled) return; var openAmount = self.getOpenAmount(); if (arguments.length === 0) { shouldOpen = openAmount <= 0; } self.content.enableAnimation(); if (!shouldOpen) { self.openPercentage(0); $rootScope.$emit('$ionicSideMenuClose', 'left'); } else { self.openPercentage(100); $rootScope.$emit('$ionicSideMenuOpen', 'left'); } };
/** * Toggle the right menu to open 100% */ self.toggleRight = function(shouldOpen) { if (isAsideExposed || !self.right.isEnabled) return; var openAmount = self.getOpenAmount(); if (arguments.length === 0) { shouldOpen = openAmount >= 0; } self.content.enableAnimation(); if (!shouldOpen) { self.openPercentage(0); $rootScope.$emit('$ionicSideMenuClose', 'right'); } else { self.openPercentage(-100); $rootScope.$emit('$ionicSideMenuOpen', 'right'); } };
self.toggle = function(side) { if (side == 'right') { self.toggleRight(); } else { self.toggleLeft(); } };
/** * Close all menus. */ self.close = function() { self.openPercentage(0); $rootScope.$emit('$ionicSideMenuClose', 'left'); $rootScope.$emit('$ionicSideMenuClose', 'right'); };
/** * @return {float} The amount the side menu is open, either positive or negative for left (positive), or right (negative) */ self.getOpenAmount = function() { return self.content && self.content.getTranslateX() || 0; };
/** * @return {float} The ratio of open amount over menu width. For example, a * menu of width 100 open 50 pixels would be open 50% or a ratio of 0.5. Value is negative * for right menu. */ self.getOpenRatio = function() { var amount = self.getOpenAmount(); if (amount >= 0) { return amount / self.left.width; } return amount / self.right.width; };
self.isOpen = function() { return self.getOpenAmount() !== 0; };
/** * @return {float} The percentage of open amount over menu width. For example, a * menu of width 100 open 50 pixels would be open 50%. Value is negative * for right menu. */ self.getOpenPercentage = function() { return self.getOpenRatio() * 100; };
/** * Open the menu with a given percentage amount. * @param {float} percentage The percentage (positive or negative for left/right) to open the menu. */ self.openPercentage = function(percentage) { var p = percentage / 100;
if (self.left && percentage >= 0) { self.openAmount(self.left.width * p); } else if (self.right && percentage < 0) { self.openAmount(self.right.width * p); }
// add the CSS class "menu-open" if the percentage does not
// equal 0, otherwise remove the class from the body element
$ionicBody.enableClass((percentage !== 0), 'menu-open');
self.content.setCanScroll(percentage == 0); };
/* function freezeAllScrolls(shouldFreeze) { if (shouldFreeze && !self.isScrollFreeze) { $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);
} else if (!shouldFreeze && self.isScrollFreeze) { $ionicScrollDelegate.freezeAllScrolls(false); } self.isScrollFreeze = shouldFreeze; } */
/** * Open the menu the given pixel amount. * @param {float} amount the pixel amount to open the menu. Positive value for left menu, * negative value for right menu (only one menu will be visible at a time). */ self.openAmount = function(amount) { var maxLeft = self.left && self.left.width || 0; var maxRight = self.right && self.right.width || 0;
// Check if we can move to that side, depending if the left/right panel is enabled
if (!(self.left && self.left.isEnabled) && amount > 0) { self.content.setTranslateX(0); return; }
if (!(self.right && self.right.isEnabled) && amount < 0) { self.content.setTranslateX(0); return; }
if (leftShowing && amount > maxLeft) { self.content.setTranslateX(maxLeft); return; }
if (rightShowing && amount < -maxRight) { self.content.setTranslateX(-maxRight); return; }
self.content.setTranslateX(amount);
if (amount >= 0) { leftShowing = true; rightShowing = false;
if (amount > 0) { // Push the z-index of the right menu down
self.right && self.right.pushDown && self.right.pushDown(); // Bring the z-index of the left menu up
self.left && self.left.bringUp && self.left.bringUp(); } } else { rightShowing = true; leftShowing = false;
// Bring the z-index of the right menu up
self.right && self.right.bringUp && self.right.bringUp(); // Push the z-index of the left menu down
self.left && self.left.pushDown && self.left.pushDown(); } };
/** * Given an event object, find the final resting position of this side * menu. For example, if the user "throws" the content to the right and * releases the touch, the left menu should snap open (animated, of course). * * @param {Event} e the gesture event to use for snapping */ self.snapToRest = function(e) { // We want to animate at the end of this
self.content.enableAnimation(); isDragging = false;
// Check how much the panel is open after the drag, and
// what the drag velocity is
var ratio = self.getOpenRatio();
if (ratio === 0) { // Just to be safe
self.openPercentage(0); return; }
var velocityThreshold = 0.3; var velocityX = e.gesture.velocityX; var direction = e.gesture.direction;
// Going right, less than half, too slow (snap back)
if (ratio > 0 && ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) { self.openPercentage(0); }
// Going left, more than half, too slow (snap back)
else if (ratio > 0.5 && direction == 'left' && velocityX < velocityThreshold) { self.openPercentage(100); }
// Going left, less than half, too slow (snap back)
else if (ratio < 0 && ratio > -0.5 && direction == 'left' && velocityX < velocityThreshold) { self.openPercentage(0); }
// Going right, more than half, too slow (snap back)
else if (ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) { self.openPercentage(-100); }
// Going right, more than half, or quickly (snap open)
else if (direction == 'right' && ratio >= 0 && (ratio >= 0.5 || velocityX > velocityThreshold)) { self.openPercentage(100); }
// Going left, more than half, or quickly (span open)
else if (direction == 'left' && ratio <= 0 && (ratio <= -0.5 || velocityX > velocityThreshold)) { self.openPercentage(-100); }
// Snap back for safety
else { self.openPercentage(0); } };
self.enableMenuWithBackViews = function(val) { if (arguments.length) { enableMenuWithBackViews = !!val; } return enableMenuWithBackViews; };
self.isAsideExposed = function() { return !!isAsideExposed; };
self.exposeAside = function(shouldExposeAside) { if (!(self.left && self.left.isEnabled) && !(self.right && self.right.isEnabled)) return; self.close();
isAsideExposed = shouldExposeAside; if ((self.left && self.left.isEnabled) && (self.right && self.right.isEnabled)) { self.content.setMarginLeftAndRight(isAsideExposed ? self.left.width : 0, isAsideExposed ? self.right.width : 0); } else if (self.left && self.left.isEnabled) { // set the left marget width if it should be exposed
// otherwise set false so there's no left margin
self.content.setMarginLeft(isAsideExposed ? self.left.width : 0); } else if (self.right && self.right.isEnabled) { self.content.setMarginRight(isAsideExposed ? self.right.width : 0); } self.$scope.$emit('$ionicExposeAside', isAsideExposed); };
self.activeAsideResizing = function(isResizing) { $ionicBody.enableClass(isResizing, 'aside-resizing'); };
// End a drag with the given event
self._endDrag = function(e) { if (isAsideExposed) return;
if (isDragging) { self.snapToRest(e); } startX = null; lastX = null; offsetX = null; };
// Handle a drag event
self._handleDrag = function(e) { if (isAsideExposed || !$scope.dragContent) return;
// If we don't have start coords, grab and store them
if (!startX) { startX = e.gesture.touches[0].pageX; lastX = startX; } else { // Grab the current tap coords
lastX = e.gesture.touches[0].pageX; }
// Calculate difference from the tap points
if (!isDragging && Math.abs(lastX - startX) > self.dragThresholdX) { // if the difference is greater than threshold, start dragging using the current
// point as the starting point
startX = lastX;
isDragging = true; // Initialize dragging
self.content.disableAnimation(); offsetX = self.getOpenAmount(); }
if (isDragging) { self.openAmount(offsetX + (lastX - startX)); //self.content.setCanScroll(false);
} };
self.canDragContent = function(canDrag) { if (arguments.length) { $scope.dragContent = !!canDrag; } return $scope.dragContent; };
self.edgeThreshold = 25; self.edgeThresholdEnabled = false; self.edgeDragThreshold = function(value) { if (arguments.length) { if (isNumber(value) && value > 0) { self.edgeThreshold = value; self.edgeThresholdEnabled = true; } else { self.edgeThresholdEnabled = !!value; } } return self.edgeThresholdEnabled; };
self.isDraggableTarget = function(e) { //Only restrict edge when sidemenu is closed and restriction is enabled
var shouldOnlyAllowEdgeDrag = self.edgeThresholdEnabled && !self.isOpen(); var startX = e.gesture.startEvent && e.gesture.startEvent.center && e.gesture.startEvent.center.pageX;
var dragIsWithinBounds = !shouldOnlyAllowEdgeDrag || startX <= self.edgeThreshold || startX >= self.content.element.offsetWidth - self.edgeThreshold;
var backView = $ionicHistory.backView(); var menuEnabled = enableMenuWithBackViews ? true : !backView; if (!menuEnabled) { var currentView = $ionicHistory.currentView() || {}; return (dragIsWithinBounds && (backView.historyId !== currentView.historyId)); }
return ($scope.dragContent || self.isOpen()) && dragIsWithinBounds && !e.gesture.srcEvent.defaultPrevented && menuEnabled && !e.target.tagName.match(/input|textarea|select|object|embed/i) && !e.target.isContentEditable && !(e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-scroll') == 'true'); };
$scope.sideMenuContentTranslateX = 0;
var deregisterBackButtonAction = noop; var closeSideMenu = angular.bind(self, self.close);
$scope.$watch(function() { return self.getOpenAmount() !== 0; }, function(isOpen) { deregisterBackButtonAction(); if (isOpen) { deregisterBackButtonAction = $ionicPlatform.registerBackButtonAction( closeSideMenu, IONIC_BACK_PRIORITY.sideMenu ); } });
var deregisterInstance = $ionicSideMenuDelegate._registerInstance( self, $attrs.delegateHandle, function() { return $ionicHistory.isActiveScope($scope); } );
$scope.$on('$destroy', function() { deregisterInstance(); deregisterBackButtonAction(); self.$scope = null; if (self.content) { self.content.setCanScroll(true); self.content.element = null; self.content = null; } });
self.initialize({ left: { width: 275 }, right: { width: 275 } });
}]);
(function(ionic) {
var TRANSLATE32 = 'translate(32,32)'; var STROKE_OPACITY = 'stroke-opacity'; var ROUND = 'round'; var INDEFINITE = 'indefinite'; var DURATION = '750ms'; var NONE = 'none'; var SHORTCUTS = { a: 'animate', an: 'attributeName', at: 'animateTransform', c: 'circle', da: 'stroke-dasharray', os: 'stroke-dashoffset', f: 'fill', lc: 'stroke-linecap', rc: 'repeatCount', sw: 'stroke-width', t: 'transform', v: 'values' };
var SPIN_ANIMATION = { v: '0,32,32;360,32,32', an: 'transform', type: 'rotate', rc: INDEFINITE, dur: DURATION };
function createSvgElement(tagName, data, parent, spinnerName) { var ele = document.createElement(SHORTCUTS[tagName] || tagName); var k, x, y;
for (k in data) {
if (angular.isArray(data[k])) { for (x = 0; x < data[k].length; x++) { if (data[k][x].fn) { for (y = 0; y < data[k][x].t; y++) { createSvgElement(k, data[k][x].fn(y, spinnerName), ele, spinnerName); } } else { createSvgElement(k, data[k][x], ele, spinnerName); } }
} else { setSvgAttribute(ele, k, data[k]); } }
parent.appendChild(ele); }
function setSvgAttribute(ele, k, v) { ele.setAttribute(SHORTCUTS[k] || k, v); }
function animationValues(strValues, i) { var values = strValues.split(';'); var back = values.slice(i); var front = values.slice(0, values.length - back.length); values = back.concat(front).reverse(); return values.join(';') + ';' + values[0]; }
var IOS_SPINNER = { sw: 4, lc: ROUND, line: [{ fn: function(i, spinnerName) { return { y1: spinnerName == 'ios' ? 17 : 12, y2: spinnerName == 'ios' ? 29 : 20, t: TRANSLATE32 + ' rotate(' + (30 * i + (i < 6 ? 180 : -180)) + ')', a: [{ fn: function() { return { an: STROKE_OPACITY, dur: DURATION, v: animationValues('0;.1;.15;.25;.35;.45;.55;.65;.7;.85;1', i), rc: INDEFINITE }; }, t: 1 }] }; }, t: 12 }] };
var spinners = {
android: { c: [{ sw: 6, da: 128, os: 82, r: 26, cx: 32, cy: 32, f: NONE }] },
ios: IOS_SPINNER,
'ios-small': IOS_SPINNER,
bubbles: { sw: 0, c: [{ fn: function(i) { return { cx: 24 * Math.cos(2 * Math.PI * i / 8), cy: 24 * Math.sin(2 * Math.PI * i / 8), t: TRANSLATE32, a: [{ fn: function() { return { an: 'r', dur: DURATION, v: animationValues('1;2;3;4;5;6;7;8', i), rc: INDEFINITE }; }, t: 1 }] }; }, t: 8 }] },
circles: {
c: [{ fn: function(i) { return { r: 5, cx: 24 * Math.cos(2 * Math.PI * i / 8), cy: 24 * Math.sin(2 * Math.PI * i / 8), t: TRANSLATE32, sw: 0, a: [{ fn: function() { return { an: 'fill-opacity', dur: DURATION, v: animationValues('.3;.3;.3;.4;.7;.85;.9;1', i), rc: INDEFINITE }; }, t: 1 }] }; }, t: 8 }] },
crescent: { c: [{ sw: 4, da: 128, os: 82, r: 26, cx: 32, cy: 32, f: NONE, at: [SPIN_ANIMATION] }] },
dots: {
c: [{ fn: function(i) { return { cx: 16 + (16 * i), cy: 32, sw: 0, a: [{ fn: function() { return { an: 'fill-opacity', dur: DURATION, v: animationValues('.5;.6;.8;1;.8;.6;.5', i), rc: INDEFINITE }; }, t: 1 }, { fn: function() { return { an: 'r', dur: DURATION, v: animationValues('4;5;6;5;4;3;3', i), rc: INDEFINITE }; }, t: 1 }] }; }, t: 3 }] },
lines: { sw: 7, lc: ROUND, line: [{ fn: function(i) { return { x1: 10 + (i * 14), x2: 10 + (i * 14), a: [{ fn: function() { return { an: 'y1', dur: DURATION, v: animationValues('16;18;28;18;16', i), rc: INDEFINITE }; }, t: 1 }, { fn: function() { return { an: 'y2', dur: DURATION, v: animationValues('48;44;36;46;48', i), rc: INDEFINITE }; }, t: 1 }, { fn: function() { return { an: STROKE_OPACITY, dur: DURATION, v: animationValues('1;.8;.5;.4;1', i), rc: INDEFINITE }; }, t: 1 }] }; }, t: 4 }] },
ripple: { f: NONE, 'fill-rule': 'evenodd', sw: 3, circle: [{ fn: function(i) { return { cx: 32, cy: 32, a: [{ fn: function() { return { an: 'r', begin: (i * -1) + 's', dur: '2s', v: '0;24', keyTimes: '0;1', keySplines: '0.1,0.2,0.3,1', calcMode: 'spline', rc: INDEFINITE }; }, t: 1 }, { fn: function() { return { an: STROKE_OPACITY, begin: (i * -1) + 's', dur: '2s', v: '.2;1;.2;0', rc: INDEFINITE }; }, t: 1 }] }; }, t: 2 }] },
spiral: { defs: [{ linearGradient: [{ id: 'sGD', gradientUnits: 'userSpaceOnUse', x1: 55, y1: 46, x2: 2, y2: 46, stop: [{ offset: 0.1, class: 'stop1' }, { offset: 1, class: 'stop2' }] }] }], g: [{ sw: 4, lc: ROUND, f: NONE, path: [{ stroke: 'url(#sGD)', d: 'M4,32 c0,15,12,28,28,28c8,0,16-4,21-9' }, { d: 'M60,32 C60,16,47.464,4,32,4S4,16,4,32' }], at: [SPIN_ANIMATION] }] }
};
var animations = {
android: function(ele) { // Note that this is called as a function, not a constructor.
var self = {};
this.stop = false;
var rIndex = 0; var rotateCircle = 0; var startTime; var svgEle = ele.querySelector('g'); var circleEle = ele.querySelector('circle');
function run() { if (self.stop) return;
var v = easeInOutCubic(Date.now() - startTime, 650); var scaleX = 1; var translateX = 0; var dasharray = (188 - (58 * v)); var dashoffset = (182 - (182 * v));
if (rIndex % 2) { scaleX = -1; translateX = -64; dasharray = (128 - (-58 * v)); dashoffset = (182 * v); }
var rotateLine = [0, -101, -90, -11, -180, 79, -270, -191][rIndex];
setSvgAttribute(circleEle, 'da', Math.max(Math.min(dasharray, 188), 128)); setSvgAttribute(circleEle, 'os', Math.max(Math.min(dashoffset, 182), 0)); setSvgAttribute(circleEle, 't', 'scale(' + scaleX + ',1) translate(' + translateX + ',0) rotate(' + rotateLine + ',32,32)');
rotateCircle += 4.1; if (rotateCircle > 359) rotateCircle = 0; setSvgAttribute(svgEle, 't', 'rotate(' + rotateCircle + ',32,32)');
if (v >= 1) { rIndex++; if (rIndex > 7) rIndex = 0; startTime = Date.now(); }
ionic.requestAnimationFrame(run); }
return function() { startTime = Date.now(); run(); return self; };
}
};
function easeInOutCubic(t, c) { t /= c / 2; if (t < 1) return 1 / 2 * t * t * t; t -= 2; return 1 / 2 * (t * t * t + 2); }
IonicModule .controller('$ionicSpinner', [ '$element', '$attrs', '$ionicConfig', function($element, $attrs, $ionicConfig) { var spinnerName, anim;
this.init = function() { spinnerName = $attrs.icon || $ionicConfig.spinner.icon();
var container = document.createElement('div'); createSvgElement('svg', { viewBox: '0 0 64 64', g: [spinners[spinnerName]] }, container, spinnerName);
// Specifically for animations to work,
// Android 4.3 and below requires the element to be
// added as an html string, rather than dynmically
// building up the svg element and appending it.
$element.html(container.innerHTML);
this.start();
return spinnerName; };
this.start = function() { animations[spinnerName] && (anim = animations[spinnerName]($element[0])()); };
this.stop = function() { animations[spinnerName] && (anim.stop = true); };
}]);
})(ionic);
IonicModule .controller('$ionicTab', [ '$scope', '$ionicHistory', '$attrs', '$location', '$state', function($scope, $ionicHistory, $attrs, $location, $state) { this.$scope = $scope;
//All of these exposed for testing
this.hrefMatchesState = function() { return $attrs.href && $location.path().indexOf( $attrs.href.replace(/^#/, '').replace(/\/$/, '') ) === 0; }; this.srefMatchesState = function() { return $attrs.uiSref && $state.includes($attrs.uiSref.split('(')[0]); }; this.navNameMatchesState = function() { return this.navViewName && $ionicHistory.isCurrentStateNavView(this.navViewName); };
this.tabMatchesState = function() { return this.hrefMatchesState() || this.srefMatchesState() || this.navNameMatchesState(); }; }]);
IonicModule .controller('$ionicTabs', [ '$scope', '$element', '$ionicHistory', function($scope, $element, $ionicHistory) { var self = this; var selectedTab = null; var previousSelectedTab = null; var selectedTabIndex; var isVisible = true; self.tabs = [];
self.selectedIndex = function() { return self.tabs.indexOf(selectedTab); }; self.selectedTab = function() { return selectedTab; }; self.previousSelectedTab = function() { return previousSelectedTab; };
self.add = function(tab) { $ionicHistory.registerHistory(tab); self.tabs.push(tab); };
self.remove = function(tab) { var tabIndex = self.tabs.indexOf(tab); if (tabIndex === -1) { return; } //Use a field like '$tabSelected' so developers won't accidentally set it in controllers etc
if (tab.$tabSelected) { self.deselect(tab); //Try to select a new tab if we're removing a tab
if (self.tabs.length === 1) { //Do nothing if there are no other tabs to select
} else { //Select previous tab if it's the last tab, else select next tab
var newTabIndex = tabIndex === self.tabs.length - 1 ? tabIndex - 1 : tabIndex + 1; self.select(self.tabs[newTabIndex]); } } self.tabs.splice(tabIndex, 1); };
self.deselect = function(tab) { if (tab.$tabSelected) { previousSelectedTab = selectedTab; selectedTab = selectedTabIndex = null; tab.$tabSelected = false; (tab.onDeselect || noop)(); tab.$broadcast && tab.$broadcast('$ionicHistory.deselect'); } };
self.select = function(tab, shouldEmitEvent) { var tabIndex; if (isNumber(tab)) { tabIndex = tab; if (tabIndex >= self.tabs.length) return; tab = self.tabs[tabIndex]; } else { tabIndex = self.tabs.indexOf(tab); }
if (arguments.length === 1) { shouldEmitEvent = !!(tab.navViewName || tab.uiSref); }
if (selectedTab && selectedTab.$historyId == tab.$historyId) { if (shouldEmitEvent) { $ionicHistory.goToHistoryRoot(tab.$historyId); }
} else if (selectedTabIndex !== tabIndex) { forEach(self.tabs, function(tab) { self.deselect(tab); });
selectedTab = tab; selectedTabIndex = tabIndex;
if (self.$scope && self.$scope.$parent) { self.$scope.$parent.$activeHistoryId = tab.$historyId; }
//Use a funny name like $tabSelected so the developer doesn't overwrite the var in a child scope
tab.$tabSelected = true; (tab.onSelect || noop)();
if (shouldEmitEvent) { $scope.$emit('$ionicHistory.change', { type: 'tab', tabIndex: tabIndex, historyId: tab.$historyId, navViewName: tab.navViewName, hasNavView: !!tab.navViewName, title: tab.title, url: tab.href, uiSref: tab.uiSref }); }
$scope.$broadcast("tabSelected", { selectedTab: tab, selectedTabIndex: tabIndex}); } };
self.hasActiveScope = function() { for (var x = 0; x < self.tabs.length; x++) { if ($ionicHistory.isActiveScope(self.tabs[x])) { return true; } } return false; };
self.showBar = function(show) { if (arguments.length) { if (show) { $element.removeClass('tabs-item-hide'); } else { $element.addClass('tabs-item-hide'); } isVisible = !!show; } return isVisible; }; }]);
IonicModule .controller('$ionicView', [ '$scope', '$element', '$attrs', '$compile', '$rootScope', function($scope, $element, $attrs, $compile, $rootScope) { var self = this; var navElementHtml = {}; var navViewCtrl; var navBarDelegateHandle; var hasViewHeaderBar; var deregisters = []; var viewTitle;
var deregIonNavBarInit = $scope.$on('ionNavBar.init', function(ev, delegateHandle) { // this view has its own ion-nav-bar, remember the navBarDelegateHandle for this view
ev.stopPropagation(); navBarDelegateHandle = delegateHandle; });
self.init = function() { deregIonNavBarInit();
var modalCtrl = $element.inheritedData('$ionModalController'); navViewCtrl = $element.inheritedData('$ionNavViewController');
// don't bother if inside a modal or there's no parent navView
if (!navViewCtrl || modalCtrl) return;
// add listeners for when this view changes
$scope.$on('$ionicView.beforeEnter', self.beforeEnter); $scope.$on('$ionicView.afterEnter', afterEnter); $scope.$on('$ionicView.beforeLeave', deregisterFns); };
self.beforeEnter = function(ev, transData) { // this event was emitted, starting at intial ion-view, then bubbles up
// only the first ion-view should do something with it, parent ion-views should ignore
if (transData && !transData.viewNotified) { transData.viewNotified = true;
if (!$rootScope.$$phase) $scope.$digest(); viewTitle = isDefined($attrs.viewTitle) ? $attrs.viewTitle : $attrs.title;
var navBarItems = {}; for (var n in navElementHtml) { navBarItems[n] = generateNavBarItem(navElementHtml[n]); }
navViewCtrl.beforeEnter(extend(transData, { title: viewTitle, showBack: !attrTrue('hideBackButton'), navBarItems: navBarItems, navBarDelegate: navBarDelegateHandle || null, showNavBar: !attrTrue('hideNavBar'), hasHeaderBar: !!hasViewHeaderBar }));
// make sure any existing observers are cleaned up
deregisterFns(); } };
function afterEnter() { // only listen for title updates after it has entered
// but also deregister the observe before it leaves
var viewTitleAttr = isDefined($attrs.viewTitle) && 'viewTitle' || isDefined($attrs.title) && 'title'; if (viewTitleAttr) { titleUpdate($attrs[viewTitleAttr]); deregisters.push($attrs.$observe(viewTitleAttr, titleUpdate)); }
if (isDefined($attrs.hideBackButton)) { deregisters.push($scope.$watch($attrs.hideBackButton, function(val) { navViewCtrl.showBackButton(!val); })); }
if (isDefined($attrs.hideNavBar)) { deregisters.push($scope.$watch($attrs.hideNavBar, function(val) { navViewCtrl.showBar(!val); })); } }
function titleUpdate(newTitle) { if (isDefined(newTitle) && newTitle !== viewTitle) { viewTitle = newTitle; navViewCtrl.title(viewTitle); } }
function deregisterFns() { // remove all existing $attrs.$observe's
for (var x = 0; x < deregisters.length; x++) { deregisters[x](); } deregisters = []; }
function generateNavBarItem(html) { if (html) { // every time a view enters we need to recreate its view buttons if they exist
return $compile(html)($scope.$new()); } }
function attrTrue(key) { return !!$scope.$eval($attrs[key]); }
self.navElement = function(type, html) { navElementHtml[type] = html; };
}]);
/* * We don't document the ionActionSheet directive, we instead document * the $ionicActionSheet service */ IonicModule .directive('ionActionSheet', ['$document', function($document) { return { restrict: 'E', scope: true, replace: true, link: function($scope, $element) {
var keyUp = function(e) { if (e.which == 27) { $scope.cancel(); $scope.$apply(); } };
var backdropClick = function(e) { if (e.target == $element[0]) { $scope.cancel(); $scope.$apply(); } }; $scope.$on('$destroy', function() { $element.remove(); $document.unbind('keyup', keyUp); });
$document.bind('keyup', keyUp); $element.bind('click', backdropClick); }, template: '<div class="action-sheet-backdrop">' + '<div class="action-sheet-wrapper">' + '<div class="action-sheet" ng-class="{\'action-sheet-has-icons\': $actionSheetHasIcon}">' + '<div class="action-sheet-group action-sheet-options">' + '<div class="action-sheet-title" ng-if="titleText" ng-bind-html="titleText"></div>' + '<button class="button action-sheet-option" ng-click="buttonClicked($index)" ng-class="b.className" ng-repeat="b in buttons" ng-bind-html="b.text"></button>' + '<button class="button destructive action-sheet-destructive" ng-if="destructiveText" ng-click="destructiveButtonClicked()" ng-bind-html="destructiveText"></button>' + '</div>' + '<div class="action-sheet-group action-sheet-cancel" ng-if="cancelText">' + '<button class="button" ng-click="cancel()" ng-bind-html="cancelText"></button>' + '</div>' + '</div>' + '</div>' + '</div>' }; }]);
/** * @ngdoc directive * @name ionCheckbox * @module ionic * @restrict E * @codepen hqcju * @description * The checkbox is no different than the HTML checkbox input, except it's styled differently. * * The checkbox behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]).
* * @usage * ```html
* <ion-checkbox ng-model="isChecked">Checkbox Label</ion-checkbox> * ```
*/
IonicModule .directive('ionCheckbox', ['$ionicConfig', function($ionicConfig) { return { restrict: 'E', replace: true, require: '?ngModel', transclude: true, template: '<label class="item item-checkbox">' + '<div class="checkbox checkbox-input-hidden disable-pointer-events">' + '<input type="checkbox">' + '<i class="checkbox-icon"></i>' + '</div>' + '<div class="item-content disable-pointer-events" ng-transclude></div>' + '</label>', compile: function(element, attr) { var input = element.find('input'); forEach({ 'name': attr.name, 'ng-value': attr.ngValue, 'ng-model': attr.ngModel, 'ng-checked': attr.ngChecked, 'ng-disabled': attr.ngDisabled, 'ng-true-value': attr.ngTrueValue, 'ng-false-value': attr.ngFalseValue, 'ng-change': attr.ngChange, 'ng-required': attr.ngRequired, 'required': attr.required }, function(value, name) { if (isDefined(value)) { input.attr(name, value); } }); var checkboxWrapper = element[0].querySelector('.checkbox'); checkboxWrapper.classList.add('checkbox-' + $ionicConfig.form.checkbox()); } }; }]);
/** * @ngdoc directive * @restrict A * @name collectionRepeat * @module ionic * @codepen 7ec1ec58f2489ab8f359fa1a0fe89c15 * @description * `collection-repeat` allows an app to show huge lists of items much more performantly than * `ng-repeat`. * * It renders into the DOM only as many items as are currently visible. * * This means that on a phone screen that can fit eight items, only the eight items matching * the current scroll position will be rendered. * * **The Basics**: * * - The data given to collection-repeat must be an array. * - If the `item-height` and `item-width` attributes are not supplied, it will be assumed that * every item in the list has the same dimensions as the first item. * - Don't use angular one-time binding (`::`) with collection-repeat. The scope of each item is * assigned new data and re-digested as you scroll. Bindings need to update, and one-time bindings * won't. * * **Performance Tips**: * * - The iOS webview has a performance bottleneck when switching out `<img src>` attributes. * To increase performance of images on iOS, cache your images in advance and, * if possible, lower the number of unique images. We're working on [a solution](https://github.com/driftyco/ionic/issues/3194).
* * @usage * #### Basic Item List ([codepen](http://codepen.io/ionic/pen/0c2c35a34a8b18ad4d793fef0b081693))
* ```html
* <ion-content> * <ion-item collection-repeat="item in items"> * {% raw %}{{item}}{% endraw %} * </ion-item> * </ion-content> * ```
* * #### Grid of Images ([codepen](http://codepen.io/ionic/pen/5515d4efd9d66f780e96787387f41664))
* ```html
* <ion-content> * <img collection-repeat="photo in photos" * item-width="33%" * item-height="200px" * ng-src="{% raw %}{{photo.url}}{% endraw %}"> * </ion-content> * ```
* * #### Horizontal Scroller, Dynamic Item Width ([codepen](http://codepen.io/ionic/pen/67cc56b349124a349acb57a0740e030e))
* ```html
* <ion-content> * <h2>Available Kittens:</h2> * <ion-scroll direction="x" class="available-scroller"> * <div class="photo" collection-repeat="photo in main.photos" * item-height="250" item-width="photo.width + 30"> * <img ng-src="{% raw %}{{photo.src}}{% endraw %}"> * </div> * </ion-scroll> * </ion-content> * ```
* * @param {expression} collection-repeat The expression indicating how to enumerate a collection, * of the format `variable in expression` – where variable is the user defined loop variable * and `expression` is a scope expression giving the collection to enumerate. * For example: `album in artist.albums` or `album in artist.albums | orderBy:'name'`. * @param {expression=} item-width The width of the repeated element. The expression must return * a number (pixels) or a percentage. Defaults to the width of the first item in the list. * (previously named collection-item-width) * @param {expression=} item-height The height of the repeated element. The expression must return * a number (pixels) or a percentage. Defaults to the height of the first item in the list. * (previously named collection-item-height) * @param {number=} item-render-buffer The number of items to load before and after the visible * items in the list. Default 3. Tip: set this higher if you have lots of images to preload, but * don't set it too high or you'll see performance loss. * @param {boolean=} force-refresh-images Force images to refresh as you scroll. This fixes a problem * where, when an element is interchanged as scrolling, its image will still have the old src * while the new src loads. Setting this to true comes with a small performance loss. */
IonicModule .directive('collectionRepeat', CollectionRepeatDirective) .factory('$ionicCollectionManager', RepeatManagerFactory);
var ONE_PX_TRANSPARENT_IMG_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; var WIDTH_HEIGHT_REGEX = /height:.*?px;\s*width:.*?px/; var DEFAULT_RENDER_BUFFER = 3;
CollectionRepeatDirective.$inject = ['$ionicCollectionManager', '$parse', '$window', '$$rAF', '$rootScope', '$timeout']; function CollectionRepeatDirective($ionicCollectionManager, $parse, $window, $$rAF, $rootScope, $timeout) { return { restrict: 'A', priority: 1000, transclude: 'element', $$tlb: true, require: '^^$ionicScroll', link: postLink };
function postLink(scope, element, attr, scrollCtrl, transclude) { var scrollView = scrollCtrl.scrollView; var node = element[0]; var containerNode = angular.element('<div class="collection-repeat-container">')[0]; node.parentNode.replaceChild(containerNode, node);
if (scrollView.options.scrollingX && scrollView.options.scrollingY) { throw new Error("collection-repeat expected a parent x or y scrollView, not " + "an xy scrollView."); }
var repeatExpr = attr.collectionRepeat; var match = repeatExpr.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); if (!match) { throw new Error("collection-repeat expected expression in form of '_item_ in " + "_collection_[ track by _id_]' but got '" + attr.collectionRepeat + "'."); } var keyExpr = match[1]; var listExpr = match[2]; var listGetter = $parse(listExpr); var heightData = {}; var widthData = {}; var computedStyleDimensions = {}; var data = []; var repeatManager;
// attr.collectionBufferSize is deprecated
var renderBufferExpr = attr.itemRenderBuffer || attr.collectionBufferSize; var renderBuffer = angular.isDefined(renderBufferExpr) ? parseInt(renderBufferExpr) : DEFAULT_RENDER_BUFFER;
// attr.collectionItemHeight is deprecated
var heightExpr = attr.itemHeight || attr.collectionItemHeight; // attr.collectionItemWidth is deprecated
var widthExpr = attr.itemWidth || attr.collectionItemWidth;
var afterItemsContainer = initAfterItemsContainer();
var changeValidator = makeChangeValidator(); initDimensions();
// Dimensions are refreshed on resize or data change.
scrollCtrl.$element.on('scroll-resize', refreshDimensions);
angular.element($window).on('resize', onResize); var unlistenToExposeAside = $rootScope.$on('$ionicExposeAside', ionic.animationFrameThrottle(function() { scrollCtrl.scrollView.resize(); onResize(); })); $timeout(refreshDimensions, 0, false);
function onResize() { if (changeValidator.resizeRequiresRefresh(scrollView.__clientWidth, scrollView.__clientHeight)) { refreshDimensions(); } }
scope.$watchCollection(listGetter, function(newValue) { data = newValue || (newValue = []); if (!angular.isArray(newValue)) { throw new Error("collection-repeat expected an array for '" + listExpr + "', " + "but got a " + typeof value); } // Wait for this digest to end before refreshing everything.
scope.$$postDigest(function() { getRepeatManager().setData(data); if (changeValidator.dataChangeRequiresRefresh(data)) refreshDimensions(); }); });
scope.$on('$destroy', function() { angular.element($window).off('resize', onResize); unlistenToExposeAside(); scrollCtrl.$element && scrollCtrl.$element.off('scroll-resize', refreshDimensions);
computedStyleNode && computedStyleNode.parentNode && computedStyleNode.parentNode.removeChild(computedStyleNode); computedStyleScope && computedStyleScope.$destroy(); computedStyleScope = computedStyleNode = null;
repeatManager && repeatManager.destroy(); repeatManager = null; });
function makeChangeValidator() { var self; return (self = { dataLength: 0, width: 0, height: 0, // A resize triggers a refresh only if we have data, the scrollView has size,
// and the size has changed.
resizeRequiresRefresh: function(newWidth, newHeight) { var requiresRefresh = self.dataLength && newWidth && newHeight && (newWidth !== self.width || newHeight !== self.height);
self.width = newWidth; self.height = newHeight;
return !!requiresRefresh; }, // A change in data only triggers a refresh if the data has length, or if the data's
// length is less than before.
dataChangeRequiresRefresh: function(newData) { var requiresRefresh = newData.length > 0 || newData.length < self.dataLength;
self.dataLength = newData.length;
return !!requiresRefresh; } }); }
function getRepeatManager() { return repeatManager || (repeatManager = new $ionicCollectionManager({ afterItemsNode: afterItemsContainer[0], containerNode: containerNode, heightData: heightData, widthData: widthData, forceRefreshImages: !!(isDefined(attr.forceRefreshImages) && attr.forceRefreshImages !== 'false'), keyExpression: keyExpr, renderBuffer: renderBuffer, scope: scope, scrollView: scrollCtrl.scrollView, transclude: transclude })); }
function initAfterItemsContainer() { var container = angular.element( scrollView.__content.querySelector('.collection-repeat-after-container') ); // Put everything in the view after the repeater into a container.
if (!container.length) { var elementIsAfterRepeater = false; var afterNodes = [].filter.call(scrollView.__content.childNodes, function(node) { if (ionic.DomUtil.contains(node, containerNode)) { elementIsAfterRepeater = true; return false; } return elementIsAfterRepeater; }); container = angular.element('<span class="collection-repeat-after-container">'); if (scrollView.options.scrollingX) { container.addClass('horizontal'); } container.append(afterNodes); scrollView.__content.appendChild(container[0]); } return container; }
function initDimensions() { //Height and width have four 'modes':
//1) Computed Mode
// - Nothing is supplied, so we getComputedStyle() on one element in the list and use
// that width and height value for the width and height of every item. This is re-computed
// every resize.
//2) Constant Mode, Static Integer
// - The user provides a constant number for width or height, in pixels. We parse it,
// store it on the `value` field, and it never changes
//3) Constant Mode, Percent
// - The user provides a percent string for width or height. The getter for percent is
// stored on the `getValue()` field, and is re-evaluated once every resize. The result
// is stored on the `value` field.
//4) Dynamic Mode
// - The user provides a dynamic expression for the width or height. This is re-evaluated
// for every item, stored on the `.getValue()` field.
if (heightExpr) { parseDimensionAttr(heightExpr, heightData); } else { heightData.computed = true; } if (widthExpr) { parseDimensionAttr(widthExpr, widthData); } else { widthData.computed = true; } }
function refreshDimensions() { var hasData = data.length > 0;
if (hasData && (heightData.computed || widthData.computed)) { computeStyleDimensions(); }
if (hasData && heightData.computed) { heightData.value = computedStyleDimensions.height; if (!heightData.value) { throw new Error('collection-repeat tried to compute the height of repeated elements "' + repeatExpr + '", but was unable to. Please provide the "item-height" attribute. ' + 'http://ionicframework.com/docs/api/directive/collectionRepeat/'); } } else if (!heightData.dynamic && heightData.getValue) { // If it's a constant with a getter (eg percent), we just refresh .value after resize
heightData.value = heightData.getValue(); }
if (hasData && widthData.computed) { widthData.value = computedStyleDimensions.width; if (!widthData.value) { throw new Error('collection-repeat tried to compute the width of repeated elements "' + repeatExpr + '", but was unable to. Please provide the "item-width" attribute. ' + 'http://ionicframework.com/docs/api/directive/collectionRepeat/'); } } else if (!widthData.dynamic && widthData.getValue) { // If it's a constant with a getter (eg percent), we just refresh .value after resize
widthData.value = widthData.getValue(); } // Dynamic dimensions aren't updated on resize. Since they're already dynamic anyway,
// .getValue() will be used.
getRepeatManager().refreshLayout(); }
function parseDimensionAttr(attrValue, dimensionData) { if (!attrValue) return;
var parsedValue; // Try to just parse the plain attr value
try { parsedValue = $parse(attrValue); } catch (e) { // If the parse fails and the value has `px` or `%` in it, surround the attr in
// quotes, to attempt to let the user provide a simple `attr="100%"` or `attr="100px"`
if (attrValue.trim().match(/\d+(px|%)$/)) { attrValue = '"' + attrValue + '"'; } parsedValue = $parse(attrValue); }
var constantAttrValue = attrValue.replace(/(\'|\"|px|%)/g, '').trim(); var isConstant = constantAttrValue.length && !/([a-zA-Z]|\$|:|\?)/.test(constantAttrValue); dimensionData.attrValue = attrValue;
// If it's a constant, it's either a percent or just a constant pixel number.
if (isConstant) { // For percents, store the percent getter on .getValue()
if (attrValue.indexOf('%') > -1) { var decimalValue = parseFloat(parsedValue()) / 100; dimensionData.getValue = dimensionData === heightData ? function() { return Math.floor(decimalValue * scrollView.__clientHeight); } : function() { return Math.floor(decimalValue * scrollView.__clientWidth); }; } else { // For static constants, just store the static constant.
dimensionData.value = parseInt(parsedValue()); }
} else { dimensionData.dynamic = true; dimensionData.getValue = dimensionData === heightData ? function heightGetter(scope, locals) { var result = parsedValue(scope, locals); if (result.charAt && result.charAt(result.length - 1) === '%') { return Math.floor(parseFloat(result) / 100 * scrollView.__clientHeight); } return parseInt(result); } : function widthGetter(scope, locals) { var result = parsedValue(scope, locals); if (result.charAt && result.charAt(result.length - 1) === '%') { return Math.floor(parseFloat(result) / 100 * scrollView.__clientWidth); } return parseInt(result); }; } }
var computedStyleNode; var computedStyleScope; function computeStyleDimensions() { if (!computedStyleNode) { transclude(computedStyleScope = scope.$new(), function(clone) { clone[0].removeAttribute('collection-repeat'); // remove absolute position styling
computedStyleNode = clone[0]; }); }
computedStyleScope[keyExpr] = (listGetter(scope) || [])[0]; if (!$rootScope.$$phase) computedStyleScope.$digest(); containerNode.appendChild(computedStyleNode);
var style = $window.getComputedStyle(computedStyleNode); computedStyleDimensions.width = parseInt(style.width); computedStyleDimensions.height = parseInt(style.height);
containerNode.removeChild(computedStyleNode); }
}
}
RepeatManagerFactory.$inject = ['$rootScope', '$window', '$$rAF']; function RepeatManagerFactory($rootScope, $window, $$rAF) { var EMPTY_DIMENSION = { primaryPos: 0, secondaryPos: 0, primarySize: 0, secondarySize: 0, rowPrimarySize: 0 };
return function RepeatController(options) { var afterItemsNode = options.afterItemsNode; var containerNode = options.containerNode; var forceRefreshImages = options.forceRefreshImages; var heightData = options.heightData; var widthData = options.widthData; var keyExpression = options.keyExpression; var renderBuffer = options.renderBuffer; var scope = options.scope; var scrollView = options.scrollView; var transclude = options.transclude;
var data = [];
var getterLocals = {}; var heightFn = heightData.getValue || function() { return heightData.value; }; var heightGetter = function(index, value) { getterLocals[keyExpression] = value; getterLocals.$index = index; return heightFn(scope, getterLocals); };
var widthFn = widthData.getValue || function() { return widthData.value; }; var widthGetter = function(index, value) { getterLocals[keyExpression] = value; getterLocals.$index = index; return widthFn(scope, getterLocals); };
var isVertical = !!scrollView.options.scrollingY;
// We say it's a grid view if we're either dynamic or not 100% width
var isGridView = isVertical ? (widthData.dynamic || widthData.value !== scrollView.__clientWidth) : (heightData.dynamic || heightData.value !== scrollView.__clientHeight);
var isStaticView = !heightData.dynamic && !widthData.dynamic;
var PRIMARY = 'PRIMARY'; var SECONDARY = 'SECONDARY'; var TRANSLATE_TEMPLATE_STR = isVertical ? 'translate3d(SECONDARYpx,PRIMARYpx,0)' : 'translate3d(PRIMARYpx,SECONDARYpx,0)'; var WIDTH_HEIGHT_TEMPLATE_STR = isVertical ? 'height: PRIMARYpx; width: SECONDARYpx;' : 'height: SECONDARYpx; width: PRIMARYpx;';
var estimatedHeight; var estimatedWidth;
var repeaterBeforeSize = 0; var repeaterAfterSize = 0;
var renderStartIndex = -1; var renderEndIndex = -1; var renderAfterBoundary = -1; var renderBeforeBoundary = -1;
var itemsPool = []; var itemsLeaving = []; var itemsEntering = []; var itemsShownMap = {}; var nextItemId = 0;
var scrollViewSetDimensions = isVertical ? function() { scrollView.setDimensions(null, null, null, view.getContentSize(), true); } : function() { scrollView.setDimensions(null, null, view.getContentSize(), null, true); };
// view is a mix of list/grid methods + static/dynamic methods.
// See bottom for implementations. Available methods:
//
// getEstimatedPrimaryPos(i), getEstimatedSecondaryPos(i), getEstimatedIndex(scrollTop),
// calculateDimensions(toIndex), getDimensions(index),
// updateRenderRange(scrollTop, scrollValueEnd), onRefreshLayout(), onRefreshData()
var view = isVertical ? new VerticalViewType() : new HorizontalViewType(); (isGridView ? GridViewType : ListViewType).call(view); (isStaticView ? StaticViewType : DynamicViewType).call(view);
var contentSizeStr = isVertical ? 'getContentHeight' : 'getContentWidth'; var originalGetContentSize = scrollView.options[contentSizeStr]; scrollView.options[contentSizeStr] = angular.bind(view, view.getContentSize);
scrollView.__$callback = scrollView.__callback; scrollView.__callback = function(transformLeft, transformTop, zoom, wasResize) { var scrollValue = view.getScrollValue(); if (renderStartIndex === -1 || scrollValue + view.scrollPrimarySize > renderAfterBoundary || scrollValue < renderBeforeBoundary) { render(); } scrollView.__$callback(transformLeft, transformTop, zoom, wasResize); };
var isLayoutReady = false; var isDataReady = false; this.refreshLayout = function() { if (data.length) { estimatedHeight = heightGetter(0, data[0]); estimatedWidth = widthGetter(0, data[0]); } else { // If we don't have any data in our array, just guess.
estimatedHeight = 100; estimatedWidth = 100; }
// Get the size of every element AFTER the repeater. We have to get the margin before and
// after the first/last element to fix a browser bug with getComputedStyle() not counting
// the first/last child's margins into height.
var style = getComputedStyle(afterItemsNode) || {}; var firstStyle = afterItemsNode.firstElementChild && getComputedStyle(afterItemsNode.firstElementChild) || {}; var lastStyle = afterItemsNode.lastElementChild && getComputedStyle(afterItemsNode.lastElementChild) || {}; repeaterAfterSize = (parseInt(style[isVertical ? 'height' : 'width']) || 0) + (firstStyle && parseInt(firstStyle[isVertical ? 'marginTop' : 'marginLeft']) || 0) + (lastStyle && parseInt(lastStyle[isVertical ? 'marginBottom' : 'marginRight']) || 0);
// Get the offsetTop of the repeater.
repeaterBeforeSize = 0; var current = containerNode; do { repeaterBeforeSize += current[isVertical ? 'offsetTop' : 'offsetLeft']; } while ( ionic.DomUtil.contains(scrollView.__content, current = current.offsetParent) );
var containerPrevNode = containerNode.previousElementSibling; var beforeStyle = containerPrevNode ? $window.getComputedStyle(containerPrevNode) : {}; var beforeMargin = parseInt(beforeStyle[isVertical ? 'marginBottom' : 'marginRight'] || 0);
// Because we position the collection container with position: relative, it doesn't take
// into account where to position itself relative to the previous element's marginBottom.
// To compensate, we translate the container up by the previous element's margin.
containerNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR .replace(PRIMARY, -beforeMargin) .replace(SECONDARY, 0); repeaterBeforeSize -= beforeMargin;
if (!scrollView.__clientHeight || !scrollView.__clientWidth) { scrollView.__clientWidth = scrollView.__container.clientWidth; scrollView.__clientHeight = scrollView.__container.clientHeight; }
(view.onRefreshLayout || angular.noop)(); view.refreshDirection(); scrollViewSetDimensions();
// Create the pool of items for reuse, setting the size to (estimatedItemsOnScreen) * 2,
// plus the size of the renderBuffer.
if (!isLayoutReady) { var poolSize = Math.max(20, renderBuffer * 3); for (var i = 0; i < poolSize; i++) { itemsPool.push(new RepeatItem()); } }
isLayoutReady = true; if (isLayoutReady && isDataReady) { // If the resize or latest data change caused the scrollValue to
// now be out of bounds, resize the scrollView.
if (scrollView.__scrollLeft > scrollView.__maxScrollLeft || scrollView.__scrollTop > scrollView.__maxScrollTop) { scrollView.resize(); } forceRerender(true); } };
this.setData = function(newData) { data = newData; (view.onRefreshData || angular.noop)(); isDataReady = true; };
this.destroy = function() { render.destroyed = true;
itemsPool.forEach(function(item) { item.scope.$destroy(); item.scope = item.element = item.node = item.images = null; }); itemsPool.length = itemsEntering.length = itemsLeaving.length = 0; itemsShownMap = {};
//Restore the scrollView's normal behavior and resize it to normal size.
scrollView.options[contentSizeStr] = originalGetContentSize; scrollView.__callback = scrollView.__$callback; scrollView.resize();
(view.onDestroy || angular.noop)(); };
function forceRerender() { return render(true); } function render(forceRerender) { if (render.destroyed) return; var i; var ii; var item; var dim; var scope; var scrollValue = view.getScrollValue(); var scrollValueEnd = scrollValue + view.scrollPrimarySize;
view.updateRenderRange(scrollValue, scrollValueEnd);
renderStartIndex = Math.max(0, renderStartIndex - renderBuffer); renderEndIndex = Math.min(data.length - 1, renderEndIndex + renderBuffer);
for (i in itemsShownMap) { if (i < renderStartIndex || i > renderEndIndex) { item = itemsShownMap[i]; delete itemsShownMap[i]; itemsLeaving.push(item); item.isShown = false; } }
// Render indicies that aren't shown yet
//
// NOTE(ajoslin): this may sound crazy, but calling any other functions during this render
// loop will often push the render time over the edge from less than one frame to over
// one frame, causing visible jank.
// DON'T call any other functions inside this loop unless it's vital.
for (i = renderStartIndex; i <= renderEndIndex; i++) { // We only go forward with render if the index is in data, the item isn't already shown,
// or forceRerender is on.
if (i >= data.length || (itemsShownMap[i] && !forceRerender)) continue;
item = itemsShownMap[i] || (itemsShownMap[i] = itemsLeaving.length ? itemsLeaving.pop() : itemsPool.length ? itemsPool.shift() : new RepeatItem()); itemsEntering.push(item); item.isShown = true;
scope = item.scope; scope.$index = i; scope[keyExpression] = data[i]; scope.$first = (i === 0); scope.$last = (i === (data.length - 1)); scope.$middle = !(scope.$first || scope.$last); scope.$odd = !(scope.$even = (i & 1) === 0);
if (scope.$$disconnected) ionic.Utils.reconnectScope(item.scope);
dim = view.getDimensions(i); if (item.secondaryPos !== dim.secondaryPos || item.primaryPos !== dim.primaryPos) { item.node.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR .replace(PRIMARY, (item.primaryPos = dim.primaryPos)) .replace(SECONDARY, (item.secondaryPos = dim.secondaryPos)); } if (item.secondarySize !== dim.secondarySize || item.primarySize !== dim.primarySize) { item.node.style.cssText = item.node.style.cssText .replace(WIDTH_HEIGHT_REGEX, WIDTH_HEIGHT_TEMPLATE_STR //TODO fix item.primarySize + 1 hack
.replace(PRIMARY, (item.primarySize = dim.primarySize) + 1) .replace(SECONDARY, (item.secondarySize = dim.secondarySize)) ); }
}
// If we reach the end of the list, render the afterItemsNode - this contains all the
// elements the developer placed after the collection-repeat
if (renderEndIndex === data.length - 1) { dim = view.getDimensions(data.length - 1) || EMPTY_DIMENSION; afterItemsNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR .replace(PRIMARY, dim.primaryPos + dim.primarySize) .replace(SECONDARY, 0); }
while (itemsLeaving.length) { item = itemsLeaving.pop(); item.scope.$broadcast('$collectionRepeatLeave'); ionic.Utils.disconnectScope(item.scope); itemsPool.push(item); item.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)'; item.primaryPos = item.secondaryPos = null; }
if (forceRefreshImages) { for (i = 0, ii = itemsEntering.length; i < ii && (item = itemsEntering[i]); i++) { if (!item.images) continue; for (var j = 0, jj = item.images.length, img; j < jj && (img = item.images[j]); j++) { var src = img.src; img.src = ONE_PX_TRANSPARENT_IMG_SRC; img.src = src; } } } if (forceRerender) { var rootScopePhase = $rootScope.$$phase; while (itemsEntering.length) { item = itemsEntering.pop(); if (!rootScopePhase) item.scope.$digest(); } } else { digestEnteringItems(); } }
function digestEnteringItems() { var item; if (digestEnteringItems.running) return; digestEnteringItems.running = true;
$$rAF(function process() { var rootScopePhase = $rootScope.$$phase; while (itemsEntering.length) { item = itemsEntering.pop(); if (item.isShown) { if (!rootScopePhase) item.scope.$digest(); } } digestEnteringItems.running = false; }); }
function RepeatItem() { var self = this; this.scope = scope.$new(); this.id = 'item' + (nextItemId++); transclude(this.scope, function(clone) { self.element = clone; self.element.data('$$collectionRepeatItem', self); // TODO destroy
self.node = clone[0]; // Batch style setting to lower repaints
self.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)'; self.node.style.cssText += ' height: 0px; width: 0px;'; ionic.Utils.disconnectScope(self.scope); containerNode.appendChild(self.node); self.images = clone[0].getElementsByTagName('img'); }); }
function VerticalViewType() { this.getItemPrimarySize = heightGetter; this.getItemSecondarySize = widthGetter;
this.getScrollValue = function() { return Math.max(0, Math.min(scrollView.__scrollTop - repeaterBeforeSize, scrollView.__maxScrollTop - repeaterBeforeSize - repeaterAfterSize)); };
this.refreshDirection = function() { this.scrollPrimarySize = scrollView.__clientHeight; this.scrollSecondarySize = scrollView.__clientWidth;
this.estimatedPrimarySize = estimatedHeight; this.estimatedSecondarySize = estimatedWidth; this.estimatedItemsAcross = isGridView && Math.floor(scrollView.__clientWidth / estimatedWidth) || 1; }; } function HorizontalViewType() { this.getItemPrimarySize = widthGetter; this.getItemSecondarySize = heightGetter;
this.getScrollValue = function() { return Math.max(0, Math.min(scrollView.__scrollLeft - repeaterBeforeSize, scrollView.__maxScrollLeft - repeaterBeforeSize - repeaterAfterSize)); };
this.refreshDirection = function() { this.scrollPrimarySize = scrollView.__clientWidth; this.scrollSecondarySize = scrollView.__clientHeight;
this.estimatedPrimarySize = estimatedWidth; this.estimatedSecondarySize = estimatedHeight; this.estimatedItemsAcross = isGridView && Math.floor(scrollView.__clientHeight / estimatedHeight) || 1; }; }
function GridViewType() { this.getEstimatedSecondaryPos = function(index) { return (index % this.estimatedItemsAcross) * this.estimatedSecondarySize; }; this.getEstimatedPrimaryPos = function(index) { return Math.floor(index / this.estimatedItemsAcross) * this.estimatedPrimarySize; }; this.getEstimatedIndex = function(scrollValue) { return Math.floor(scrollValue / this.estimatedPrimarySize) * this.estimatedItemsAcross; }; }
function ListViewType() { this.getEstimatedSecondaryPos = function() { return 0; }; this.getEstimatedPrimaryPos = function(index) { return index * this.estimatedPrimarySize; }; this.getEstimatedIndex = function(scrollValue) { return Math.floor((scrollValue) / this.estimatedPrimarySize); }; }
function StaticViewType() { this.getContentSize = function() { return this.getEstimatedPrimaryPos(data.length - 1) + this.estimatedPrimarySize + repeaterBeforeSize + repeaterAfterSize; }; // static view always returns the same object for getDimensions, to avoid memory allocation
// while scrolling. This could be dangerous if this was a public function, but it's not.
// Only we use it.
var dim = {}; this.getDimensions = function(index) { dim.primaryPos = this.getEstimatedPrimaryPos(index); dim.secondaryPos = this.getEstimatedSecondaryPos(index); dim.primarySize = this.estimatedPrimarySize; dim.secondarySize = this.estimatedSecondarySize; return dim; }; this.updateRenderRange = function(scrollValue, scrollValueEnd) { renderStartIndex = Math.max(0, this.getEstimatedIndex(scrollValue));
// Make sure the renderEndIndex takes into account all the items on the row
renderEndIndex = Math.min(data.length - 1, this.getEstimatedIndex(scrollValueEnd) + this.estimatedItemsAcross - 1);
renderBeforeBoundary = Math.max(0, this.getEstimatedPrimaryPos(renderStartIndex)); renderAfterBoundary = this.getEstimatedPrimaryPos(renderEndIndex) + this.estimatedPrimarySize; }; }
function DynamicViewType() { var self = this; var debouncedScrollViewSetDimensions = ionic.debounce(scrollViewSetDimensions, 25, true); var calculateDimensions = isGridView ? calculateDimensionsGrid : calculateDimensionsList; var dimensionsIndex; var dimensions = [];
// Get the dimensions at index. {width, height, left, top}.
// We start with no dimensions calculated, then any time dimensions are asked for at an
// index we calculate dimensions up to there.
function calculateDimensionsList(toIndex) { var i, prevDimension, dim; for (i = Math.max(0, dimensionsIndex); i <= toIndex && (dim = dimensions[i]); i++) { prevDimension = dimensions[i - 1] || EMPTY_DIMENSION; dim.primarySize = self.getItemPrimarySize(i, data[i]); dim.secondarySize = self.scrollSecondarySize; dim.primaryPos = prevDimension.primaryPos + prevDimension.primarySize; dim.secondaryPos = 0; } } function calculateDimensionsGrid(toIndex) { var i, prevDimension, dim; for (i = Math.max(dimensionsIndex, 0); i <= toIndex && (dim = dimensions[i]); i++) { prevDimension = dimensions[i - 1] || EMPTY_DIMENSION; dim.secondarySize = Math.min( self.getItemSecondarySize(i, data[i]), self.scrollSecondarySize ); dim.secondaryPos = prevDimension.secondaryPos + prevDimension.secondarySize;
if (i === 0 || dim.secondaryPos + dim.secondarySize > self.scrollSecondarySize) { dim.secondaryPos = 0; dim.primarySize = self.getItemPrimarySize(i, data[i]); dim.primaryPos = prevDimension.primaryPos + prevDimension.rowPrimarySize;
dim.rowStartIndex = i; dim.rowPrimarySize = dim.primarySize; } else { dim.primarySize = self.getItemPrimarySize(i, data[i]); dim.primaryPos = prevDimension.primaryPos; dim.rowStartIndex = prevDimension.rowStartIndex;
dimensions[dim.rowStartIndex].rowPrimarySize = dim.rowPrimarySize = Math.max( dimensions[dim.rowStartIndex].rowPrimarySize, dim.primarySize ); dim.rowPrimarySize = Math.max(dim.primarySize, dim.rowPrimarySize); } } }
this.getContentSize = function() { var dim = dimensions[dimensionsIndex] || EMPTY_DIMENSION; return ((dim.primaryPos + dim.primarySize) || 0) + this.getEstimatedPrimaryPos(data.length - dimensionsIndex - 1) + repeaterBeforeSize + repeaterAfterSize; }; this.onDestroy = function() { dimensions.length = 0; };
this.onRefreshData = function() { var i; var ii; // Make sure dimensions has as many items as data.length.
// This is to be sure we don't have to allocate objects while scrolling.
for (i = dimensions.length, ii = data.length; i < ii; i++) { dimensions.push({}); } dimensionsIndex = -1; }; this.onRefreshLayout = function() { dimensionsIndex = -1; }; this.getDimensions = function(index) { index = Math.min(index, data.length - 1);
if (dimensionsIndex < index) { // Once we start asking for dimensions near the end of the list, go ahead and calculate
// everything. This is to make sure when the user gets to the end of the list, the
// scroll height of the list is 100% accurate (not estimated anymore).
if (index > data.length * 0.9) { calculateDimensions(data.length - 1); dimensionsIndex = data.length - 1; scrollViewSetDimensions(); } else { calculateDimensions(index); dimensionsIndex = index; debouncedScrollViewSetDimensions(); }
} return dimensions[index]; };
var oldRenderStartIndex = -1; var oldScrollValue = -1; this.updateRenderRange = function(scrollValue, scrollValueEnd) { var i; var len; var dim;
// Calculate more dimensions than we estimate we'll need, to be sure.
this.getDimensions( this.getEstimatedIndex(scrollValueEnd) * 2 );
// -- Calculate renderStartIndex
// base case: start at 0
if (oldRenderStartIndex === -1 || scrollValue === 0) { i = 0; // scrolling down
} else if (scrollValue >= oldScrollValue) { for (i = oldRenderStartIndex, len = data.length; i < len; i++) { if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize >= scrollValue) { break; } } // scrolling up
} else { for (i = oldRenderStartIndex; i >= 0; i--) { if ((dim = this.getDimensions(i)) && dim.primaryPos <= scrollValue) { // when grid view, make sure the render starts at the beginning of a row.
i = isGridView ? dim.rowStartIndex : i; break; } } }
renderStartIndex = Math.min(Math.max(0, i), data.length - 1); renderBeforeBoundary = renderStartIndex !== -1 ? this.getDimensions(renderStartIndex).primaryPos : -1;
// -- Calculate renderEndIndex
var lastRowDim; for (i = renderStartIndex + 1, len = data.length; i < len; i++) { if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize > scrollValueEnd) {
// Go all the way to the end of the row if we're in a grid
if (isGridView) { lastRowDim = dim; while (i < len - 1 && (dim = this.getDimensions(i + 1)).primaryPos === lastRowDim.primaryPos) { i++; } } break; } }
renderEndIndex = Math.min(i, data.length - 1); renderAfterBoundary = renderEndIndex !== -1 ? ((dim = this.getDimensions(renderEndIndex)).primaryPos + (dim.rowPrimarySize || dim.primarySize)) : -1;
oldScrollValue = scrollValue; oldRenderStartIndex = renderStartIndex; }; }
};
}
/** * @ngdoc directive * @name ionContent * @module ionic * @delegate ionic.service:$ionicScrollDelegate * @restrict E * * @description * The ionContent directive provides an easy to use content area that can be configured * to use Ionic's custom Scroll View, or the built in overflow scrolling of the browser. * * While we recommend using the custom Scroll features in Ionic in most cases, sometimes * (for performance reasons) only the browser's native overflow scrolling will suffice, * and so we've made it easy to toggle between the Ionic scroll implementation and * overflow scrolling. * * You can implement pull-to-refresh with the {@link ionic.directive:ionRefresher} * directive, and infinite scrolling with the {@link ionic.directive:ionInfiniteScroll} * directive. * * If there is any dynamic content inside the ion-content, be sure to call `.resize()` with {@link ionic.service:$ionicScrollDelegate} * after the content has been added. * * Be aware that this directive gets its own child scope. If you do not understand why this * is important, you can read [https://docs.angularjs.org/guide/scope](https://docs.angularjs.org/guide/scope).
* * @param {string=} delegate-handle The handle used to identify this scrollView * with {@link ionic.service:$ionicScrollDelegate}. * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'. * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true. * @param {boolean=} padding Whether to add padding to the content. * Defaults to true on iOS, false on Android. * @param {boolean=} scroll Whether to allow scrolling of content. Defaults to true. * @param {boolean=} overflow-scroll Whether to use overflow-scrolling instead of * Ionic scroll. See {@link ionic.provider:$ionicConfigProvider} to set this as the global default. * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true. * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true. * @param {string=} start-x Initial horizontal scroll position. Default 0. * @param {string=} start-y Initial vertical scroll position. Default 0. * @param {expression=} on-scroll Expression to evaluate when the content is scrolled. * @param {expression=} on-scroll-complete Expression to evaluate when a scroll action completes. Has access to 'scrollLeft' and 'scrollTop' locals. * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges * of the content. Defaults to true on iOS, false on Android. * @param {number=} scroll-event-interval Number of milliseconds between each firing of the 'on-scroll' expression. Default 10. */ IonicModule .directive('ionContent', [ '$timeout', '$controller', '$ionicBind', '$ionicConfig', function($timeout, $controller, $ionicBind, $ionicConfig) { return { restrict: 'E', require: '^?ionNavView', scope: true, priority: 800, compile: function(element, attr) { var innerElement; var scrollCtrl;
element.addClass('scroll-content ionic-scroll');
if (attr.scroll != 'false') { //We cannot use normal transclude here because it breaks element.data()
//inheritance on compile
innerElement = jqLite('<div class="scroll"></div>'); innerElement.append(element.contents()); element.append(innerElement); } else { element.addClass('scroll-content-false'); }
var nativeScrolling = attr.overflowScroll !== "false" && (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling());
// collection-repeat requires JS scrolling
if (nativeScrolling) { nativeScrolling = !element[0].querySelector('[collection-repeat]'); } return { pre: prelink }; function prelink($scope, $element, $attr) { var parentScope = $scope.$parent; $scope.$watch(function() { return (parentScope.$hasHeader ? ' has-header' : '') + (parentScope.$hasSubheader ? ' has-subheader' : '') + (parentScope.$hasFooter ? ' has-footer' : '') + (parentScope.$hasSubfooter ? ' has-subfooter' : '') + (parentScope.$hasTabs ? ' has-tabs' : '') + (parentScope.$hasTabsTop ? ' has-tabs-top' : ''); }, function(className, oldClassName) { $element.removeClass(oldClassName); $element.addClass(className); });
//Only this ionContent should use these variables from parent scopes
$scope.$hasHeader = $scope.$hasSubheader = $scope.$hasFooter = $scope.$hasSubfooter = $scope.$hasTabs = $scope.$hasTabsTop = false; $ionicBind($scope, $attr, { $onScroll: '&onScroll', $onScrollComplete: '&onScrollComplete', hasBouncing: '@', padding: '@', direction: '@', scrollbarX: '@', scrollbarY: '@', startX: '@', startY: '@', scrollEventInterval: '@' }); $scope.direction = $scope.direction || 'y';
if (isDefined($attr.padding)) { $scope.$watch($attr.padding, function(newVal) { (innerElement || $element).toggleClass('padding', !!newVal); }); }
if ($attr.scroll === "false") { //do nothing
} else { var scrollViewOptions = {};
// determined in compile phase above
if (nativeScrolling) { // use native scrolling
$element.addClass('overflow-scroll');
scrollViewOptions = { el: $element[0], delegateHandle: attr.delegateHandle, startX: $scope.$eval($scope.startX) || 0, startY: $scope.$eval($scope.startY) || 0, nativeScrolling: true };
} else { // Use JS scrolling
scrollViewOptions = { el: $element[0], delegateHandle: attr.delegateHandle, locking: (attr.locking || 'true') === 'true', bouncing: $scope.$eval($scope.hasBouncing), startX: $scope.$eval($scope.startX) || 0, startY: $scope.$eval($scope.startY) || 0, scrollbarX: $scope.$eval($scope.scrollbarX) !== false, scrollbarY: $scope.$eval($scope.scrollbarY) !== false, scrollingX: $scope.direction.indexOf('x') >= 0, scrollingY: $scope.direction.indexOf('y') >= 0, scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 10, scrollingComplete: onScrollComplete }; }
// init scroll controller with appropriate options
scrollCtrl = $controller('$ionicScroll', { $scope: $scope, scrollViewOptions: scrollViewOptions });
$scope.scrollCtrl = scrollCtrl;
$scope.$on('$destroy', function() { if (scrollViewOptions) { scrollViewOptions.scrollingComplete = noop; delete scrollViewOptions.el; } innerElement = null; $element = null; attr.$$element = null; }); }
function onScrollComplete() { $scope.$onScrollComplete({ scrollTop: scrollCtrl.scrollView.__scrollTop, scrollLeft: scrollCtrl.scrollView.__scrollLeft }); }
} } }; }]);
/** * @ngdoc directive * @name exposeAsideWhen * @module ionic * @restrict A * @parent ionic.directive:ionSideMenus * * @description * It is common for a tablet application to hide a menu when in portrait mode, but to show the * same menu on the left side when the tablet is in landscape mode. The `exposeAsideWhen` attribute * directive can be used to accomplish a similar interface. * * By default, side menus are hidden underneath its side menu content, and can be opened by either * swiping the content left or right, or toggling a button to show the side menu. However, by adding the * `exposeAsideWhen` attribute directive to an {@link ionic.directive:ionSideMenu} element directive, * a side menu can be given instructions on "when" the menu should be exposed (always viewable). For * example, the `expose-aside-when="large"` attribute will keep the side menu hidden when the viewport's * width is less than `768px`, but when the viewport's width is `768px` or greater, the menu will then * always be shown and can no longer be opened or closed like it could when it was hidden for smaller * viewports. * * Using `large` as the attribute's value is a shortcut value to `(min-width:768px)` since it is * the most common use-case. However, for added flexibility, any valid media query could be added * as the value, such as `(min-width:600px)` or even multiple queries such as * `(min-width:750px) and (max-width:1200px)`. * @usage * ```html
* <ion-side-menus> * <!-- Center content --> * <ion-side-menu-content> * </ion-side-menu-content> * * <!-- Left menu --> * <ion-side-menu expose-aside-when="large"> * </ion-side-menu> * </ion-side-menus> * ```
* For a complete side menu example, see the * {@link ionic.directive:ionSideMenus} documentation. */
IonicModule.directive('exposeAsideWhen', ['$window', function($window) { return { restrict: 'A', require: '^ionSideMenus', link: function($scope, $element, $attr, sideMenuCtrl) {
var prevInnerWidth = $window.innerWidth; var prevInnerHeight = $window.innerHeight;
ionic.on('resize', function() { if (prevInnerWidth === $window.innerWidth && prevInnerHeight === $window.innerHeight) { return; } prevInnerWidth = $window.innerWidth; prevInnerHeight = $window.innerHeight; onResize(); }, $window);
function checkAsideExpose() { var mq = $attr.exposeAsideWhen == 'large' ? '(min-width:768px)' : $attr.exposeAsideWhen; sideMenuCtrl.exposeAside($window.matchMedia(mq).matches); sideMenuCtrl.activeAsideResizing(false); }
function onResize() { sideMenuCtrl.activeAsideResizing(true); debouncedCheck(); }
var debouncedCheck = ionic.debounce(function() { $scope.$apply(checkAsideExpose); }, 300, false);
$scope.$evalAsync(checkAsideExpose); } }; }]);
var GESTURE_DIRECTIVES = 'onHold onTap onDoubleTap onTouch onRelease onDragStart onDrag onDragEnd onDragUp onDragRight onDragDown onDragLeft onSwipe onSwipeUp onSwipeRight onSwipeDown onSwipeLeft'.split(' ');
GESTURE_DIRECTIVES.forEach(function(name) { IonicModule.directive(name, gestureDirective(name)); });
/** * @ngdoc directive * @name onHold * @module ionic * @restrict A * * @description * Touch stays at the same location for 500ms. Similar to long touch events available for AngularJS and jQuery. * * @usage * ```html
* <button on-hold="onHold()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onTap * @module ionic * @restrict A * * @description * Quick touch at a location. If the duration of the touch goes * longer than 250ms it is no longer a tap gesture. * * @usage * ```html
* <button on-tap="onTap()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onDoubleTap * @module ionic * @restrict A * * @description * Double tap touch at a location. * * @usage * ```html
* <button on-double-tap="onDoubleTap()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onTouch * @module ionic * @restrict A * * @description * Called immediately when the user first begins a touch. This * gesture does not wait for a touchend/mouseup. * * @usage * ```html
* <button on-touch="onTouch()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onRelease * @module ionic * @restrict A * * @description * Called when the user ends a touch. * * @usage * ```html
* <button on-release="onRelease()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onDragStart * @module ionic * @restrict A * * @description * Called when a drag gesture has started. * * @usage * ```html
* <button on-drag-start="onDragStart()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onDrag * @module ionic * @restrict A * * @description * Move with one touch around on the page. Blocking the scrolling when * moving left and right is a good practice. When all the drag events are * blocking you disable scrolling on that area. * * @usage * ```html
* <button on-drag="onDrag()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onDragEnd * @module ionic * @restrict A * * @description * Called when a drag gesture has ended. * * @usage * ```html
* <button on-drag-end="onDragEnd()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onDragUp * @module ionic * @restrict A * * @description * Called when the element is dragged up. * * @usage * ```html
* <button on-drag-up="onDragUp()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onDragRight * @module ionic * @restrict A * * @description * Called when the element is dragged to the right. * * @usage * ```html
* <button on-drag-right="onDragRight()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onDragDown * @module ionic * @restrict A * * @description * Called when the element is dragged down. * * @usage * ```html
* <button on-drag-down="onDragDown()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onDragLeft * @module ionic * @restrict A * * @description * Called when the element is dragged to the left. * * @usage * ```html
* <button on-drag-left="onDragLeft()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onSwipe * @module ionic * @restrict A * * @description * Called when a moving touch has a high velocity in any direction. * * @usage * ```html
* <button on-swipe="onSwipe()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onSwipeUp * @module ionic * @restrict A * * @description * Called when a moving touch has a high velocity moving up. * * @usage * ```html
* <button on-swipe-up="onSwipeUp()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onSwipeRight * @module ionic * @restrict A * * @description * Called when a moving touch has a high velocity moving to the right. * * @usage * ```html
* <button on-swipe-right="onSwipeRight()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onSwipeDown * @module ionic * @restrict A * * @description * Called when a moving touch has a high velocity moving down. * * @usage * ```html
* <button on-swipe-down="onSwipeDown()" class="button">Test</button> * ```
*/
/** * @ngdoc directive * @name onSwipeLeft * @module ionic * @restrict A * * @description * Called when a moving touch has a high velocity moving to the left. * * @usage * ```html
* <button on-swipe-left="onSwipeLeft()" class="button">Test</button> * ```
*/
function gestureDirective(directiveName) { return ['$ionicGesture', '$parse', function($ionicGesture, $parse) { var eventType = directiveName.substr(2).toLowerCase();
return function(scope, element, attr) { var fn = $parse( attr[directiveName] );
var listener = function(ev) { scope.$apply(function() { fn(scope, { $event: ev }); }); };
var gesture = $ionicGesture.on(eventType, listener, element);
scope.$on('$destroy', function() { $ionicGesture.off(gesture, eventType, listener); }); }; }]; }
IonicModule //.directive('ionHeaderBar', tapScrollToTopDirective())
/** * @ngdoc directive * @name ionHeaderBar * @module ionic * @restrict E * * @description * Adds a fixed header bar above some content. * * Can also be a subheader (lower down) if the 'bar-subheader' class is applied. * See [the header CSS docs](/docs/components/#subheader). * * @param {string=} align-title How to align the title. By default the title * will be aligned the same as how the platform aligns its titles (iOS centers * titles, Android aligns them left). * Available: 'left', 'right', or 'center'. Defaults to the same as the platform. * @param {boolean=} no-tap-scroll By default, the header bar will scroll the * content to the top when tapped. Set no-tap-scroll to true to disable this * behavior. * Available: true or false. Defaults to false. * * @usage * ```html
* <ion-header-bar align-title="left" class="bar-positive"> * <div class="buttons"> * <button class="button" ng-click="doSomething()">Left Button</button> * </div> * <h1 class="title">Title!</h1> * <div class="buttons"> * <button class="button">Right Button</button> * </div> * </ion-header-bar> * <ion-content class="has-header"> * Some content! * </ion-content> * ```
*/ .directive('ionHeaderBar', headerFooterBarDirective(true))
/** * @ngdoc directive * @name ionFooterBar * @module ionic * @restrict E * * @description * Adds a fixed footer bar below some content. * * Can also be a subfooter (higher up) if the 'bar-subfooter' class is applied. * See [the footer CSS docs](/docs/components/#footer). * * Note: If you use ionFooterBar in combination with ng-if, the surrounding content * will not align correctly. This will be fixed soon. * * @param {string=} align-title Where to align the title. * Available: 'left', 'right', or 'center'. Defaults to 'center'. * * @usage * ```html
* <ion-content class="has-footer"> * Some content! * </ion-content> * <ion-footer-bar align-title="left" class="bar-assertive"> * <div class="buttons"> * <button class="button">Left Button</button> * </div> * <h1 class="title">Title!</h1> * <div class="buttons" ng-click="doSomething()"> * <button class="button">Right Button</button> * </div> * </ion-footer-bar> * ```
*/ .directive('ionFooterBar', headerFooterBarDirective(false));
function tapScrollToTopDirective() { //eslint-disable-line no-unused-vars
return ['$ionicScrollDelegate', function($ionicScrollDelegate) { return { restrict: 'E', link: function($scope, $element, $attr) { if ($attr.noTapScroll == 'true') { return; } ionic.on('tap', onTap, $element[0]); $scope.$on('$destroy', function() { ionic.off('tap', onTap, $element[0]); });
function onTap(e) { var depth = 3; var current = e.target; //Don't scroll to top in certain cases
while (depth-- && current) { if (current.classList.contains('button') || current.tagName.match(/input|textarea|select/i) || current.isContentEditable) { return; } current = current.parentNode; } var touch = e.gesture && e.gesture.touches[0] || e.detail.touches[0]; var bounds = $element[0].getBoundingClientRect(); if (ionic.DomUtil.rectContains( touch.pageX, touch.pageY, bounds.left, bounds.top - 20, bounds.left + bounds.width, bounds.top + bounds.height )) { $ionicScrollDelegate.scrollTop(true); } } } }; }]; }
function headerFooterBarDirective(isHeader) { return ['$document', '$timeout', function($document, $timeout) { return { restrict: 'E', controller: '$ionicHeaderBar', compile: function(tElement) { tElement.addClass(isHeader ? 'bar bar-header' : 'bar bar-footer'); // top style tabs? if so, remove bottom border for seamless display
$timeout(function() { if (isHeader && $document[0].getElementsByClassName('tabs-top').length) tElement.addClass('has-tabs-top'); });
return { pre: prelink }; function prelink($scope, $element, $attr, ctrl) { if (isHeader) { $scope.$watch(function() { return $element[0].className; }, function(value) { var isShown = value.indexOf('ng-hide') === -1; var isSubheader = value.indexOf('bar-subheader') !== -1; $scope.$hasHeader = isShown && !isSubheader; $scope.$hasSubheader = isShown && isSubheader; $scope.$emit('$ionicSubheader', $scope.$hasSubheader); }); $scope.$on('$destroy', function() { delete $scope.$hasHeader; delete $scope.$hasSubheader; }); ctrl.align(); $scope.$on('$ionicHeader.align', function() { ionic.requestAnimationFrame(function() { ctrl.align(); }); });
} else { $scope.$watch(function() { return $element[0].className; }, function(value) { var isShown = value.indexOf('ng-hide') === -1; var isSubfooter = value.indexOf('bar-subfooter') !== -1; $scope.$hasFooter = isShown && !isSubfooter; $scope.$hasSubfooter = isShown && isSubfooter; }); $scope.$on('$destroy', function() { delete $scope.$hasFooter; delete $scope.$hasSubfooter; }); $scope.$watch('$hasTabs', function(val) { $element.toggleClass('has-tabs', !!val); }); ctrl.align(); $scope.$on('$ionicFooter.align', function() { ionic.requestAnimationFrame(function() { ctrl.align(); }); }); } } } }; }]; }
/** * @ngdoc directive * @name ionInfiniteScroll * @module ionic * @parent ionic.directive:ionContent, ionic.directive:ionScroll * @restrict E * * @description * The ionInfiniteScroll directive allows you to call a function whenever * the user gets to the bottom of the page or near the bottom of the page. * * The expression you pass in for `on-infinite` is called when the user scrolls * greater than `distance` away from the bottom of the content. Once `on-infinite` * is done loading new data, it should broadcast the `scroll.infiniteScrollComplete` * event from your controller (see below example). * * @param {expression} on-infinite What to call when the scroller reaches the * bottom. * @param {string=} distance The distance from the bottom that the scroll must * reach to trigger the on-infinite expression. Default: 1%. * @param {string=} spinner The {@link ionic.directive:ionSpinner} to show while loading. The SVG * {@link ionic.directive:ionSpinner} is now the default, replacing rotating font icons. * @param {string=} icon The icon to show while loading. Default: 'ion-load-d'. This is depreicated * in favor of the SVG {@link ionic.directive:ionSpinner}. * @param {boolean=} immediate-check Whether to check the infinite scroll bounds immediately on load. * * @usage * ```html
* <ion-content ng-controller="MyController"> * <ion-list> * .... * .... * </ion-list> * * <ion-infinite-scroll * on-infinite="loadMore()" * distance="1%"> * </ion-infinite-scroll> * </ion-content> * ```
* ```js
* function MyController($scope, $http) { * $scope.items = []; * $scope.loadMore = function() { * $http.get('/more-items').success(function(items) { * useItems(items); * $scope.$broadcast('scroll.infiniteScrollComplete'); * }); * }; * * $scope.$on('$stateChangeSuccess', function() { * $scope.loadMore(); * }); * } * ```
* * An easy to way to stop infinite scroll once there is no more data to load * is to use angular's `ng-if` directive: * * ```html
* <ion-infinite-scroll * ng-if="moreDataCanBeLoaded()" * icon="ion-loading-c" * on-infinite="loadMoreData()"> * </ion-infinite-scroll> * ```
*/ IonicModule .directive('ionInfiniteScroll', ['$timeout', function($timeout) { return { restrict: 'E', require: ['?^$ionicScroll', 'ionInfiniteScroll'], template: function($element, $attrs) { if ($attrs.icon) return '<i class="icon {{icon()}} icon-refreshing {{scrollingType}}"></i>'; return '<ion-spinner icon="{{spinner()}}"></ion-spinner>'; }, scope: true, controller: '$ionInfiniteScroll', link: function($scope, $element, $attrs, ctrls) { var infiniteScrollCtrl = ctrls[1]; var scrollCtrl = infiniteScrollCtrl.scrollCtrl = ctrls[0]; var jsScrolling = infiniteScrollCtrl.jsScrolling = !scrollCtrl.isNative();
// if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling
if (jsScrolling) { infiniteScrollCtrl.scrollView = scrollCtrl.scrollView; $scope.scrollingType = 'js-scrolling'; //bind to JS scroll events
scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds); } else { // grabbing the scrollable element, to determine dimensions, and current scroll pos
var scrollEl = ionic.DomUtil.getParentOrSelfWithClass($element[0].parentNode, 'overflow-scroll'); infiniteScrollCtrl.scrollEl = scrollEl; // if there's no scroll controller, and no overflow scroll div, infinite scroll wont work
if (!scrollEl) { throw 'Infinite scroll must be used inside a scrollable div'; } //bind to native scroll events
infiniteScrollCtrl.scrollEl.addEventListener('scroll', infiniteScrollCtrl.checkBounds); }
// Optionally check bounds on start after scrollView is fully rendered
var doImmediateCheck = isDefined($attrs.immediateCheck) ? $scope.$eval($attrs.immediateCheck) : true; if (doImmediateCheck) { $timeout(function() { infiniteScrollCtrl.checkBounds(); }); } } }; }]);
/** * @ngdoc directive * @name ionInput * @parent ionic.directive:ionList * @module ionic * @restrict E * Creates a text input group that can easily be focused * * @usage * * ```html
* <ion-list> * <ion-input> * <input type="text" placeholder="First Name"> * </ion-input> * * <ion-input> * <ion-label>Username</ion-label> * <input type="text"> * </ion-input> * </ion-list> * ```
*/
var labelIds = -1;
IonicModule .directive('ionInput', [function() { return { restrict: 'E', controller: ['$scope', '$element', function($scope, $element) { this.$scope = $scope; this.$element = $element;
this.setInputAriaLabeledBy = function(id) { var inputs = $element[0].querySelectorAll('input,textarea'); inputs.length && inputs[0].setAttribute('aria-labelledby', id); };
this.focus = function() { var inputs = $element[0].querySelectorAll('input,textarea'); inputs.length && inputs[0].focus(); }; }] }; }]);
/** * @ngdoc directive * @name ionLabel * @parent ionic.directive:ionList * @module ionic * @restrict E * * New in Ionic 1.2. It is strongly recommended that you use `<ion-label>` in place * of any `<label>` elements for maximum cross-browser support and performance. * * Creates a label for a form input. * * @usage * * ```html
* <ion-list> * <ion-input> * <ion-label>Username</ion-label> * <input type="text"> * </ion-input> * </ion-list> * ```
*/ IonicModule .directive('ionLabel', [function() { return { restrict: 'E', require: '?^ionInput', compile: function() {
return function link($scope, $element, $attrs, ionInputCtrl) { var element = $element[0];
$element.addClass('input-label');
$element.attr('aria-label', $element.text()); var id = element.id || '_label-' + ++labelIds;
if (!element.id) { $element.attr('id', id); }
if (ionInputCtrl) {
ionInputCtrl.setInputAriaLabeledBy(id);
$element.on('click', function() { ionInputCtrl.focus(); }); } }; } }; }]);
/** * Input label adds accessibility to <span class="input-label">. */ IonicModule .directive('inputLabel', [function() { return { restrict: 'C', require: '?^ionInput', compile: function() {
return function link($scope, $element, $attrs, ionInputCtrl) { var element = $element[0];
$element.attr('aria-label', $element.text()); var id = element.id || '_label-' + ++labelIds;
if (!element.id) { $element.attr('id', id); }
if (ionInputCtrl) { ionInputCtrl.setInputAriaLabeledBy(id); }
}; } }; }]);
/** * @ngdoc directive * @name ionItem * @parent ionic.directive:ionList * @module ionic * @restrict E * Creates a list-item that can easily be swiped, * deleted, reordered, edited, and more. * * See {@link ionic.directive:ionList} for a complete example & explanation. * * Can be assigned any item class name. See the * [list CSS documentation](/docs/components/#list). * * @usage * * ```html
* <ion-list> * <ion-item>Hello!</ion-item> * <ion-item href="#/detail"> * Link to detail page * </ion-item> * </ion-list> * ```
*/ IonicModule .directive('ionItem', ['$$rAF', function($$rAF) { return { restrict: 'E', controller: ['$scope', '$element', function($scope, $element) { this.$scope = $scope; this.$element = $element; }], scope: true, compile: function($element, $attrs) { var isAnchor = isDefined($attrs.href) || isDefined($attrs.ngHref) || isDefined($attrs.uiSref); var isComplexItem = isAnchor || //Lame way of testing, but we have to know at compile what to do with the element
/ion-(delete|option|reorder)-button/i.test($element.html());
if (isComplexItem) { var innerElement = jqLite(isAnchor ? '<a></a>' : '<div></div>'); innerElement.addClass('item-content');
if (isDefined($attrs.href) || isDefined($attrs.ngHref)) { innerElement.attr('ng-href', '{{$href()}}'); if (isDefined($attrs.target)) { innerElement.attr('target', '{{$target()}}'); } }
innerElement.append($element.contents());
$element.addClass('item item-complex') .append(innerElement); } else { $element.addClass('item'); }
return function link($scope, $element, $attrs) { $scope.$href = function() { return $attrs.href || $attrs.ngHref; }; $scope.$target = function() { return $attrs.target; };
var content = $element[0].querySelector('.item-content'); if (content) { $scope.$on('$collectionRepeatLeave', function() { if (content && content.$$ionicOptionsOpen) { content.style[ionic.CSS.TRANSFORM] = ''; content.style[ionic.CSS.TRANSITION] = 'none'; $$rAF(function() { content.style[ionic.CSS.TRANSITION] = ''; }); content.$$ionicOptionsOpen = false; } }); } };
} }; }]);
var ITEM_TPL_DELETE_BUTTON = '<div class="item-left-edit item-delete enable-pointer-events">' + '</div>'; /** * @ngdoc directive * @name ionDeleteButton * @parent ionic.directive:ionItem * @module ionic * @restrict E * Creates a delete button inside a list item, that is visible when the * {@link ionic.directive:ionList ionList parent's} `show-delete` evaluates to true or * `$ionicListDelegate.showDelete(true)` is called. * * Takes any ionicon as a class. * * See {@link ionic.directive:ionList} for a complete example & explanation. * * @usage * * ```html
* <ion-list show-delete="shouldShowDelete"> * <ion-item> * <ion-delete-button class="ion-minus-circled"></ion-delete-button> * Hello, list item! * </ion-item> * </ion-list> * <ion-toggle ng-model="shouldShowDelete"> * Show Delete? * </ion-toggle> * ```
*/ IonicModule .directive('ionDeleteButton', function() {
function stopPropagation(ev) { ev.stopPropagation(); }
return { restrict: 'E', require: ['^^ionItem', '^?ionList'], //Run before anything else, so we can move it before other directives process
//its location (eg ngIf relies on the location of the directive in the dom)
priority: Number.MAX_VALUE, compile: function($element, $attr) { //Add the classes we need during the compile phase, so that they stay
//even if something else like ngIf removes the element and re-addss it
$attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true); return function($scope, $element, $attr, ctrls) { var itemCtrl = ctrls[0]; var listCtrl = ctrls[1]; var container = jqLite(ITEM_TPL_DELETE_BUTTON); container.append($element); itemCtrl.$element.append(container).addClass('item-left-editable');
//Don't bubble click up to main .item
$element.on('click', stopPropagation);
init(); $scope.$on('$ionic.reconnectScope', init); function init() { listCtrl = listCtrl || $element.controller('ionList'); if (listCtrl && listCtrl.showDelete()) { container.addClass('visible active'); } } }; } }; });
IonicModule .directive('itemFloatingLabel', function() { return { restrict: 'C', link: function(scope, element) { var el = element[0]; var input = el.querySelector('input, textarea'); var inputLabel = el.querySelector('.input-label');
if (!input || !inputLabel) return;
var onInput = function() { if (input.value) { inputLabel.classList.add('has-input'); } else { inputLabel.classList.remove('has-input'); } };
input.addEventListener('input', onInput);
var ngModelCtrl = jqLite(input).controller('ngModel'); if (ngModelCtrl) { ngModelCtrl.$render = function() { input.value = ngModelCtrl.$viewValue || ''; onInput(); }; }
scope.$on('$destroy', function() { input.removeEventListener('input', onInput); }); } }; });
var ITEM_TPL_OPTION_BUTTONS = '<div class="item-options invisible">' + '</div>'; /** * @ngdoc directive * @name ionOptionButton * @parent ionic.directive:ionItem * @module ionic * @restrict E * @description * Creates an option button inside a list item, that is visible when the item is swiped * to the left by the user. Swiped open option buttons can be hidden with * {@link ionic.service:$ionicListDelegate#closeOptionButtons $ionicListDelegate.closeOptionButtons}. * * Can be assigned any button class. * * See {@link ionic.directive:ionList} for a complete example & explanation. * * @usage * * ```html
* <ion-list> * <ion-item> * I love kittens! * <ion-option-button class="button-positive">Share</ion-option-button> * <ion-option-button class="button-assertive">Edit</ion-option-button> * </ion-item> * </ion-list> * ```
*/ IonicModule.directive('ionOptionButton', [function() { function stopPropagation(e) { e.stopPropagation(); } return { restrict: 'E', require: '^ionItem', priority: Number.MAX_VALUE, compile: function($element, $attr) { $attr.$set('class', ($attr['class'] || '') + ' button', true); return function($scope, $element, $attr, itemCtrl) { if (!itemCtrl.optionsContainer) { itemCtrl.optionsContainer = jqLite(ITEM_TPL_OPTION_BUTTONS); itemCtrl.$element.append(itemCtrl.optionsContainer); } itemCtrl.optionsContainer.append($element);
itemCtrl.$element.addClass('item-right-editable');
//Don't bubble click up to main .item
$element.on('click', stopPropagation); }; } }; }]);
var ITEM_TPL_REORDER_BUTTON = '<div data-prevent-scroll="true" class="item-right-edit item-reorder enable-pointer-events">' + '</div>';
/** * @ngdoc directive * @name ionReorderButton * @parent ionic.directive:ionItem * @module ionic * @restrict E * Creates a reorder button inside a list item, that is visible when the * {@link ionic.directive:ionList ionList parent's} `show-reorder` evaluates to true or * `$ionicListDelegate.showReorder(true)` is called. * * Can be dragged to reorder items in the list. Takes any ionicon class. * * Note: Reordering works best when used with `ng-repeat`. Be sure that all `ion-item` children of an `ion-list` are part of the same `ng-repeat` expression. * * When an item reorder is complete, the expression given in the `on-reorder` attribute is called. The `on-reorder` expression is given two locals that can be used: `$fromIndex` and `$toIndex`. See below for an example. * * Look at {@link ionic.directive:ionList} for more examples. * * @usage * * ```html
* <ion-list ng-controller="MyCtrl" show-reorder="true"> * <ion-item ng-repeat="item in items"> * Item {{item}} * <ion-reorder-button class="ion-navicon" * on-reorder="moveItem(item, $fromIndex, $toIndex)"> * </ion-reorder-button> * </ion-item> * </ion-list> * ```
* ```js
* function MyCtrl($scope) { * $scope.items = [1, 2, 3, 4]; * $scope.moveItem = function(item, fromIndex, toIndex) { * //Move the item in the array
* $scope.items.splice(fromIndex, 1); * $scope.items.splice(toIndex, 0, item); * }; * } * ```
* * @param {expression=} on-reorder Expression to call when an item is reordered. * Parameters given: $fromIndex, $toIndex. */ IonicModule .directive('ionReorderButton', ['$parse', function($parse) { return { restrict: 'E', require: ['^ionItem', '^?ionList'], priority: Number.MAX_VALUE, compile: function($element, $attr) { $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true); $element[0].setAttribute('data-prevent-scroll', true); return function($scope, $element, $attr, ctrls) { var itemCtrl = ctrls[0]; var listCtrl = ctrls[1]; var onReorderFn = $parse($attr.onReorder);
$scope.$onReorder = function(oldIndex, newIndex) { onReorderFn($scope, { $fromIndex: oldIndex, $toIndex: newIndex }); };
// prevent clicks from bubbling up to the item
if (!$attr.ngClick && !$attr.onClick && !$attr.onclick) { $element[0].onclick = function(e) { e.stopPropagation(); return false; }; }
var container = jqLite(ITEM_TPL_REORDER_BUTTON); container.append($element); itemCtrl.$element.append(container).addClass('item-right-editable');
if (listCtrl && listCtrl.showReorder()) { container.addClass('visible active'); } }; } }; }]);
/** * @ngdoc directive * @name keyboardAttach * @module ionic * @restrict A * * @description * keyboard-attach is an attribute directive which will cause an element to float above * the keyboard when the keyboard shows. Currently only supports the * [ion-footer-bar]({{ page.versionHref }}/api/directive/ionFooterBar/) directive. * * ### Notes * - This directive requires the * [Ionic Keyboard Plugin](https://github.com/driftyco/ionic-plugins-keyboard).
* - On Android not in fullscreen mode, i.e. you have * `<preference name="Fullscreen" value="false" />` or no preference in your `config.xml` file, * this directive is unnecessary since it is the default behavior. * - On iOS, if there is an input in your footer, you will need to set * `cordova.plugins.Keyboard.disableScroll(true)`. * * @usage * * ```html
* <ion-footer-bar align-title="left" keyboard-attach class="bar-assertive"> * <h1 class="title">Title!</h1> * </ion-footer-bar> * ```
*/
IonicModule .directive('keyboardAttach', function() { return function(scope, element) { ionic.on('native.keyboardshow', onShow, window); ionic.on('native.keyboardhide', onHide, window);
//deprecated
ionic.on('native.showkeyboard', onShow, window); ionic.on('native.hidekeyboard', onHide, window);
var scrollCtrl;
function onShow(e) { if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) { return; }
//for testing
var keyboardHeight = e.keyboardHeight || (e.detail && e.detail.keyboardHeight); element.css('bottom', keyboardHeight + "px"); scrollCtrl = element.controller('$ionicScroll'); if (scrollCtrl) { scrollCtrl.scrollView.__container.style.bottom = keyboardHeight + keyboardAttachGetClientHeight(element[0]) + "px"; } }
function onHide() { if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) { return; }
element.css('bottom', ''); if (scrollCtrl) { scrollCtrl.scrollView.__container.style.bottom = ''; } }
scope.$on('$destroy', function() { ionic.off('native.keyboardshow', onShow, window); ionic.off('native.keyboardhide', onHide, window);
//deprecated
ionic.off('native.showkeyboard', onShow, window); ionic.off('native.hidekeyboard', onHide, window); }); }; });
function keyboardAttachGetClientHeight(element) { return element.clientHeight; }
/** * @ngdoc directive * @name ionList * @module ionic * @delegate ionic.service:$ionicListDelegate * @codepen JsHjf * @restrict E * @description * The List is a widely used interface element in almost any mobile app, and can include * content ranging from basic text all the way to buttons, toggles, icons, and thumbnails. * * Both the list, which contains items, and the list items themselves can be any HTML * element. The containing element requires the `list` class and each list item requires * the `item` class. * * However, using the ionList and ionItem directives make it easy to support various * interaction modes such as swipe to edit, drag to reorder, and removing items. * * Related: {@link ionic.directive:ionItem}, {@link ionic.directive:ionOptionButton} * {@link ionic.directive:ionReorderButton}, {@link ionic.directive:ionDeleteButton}, [`list CSS documentation`](/docs/components/#list). * * @usage * * Basic Usage: * * ```html
* <ion-list> * <ion-item ng-repeat="item in items"> * {% raw %}Hello, {{item}}!{% endraw %} * </ion-item> * </ion-list> * ```
* * Advanced Usage: Thumbnails, Delete buttons, Reordering, Swiping * * ```html
* <ion-list ng-controller="MyCtrl" * show-delete="shouldShowDelete" * show-reorder="shouldShowReorder" * can-swipe="listCanSwipe"> * <ion-item ng-repeat="item in items" * class="item-thumbnail-left"> * * {% raw %}<img ng-src="{{item.img}}"> * <h2>{{item.title}}</h2> * <p>{{item.description}}</p>{% endraw %} * <ion-option-button class="button-positive" * ng-click="share(item)"> * Share * </ion-option-button> * <ion-option-button class="button-info" * ng-click="edit(item)"> * Edit * </ion-option-button> * <ion-delete-button class="ion-minus-circled" * ng-click="items.splice($index, 1)"> * </ion-delete-button> * <ion-reorder-button class="ion-navicon" * on-reorder="reorderItem(item, $fromIndex, $toIndex)"> * </ion-reorder-button> * * </ion-item> * </ion-list> * ```
* *```javascript
* app.controller('MyCtrl', function($scope) { * $scope.shouldShowDelete = false; * $scope.shouldShowReorder = false; * $scope.listCanSwipe = true * }); *```
* * @param {string=} delegate-handle The handle used to identify this list with * {@link ionic.service:$ionicListDelegate}. * @param type {string=} The type of list to use (list-inset or card) * @param show-delete {boolean=} Whether the delete buttons for the items in the list are * currently shown or hidden. * @param show-reorder {boolean=} Whether the reorder buttons for the items in the list are * currently shown or hidden. * @param can-swipe {boolean=} Whether the items in the list are allowed to be swiped to reveal * option buttons. Default: true. */ IonicModule .directive('ionList', [ '$timeout', function($timeout) { return { restrict: 'E', require: ['ionList', '^?$ionicScroll'], controller: '$ionicList', compile: function($element, $attr) { var listEl = jqLite('<div class="list">') .append($element.contents()) .addClass($attr.type);
$element.append(listEl);
return function($scope, $element, $attrs, ctrls) { var listCtrl = ctrls[0]; var scrollCtrl = ctrls[1];
// Wait for child elements to render...
$timeout(init);
function init() { var listView = listCtrl.listView = new ionic.views.ListView({ el: $element[0], listEl: $element.children()[0], scrollEl: scrollCtrl && scrollCtrl.element, scrollView: scrollCtrl && scrollCtrl.scrollView, onReorder: function(el, oldIndex, newIndex) { var itemScope = jqLite(el).scope(); if (itemScope && itemScope.$onReorder) { // Make sure onReorder is called in apply cycle,
// but also make sure it has no conflicts by doing
// $evalAsync
$timeout(function() { itemScope.$onReorder(oldIndex, newIndex); }); } }, canSwipe: function() { return listCtrl.canSwipeItems(); } });
$scope.$on('$destroy', function() { if (listView) { listView.deregister && listView.deregister(); listView = null; } });
if (isDefined($attr.canSwipe)) { $scope.$watch('!!(' + $attr.canSwipe + ')', function(value) { listCtrl.canSwipeItems(value); }); } if (isDefined($attr.showDelete)) { $scope.$watch('!!(' + $attr.showDelete + ')', function(value) { listCtrl.showDelete(value); }); } if (isDefined($attr.showReorder)) { $scope.$watch('!!(' + $attr.showReorder + ')', function(value) { listCtrl.showReorder(value); }); }
$scope.$watch(function() { return listCtrl.showDelete(); }, function(isShown, wasShown) { //Only use isShown=false if it was already shown
if (!isShown && !wasShown) { return; }
if (isShown) listCtrl.closeOptionButtons(); listCtrl.canSwipeItems(!isShown);
$element.children().toggleClass('list-left-editing', isShown); $element.toggleClass('disable-pointer-events', isShown);
var deleteButton = jqLite($element[0].getElementsByClassName('item-delete')); setButtonShown(deleteButton, listCtrl.showDelete); });
$scope.$watch(function() { return listCtrl.showReorder(); }, function(isShown, wasShown) { //Only use isShown=false if it was already shown
if (!isShown && !wasShown) { return; }
if (isShown) listCtrl.closeOptionButtons(); listCtrl.canSwipeItems(!isShown);
$element.children().toggleClass('list-right-editing', isShown); $element.toggleClass('disable-pointer-events', isShown);
var reorderButton = jqLite($element[0].getElementsByClassName('item-reorder')); setButtonShown(reorderButton, listCtrl.showReorder); });
function setButtonShown(el, shown) { shown() && el.addClass('visible') || el.removeClass('active'); ionic.requestAnimationFrame(function() { shown() && el.addClass('active') || el.removeClass('visible'); }); } }
}; } }; }]);
/** * @ngdoc directive * @name menuClose * @module ionic * @restrict AC * * @description * `menu-close` is an attribute directive that closes a currently opened side menu. * Note that by default, navigation transitions will not animate between views when * the menu is open. Additionally, this directive will reset the entering view's * history stack, making the new page the root of the history stack. This is done * to replicate the user experience seen in most side menu implementations, which is * to not show the back button at the root of the stack and show only the * menu button. We recommend that you also use the `enable-menu-with-back-views="false"` * {@link ionic.directive:ionSideMenus} attribute when using the menuClose directive. * * @usage * Below is an example of a link within a side menu. Tapping this link would * automatically close the currently opened menu. * * ```html
* <a menu-close href="#/home" class="item">Home</a> * ```
* * Note that if your destination state uses a resolve and that resolve asynchronously * takes longer than a standard transition (300ms), you'll need to set the * `nextViewOptions` manually as your resolve completes. * * ```js
* $ionicHistory.nextViewOptions({ * historyRoot: true, * disableAnimate: true, * expire: 300 * }); * ```
*/ IonicModule .directive('menuClose', ['$ionicHistory', '$timeout', function($ionicHistory, $timeout) { return { restrict: 'AC', link: function($scope, $element) { $element.bind('click', function() { var sideMenuCtrl = $element.inheritedData('$ionSideMenusController'); if (sideMenuCtrl) { $ionicHistory.nextViewOptions({ historyRoot: true, disableAnimate: true, expire: 300 }); // if no transition in 300ms, reset nextViewOptions
// the expire should take care of it, but will be cancelled in some
// cases. This directive is an exception to the rules of history.js
$timeout( function() { $ionicHistory.nextViewOptions({ historyRoot: false, disableAnimate: false }); }, 300); sideMenuCtrl.close(); } }); } }; }]);
/** * @ngdoc directive * @name menuToggle * @module ionic * @restrict AC * * @description * Toggle a side menu on the given side. * * @usage * Below is an example of a link within a nav bar. Tapping this button * would open the given side menu, and tapping it again would close it. * * ```html
* <ion-nav-bar> * <ion-nav-buttons side="left"> * <!-- Toggle left side menu --> * <button menu-toggle="left" class="button button-icon icon ion-navicon"></button> * </ion-nav-buttons> * <ion-nav-buttons side="right"> * <!-- Toggle right side menu --> * <button menu-toggle="right" class="button button-icon icon ion-navicon"></button> * </ion-nav-buttons> * </ion-nav-bar> * ```
* * ### Button Hidden On Child Views * By default, the menu toggle button will only appear on a root * level side-menu page. Navigating in to child views will hide the menu- * toggle button. They can be made visible on child pages by setting the * enable-menu-with-back-views attribute of the {@link ionic.directive:ionSideMenus} * directive to true. * * ```html
* <ion-side-menus enable-menu-with-back-views="true"> * ```
*/ IonicModule .directive('menuToggle', function() { return { restrict: 'AC', link: function($scope, $element, $attr) { $scope.$on('$ionicView.beforeEnter', function(ev, viewData) { if (viewData.enableBack) { var sideMenuCtrl = $element.inheritedData('$ionSideMenusController'); if (!sideMenuCtrl.enableMenuWithBackViews()) { $element.addClass('hide'); } } else { $element.removeClass('hide'); } });
$element.bind('click', function() { var sideMenuCtrl = $element.inheritedData('$ionSideMenusController'); sideMenuCtrl && sideMenuCtrl.toggle($attr.menuToggle); }); } }; });
/* * We don't document the ionModal directive, we instead document * the $ionicModal service */ IonicModule .directive('ionModal', [function() { return { restrict: 'E', transclude: true, replace: true, controller: [function() {}], template: '<div class="modal-backdrop">' + '<div class="modal-backdrop-bg"></div>' + '<div class="modal-wrapper" ng-transclude></div>' + '</div>' }; }]);
IonicModule .directive('ionModalView', function() { return { restrict: 'E', compile: function(element) { element.addClass('modal'); } }; });
/** * @ngdoc directive * @name ionNavBackButton * @module ionic * @restrict E * @parent ionNavBar * @description * Creates a back button inside an {@link ionic.directive:ionNavBar}. * * The back button will appear when the user is able to go back in the current navigation stack. By * default, the markup of the back button is automatically built using platform-appropriate defaults * (iOS back button icon on iOS and Android icon on Android). * * Additionally, the button is automatically set to `$ionicGoBack()` on click/tap. By default, the * app will navigate back one view when the back button is clicked. More advanced behavior is also * possible, as outlined below. * * @usage * * Recommended markup for default settings: * * ```html
* <ion-nav-bar> * <ion-nav-back-button> * </ion-nav-back-button> * </ion-nav-bar> * ```
* * With custom inner markup, and automatically adds a default click action: * * ```html
* <ion-nav-bar> * <ion-nav-back-button class="button-clear"> * <i class="ion-arrow-left-c"></i> Back * </ion-nav-back-button> * </ion-nav-bar> * ```
* * With custom inner markup and custom click action, using {@link ionic.service:$ionicHistory}: * * ```html
* <ion-nav-bar ng-controller="MyCtrl"> * <ion-nav-back-button class="button-clear" * ng-click="myGoBack()"> * <i class="ion-arrow-left-c"></i> Back * </ion-nav-back-button> * </ion-nav-bar> * ```
* ```js
* function MyCtrl($scope, $ionicHistory) { * $scope.myGoBack = function() { * $ionicHistory.goBack(); * }; * } * ```
*/ IonicModule .directive('ionNavBackButton', ['$ionicConfig', '$document', function($ionicConfig, $document) { return { restrict: 'E', require: '^ionNavBar', compile: function(tElement, tAttrs) {
// clone the back button, but as a <div>
var buttonEle = $document[0].createElement('button'); for (var n in tAttrs.$attr) { buttonEle.setAttribute(tAttrs.$attr[n], tAttrs[n]); }
if (!tAttrs.ngClick) { buttonEle.setAttribute('ng-click', '$ionicGoBack()'); }
buttonEle.className = 'button back-button hide buttons ' + (tElement.attr('class') || ''); buttonEle.innerHTML = tElement.html() || '';
var childNode; var hasIcon = hasIconClass(tElement[0]); var hasInnerText; var hasButtonText; var hasPreviousTitle;
for (var x = 0; x < tElement[0].childNodes.length; x++) { childNode = tElement[0].childNodes[x]; if (childNode.nodeType === 1) { if (hasIconClass(childNode)) { hasIcon = true; } else if (childNode.classList.contains('default-title')) { hasButtonText = true; } else if (childNode.classList.contains('previous-title')) { hasPreviousTitle = true; } } else if (!hasInnerText && childNode.nodeType === 3) { hasInnerText = !!childNode.nodeValue.trim(); } }
function hasIconClass(ele) { return /ion-|icon/.test(ele.className); }
var defaultIcon = $ionicConfig.backButton.icon(); if (!hasIcon && defaultIcon && defaultIcon !== 'none') { buttonEle.innerHTML = '<i class="icon ' + defaultIcon + '"></i> ' + buttonEle.innerHTML; buttonEle.className += ' button-clear'; }
if (!hasInnerText) { var buttonTextEle = $document[0].createElement('span'); buttonTextEle.className = 'back-text';
if (!hasButtonText && $ionicConfig.backButton.text()) { buttonTextEle.innerHTML += '<span class="default-title">' + $ionicConfig.backButton.text() + '</span>'; } if (!hasPreviousTitle && $ionicConfig.backButton.previousTitleText()) { buttonTextEle.innerHTML += '<span class="previous-title"></span>'; } buttonEle.appendChild(buttonTextEle);
}
tElement.attr('class', 'hide'); tElement.empty();
return { pre: function($scope, $element, $attr, navBarCtrl) { // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
navBarCtrl.navElement('backButton', buttonEle.outerHTML); buttonEle = null; } }; } }; }]);
/** * @ngdoc directive * @name ionNavBar * @module ionic * @delegate ionic.service:$ionicNavBarDelegate * @restrict E * * @description * If we have an {@link ionic.directive:ionNavView} directive, we can also create an * `<ion-nav-bar>`, which will create a topbar that updates as the application state changes. * * We can add a back button by putting an {@link ionic.directive:ionNavBackButton} inside. * * We can add buttons depending on the currently visible view using * {@link ionic.directive:ionNavButtons}. * * Note that the ion-nav-bar element will only work correctly if your content has an * ionView around it. * * @usage * * ```html
* <body ng-app="starter"> * <!-- The nav bar that will be updated as we navigate --> * <ion-nav-bar class="bar-positive"> * </ion-nav-bar> * * <!-- where the initial view template will be rendered --> * <ion-nav-view> * <ion-view> * <ion-content>Hello!</ion-content> * </ion-view> * </ion-nav-view> * </body> * ```
* * @param {string=} delegate-handle The handle used to identify this navBar * with {@link ionic.service:$ionicNavBarDelegate}. * @param align-title {string=} Where to align the title of the navbar. * Available: 'left', 'right', 'center'. Defaults to 'center'. * @param {boolean=} no-tap-scroll By default, the navbar will scroll the content * to the top when tapped. Set no-tap-scroll to true to disable this behavior. * * </table><br/> */ IonicModule .directive('ionNavBar', function() { return { restrict: 'E', controller: '$ionicNavBar', scope: true, link: function($scope, $element, $attr, ctrl) { ctrl.init(); } }; });
/** * @ngdoc directive * @name ionNavButtons * @module ionic * @restrict E * @parent ionNavView * * @description * Use nav buttons to set the buttons on your {@link ionic.directive:ionNavBar} * from within an {@link ionic.directive:ionView}. This gives each * view template the ability to specify which buttons should show in the nav bar, * overriding any default buttons already placed in the nav bar. * * Any buttons you declare will be positioned on the navbar's corresponding side. Primary * buttons generally map to the left side of the header, and secondary buttons are * generally on the right side. However, their exact locations are platform-specific. * For example, in iOS, the primary buttons are on the far left of the header, and * secondary buttons are on the far right, with the header title centered between them. * For Android, however, both groups of buttons are on the far right of the header, * with the header title aligned left. * * We recommend always using `primary` and `secondary`, so the buttons correctly map * to the side familiar to users of each platform. However, in cases where buttons should * always be on an exact side, both `left` and `right` sides are still available. For * example, a toggle button for a left side menu should be on the left side; in this case, * we'd recommend using `side="left"`, so it's always on the left, no matter the platform. * * ***Note*** that `ion-nav-buttons` must be immediate descendants of the `ion-view` or * `ion-nav-bar` element (basically, don't wrap it in another div). * * @usage * ```html
* <ion-nav-bar> * </ion-nav-bar> * <ion-nav-view> * <ion-view> * <ion-nav-buttons side="primary"> * <button class="button" ng-click="doSomething()"> * I'm a button on the primary of the navbar! * </button> * </ion-nav-buttons> * <ion-content> * Some super content here! * </ion-content> * </ion-view> * </ion-nav-view> * ```
* * @param {string} side The side to place the buttons in the * {@link ionic.directive:ionNavBar}. Available sides: `primary`, `secondary`, `left`, and `right`. */ IonicModule .directive('ionNavButtons', ['$document', function($document) { return { require: '^ionNavBar', restrict: 'E', compile: function(tElement, tAttrs) { var side = 'left';
if (/^primary|secondary|right$/i.test(tAttrs.side || '')) { side = tAttrs.side.toLowerCase(); }
var spanEle = $document[0].createElement('span'); spanEle.className = side + '-buttons'; spanEle.innerHTML = tElement.html();
var navElementType = side + 'Buttons';
tElement.attr('class', 'hide'); tElement.empty();
return { pre: function($scope, $element, $attrs, navBarCtrl) { // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
var parentViewCtrl = $element.parent().data('$ionViewController'); if (parentViewCtrl) { // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
parentViewCtrl.navElement(navElementType, spanEle.outerHTML);
} else { // these are buttons for all views that do not have their own ion-nav-buttons
navBarCtrl.navElement(navElementType, spanEle.outerHTML); }
spanEle = null; } }; } }; }]);
/** * @ngdoc directive * @name navDirection * @module ionic * @restrict A * * @description * The direction which the nav view transition should animate. Available options * are: `forward`, `back`, `enter`, `exit`, `swap`. * * @usage * * ```html
* <a nav-direction="forward" href="#/home">Home</a> * ```
*/ IonicModule .directive('navDirection', ['$ionicViewSwitcher', function($ionicViewSwitcher) { return { restrict: 'A', priority: 1000, link: function($scope, $element, $attr) { $element.bind('click', function() { $ionicViewSwitcher.nextDirection($attr.navDirection); }); } }; }]);
/** * @ngdoc directive * @name ionNavTitle * @module ionic * @restrict E * @parent ionNavView * * @description * * The nav title directive replaces an {@link ionic.directive:ionNavBar} title text with * custom HTML from within an {@link ionic.directive:ionView} template. This gives each * view the ability to specify its own custom title element, such as an image or any HTML, * rather than being text-only. Alternatively, text-only titles can be updated using the * `view-title` {@link ionic.directive:ionView} attribute. * * Note that `ion-nav-title` must be an immediate descendant of the `ion-view` or * `ion-nav-bar` element (basically don't wrap it in another div). * * @usage * ```html
* <ion-nav-bar> * </ion-nav-bar> * <ion-nav-view> * <ion-view> * <ion-nav-title> * <img src="logo.svg"> * </ion-nav-title> * <ion-content> * Some super content here! * </ion-content> * </ion-view> * </ion-nav-view> * ```
* */ IonicModule .directive('ionNavTitle', ['$document', function($document) { return { require: '^ionNavBar', restrict: 'E', compile: function(tElement, tAttrs) { var navElementType = 'title'; var spanEle = $document[0].createElement('span'); for (var n in tAttrs.$attr) { spanEle.setAttribute(tAttrs.$attr[n], tAttrs[n]); } spanEle.classList.add('nav-bar-title'); spanEle.innerHTML = tElement.html();
tElement.attr('class', 'hide'); tElement.empty();
return { pre: function($scope, $element, $attrs, navBarCtrl) { // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
var parentViewCtrl = $element.parent().data('$ionViewController'); if (parentViewCtrl) { // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
parentViewCtrl.navElement(navElementType, spanEle.outerHTML);
} else { // these are buttons for all views that do not have their own ion-nav-buttons
navBarCtrl.navElement(navElementType, spanEle.outerHTML); }
spanEle = null; } }; } }; }]);
/** * @ngdoc directive * @name navTransition * @module ionic * @restrict A * * @description * The transition type which the nav view transition should use when it animates. * Current, options are `ios`, `android`, and `none`. More options coming soon. * * @usage * * ```html
* <a nav-transition="none" href="#/home">Home</a> * ```
*/ IonicModule .directive('navTransition', ['$ionicViewSwitcher', function($ionicViewSwitcher) { return { restrict: 'A', priority: 1000, link: function($scope, $element, $attr) { $element.bind('click', function() { $ionicViewSwitcher.nextTransition($attr.navTransition); }); } }; }]);
/** * @ngdoc directive * @name ionNavView * @module ionic * @restrict E * @codepen odqCz * * @description * As a user navigates throughout your app, Ionic is able to keep track of their * navigation history. By knowing their history, transitions between views * correctly enter and exit using the platform's transition style. An additional * benefit to Ionic's navigation system is its ability to manage multiple * histories. For example, each tab can have it's own navigation history stack. * * Ionic uses the AngularUI Router module so app interfaces can be organized * into various "states". Like Angular's core $route service, URLs can be used * to control the views. However, the AngularUI Router provides a more powerful * state manager in that states are bound to named, nested, and parallel views, * allowing more than one template to be rendered on the same page. * Additionally, each state is not required to be bound to a URL, and data can * be pushed to each state which allows much flexibility. * * The ionNavView directive is used to render templates in your application. Each template * is part of a state. States are usually mapped to a url, and are defined programatically * using angular-ui-router (see [their docs](https://github.com/angular-ui/ui-router/wiki),
* and remember to replace ui-view with ion-nav-view in examples). * * @usage * In this example, we will create a navigation view that contains our different states for the app. * * To do this, in our markup we use ionNavView top level directive. To display a header bar we use * the {@link ionic.directive:ionNavBar} directive that updates as we navigate through the * navigation stack. * * Next, we need to setup our states that will be rendered. * * ```js
* var app = angular.module('myApp', ['ionic']); * app.config(function($stateProvider) { * $stateProvider * .state('index', { * url: '/', * templateUrl: 'home.html' * }) * .state('music', { * url: '/music', * templateUrl: 'music.html' * }); * }); * ```
* Then on app start, $stateProvider will look at the url, see if it matches the index state, * and then try to load home.html into the `<ion-nav-view>`. * * Pages are loaded by the URLs given. One simple way to create templates in Angular is to put * them directly into your HTML file and use the `<script type="text/ng-template">` syntax. * So here is one way to put home.html into our app: * * ```html
* <script id="home" type="text/ng-template"> * <!-- The title of the ion-view will be shown on the navbar --> * <ion-view view-title="Home"> * <ion-content ng-controller="HomeCtrl"> * <!-- The content of the page --> * <a href="#/music">Go to music page!</a> * </ion-content> * </ion-view> * </script> * ```
* * This is good to do because the template will be cached for very fast loading, instead of * having to fetch them from the network. * * ## Caching * * By default, views are cached to improve performance. When a view is navigated away from, its * element is left in the DOM, and its scope is disconnected from the `$watch` cycle. When * navigating to a view that is already cached, its scope is then reconnected, and the existing * element that was left in the DOM becomes the active view. This also allows for the scroll * position of previous views to be maintained. * * Caching can be disabled and enabled in multiple ways. By default, Ionic will cache a maximum of * 10 views, and not only can this be configured, but apps can also explicitly state which views * should and should not be cached. * * Note that because we are caching these views, *we aren’t destroying scopes*. Instead, scopes * are being disconnected from the watch cycle. Because scopes are not being destroyed and * recreated, controllers are not loading again on a subsequent viewing. If the app/controller * needs to know when a view has entered or has left, then view events emitted from the * {@link ionic.directive:ionView} scope, such as `$ionicView.enter`, may be useful. * * By default, when navigating back in the history, the "forward" views are removed from the cache. * If you navigate forward to the same view again, it'll create a new DOM element and controller * instance. Basically, any forward views are reset each time. This can be configured using the * {@link ionic.provider:$ionicConfigProvider}: * * ```js
* $ionicConfigProvider.views.forwardCache(true); * ```
* * #### Disable cache globally * * The {@link ionic.provider:$ionicConfigProvider} can be used to set the maximum allowable views * which can be cached, but this can also be use to disable all caching by setting it to 0. * * ```js
* $ionicConfigProvider.views.maxCache(0); * ```
* * #### Disable cache within state provider * * ```js
* $stateProvider.state('myState', { * cache: false, * url : '/myUrl', * templateUrl : 'my-template.html' * }) * ```
* * #### Disable cache with an attribute * * ```html
* <ion-view cache-view="false" view-title="My Title!"> * ... * </ion-view> * ```
* * * ## AngularUI Router * * Please visit [AngularUI Router's docs](https://github.com/angular-ui/ui-router/wiki) for
* more info. Below is a great video by the AngularUI Router team that may help to explain * how it all works: * * <iframe width="560" height="315" src="//www.youtube.com/embed/dqJRoh8MnBo" * frameborder="0" allowfullscreen></iframe> * * Note: We do not recommend using [resolve](https://github.com/angular-ui/ui-router/wiki#resolve)
* of AngularUI Router. The recommended approach is to execute any logic needed before beginning the state transition. * * @param {string=} name A view name. The name should be unique amongst the other views in the * same state. You can have views of the same name that live in different states. For more * information, see ui-router's * [ui-view documentation](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.directive:ui-view).
*/ IonicModule .directive('ionNavView', [ '$state', '$ionicConfig', function($state, $ionicConfig) { // IONIC's fork of Angular UI Router, v0.2.10
// the navView handles registering views in the history and how to transition between them
return { restrict: 'E', terminal: true, priority: 2000, transclude: true, controller: '$ionicNavView', compile: function(tElement, tAttrs, transclude) {
// a nav view element is a container for numerous views
tElement.addClass('view-container'); ionic.DomUtil.cachedAttr(tElement, 'nav-view-transition', $ionicConfig.views.transition());
return function($scope, $element, $attr, navViewCtrl) { var latestLocals;
// Put in the compiled initial view
transclude($scope, function(clone) { $element.append(clone); });
var viewData = navViewCtrl.init();
// listen for $stateChangeSuccess
$scope.$on('$stateChangeSuccess', function() { updateView(false); }); $scope.$on('$viewContentLoading', function() { updateView(false); });
// initial load, ready go
updateView(true);
function updateView(firstTime) { // get the current local according to the $state
var viewLocals = $state.$current && $state.$current.locals[viewData.name];
// do not update THIS nav-view if its is not the container for the given state
// if the viewLocals are the same as THIS latestLocals, then nothing to do
if (!viewLocals || (!firstTime && viewLocals === latestLocals)) return;
// update the latestLocals
latestLocals = viewLocals; viewData.state = viewLocals.$$state;
// register, update and transition to the new view
navViewCtrl.register(viewLocals); }
}; } }; }]);
IonicModule
.config(['$provide', function($provide) { $provide.decorator('ngClickDirective', ['$delegate', function($delegate) { // drop the default ngClick directive
$delegate.shift(); return $delegate; }]); }])
/** * @private */ .factory('$ionicNgClick', ['$parse', function($parse) { return function(scope, element, clickExpr) { var clickHandler = angular.isFunction(clickExpr) ? clickExpr : $parse(clickExpr);
element.on('click', function(event) { scope.$apply(function() { clickHandler(scope, {$event: (event)}); }); });
// Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
// something else nearby.
element.onclick = noop; }; }])
.directive('ngClick', ['$ionicNgClick', function($ionicNgClick) { return function(scope, element, attr) { $ionicNgClick(scope, element, attr.ngClick); }; }])
.directive('ionStopEvent', function() { return { restrict: 'A', link: function(scope, element, attr) { element.bind(attr.ionStopEvent, eventStopPropagation); } }; }); function eventStopPropagation(e) { e.stopPropagation(); }
/** * @ngdoc directive * @name ionPane * @module ionic * @restrict E * * @description A simple container that fits content, with no side effects. Adds the 'pane' class to the element. */ IonicModule .directive('ionPane', function() { return { restrict: 'E', link: function(scope, element) { element.addClass('pane'); } }; });
/* * We don't document the ionPopover directive, we instead document * the $ionicPopover service */ IonicModule .directive('ionPopover', [function() { return { restrict: 'E', transclude: true, replace: true, controller: [function() {}], template: '<div class="popover-backdrop">' + '<div class="popover-wrapper" ng-transclude></div>' + '</div>' }; }]);
IonicModule .directive('ionPopoverView', function() { return { restrict: 'E', compile: function(element) { element.append(jqLite('<div class="popover-arrow">')); element.addClass('popover'); } }; });
/** * @ngdoc directive * @name ionRadio * @module ionic * @restrict E * @codepen saoBG * @description * The radio directive is no different than the HTML radio input, except it's styled differently. * * Radio behaves like [AngularJS radio](http://docs.angularjs.org/api/ng/input/input[radio]).
* * @usage * ```html
* <ion-radio ng-model="choice" ng-value="'A'">Choose A</ion-radio> * <ion-radio ng-model="choice" ng-value="'B'">Choose B</ion-radio> * <ion-radio ng-model="choice" ng-value="'C'">Choose C</ion-radio> * ```
* * @param {string=} name The name of the radio input. * @param {expression=} value The value of the radio input. * @param {boolean=} disabled The state of the radio input. * @param {string=} icon The icon to use when the radio input is selected. * @param {expression=} ng-value Angular equivalent of the value attribute. * @param {expression=} ng-model The angular model for the radio input. * @param {boolean=} ng-disabled Angular equivalent of the disabled attribute. * @param {expression=} ng-change Triggers given expression when radio input's model changes */ IonicModule .directive('ionRadio', function() { return { restrict: 'E', replace: true, require: '?ngModel', transclude: true, template: '<label class="item item-radio">' + '<input type="radio" name="radio-group">' + '<div class="radio-content">' + '<div class="item-content disable-pointer-events" ng-transclude></div>' + '<i class="radio-icon disable-pointer-events icon ion-checkmark"></i>' + '</div>' + '</label>',
compile: function(element, attr) { if (attr.icon) { var iconElm = element.find('i'); iconElm.removeClass('ion-checkmark').addClass(attr.icon); }
var input = element.find('input'); forEach({ 'name': attr.name, 'value': attr.value, 'disabled': attr.disabled, 'ng-value': attr.ngValue, 'ng-model': attr.ngModel, 'ng-disabled': attr.ngDisabled, 'ng-change': attr.ngChange, 'ng-required': attr.ngRequired, 'required': attr.required }, function(value, name) { if (isDefined(value)) { input.attr(name, value); } });
return function(scope, element, attr) { scope.getValue = function() { return scope.ngValue || attr.value; }; }; } }; });
/** * @ngdoc directive * @name ionRefresher * @module ionic * @restrict E * @parent ionic.directive:ionContent, ionic.directive:ionScroll * @description * Allows you to add pull-to-refresh to a scrollView. * * Place it as the first child of your {@link ionic.directive:ionContent} or * {@link ionic.directive:ionScroll} element. * * When refreshing is complete, $broadcast the 'scroll.refreshComplete' event * from your controller. * * @usage * * ```html
* <ion-content ng-controller="MyController"> * <ion-refresher * pulling-text="Pull to refresh..." * on-refresh="doRefresh()"> * </ion-refresher> * <ion-list> * <ion-item ng-repeat="item in items"></ion-item> * </ion-list> * </ion-content> * ```
* ```js
* angular.module('testApp', ['ionic']) * .controller('MyController', function($scope, $http) { * $scope.items = [1,2,3]; * $scope.doRefresh = function() { * $http.get('/new-items') * .success(function(newItems) { * $scope.items = newItems; * }) * .finally(function() { * // Stop the ion-refresher from spinning
* $scope.$broadcast('scroll.refreshComplete'); * }); * }; * }); * ```
* * @param {expression=} on-refresh Called when the user pulls down enough and lets go * of the refresher. * @param {expression=} on-pulling Called when the user starts to pull down * on the refresher. * @param {string=} pulling-text The text to display while the user is pulling down. * @param {string=} pulling-icon The icon to display while the user is pulling down. * Default: 'ion-android-arrow-down'. * @param {string=} spinner The {@link ionic.directive:ionSpinner} icon to display * after user lets go of the refresher. The SVG {@link ionic.directive:ionSpinner} * is now the default, replacing rotating font icons. Set to `none` to disable both the * spinner and the icon. * @param {string=} refreshing-icon The font icon to display after user lets go of the * refresher. This is deprecated in favor of the SVG {@link ionic.directive:ionSpinner}. * @param {boolean=} disable-pulling-rotation Disables the rotation animation of the pulling * icon when it reaches its activated threshold. To be used with a custom `pulling-icon`. * */ IonicModule .directive('ionRefresher', [function() { return { restrict: 'E', replace: true, require: ['?^$ionicScroll', 'ionRefresher'], controller: '$ionicRefresher', template: '<div class="scroll-refresher invisible" collection-repeat-ignore>' + '<div class="ionic-refresher-content" ' + 'ng-class="{\'ionic-refresher-with-text\': pullingText || refreshingText}">' + '<div class="icon-pulling" ng-class="{\'pulling-rotation-disabled\':disablePullingRotation}">' + '<i class="icon {{pullingIcon}}"></i>' + '</div>' + '<div class="text-pulling" ng-bind-html="pullingText"></div>' + '<div class="icon-refreshing">' + '<ion-spinner ng-if="showSpinner" icon="{{spinner}}"></ion-spinner>' + '<i ng-if="showIcon" class="icon {{refreshingIcon}}"></i>' + '</div>' + '<div class="text-refreshing" ng-bind-html="refreshingText"></div>' + '</div>' + '</div>', link: function($scope, $element, $attrs, ctrls) {
// JS Scrolling uses the scroll controller
var scrollCtrl = ctrls[0], refresherCtrl = ctrls[1]; if (!scrollCtrl || scrollCtrl.isNative()) { // Kick off native scrolling
refresherCtrl.init(); } else { $element[0].classList.add('js-scrolling'); scrollCtrl._setRefresher( $scope, $element[0], refresherCtrl.getRefresherDomMethods() );
$scope.$on('scroll.refreshComplete', function() { $scope.$evalAsync(function() { scrollCtrl.scrollView.finishPullToRefresh(); }); }); }
} }; }]);
/** * @ngdoc directive * @name ionScroll * @module ionic * @delegate ionic.service:$ionicScrollDelegate * @codepen mwFuh * @restrict E * * @description * Creates a scrollable container for all content inside. * * @usage * * Basic usage: * * ```html
* <ion-scroll zooming="true" direction="xy" style="width: 500px; height: 500px"> * <div style="width: 5000px; height: 5000px; background: url('https://upload.wikimedia.org/wikipedia/commons/a/ad/Europe_geological_map-en.jpg') repeat"></div> * </ion-scroll> * ```
* * Note that it's important to set the height of the scroll box as well as the height of the inner * content to enable scrolling. This makes it possible to have full control over scrollable areas. * * If you'd just like to have a center content scrolling area, use {@link ionic.directive:ionContent} instead. * * @param {string=} delegate-handle The handle used to identify this scrollView * with {@link ionic.service:$ionicScrollDelegate}. * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'. * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true. * @param {boolean=} paging Whether to scroll with paging. * @param {expression=} on-refresh Called on pull-to-refresh, triggered by an {@link ionic.directive:ionRefresher}. * @param {expression=} on-scroll Called whenever the user scrolls. * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true. * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true. * @param {boolean=} zooming Whether to support pinch-to-zoom * @param {integer=} min-zoom The smallest zoom amount allowed (default is 0.5) * @param {integer=} max-zoom The largest zoom amount allowed (default is 3) * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges * of the content. Defaults to true on iOS, false on Android. */ IonicModule .directive('ionScroll', [ '$timeout', '$controller', '$ionicBind', '$ionicConfig', function($timeout, $controller, $ionicBind, $ionicConfig) { return { restrict: 'E', scope: true, controller: function() {}, compile: function(element, attr) { element.addClass('scroll-view ionic-scroll');
//We cannot transclude here because it breaks element.data() inheritance on compile
var innerElement = jqLite('<div class="scroll"></div>'); innerElement.append(element.contents()); element.append(innerElement);
var nativeScrolling = attr.overflowScroll !== "false" && (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling());
return { pre: prelink }; function prelink($scope, $element, $attr) { $ionicBind($scope, $attr, { direction: '@', paging: '@', $onScroll: '&onScroll', scroll: '@', scrollbarX: '@', scrollbarY: '@', zooming: '@', minZoom: '@', maxZoom: '@' }); $scope.direction = $scope.direction || 'y';
if (isDefined($attr.padding)) { $scope.$watch($attr.padding, function(newVal) { innerElement.toggleClass('padding', !!newVal); }); } if ($scope.$eval($scope.paging) === true) { innerElement.addClass('scroll-paging'); }
if (!$scope.direction) { $scope.direction = 'y'; } var isPaging = $scope.$eval($scope.paging) === true;
if (nativeScrolling) { $element.addClass('overflow-scroll'); }
$element.addClass('scroll-' + $scope.direction);
var scrollViewOptions = { el: $element[0], delegateHandle: $attr.delegateHandle, locking: ($attr.locking || 'true') === 'true', bouncing: $scope.$eval($attr.hasBouncing), paging: isPaging, scrollbarX: $scope.$eval($scope.scrollbarX) !== false, scrollbarY: $scope.$eval($scope.scrollbarY) !== false, scrollingX: $scope.direction.indexOf('x') >= 0, scrollingY: $scope.direction.indexOf('y') >= 0, zooming: $scope.$eval($scope.zooming) === true, maxZoom: $scope.$eval($scope.maxZoom) || 3, minZoom: $scope.$eval($scope.minZoom) || 0.5, preventDefault: true, nativeScrolling: nativeScrolling };
if (isPaging) { scrollViewOptions.speedMultiplier = 0.8; scrollViewOptions.bouncing = false; }
$controller('$ionicScroll', { $scope: $scope, scrollViewOptions: scrollViewOptions }); } } }; }]);
/** * @ngdoc directive * @name ionSideMenu * @module ionic * @restrict E * @parent ionic.directive:ionSideMenus * * @description * A container for a side menu, sibling to an {@link ionic.directive:ionSideMenuContent} directive. * * @usage * ```html
* <ion-side-menu * side="left" * width="myWidthValue + 20" * is-enabled="shouldLeftSideMenuBeEnabled()"> * </ion-side-menu> * ```
* For a complete side menu example, see the * {@link ionic.directive:ionSideMenus} documentation. * * @param {string} side Which side the side menu is currently on. Allowed values: 'left' or 'right'. * @param {boolean=} is-enabled Whether this side menu is enabled. * @param {number=} width How many pixels wide the side menu should be. Defaults to 275. */ IonicModule .directive('ionSideMenu', function() { return { restrict: 'E', require: '^ionSideMenus', scope: true, compile: function(element, attr) { angular.isUndefined(attr.isEnabled) && attr.$set('isEnabled', 'true'); angular.isUndefined(attr.width) && attr.$set('width', '275');
element.addClass('menu menu-' + attr.side);
return function($scope, $element, $attr, sideMenuCtrl) { $scope.side = $attr.side || 'left';
var sideMenu = sideMenuCtrl[$scope.side] = new ionic.views.SideMenu({ width: attr.width, el: $element[0], isEnabled: true });
$scope.$watch($attr.width, function(val) { var numberVal = +val; if (numberVal && numberVal == val) { sideMenu.setWidth(+val); } }); $scope.$watch($attr.isEnabled, function(val) { sideMenu.setIsEnabled(!!val); }); }; } }; });
/** * @ngdoc directive * @name ionSideMenuContent * @module ionic * @restrict E * @parent ionic.directive:ionSideMenus * * @description * A container for the main visible content, sibling to one or more * {@link ionic.directive:ionSideMenu} directives. * * @usage * ```html
* <ion-side-menu-content * edge-drag-threshold="true" * drag-content="true"> * </ion-side-menu-content> * ```
* For a complete side menu example, see the * {@link ionic.directive:ionSideMenus} documentation. * * @param {boolean=} drag-content Whether the content can be dragged. Default true. * @param {boolean|number=} edge-drag-threshold Whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Default false. Accepts three types of values: * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu. * - If true is given, the default number of pixels (25) is used as the maximum allowed distance. * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed. * */ IonicModule .directive('ionSideMenuContent', [ '$timeout', '$ionicGesture', '$window', function($timeout, $ionicGesture, $window) {
return { restrict: 'EA', //DEPRECATED 'A'
require: '^ionSideMenus', scope: true, compile: function(element, attr) { element.addClass('menu-content pane');
return { pre: prelink }; function prelink($scope, $element, $attr, sideMenuCtrl) { var startCoord = null; var primaryScrollAxis = null;
if (isDefined(attr.dragContent)) { $scope.$watch(attr.dragContent, function(value) { sideMenuCtrl.canDragContent(value); }); } else { sideMenuCtrl.canDragContent(true); }
if (isDefined(attr.edgeDragThreshold)) { $scope.$watch(attr.edgeDragThreshold, function(value) { sideMenuCtrl.edgeDragThreshold(value); }); }
// Listen for taps on the content to close the menu
function onContentTap(gestureEvt) { if (sideMenuCtrl.getOpenAmount() !== 0) { sideMenuCtrl.close(); gestureEvt.gesture.srcEvent.preventDefault(); startCoord = null; primaryScrollAxis = null; } else if (!startCoord) { startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent); } }
function onDragX(e) { if (!sideMenuCtrl.isDraggableTarget(e)) return;
if (getPrimaryScrollAxis(e) == 'x') { sideMenuCtrl._handleDrag(e); e.gesture.srcEvent.preventDefault(); } }
function onDragY(e) { if (getPrimaryScrollAxis(e) == 'x') { e.gesture.srcEvent.preventDefault(); } }
function onDragRelease(e) { sideMenuCtrl._endDrag(e); startCoord = null; primaryScrollAxis = null; }
function getPrimaryScrollAxis(gestureEvt) { // gets whether the user is primarily scrolling on the X or Y
// If a majority of the drag has been on the Y since the start of
// the drag, but the X has moved a little bit, it's still a Y drag
if (primaryScrollAxis) { // we already figured out which way they're scrolling
return primaryScrollAxis; }
if (gestureEvt && gestureEvt.gesture) {
if (!startCoord) { // get the starting point
startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
} else { // we already have a starting point, figure out which direction they're going
var endCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
var xDistance = Math.abs(endCoord.x - startCoord.x); var yDistance = Math.abs(endCoord.y - startCoord.y);
var scrollAxis = (xDistance < yDistance ? 'y' : 'x');
if (Math.max(xDistance, yDistance) > 30) { // ok, we pretty much know which way they're going
// let's lock it in
primaryScrollAxis = scrollAxis; }
return scrollAxis; } } return 'y'; }
var content = { element: element[0], onDrag: function() {}, endDrag: function() {}, setCanScroll: function(canScroll) { var c = $element[0].querySelector('.scroll');
if (!c) { return; }
var content = angular.element(c.parentElement); if (!content) { return; }
// freeze our scroll container if we have one
var scrollScope = content.scope(); scrollScope.scrollCtrl && scrollScope.scrollCtrl.freezeScrollShut(!canScroll); }, getTranslateX: function() { return $scope.sideMenuContentTranslateX || 0; }, setTranslateX: ionic.animationFrameThrottle(function(amount) { var xTransform = content.offsetX + amount; $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + xTransform + 'px,0,0)'; $timeout(function() { $scope.sideMenuContentTranslateX = amount; }); }), setMarginLeft: ionic.animationFrameThrottle(function(amount) { if (amount) { amount = parseInt(amount, 10); $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amount + 'px,0,0)'; $element[0].style.width = ($window.innerWidth - amount) + 'px'; content.offsetX = amount; } else { $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)'; $element[0].style.width = ''; content.offsetX = 0; } }), setMarginRight: ionic.animationFrameThrottle(function(amount) { if (amount) { amount = parseInt(amount, 10); $element[0].style.width = ($window.innerWidth - amount) + 'px'; content.offsetX = amount; } else { $element[0].style.width = ''; content.offsetX = 0; } // reset incase left gets grabby
$element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)'; }), setMarginLeftAndRight: ionic.animationFrameThrottle(function(amountLeft, amountRight) { amountLeft = amountLeft && parseInt(amountLeft, 10) || 0; amountRight = amountRight && parseInt(amountRight, 10) || 0;
var amount = amountLeft + amountRight;
if (amount > 0) { $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amountLeft + 'px,0,0)'; $element[0].style.width = ($window.innerWidth - amount) + 'px'; content.offsetX = amountLeft; } else { $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)'; $element[0].style.width = ''; content.offsetX = 0; } // reset incase left gets grabby
//$element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
}), enableAnimation: function() { $scope.animationEnabled = true; $element[0].classList.add('menu-animated'); }, disableAnimation: function() { $scope.animationEnabled = false; $element[0].classList.remove('menu-animated'); }, offsetX: 0 };
sideMenuCtrl.setContent(content);
// add gesture handlers
var gestureOpts = { stop_browser_behavior: false }; gestureOpts.prevent_default_directions = ['left', 'right']; var contentTapGesture = $ionicGesture.on('tap', onContentTap, $element, gestureOpts); var dragRightGesture = $ionicGesture.on('dragright', onDragX, $element, gestureOpts); var dragLeftGesture = $ionicGesture.on('dragleft', onDragX, $element, gestureOpts); var dragUpGesture = $ionicGesture.on('dragup', onDragY, $element, gestureOpts); var dragDownGesture = $ionicGesture.on('dragdown', onDragY, $element, gestureOpts); var releaseGesture = $ionicGesture.on('release', onDragRelease, $element, gestureOpts);
// Cleanup
$scope.$on('$destroy', function() { if (content) { content.element = null; content = null; } $ionicGesture.off(dragLeftGesture, 'dragleft', onDragX); $ionicGesture.off(dragRightGesture, 'dragright', onDragX); $ionicGesture.off(dragUpGesture, 'dragup', onDragY); $ionicGesture.off(dragDownGesture, 'dragdown', onDragY); $ionicGesture.off(releaseGesture, 'release', onDragRelease); $ionicGesture.off(contentTapGesture, 'tap', onContentTap); }); } } }; }]);
IonicModule
/** * @ngdoc directive * @name ionSideMenus * @module ionic * @delegate ionic.service:$ionicSideMenuDelegate * @restrict E * * @description * A container element for side menu(s) and the main content. Allows the left and/or right side menu * to be toggled by dragging the main content area side to side. * * To automatically close an opened menu, you can add the {@link ionic.directive:menuClose} attribute * directive. The `menu-close` attribute is usually added to links and buttons within * `ion-side-menu-content`, so that when the element is clicked, the opened side menu will * automatically close. * * "Burger Icon" toggles can be added to the header with the {@link ionic.directive:menuToggle} * attribute directive. Clicking the toggle will open and close the side menu like the `menu-close` * directive. The side menu will automatically hide on child pages, but can be overridden with the * enable-menu-with-back-views attribute mentioned below. * * By default, side menus are hidden underneath their side menu content and can be opened by swiping * the content left or right or by toggling a button to show the side menu. Additionally, by adding the * {@link ionic.directive:exposeAsideWhen} attribute directive to an * {@link ionic.directive:ionSideMenu} element directive, a side menu can be given instructions about * "when" the menu should be exposed (always viewable). * * ![Side Menu](http://ionicframework.com.s3.amazonaws.com/docs/controllers/sidemenu.gif)
* * For more information on side menus, check out: * * - {@link ionic.directive:ionSideMenuContent} * - {@link ionic.directive:ionSideMenu} * - {@link ionic.directive:menuToggle} * - {@link ionic.directive:menuClose} * - {@link ionic.directive:exposeAsideWhen} * * @usage * To use side menus, add an `<ion-side-menus>` parent element. This will encompass all pages that have a * side menu, and have at least 2 child elements: 1 `<ion-side-menu-content>` for the center content, * and one or more `<ion-side-menu>` directives for each side menu(left/right) that you wish to place. * * ```html
* <ion-side-menus> * <!-- Left menu --> * <ion-side-menu side="left"> * </ion-side-menu> * * <ion-side-menu-content> * <!-- Main content, usually <ion-nav-view> --> * </ion-side-menu-content> * * <!-- Right menu --> * <ion-side-menu side="right"> * </ion-side-menu> * * </ion-side-menus> * ```
* ```js
* function ContentController($scope, $ionicSideMenuDelegate) { * $scope.toggleLeft = function() { * $ionicSideMenuDelegate.toggleLeft(); * }; * } * ```
* * @param {bool=} enable-menu-with-back-views Determines whether the side menu is enabled when the * back button is showing. When set to `false`, any {@link ionic.directive:menuToggle} will be hidden, * and the user cannot swipe to open the menu. When going back to the root page of the side menu (the * page without a back button visible), then any menuToggle buttons will show again, and menus will be * enabled again. * @param {string=} delegate-handle The handle used to identify this side menu * with {@link ionic.service:$ionicSideMenuDelegate}. * */ .directive('ionSideMenus', ['$ionicBody', function($ionicBody) { return { restrict: 'ECA', controller: '$ionicSideMenus', compile: function(element, attr) { attr.$set('class', (attr['class'] || '') + ' view');
return { pre: prelink }; function prelink($scope, $element, $attrs, ctrl) {
ctrl.enableMenuWithBackViews($scope.$eval($attrs.enableMenuWithBackViews));
$scope.$on('$ionicExposeAside', function(evt, isAsideExposed) { if (!$scope.$exposeAside) $scope.$exposeAside = {}; $scope.$exposeAside.active = isAsideExposed; $ionicBody.enableClass(isAsideExposed, 'aside-open'); });
$scope.$on('$ionicView.beforeEnter', function(ev, d) { if (d.historyId) { $scope.$activeHistoryId = d.historyId; } });
$scope.$on('$destroy', function() { $ionicBody.removeClass('menu-open', 'aside-open'); });
} } }; }]);
/** * @ngdoc directive * @name ionSlideBox * @module ionic * @codepen AjgEB * @deprecated will be removed in the next Ionic release in favor of the new ion-slides component. * Don't depend on the internal behavior of this widget. * @delegate ionic.service:$ionicSlideBoxDelegate * @restrict E * @description * The Slide Box is a multi-page container where each page can be swiped or dragged between: * * * @usage * ```html
* <ion-slide-box on-slide-changed="slideHasChanged($index)"> * <ion-slide> * <div class="box blue"><h1>BLUE</h1></div> * </ion-slide> * <ion-slide> * <div class="box yellow"><h1>YELLOW</h1></div> * </ion-slide> * <ion-slide> * <div class="box pink"><h1>PINK</h1></div> * </ion-slide> * </ion-slide-box> * ```
* * @param {string=} delegate-handle The handle used to identify this slideBox * with {@link ionic.service:$ionicSlideBoxDelegate}. * @param {boolean=} does-continue Whether the slide box should loop. * @param {boolean=} auto-play Whether the slide box should automatically slide. Default true if does-continue is true. * @param {number=} slide-interval How many milliseconds to wait to change slides (if does-continue is true). Defaults to 4000. * @param {boolean=} show-pager Whether a pager should be shown for this slide box. Accepts expressions via `show-pager="{{shouldShow()}}"`. Defaults to true. * @param {expression=} pager-click Expression to call when a pager is clicked (if show-pager is true). Is passed the 'index' variable. * @param {expression=} on-slide-changed Expression called whenever the slide is changed. Is passed an '$index' variable. * @param {expression=} active-slide Model to bind the current slide index to. */ IonicModule .directive('ionSlideBox', [ '$animate', '$timeout', '$compile', '$ionicSlideBoxDelegate', '$ionicHistory', '$ionicScrollDelegate', function($animate, $timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory, $ionicScrollDelegate) { return { restrict: 'E', replace: true, transclude: true, scope: { autoPlay: '=', doesContinue: '@', slideInterval: '@', showPager: '@', pagerClick: '&', disableScroll: '@', onSlideChanged: '&', activeSlide: '=?', bounce: '@' }, controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { var _this = this;
var continuous = $scope.$eval($scope.doesContinue) === true; var bouncing = ($scope.$eval($scope.bounce) !== false); //Default to true
var shouldAutoPlay = isDefined($attrs.autoPlay) ? !!$scope.autoPlay : false; var slideInterval = shouldAutoPlay ? $scope.$eval($scope.slideInterval) || 4000 : 0;
var slider = new ionic.views.Slider({ el: $element[0], auto: slideInterval, continuous: continuous, startSlide: $scope.activeSlide, bouncing: bouncing, slidesChanged: function() { $scope.currentSlide = slider.currentIndex();
// Try to trigger a digest
$timeout(function() {}); }, callback: function(slideIndex) { $scope.currentSlide = slideIndex; $scope.onSlideChanged({ index: $scope.currentSlide, $index: $scope.currentSlide}); $scope.$parent.$broadcast('slideBox.slideChanged', slideIndex); $scope.activeSlide = slideIndex; // Try to trigger a digest
$timeout(function() {}); }, onDrag: function() { freezeAllScrolls(true); }, onDragEnd: function() { freezeAllScrolls(false); } });
function freezeAllScrolls(shouldFreeze) { if (shouldFreeze && !_this.isScrollFreeze) { $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);
} else if (!shouldFreeze && _this.isScrollFreeze) { $ionicScrollDelegate.freezeAllScrolls(false); } _this.isScrollFreeze = shouldFreeze; }
slider.enableSlide($scope.$eval($attrs.disableScroll) !== true);
$scope.$watch('activeSlide', function(nv) { if (isDefined(nv)) { slider.slide(nv); } });
$scope.$on('slideBox.nextSlide', function() { slider.next(); });
$scope.$on('slideBox.prevSlide', function() { slider.prev(); });
$scope.$on('slideBox.setSlide', function(e, index) { slider.slide(index); });
//Exposed for testing
this.__slider = slider;
var deregisterInstance = $ionicSlideBoxDelegate._registerInstance( slider, $attrs.delegateHandle, function() { return $ionicHistory.isActiveScope($scope); } ); $scope.$on('$destroy', function() { deregisterInstance(); slider.kill(); });
this.slidesCount = function() { return slider.slidesCount(); };
this.onPagerClick = function(index) { $scope.pagerClick({index: index}); };
$timeout(function() { slider.load(); }); }], template: '<div class="slider">' + '<div class="slider-slides" ng-transclude>' + '</div>' + '</div>',
link: function($scope, $element, $attr) { // Disable ngAnimate for slidebox and its children
$animate.enabled($element, false);
// if showPager is undefined, show the pager
if (!isDefined($attr.showPager)) { $scope.showPager = true; getPager().toggleClass('hide', !true); }
$attr.$observe('showPager', function(show) { if (show === undefined) return; show = $scope.$eval(show); getPager().toggleClass('hide', !show); });
var pager; function getPager() { if (!pager) { var childScope = $scope.$new(); pager = jqLite('<ion-pager></ion-pager>'); $element.append(pager); pager = $compile(pager)(childScope); } return pager; } } }; }]) .directive('ionSlide', function() { return { restrict: 'E', require: '?^ionSlideBox', compile: function(element) { element.addClass('slider-slide'); } }; })
.directive('ionPager', function() { return { restrict: 'E', replace: true, require: '^ionSlideBox', template: '<div class="slider-pager"><span class="slider-pager-page" ng-repeat="slide in numSlides() track by $index" ng-class="{active: $index == currentSlide}" ng-click="pagerClick($index)"><i class="icon ion-record"></i></span></div>', link: function($scope, $element, $attr, slideBox) { var selectPage = function(index) { var children = $element[0].children; var length = children.length; for (var i = 0; i < length; i++) { if (i == index) { children[i].classList.add('active'); } else { children[i].classList.remove('active'); } } };
$scope.pagerClick = function(index) { slideBox.onPagerClick(index); };
$scope.numSlides = function() { return new Array(slideBox.slidesCount()); };
$scope.$watch('currentSlide', function(v) { selectPage(v); }); } };
});
/** * @ngdoc directive * @name ionSlides * @module ionic * @delegate ionic.service:$ionicSlideBoxDelegate * @restrict E * @description * The Slides component is a powerful multi-page container where each page can be swiped or dragged between. * * Note: this is a new version of the Ionic Slide Box based on the [Swiper](http://www.idangero.us/swiper/#.Vmc1J-ODFBc) widget from
* [idangerous](http://www.idangero.us/).
* * ![SlideBox](http://ionicframework.com.s3.amazonaws.com/docs/controllers/slideBox.gif)
* * @usage * ```html
* <ion-content scroll="false"> * <ion-slides options="options" slider="data.slider"> * <ion-slide-page> * <div class="box blue"><h1>BLUE</h1></div> * </ion-slide-page> * <ion-slide-page> * <div class="box yellow"><h1>YELLOW</h1></div> * </ion-slide-page> * <ion-slide-page> * <div class="box pink"><h1>PINK</h1></div> * </ion-slide-page> * </ion-slides> * </ion-content> * ```
* * ```js
* $scope.options = { * loop: false, * effect: 'fade', * speed: 500, * } * * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){ * // data.slider is the instance of Swiper
* $scope.slider = data.slider; * }); * * $scope.$on("$ionicSlides.slideChangeStart", function(event, data){ * console.log('Slide change is beginning'); * }); * * $scope.$on("$ionicSlides.slideChangeEnd", function(event, data){ * // note: the indexes are 0-based
* $scope.activeIndex = data.activeIndex; * $scope.previousIndex = data.previousIndex; * }); * * ```
* * ## Slide Events * * The slides component dispatches events when the active slide changes * * <table class="table"> * <tr> * <td><code>$ionicSlides.slideChangeStart</code></td> * <td>This event is emitted when a slide change begins</td> * </tr> * <tr> * <td><code>$ionicSlides.slideChangeEnd</code></td> * <td>This event is emitted when a slide change completes</td> * </tr> * <tr> * <td><code>$ionicSlides.sliderInitialized</code></td> * <td>This event is emitted when the slider is initialized. It provides access to an instance of the slider.</td> * </tr> * </table> * * * ## Updating Slides Dynamically * When applying data to the slider at runtime, typically everything will work as expected. * * In the event that the slides are looped, use the `updateLoop` method on the slider to ensure the slides update correctly. * * ```
* $scope.$on("$ionicSlides.sliderInitialized", function(event, data){ * // grab an instance of the slider
* $scope.slider = data.slider; * }); * * function dataChangeHandler(){ * // call this function when data changes, such as an HTTP request, etc
* if ( $scope.slider ){ * $scope.slider.updateLoop(); * } * } * ```
* */ IonicModule .directive('ionSlides', [ '$animate', '$timeout', '$compile', function($animate, $timeout, $compile) { return { restrict: 'E', transclude: true, scope: { options: '=', slider: '=' }, template: '<div class="swiper-container">' + '<div class="swiper-wrapper" ng-transclude>' + '</div>' + '<div ng-hide="!showPager" class="swiper-pagination"></div>' + '</div>', controller: ['$scope', '$element', function($scope, $element) { var _this = this;
this.update = function() { $timeout(function() { if (!_this.__slider) { return; }
_this.__slider.update(); if (_this._options.loop) { _this.__slider.createLoop(); }
var slidesLength = _this.__slider.slides.length;
// Don't allow pager to show with > 10 slides
if (slidesLength > 10) { $scope.showPager = false; }
// When slide index is greater than total then slide to last index
if (_this.__slider.activeIndex > slidesLength - 1) { _this.__slider.slideTo(slidesLength - 1); } }); };
this.rapidUpdate = ionic.debounce(function() { _this.update(); }, 50);
this.getSlider = function() { return _this.__slider; };
var options = $scope.options || {};
var newOptions = angular.extend({ pagination: $element.children().children()[1], paginationClickable: true, lazyLoading: true, preloadImages: false }, options);
this._options = newOptions;
$timeout(function() { var slider = new ionic.views.Swiper($element.children()[0], newOptions, $scope, $compile);
$scope.$emit("$ionicSlides.sliderInitialized", { slider: slider });
_this.__slider = slider; $scope.slider = _this.__slider;
$scope.$on('$destroy', function() { slider.destroy(); _this.__slider = null; }); });
$timeout(function() { // if it's a loop, render the slides again just incase
_this.rapidUpdate(); }, 200);
}],
link: function($scope) { $scope.showPager = true; // Disable ngAnimate for slidebox and its children
//$animate.enabled(false, $element);
} }; }]) .directive('ionSlidePage', [function() { return { restrict: 'E', require: '?^ionSlides', transclude: true, replace: true, template: '<div class="swiper-slide" ng-transclude></div>', link: function($scope, $element, $attr, ionSlidesCtrl) { ionSlidesCtrl.rapidUpdate();
$scope.$on('$destroy', function() { ionSlidesCtrl.rapidUpdate(); }); } }; }]);
/** * @ngdoc directive * @name ionSpinner * @module ionic * @restrict E * * @description * The `ionSpinner` directive provides a variety of animated spinners. * Spinners enables you to give your users feedback that the app is * processing/thinking/waiting/chillin' out, or whatever you'd like it to indicate. * By default, the {@link ionic.directive:ionRefresher} feature uses this spinner, rather * than rotating font icons (previously included in [ionicons](http://ionicons.com/)).
* While font icons are great for simple or stationary graphics, they're not suited to * provide great animations, which is why Ionic uses SVG instead. * * Ionic offers ten spinners out of the box, and by default, it will use the appropriate spinner * for the platform on which it's running. Under the hood, the `ionSpinner` directive dynamically * builds the required SVG element, which allows Ionic to provide all ten of the animated SVGs * within 3KB. * * <style> * .spinner-table { * max-width: 280px; * } * .spinner-table tbody > tr > th, .spinner-table tbody > tr > td { * vertical-align: middle; * width: 42px; * height: 42px; * } * .spinner { * stroke: #444; * fill: #444; } * .spinner svg { * width: 28px; * height: 28px; } * .spinner.spinner-inverse { * stroke: #fff; * fill: #fff; } * * .spinner-android { * stroke: #4b8bf4; } * * .spinner-ios, .spinner-ios-small { * stroke: #69717d; } * * .spinner-spiral .stop1 { * stop-color: #fff; * stop-opacity: 0; } * .spinner-spiral.spinner-inverse .stop1 { * stop-color: #000; } * .spinner-spiral.spinner-inverse .stop2 { * stop-color: #fff; } * </style> * * <script src="http://code.ionicframework.com/nightly/js/ionic.bundle.min.js"></script> * <table class="table spinner-table" ng-app="ionic"> * <tr> * <th> * <code>android</code> * </th> * <td> * <ion-spinner icon="android"></ion-spinner> * </td> * </tr> * <tr> * <th> * <code>ios</code> * </th> * <td> * <ion-spinner icon="ios"></ion-spinner> * </td> * </tr> * <tr> * <th> * <code>ios-small</code> * </th> * <td> * <ion-spinner icon="ios-small"></ion-spinner> * </td> * </tr> * <tr> * <th> * <code>bubbles</code> * </th> * <td> * <ion-spinner icon="bubbles"></ion-spinner> * </td> * </tr> * <tr> * <th> * <code>circles</code> * </th> * <td> * <ion-spinner icon="circles"></ion-spinner> * </td> * </tr> * <tr> * <th> * <code>crescent</code> * </th> * <td> * <ion-spinner icon="crescent"></ion-spinner> * </td> * </tr> * <tr> * <th> * <code>dots</code> * </th> * <td> * <ion-spinner icon="dots"></ion-spinner> * </td> * </tr> * <tr> * <th> * <code>lines</code> * </th> * <td> * <ion-spinner icon="lines"></ion-spinner> * </td> * </tr> * <tr> * <th> * <code>ripple</code> * </th> * <td> * <ion-spinner icon="ripple"></ion-spinner> * </td> * </tr> * <tr> * <th> * <code>spiral</code> * </th> * <td> * <ion-spinner icon="spiral"></ion-spinner> * </td> * </tr> * </table> * * Each spinner uses SVG with SMIL animations, however, the Android spinner also uses JavaScript * so it also works on Android 4.0-4.3. Additionally, each spinner can be styled with CSS, * and scaled to any size. * * * @usage * The following code would use the default spinner for the platform it's running from. If it's neither * iOS or Android, it'll default to use `ios`. * * ```html
* <ion-spinner></ion-spinner> * ```
* * By setting the `icon` attribute, you can specify which spinner to use, no matter what * the platform is. * * ```html
* <ion-spinner icon="spiral"></ion-spinner> * ```
* * ## Spinner Colors * Like with most of Ionic's other components, spinners can also be styled using * Ionic's standard color naming convention. For example: * * ```html
* <ion-spinner class="spinner-energized"></ion-spinner> * ```
* * * ## Styling SVG with CSS * One cool thing about SVG is its ability to be styled with CSS! Some of the properties * have different names, for example, SVG uses the term `stroke` instead of `border`, and * `fill` instead of `background-color`. * * ```css
* .spinner svg { * width: 28px; * height: 28px; * stroke: #444; * fill: #444; * } * ```
* */ IonicModule .directive('ionSpinner', function() { return { restrict: 'E', controller: '$ionicSpinner', link: function($scope, $element, $attrs, ctrl) { var spinnerName = ctrl.init(); $element.addClass('spinner spinner-' + spinnerName);
$element.on('$destroy', function onDestroy() { ctrl.stop(); }); } }; });
/** * @ngdoc directive * @name ionTab * @module ionic * @restrict E * @parent ionic.directive:ionTabs * * @description * Contains a tab's content. The content only exists while the given tab is selected. * * Each ionTab has its own view history. * * @usage * ```html
* <ion-tab * title="Tab!" * icon="my-icon" * href="#/tab/tab-link" * on-select="onTabSelected()" * on-deselect="onTabDeselected()"> * </ion-tab> * ```
* For a complete, working tab bar example, see the {@link ionic.directive:ionTabs} documentation. * * @param {string} title The title of the tab. * @param {string=} href The link that this tab will navigate to when tapped. * @param {string=} icon The icon of the tab. If given, this will become the default for icon-on and icon-off. * @param {string=} icon-on The icon of the tab while it is selected. * @param {string=} icon-off The icon of the tab while it is not selected. * @param {expression=} badge The badge to put on this tab (usually a number). * @param {expression=} badge-style The style of badge to put on this tab (eg: badge-positive). * @param {expression=} on-select Called when this tab is selected. * @param {expression=} on-deselect Called when this tab is deselected. * @param {expression=} ng-click By default, the tab will be selected on click. If ngClick is set, it will not. You can explicitly switch tabs using {@link ionic.service:$ionicTabsDelegate#select $ionicTabsDelegate.select()}. * @param {expression=} hidden Whether the tab is to be hidden or not. * @param {expression=} disabled Whether the tab is to be disabled or not. */ IonicModule .directive('ionTab', [ '$compile', '$ionicConfig', '$ionicBind', '$ionicViewSwitcher', function($compile, $ionicConfig, $ionicBind, $ionicViewSwitcher) {
//Returns ' key="value"' if value exists
function attrStr(k, v) { return isDefined(v) ? ' ' + k + '="' + v + '"' : ''; } return { restrict: 'E', require: ['^ionTabs', 'ionTab'], controller: '$ionicTab', scope: true, compile: function(element, attr) {
//We create the tabNavTemplate in the compile phase so that the
//attributes we pass down won't be interpolated yet - we want
//to pass down the 'raw' versions of the attributes
var tabNavTemplate = '<ion-tab-nav' + attrStr('ng-click', attr.ngClick) + attrStr('title', attr.title) + attrStr('icon', attr.icon) + attrStr('icon-on', attr.iconOn) + attrStr('icon-off', attr.iconOff) + attrStr('badge', attr.badge) + attrStr('badge-style', attr.badgeStyle) + attrStr('hidden', attr.hidden) + attrStr('disabled', attr.disabled) + attrStr('class', attr['class']) + '></ion-tab-nav>';
//Remove the contents of the element so we can compile them later, if tab is selected
var tabContentEle = document.createElement('div'); for (var x = 0; x < element[0].children.length; x++) { tabContentEle.appendChild(element[0].children[x].cloneNode(true)); } var childElementCount = tabContentEle.childElementCount; element.empty();
var navViewName, isNavView; if (childElementCount) { if (tabContentEle.children[0].tagName === 'ION-NAV-VIEW') { // get the name if it's a nav-view
navViewName = tabContentEle.children[0].getAttribute('name'); tabContentEle.children[0].classList.add('view-container'); isNavView = true; } if (childElementCount === 1) { // make the 1 child element the primary tab content container
tabContentEle = tabContentEle.children[0]; } if (!isNavView) tabContentEle.classList.add('pane'); tabContentEle.classList.add('tab-content'); }
return function link($scope, $element, $attr, ctrls) { var childScope; var childElement; var tabsCtrl = ctrls[0]; var tabCtrl = ctrls[1]; var isTabContentAttached = false; $scope.$tabSelected = false;
$ionicBind($scope, $attr, { onSelect: '&', onDeselect: '&', title: '@', uiSref: '@', href: '@' });
tabsCtrl.add($scope); $scope.$on('$destroy', function() { if (!$scope.$tabsDestroy) { // if the containing ionTabs directive is being destroyed
// then don't bother going through the controllers remove
// method, since remove will reset the active tab as each tab
// is being destroyed, causing unnecessary view loads and transitions
tabsCtrl.remove($scope); } tabNavElement.isolateScope().$destroy(); tabNavElement.remove(); tabNavElement = tabContentEle = childElement = null; });
//Remove title attribute so browser-tooltip does not apear
$element[0].removeAttribute('title');
if (navViewName) { tabCtrl.navViewName = $scope.navViewName = navViewName; } $scope.$on('$stateChangeSuccess', selectIfMatchesState); selectIfMatchesState(); function selectIfMatchesState() { if (tabCtrl.tabMatchesState()) { tabsCtrl.select($scope, false); } }
var tabNavElement = jqLite(tabNavTemplate); tabNavElement.data('$ionTabsController', tabsCtrl); tabNavElement.data('$ionTabController', tabCtrl); tabsCtrl.$tabsElement.append($compile(tabNavElement)($scope));
function tabSelected(isSelected) { if (isSelected && childElementCount) { // this tab is being selected
// check if the tab is already in the DOM
// only do this if the tab has child elements
if (!isTabContentAttached) { // tab should be selected and is NOT in the DOM
// create a new scope and append it
childScope = $scope.$new(); childElement = jqLite(tabContentEle); $ionicViewSwitcher.viewEleIsActive(childElement, true); tabsCtrl.$element.append(childElement); $compile(childElement)(childScope); isTabContentAttached = true; }
// remove the hide class so the tabs content shows up
$ionicViewSwitcher.viewEleIsActive(childElement, true);
} else if (isTabContentAttached && childElement) { // this tab should NOT be selected, and it is already in the DOM
if ($ionicConfig.views.maxCache() > 0) { // keep the tabs in the DOM, only css hide it
$ionicViewSwitcher.viewEleIsActive(childElement, false);
} else { // do not keep tabs in the DOM
destroyTab(); }
} }
function destroyTab() { childScope && childScope.$destroy(); isTabContentAttached && childElement && childElement.remove(); tabContentEle.innerHTML = ''; isTabContentAttached = childScope = childElement = null; }
$scope.$watch('$tabSelected', tabSelected);
$scope.$on('$ionicView.afterEnter', function() { $ionicViewSwitcher.viewEleIsActive(childElement, $scope.$tabSelected); });
$scope.$on('$ionicView.clearCache', function() { if (!$scope.$tabSelected) { destroyTab(); } });
}; } }; }]);
IonicModule .directive('ionTabNav', [function() { return { restrict: 'E', replace: true, require: ['^ionTabs', '^ionTab'], template: '<a ng-class="{\'has-badge\':badge, \'tab-hidden\':isHidden(), \'tab-item-active\': isTabActive()}" ' + ' ng-disabled="disabled()" class="tab-item">' + '<span class="badge {{badgeStyle}}" ng-if="badge">{{badge}}</span>' + '<i class="icon {{getIcon()}}" ng-if="getIcon()"></i>' + '<span class="tab-title" ng-bind-html="title"></span>' + '</a>', scope: { title: '@', icon: '@', iconOn: '@', iconOff: '@', badge: '=', hidden: '@', disabled: '&', badgeStyle: '@', 'class': '@' }, link: function($scope, $element, $attrs, ctrls) { var tabsCtrl = ctrls[0], tabCtrl = ctrls[1];
//Remove title attribute so browser-tooltip does not apear
$element[0].removeAttribute('title');
$scope.selectTab = function(e) { e.preventDefault(); tabsCtrl.select(tabCtrl.$scope, true); }; if (!$attrs.ngClick) { $element.on('click', function(event) { $scope.$apply(function() { $scope.selectTab(event); }); }); }
$scope.isHidden = function() { if ($attrs.hidden === 'true' || $attrs.hidden === true) return true; return false; };
$scope.getIconOn = function() { return $scope.iconOn || $scope.icon; }; $scope.getIconOff = function() { return $scope.iconOff || $scope.icon; };
$scope.isTabActive = function() { return tabsCtrl.selectedTab() === tabCtrl.$scope; };
$scope.getIcon = function() { if ( tabsCtrl.selectedTab() === tabCtrl.$scope ) { // active
return $scope.iconOn || $scope.icon; } else { // inactive
return $scope.iconOff || $scope.icon; } }; } }; }]);
/** * @ngdoc directive * @name ionTabs * @module ionic * @delegate ionic.service:$ionicTabsDelegate * @restrict E * @codepen odqCz * * @description * Powers a multi-tabbed interface with a Tab Bar and a set of "pages" that can be tabbed * through. * * Assign any [tabs class](/docs/components#tabs) to the element to define * its look and feel. * * For iOS, tabs will appear at the bottom of the screen. For Android, tabs will be at the top * of the screen, below the nav-bar. This follows each OS's design specification, but can be * configured with the {@link ionic.provider:$ionicConfigProvider}. * * See the {@link ionic.directive:ionTab} directive's documentation for more details on * individual tabs. * * Note: do not place ion-tabs inside of an ion-content element; it has been known to cause a * certain CSS bug. * * @usage * ```html
* <ion-tabs class="tabs-positive tabs-icon-top"> * * <ion-tab title="Home" icon-on="ion-ios-filing" icon-off="ion-ios-filing-outline"> * <!-- Tab 1 content --> * </ion-tab> * * <ion-tab title="About" icon-on="ion-ios-clock" icon-off="ion-ios-clock-outline"> * <!-- Tab 2 content --> * </ion-tab> * * <ion-tab title="Settings" icon-on="ion-ios-gear" icon-off="ion-ios-gear-outline"> * <!-- Tab 3 content --> * </ion-tab> * * </ion-tabs> * ```
* * @param {string=} delegate-handle The handle used to identify these tabs * with {@link ionic.service:$ionicTabsDelegate}. */
IonicModule .directive('ionTabs', [ '$ionicTabsDelegate', '$ionicConfig', function($ionicTabsDelegate, $ionicConfig) { return { restrict: 'E', scope: true, controller: '$ionicTabs', compile: function(tElement) { //We cannot use regular transclude here because it breaks element.data()
//inheritance on compile
var innerElement = jqLite('<div class="tab-nav tabs">'); innerElement.append(tElement.contents());
tElement.append(innerElement) .addClass('tabs-' + $ionicConfig.tabs.position() + ' tabs-' + $ionicConfig.tabs.style());
return { pre: prelink, post: postLink }; function prelink($scope, $element, $attr, tabsCtrl) { var deregisterInstance = $ionicTabsDelegate._registerInstance( tabsCtrl, $attr.delegateHandle, tabsCtrl.hasActiveScope );
tabsCtrl.$scope = $scope; tabsCtrl.$element = $element; tabsCtrl.$tabsElement = jqLite($element[0].querySelector('.tabs'));
$scope.$watch(function() { return $element[0].className; }, function(value) { var isTabsTop = value.indexOf('tabs-top') !== -1; var isHidden = value.indexOf('tabs-item-hide') !== -1; $scope.$hasTabs = !isTabsTop && !isHidden; $scope.$hasTabsTop = isTabsTop && !isHidden; $scope.$emit('$ionicTabs.top', $scope.$hasTabsTop); });
function emitLifecycleEvent(ev, data) { ev.stopPropagation(); var previousSelectedTab = tabsCtrl.previousSelectedTab(); if (previousSelectedTab) { previousSelectedTab.$broadcast(ev.name.replace('NavView', 'Tabs'), data); } }
$scope.$on('$ionicNavView.beforeLeave', emitLifecycleEvent); $scope.$on('$ionicNavView.afterLeave', emitLifecycleEvent); $scope.$on('$ionicNavView.leave', emitLifecycleEvent);
$scope.$on('$destroy', function() { // variable to inform child tabs that they're all being blown away
// used so that while destorying an individual tab, each one
// doesn't select the next tab as the active one, which causes unnecessary
// loading of tab views when each will eventually all go away anyway
$scope.$tabsDestroy = true; deregisterInstance(); tabsCtrl.$tabsElement = tabsCtrl.$element = tabsCtrl.$scope = innerElement = null; delete $scope.$hasTabs; delete $scope.$hasTabsTop; }); }
function postLink($scope, $element, $attr, tabsCtrl) { if (!tabsCtrl.selectedTab()) { // all the tabs have been added
// but one hasn't been selected yet
tabsCtrl.select(0); } } } }; }]);
/** * @ngdoc directive * @name ionTitle * @module ionic * @restrict E * * Used for titles in header and nav bars. New in 1.2 * * Identical to <div class="title"> but with future compatibility for Ionic 2 * * @usage * * ```html
* <ion-nav-bar> * <ion-title>Hello</ion-title> * <ion-nav-bar> * ```
*/ IonicModule .directive('ionTitle', [function() { return { restrict: 'E', compile: function(element) { element.addClass('title'); } }; }]);
/** * @ngdoc directive * @name ionToggle * @module ionic * @codepen tfAzj * @restrict E * * @description * A toggle is an animated switch which binds a given model to a boolean. * * Allows dragging of the switch's nub. * * The toggle behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]) otherwise.
* * @param toggle-class {string=} Sets the CSS class on the inner `label.toggle` element created by the directive. * * @usage * Below is an example of a toggle directive which is wired up to the `airplaneMode` model * and has the `toggle-calm` CSS class assigned to the inner element. * * ```html
* <ion-toggle ng-model="airplaneMode" toggle-class="toggle-calm">Airplane Mode</ion-toggle> * ```
*/ IonicModule .directive('ionToggle', [ '$timeout', '$ionicConfig', function($timeout, $ionicConfig) {
return { restrict: 'E', replace: true, require: '?ngModel', transclude: true, template: '<div class="item item-toggle">' + '<div ng-transclude></div>' + '<label class="toggle">' + '<input type="checkbox">' + '<div class="track">' + '<div class="handle"></div>' + '</div>' + '</label>' + '</div>',
compile: function(element, attr) { var input = element.find('input'); forEach({ 'name': attr.name, 'ng-value': attr.ngValue, 'ng-model': attr.ngModel, 'ng-checked': attr.ngChecked, 'ng-disabled': attr.ngDisabled, 'ng-true-value': attr.ngTrueValue, 'ng-false-value': attr.ngFalseValue, 'ng-change': attr.ngChange, 'ng-required': attr.ngRequired, 'required': attr.required }, function(value, name) { if (isDefined(value)) { input.attr(name, value); } });
if (attr.toggleClass) { element[0].getElementsByTagName('label')[0].classList.add(attr.toggleClass); }
element.addClass('toggle-' + $ionicConfig.form.toggle());
return function($scope, $element) { var el = $element[0].getElementsByTagName('label')[0]; var checkbox = el.children[0]; var track = el.children[1]; var handle = track.children[0];
var ngModelController = jqLite(checkbox).controller('ngModel');
$scope.toggle = new ionic.views.Toggle({ el: el, track: track, checkbox: checkbox, handle: handle, onChange: function() { if (ngModelController) { ngModelController.$setViewValue(checkbox.checked); $scope.$apply(); } } });
$scope.$on('$destroy', function() { $scope.toggle.destroy(); }); }; }
}; }]);
/** * @ngdoc directive * @name ionView * @module ionic * @restrict E * @parent ionNavView * * @description * A container for view content and any navigational and header bar information. When a view * enters and exits its parent {@link ionic.directive:ionNavView}, the view also emits view * information, such as its title, whether the back button should be displayed or not, whether the * corresponding {@link ionic.directive:ionNavBar} should be displayed or not, which transition the view * should use to animate, and which direction to animate. * * *Views are cached to improve performance.* When a view is navigated away from, its element is * left in the DOM, and its scope is disconnected from the `$watch` cycle. When navigating to a * view that is already cached, its scope is reconnected, and the existing element, which was * left in the DOM, becomes active again. This can be disabled, or the maximum number of cached * views changed in {@link ionic.provider:$ionicConfigProvider}, in the view's `$state` configuration, or * as an attribute on the view itself (see below). * * @usage * Below is an example where our page will load with a {@link ionic.directive:ionNavBar} containing * "My Page" as the title. * * ```html
* <ion-nav-bar></ion-nav-bar> * <ion-nav-view> * <ion-view view-title="My Page"> * <ion-content> * Hello! * </ion-content> * </ion-view> * </ion-nav-view> * ```
* * ## View LifeCycle and Events * * Views can be cached, which means ***controllers normally only load once***, which may * affect your controller logic. To know when a view has entered or left, events * have been added that are emitted from the view's scope. These events also * contain data about the view, such as the title and whether the back button should * show. Also contained is transition data, such as the transition type and * direction that will be or was used. * * Life cycle events are emitted upwards from the transitioning view's scope. In some cases, it is * desirable for a child/nested view to be notified of the event. * For this use case, `$ionicParentView` life cycle events are broadcast downwards. * * <table class="table"> * <tr> * <td><code>$ionicView.loaded</code></td> * <td>The view has loaded. This event only happens once per * view being created and added to the DOM. If a view leaves but is cached, * then this event will not fire again on a subsequent viewing. The loaded event * is good place to put your setup code for the view; however, it is not the * recommended event to listen to when a view becomes active.</td> * </tr> * <tr> * <td><code>$ionicView.enter</code></td> * <td>The view has fully entered and is now the active view. * This event will fire, whether it was the first load or a cached view.</td> * </tr> * <tr> * <td><code>$ionicView.leave</code></td> * <td>The view has finished leaving and is no longer the * active view. This event will fire, whether it is cached or destroyed.</td> * </tr> * <tr> * <td><code>$ionicView.beforeEnter</code></td> * <td>The view is about to enter and become the active view.</td> * </tr> * <tr> * <td><code>$ionicView.beforeLeave</code></td> * <td>The view is about to leave and no longer be the active view.</td> * </tr> * <tr> * <td><code>$ionicView.afterEnter</code></td> * <td>The view has fully entered and is now the active view.</td> * </tr> * <tr> * <td><code>$ionicView.afterLeave</code></td> * <td>The view has finished leaving and is no longer the active view.</td> * </tr> * <tr> * <td><code>$ionicView.unloaded</code></td> * <td>The view's controller has been destroyed and its element has been * removed from the DOM.</td> * </tr> * <tr> * <td><code>$ionicParentView.enter</code></td> * <td>The parent view has fully entered and is now the active view. * This event will fire, whether it was the first load or a cached view.</td> * </tr> * <tr> * <td><code>$ionicParentView.leave</code></td> * <td>The parent view has finished leaving and is no longer the * active view. This event will fire, whether it is cached or destroyed.</td> * </tr> * <tr> * <td><code>$ionicParentView.beforeEnter</code></td> * <td>The parent view is about to enter and become the active view.</td> * </tr> * <tr> * <td><code>$ionicParentView.beforeLeave</code></td> * <td>The parent view is about to leave and no longer be the active view.</td> * </tr> * <tr> * <td><code>$ionicParentView.afterEnter</code></td> * <td>The parent view has fully entered and is now the active view.</td> * </tr> * <tr> * <td><code>$ionicParentView.afterLeave</code></td> * <td>The parent view has finished leaving and is no longer the active view.</td> * </tr> * </table> * * ## LifeCycle Event Usage * * Below is an example of how to listen to life cycle events and * access state parameter data * * ```js
* $scope.$on("$ionicView.beforeEnter", function(event, data){ * // handle event
* console.log("State Params: ", data.stateParams); * }); * * $scope.$on("$ionicView.enter", function(event, data){ * // handle event
* console.log("State Params: ", data.stateParams); * }); * * $scope.$on("$ionicView.afterEnter", function(event, data){ * // handle event
* console.log("State Params: ", data.stateParams); * }); * ```
* * ## Caching * * Caching can be disabled and enabled in multiple ways. By default, Ionic will * cache a maximum of 10 views. You can optionally choose to disable caching at * either an individual view basis, or by global configuration. Please see the * _Caching_ section in {@link ionic.directive:ionNavView} for more info. * * @param {string=} view-title A text-only title to display on the parent {@link ionic.directive:ionNavBar}. * For an HTML title, such as an image, see {@link ionic.directive:ionNavTitle} instead. * @param {boolean=} cache-view If this view should be allowed to be cached or not. * Please see the _Caching_ section in {@link ionic.directive:ionNavView} for * more info. Default `true` * @param {boolean=} can-swipe-back If this view should be allowed to use the swipe to go back gesture or not. * This does not enable the swipe to go back feature if it is not available for the platform it's running * from, or there isn't a previous view. Default `true` * @param {boolean=} hide-back-button Whether to hide the back button on the parent * {@link ionic.directive:ionNavBar} by default. * @param {boolean=} hide-nav-bar Whether to hide the parent * {@link ionic.directive:ionNavBar} by default. */ IonicModule .directive('ionView', function() { return { restrict: 'EA', priority: 1000, controller: '$ionicView', compile: function(tElement) { tElement.addClass('pane'); tElement[0].removeAttribute('title'); return function link($scope, $element, $attrs, viewCtrl) { viewCtrl.init(); }; } }; });
})();
|