/*! * Copyright 2014 Drifty Co. * http://drifty.com/ * * Ionic, v1.0.0-beta.14 * A powerful HTML5 mobile app framework. * http://ionicframework.com/ * * By @maxlynch, @benjsperry, @adamdbradley <3 * * Licensed under the MIT license. Please see LICENSE for more information. * */ (function() { /* * deprecated.js * https://github.com/wearefractal/deprecated/ * Copyright (c) 2014 Fractal * License MIT */ //Interval object var deprecated = { method: function(msg, log, fn) { var called = false; return function deprecatedMethod() { if (!called) { called = true; log(msg); } return fn.apply(this, arguments); }; }, field: function(msg, log, parent, field, val) { var called = false; var getter = function() { if (!called) { called = true; log(msg); } return val; }; var setter = function(v) { if (!called) { called = true; log(msg); } val = v; return v; }; Object.defineProperty(parent, field, { get: getter, set: setter, enumerable: true }); return; } }; var IonicModule = angular.module('ionic', ['ngAnimate', 'ngSanitize', 'ui.router']), extend = angular.extend, forEach = angular.forEach, isDefined = angular.isDefined, isNumber = angular.isNumber, isString = angular.isString, jqLite = angular.element; /** * @ngdoc service * @name $ionicActionSheet * @module ionic * @description * The Action Sheet is a slide-up pane that lets the user choose from a set of options. * Dangerous options are highlighted in red and made obvious. * * There are easy ways to cancel out of the action sheet, such as tapping the backdrop or even * hitting escape on the keyboard for desktop testing. * * ![Action Sheet](http://ionicframework.com.s3.amazonaws.com/docs/controllers/actionSheet.gif) * * @usage * To trigger an Action Sheet in your code, use the $ionicActionSheet service in your angular controllers: * * ```js * angular.module('mySuperApp', ['ionic']) * .controller(function($scope, $ionicActionSheet, $timeout) { * * // Triggered on a button click, or some other target * $scope.show = function() { * * // Show the action sheet * var hideSheet = $ionicActionSheet.show({ * buttons: [ * { text: 'Share This' }, * { text: 'Move' } * ], * destructiveText: 'Delete', * titleText: 'Modify your album', * cancelText: 'Cancel', * cancel: function() { // add cancel code.. }, * buttonClicked: function(index) { * return true; * } * }); * * // For example's sake, hide the sheet after two seconds * $timeout(function() { * hideSheet(); * }, 2000); * * }; * }); * ``` * */ IonicModule .factory('$ionicActionSheet', [ '$rootScope', '$compile', '$animate', '$timeout', '$ionicTemplateLoader', '$ionicPlatform', '$ionicBody', function($rootScope, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform, $ionicBody) { return { show: actionSheet }; /** * @ngdoc method * @name $ionicActionSheet#show * @description * Load and return a new action sheet. * * A new isolated scope will be created for the * action sheet and the new element will be appended into the body. * * @param {object} options The options for this ActionSheet. Properties: * * - `[Object]` `buttons` Which buttons to show. Each button is an object with a `text` field. * - `{string}` `titleText` The title to show on the action sheet. * - `{string=}` `cancelText` the text for a 'cancel' button on the action sheet. * - `{string=}` `destructiveText` The text for a 'danger' on the action sheet. * - `{function=}` `cancel` Called if the cancel button is pressed, the backdrop is tapped or * the hardware back button is pressed. * - `{function=}` `buttonClicked` Called when one of the non-destructive buttons is clicked, * with the index of the button that was clicked and the button object. Return true to close * the action sheet, or false to keep it opened. * - `{function=}` `destructiveButtonClicked` Called when the destructive button is clicked. * Return true to close the action sheet, or false to keep it opened. * - `{boolean=}` `cancelOnStateChange` Whether to cancel the actionSheet when navigating * to a new state. Default true. * - `{string}` `cssClass` The custom CSS class name. * * @returns {function} `hideSheet` A function which, when called, hides & cancels the action sheet. */ function actionSheet(opts) { var scope = $rootScope.$new(true); angular.extend(scope, { cancel: angular.noop, destructiveButtonClicked: angular.noop, buttonClicked: angular.noop, $deregisterBackButton: angular.noop, buttons: [], cancelOnStateChange: true }, opts || {}); // Compile the template var element = scope.element = $compile('')(scope); // Grab the sheet element for animation var sheetEl = jqLite(element[0].querySelector('.action-sheet-wrapper')); var stateChangeListenDone = scope.cancelOnStateChange ? $rootScope.$on('$stateChangeSuccess', function() { scope.cancel(); }) : angular.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 || angular.noop)(); }); }; scope.showSheet = function(done) { if (scope.removed) return; $ionicBody.append(element) .addClass('action-sheet-open'); $animate.addClass(element, 'active').then(function() { if (scope.removed) return; (done || angular.noop)(); }); $timeout(function() { if (scope.removed) return; sheetEl.addClass('action-sheet-up'); }, 20, false); }; // registerBackButtonAction returns a callback to deregister the action scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction( function() { $timeout(scope.cancel); }, PLATFORM_BACK_BUTTON_PRIORITY_ACTION_SHEET ); // called when the user presses the cancel button scope.cancel = function() { // after the animation is out, call the cancel callback scope.removeSheet(opts.cancel); }; scope.buttonClicked = function(index) { // Check if the button click event returned true, which means // we can close the action sheet if (opts.buttonClicked(index, opts.buttons[index]) === true) { scope.removeSheet(); } }; scope.destructiveButtonClicked = function() { // Check if the destructive button click event returned true, which means // we can close the action sheet if (opts.destructiveButtonClicked() === true) { scope.removeSheet(); } }; scope.showSheet(); // Expose the scope on $ionicActionSheet's return value for the sake // of testing it. scope.cancel.$scope = scope; return scope.cancel; } }]); jqLite.prototype.addClass = function(cssClasses) { var x, y, cssClass, el, splitClasses, existingClasses; if (cssClasses && cssClasses != 'ng-scope' && cssClasses != 'ng-isolate-scope') { for (x = 0; x < this.length; x++) { el = this[x]; if (el.setAttribute) { if (cssClasses.indexOf(' ') < 0 && el.classList.add) { el.classList.add(cssClasses); } else { existingClasses = (' ' + (el.getAttribute('class') || '') + ' ') .replace(/[\n\t]/g, " "); splitClasses = cssClasses.split(' '); for (y = 0; y < splitClasses.length; y++) { cssClass = splitClasses[y].trim(); if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { existingClasses += cssClass + ' '; } } el.setAttribute('class', existingClasses.trim()); } } } } return this; }; jqLite.prototype.removeClass = function(cssClasses) { var x, y, splitClasses, cssClass, el; if (cssClasses) { for (x = 0; x < this.length; x++) { el = this[x]; if (el.getAttribute) { if (cssClasses.indexOf(' ') < 0 && el.classList.remove) { el.classList.remove(cssClasses); } else { splitClasses = cssClasses.split(' '); for (y = 0; y < splitClasses.length; y++) { cssClass = splitClasses[y]; el.setAttribute('class', ( (" " + (el.getAttribute('class') || '') + " ") .replace(/[\n\t]/g, " ") .replace(" " + cssClass.trim() + " ", " ")).trim() ); } } } } } return this; }; /** * @private */ IonicModule .factory('$$ionicAttachDrag', [function() { return attachDrag; function attachDrag(scope, element, options) { var opts = extend({}, { getDistance: function() { return opts.element.prop('offsetWidth'); }, onDragStart: angular.noop, onDrag: angular.noop, onDragEnd: angular.noop }, options); var dragStartGesture = ionic.onGesture('dragstart', handleDragStart, element[0]); var dragGesture = ionic.onGesture('drag', handleDrag, element[0]); var dragEndGesture = ionic.onGesture('dragend', handleDragEnd, element[0]); scope.$on('$destroy', function() { ionic.offGesture(dragStartGesture, 'dragstart', handleDragStart); ionic.offGesture(dragGesture, 'drag', handleDrag); ionic.offGesture(dragEndGesture, 'dragend', handleDragEnd); }); var isDragging = false; element.on('touchmove pointermove mousemove', function(ev) { if (isDragging) ev.preventDefault(); }); element.on('touchend mouseup mouseleave', function(ev) { isDragging = false; }); var dragState; function handleDragStart(ev) { if (dragState) return; if (opts.onDragStart() !== false) { dragState = { startX: ev.gesture.center.pageX, startY: ev.gesture.center.pageY, distance: opts.getDistance() }; } } function handleDrag(ev) { if (!dragState) return; var deltaX = dragState.startX - ev.gesture.center.pageX; var deltaY = dragState.startY - ev.gesture.center.pageY; var isVertical = ev.gesture.direction === 'up' || ev.gesture.direction === 'down'; if (isVertical && Math.abs(deltaY) > Math.abs(deltaX) * 2) { handleDragEnd(ev); return; } if (Math.abs(deltaX) > Math.abs(deltaY) * 2) { isDragging = true; } var percent = getDragPercent(ev.gesture.center.pageX); opts.onDrag(percent); } function handleDragEnd(ev) { if (!dragState) return; var percent = getDragPercent(ev.gesture.center.pageX); options.onDragEnd(percent, ev.gesture.velocityX); dragState = null; } function getDragPercent(x) { var delta = dragState.startX - x; var percent = delta / dragState.distance; return percent; } } }]); /** * @ngdoc service * @name $ionicBackdrop * @module ionic * @description * Shows and hides a backdrop over the UI. Appears behind popups, loading, * and other overlays. * * Often, multiple UI components require a backdrop, but only one backdrop is * ever needed in the DOM at a time. * * Therefore, each component that requires the backdrop to be shown calls * `$ionicBackdrop.retain()` when it wants the backdrop, then `$ionicBackdrop.release()` * when it is done with the backdrop. * * For each time `retain` is called, the backdrop will be shown until `release` is called. * * For example, if `retain` is called three times, the backdrop will be shown until `release` * is called three times. * * @usage * * ```js * function MyController($scope, $ionicBackdrop, $timeout) { * //Show a backdrop for one second * $scope.action = function() { * $ionicBackdrop.retain(); * $timeout(function() { * $ionicBackdrop.release(); * }, 1000); * }; * } * ``` */ IonicModule .factory('$ionicBackdrop', [ '$document', '$timeout', function($document, $timeout) { var el = jqLite('
'); var backdropHolds = 0; $document[0].body.appendChild(el[0]); return { /** * @ngdoc method * @name $ionicBackdrop#retain * @description Retains the backdrop. */ retain: retain, /** * @ngdoc method * @name $ionicBackdrop#release * @description * Releases the backdrop. */ release: release, getElement: getElement, // exposed for testing _element: el }; function retain() { if ((++backdropHolds) === 1) { el.addClass('visible'); ionic.requestAnimationFrame(function() { backdropHolds && el.addClass('active'); }); } } function release() { if ((--backdropHolds) === 0) { el.removeClass('active'); $timeout(function() { !backdropHolds && el.removeClass('visible'); }, 400, false); } } function getElement() { return el; } }]); /** * @private */ IonicModule .factory('$ionicBind', ['$parse', '$interpolate', function($parse, $interpolate) { var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; return function(scope, attrs, bindDefinition) { forEach(bindDefinition || {}, function (definition, scopeName) { //Adapted from angular.js $compile var match = definition.match(LOCAL_REGEXP) || [], attrName = match[3] || scopeName, mode = match[1], // @, =, or & parentGet, unwatch; switch(mode) { case '@': if (!attrs[attrName]) { return; } attrs.$observe(attrName, function(value) { scope[scopeName] = value; }); // we trigger an interpolation to ensure // the value is there for use immediately if (attrs[attrName]) { scope[scopeName] = $interpolate(attrs[attrName])(scope); } break; case '=': if (!attrs[attrName]) { return; } unwatch = scope.$watch(attrs[attrName], function(value) { scope[scopeName] = value; }); //Destroy parent scope watcher when this scope is destroyed scope.$on('$destroy', unwatch); break; case '&': /* jshint -W044 */ if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) { throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' + attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.'); } parentGet = $parse(attrs[attrName]); scope[scopeName] = function(locals) { return parentGet(scope, locals); }; break; } }); }; }]); /** * @ngdoc service * @name $ionicBody * @module ionic * @description An angular utility service to easily and efficiently * add and remove CSS classes from the document's body element. */ IonicModule .factory('$ionicBody', ['$document', function($document) { return { /** * @ngdoc method * @name $ionicBody#add * @description Add a class to the document's body element. * @param {string} class Each argument will be added to the body element. * @returns {$ionicBody} The $ionicBody service so methods can be chained. */ addClass: function() { for (var x = 0; x < arguments.length; x++) { $document[0].body.classList.add(arguments[x]); } return this; }, /** * @ngdoc method * @name $ionicBody#removeClass * @description Remove a class from the document's body element. * @param {string} class Each argument will be removed from the body element. * @returns {$ionicBody} The $ionicBody service so methods can be chained. */ removeClass: function() { for (var x = 0; x < arguments.length; x++) { $document[0].body.classList.remove(arguments[x]); } return this; }, /** * @ngdoc method * @name $ionicBody#enableClass * @description Similar to the `add` method, except the first parameter accepts a boolean * value determining if the class should be added or removed. Rather than writing user code, * such as "if true then add the class, else then remove the class", this method can be * given a true or false value which reduces redundant code. * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed. * @param {string} class Each remaining argument would be added or removed depending on * the first argument. * @returns {$ionicBody} The $ionicBody service so methods can be chained. */ enableClass: function(shouldEnableClass) { var args = Array.prototype.slice.call(arguments).slice(1); if (shouldEnableClass) { this.addClass.apply(this, args); } else { this.removeClass.apply(this, args); } return this; }, /** * @ngdoc method * @name $ionicBody#append * @description Append a child to the document's body. * @param {element} element The element to be appended to the body. The passed in element * can be either a jqLite element, or a DOM element. * @returns {$ionicBody} The $ionicBody service so methods can be chained. */ append: function(ele) { $document[0].body.appendChild(ele.length ? ele[0] : ele); return this; }, /** * @ngdoc method * @name $ionicBody#get * @description Get the document's body element. * @returns {element} Returns the document's body element. */ get: function() { return $document[0].body; } }; }]); IonicModule .factory('$ionicClickBlock', [ '$document', '$ionicBody', '$timeout', function($document, $ionicBody, $timeout) { var CSS_HIDE = 'click-block-hide'; var cbEle, fallbackTimer, pendingShow; function addClickBlock() { if (pendingShow) { if (cbEle) { cbEle.classList.remove(CSS_HIDE); } else { cbEle = $document[0].createElement('div'); cbEle.className = 'click-block'; $ionicBody.append(cbEle); } 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); ionic.requestAnimationFrame(addClickBlock); }, hide: function() { pendingShow = false; $timeout.cancel(fallbackTimer); ionic.requestAnimationFrame(removeClickBlock); } }; }]); IonicModule .factory('$collectionDataSource', [ '$cacheFactory', '$parse', '$rootScope', function($cacheFactory, $parse, $rootScope) { function hideWithTransform(element) { element.css(ionic.CSS.TRANSFORM, 'translate3d(-2000px,-2000px,0)'); } function CollectionRepeatDataSource(options) { var self = this; this.scope = options.scope; this.transcludeFn = options.transcludeFn; this.transcludeParent = options.transcludeParent; this.element = options.element; this.keyExpr = options.keyExpr; this.listExpr = options.listExpr; this.trackByExpr = options.trackByExpr; this.heightGetter = options.heightGetter; this.widthGetter = options.widthGetter; this.dimensions = []; this.data = []; this.attachedItems = {}; this.BACKUP_ITEMS_LENGTH = 20; this.backupItemsArray = []; } CollectionRepeatDataSource.prototype = { setup: function() { if (this.isSetup) return; this.isSetup = true; for (var i = 0; i < this.BACKUP_ITEMS_LENGTH; i++) { this.detachItem(this.createItem()); } }, destroy: function() { this.dimensions.length = 0; this.data = null; this.backupItemsArray.length = 0; this.attachedItems = {}; }, calculateDataDimensions: function() { var locals = {}; this.dimensions = this.data.map(function(value, index) { locals[this.keyExpr] = value; locals.$index = index; return { width: this.widthGetter(this.scope, locals), height: this.heightGetter(this.scope, locals) }; }, this); this.dimensions = this.beforeSiblings.concat(this.dimensions).concat(this.afterSiblings); this.dataStartIndex = this.beforeSiblings.length; }, createItem: function() { var item = {}; item.scope = this.scope.$new(); this.transcludeFn(item.scope, function(clone) { clone.css('position', 'absolute'); item.element = clone; }); this.transcludeParent.append(item.element); return item; }, getItem: function(index) { var item; if ( (item = this.attachedItems[index]) ) { //do nothing, the item is good } else if ( (item = this.backupItemsArray.pop()) ) { ionic.Utils.reconnectScope(item.scope); } else { item = this.createItem(); } return item; }, attachItemAtIndex: function(index) { if (index < this.dataStartIndex) { return this.beforeSiblings[index]; } // Subtract so we start at the beginning of this.data, after // this.beforeSiblings. index -= this.dataStartIndex; if (index > this.data.length - 1) { return this.afterSiblings[index - this.dataStartIndex]; } var item = this.getItem(index); var value = this.data[index]; if (item.index !== index || item.scope[this.keyExpr] !== value) { item.index = item.scope.$index = index; item.scope[this.keyExpr] = value; item.scope.$first = (index === 0); item.scope.$last = (index === (this.getLength() - 1)); item.scope.$middle = !(item.scope.$first || item.scope.$last); item.scope.$odd = !(item.scope.$even = (index&1) === 0); //We changed the scope, so digest if needed if (!$rootScope.$$phase) { item.scope.$digest(); } } this.attachedItems[index] = item; return item; }, destroyItem: function(item) { item.element.remove(); item.scope.$destroy(); item.scope = null; item.element = null; }, detachItem: function(item) { delete this.attachedItems[item.index]; //If it's an outside item, only hide it. These items aren't part of collection //repeat's list, only sit outside if (item.isOutside) { hideWithTransform(item.element); // If we are at the limit of backup items, just get rid of the this element } else if (this.backupItemsArray.length >= this.BACKUP_ITEMS_LENGTH) { this.destroyItem(item); // Otherwise, add it to our backup items } else { this.backupItemsArray.push(item); hideWithTransform(item.element); //Don't .$destroy(), just stop watchers and events firing ionic.Utils.disconnectScope(item.scope); } }, getLength: function() { return this.dimensions && this.dimensions.length || 0; }, setData: function(value, beforeSiblings, afterSiblings) { this.data = value || []; this.beforeSiblings = beforeSiblings || []; this.afterSiblings = afterSiblings || []; this.calculateDataDimensions(); this.afterSiblings.forEach(function(item) { item.element.css({position: 'absolute', top: '0', left: '0' }); hideWithTransform(item.element); }); }, }; return CollectionRepeatDataSource; }]); IonicModule .factory('$collectionRepeatManager', [ '$rootScope', '$timeout', function($rootScope, $timeout) { /** * Vocabulary: "primary" and "secondary" size/direction/position mean * "y" and "x" for vertical scrolling, or "x" and "y" for horizontal scrolling. */ function CollectionRepeatManager(options) { var self = this; this.dataSource = options.dataSource; this.element = options.element; this.scrollView = options.scrollView; this.isVertical = !!this.scrollView.options.scrollingY; this.renderedItems = {}; this.dimensions = []; this.setCurrentIndex(0); //Override scrollview's render callback this.scrollView.__$callback = this.scrollView.__callback; this.scrollView.__callback = angular.bind(this, this.renderScroll); function getViewportSize() { return self.viewportSize; } //Set getters and setters to match whether this scrollview is vertical or not if (this.isVertical) { this.scrollView.options.getContentHeight = getViewportSize; this.scrollValue = function() { return this.scrollView.__scrollTop; }; this.scrollMaxValue = function() { return this.scrollView.__maxScrollTop; }; this.scrollSize = function() { return this.scrollView.__clientHeight; }; this.secondaryScrollSize = function() { return this.scrollView.__clientWidth; }; this.transformString = function(y, x) { return 'translate3d('+x+'px,'+y+'px,0)'; }; this.primaryDimension = function(dim) { return dim.height; }; this.secondaryDimension = function(dim) { return dim.width; }; } else { this.scrollView.options.getContentWidth = getViewportSize; this.scrollValue = function() { return this.scrollView.__scrollLeft; }; this.scrollMaxValue = function() { return this.scrollView.__maxScrollLeft; }; this.scrollSize = function() { return this.scrollView.__clientWidth; }; this.secondaryScrollSize = function() { return this.scrollView.__clientHeight; }; this.transformString = function(x, y) { return 'translate3d('+x+'px,'+y+'px,0)'; }; this.primaryDimension = function(dim) { return dim.width; }; this.secondaryDimension = function(dim) { return dim.height; }; } } CollectionRepeatManager.prototype = { destroy: function() { this.renderedItems = {}; this.render = angular.noop; this.calculateDimensions = angular.noop; this.dimensions = []; }, /* * Pre-calculate the position of all items in the data list. * Do this using the provided width and height (primarySize and secondarySize) * provided by the dataSource. */ calculateDimensions: function() { /* * For the sake of explanations below, we're going to pretend we are scrolling * vertically: Items are laid out with primarySize being height, * secondarySize being width. */ var primaryPos = 0; var secondaryPos = 0; var secondaryScrollSize = this.secondaryScrollSize(); var previousItem; this.dataSource.beforeSiblings && this.dataSource.beforeSiblings.forEach(calculateSize, this); var beforeSize = primaryPos + (previousItem ? previousItem.primarySize : 0); primaryPos = secondaryPos = 0; previousItem = null; var dimensions = this.dataSource.dimensions.map(calculateSize, this); var totalSize = primaryPos + (previousItem ? previousItem.primarySize : 0); return { beforeSize: beforeSize, totalSize: totalSize, dimensions: dimensions }; function calculateSize(dim) { //Each dimension is an object {width: Number, height: Number} provided by //the dataSource var rect = { //Get the height out of the dimension object primarySize: this.primaryDimension(dim), //Max out the item's width to the width of the scrollview secondarySize: Math.min(this.secondaryDimension(dim), secondaryScrollSize) }; //If this isn't the first item if (previousItem) { //Move the item's x position over by the width of the previous item secondaryPos += previousItem.secondarySize; //If the y position is the same as the previous item and //the x position is bigger than the scroller's width if (previousItem.primaryPos === primaryPos && secondaryPos + rect.secondarySize > secondaryScrollSize) { //Then go to the next row, with x position 0 secondaryPos = 0; primaryPos += previousItem.primarySize; } } rect.primaryPos = primaryPos; rect.secondaryPos = secondaryPos; previousItem = rect; return rect; } }, resize: function() { var result = this.calculateDimensions(); this.dimensions = result.dimensions; this.viewportSize = result.totalSize; this.beforeSize = result.beforeSize; this.setCurrentIndex(0); this.render(true); this.dataSource.setup(); }, /* * setCurrentIndex sets the index in the list that matches the scroller's position. * Also save the position in the scroller for next and previous items (if they exist) */ setCurrentIndex: function(index, height) { var currentPos = (this.dimensions[index] || {}).primaryPos || 0; this.currentIndex = index; this.hasPrevIndex = index > 0; if (this.hasPrevIndex) { this.previousPos = Math.max( currentPos - this.dimensions[index - 1].primarySize, this.dimensions[index - 1].primaryPos ); } this.hasNextIndex = index + 1 < this.dataSource.getLength(); if (this.hasNextIndex) { this.nextPos = Math.min( currentPos + this.dimensions[index + 1].primarySize, this.dimensions[index + 1].primaryPos ); } }, /** * override the scroller's render callback to check if we need to * re-render our collection */ renderScroll: ionic.animationFrameThrottle(function(transformLeft, transformTop, zoom, wasResize) { if (this.isVertical) { this.renderIfNeeded(transformTop); } else { this.renderIfNeeded(transformLeft); } return this.scrollView.__$callback(transformLeft, transformTop, zoom, wasResize); }), renderIfNeeded: function(scrollPos) { if ((this.hasNextIndex && scrollPos >= this.nextPos) || (this.hasPrevIndex && scrollPos < this.previousPos)) { // Math.abs(transformPos - this.lastRenderScrollValue) > 100) { this.render(); } }, /* * getIndexForScrollValue: Given the most recent data index and a new scrollValue, * find the data index that matches that scrollValue. * * Strategy (if we are scrolling down): keep going forward in the dimensions list, * starting at the given index, until an item with height matching the new scrollValue * is found. * * This is a while loop. In the worst case it will have to go through the whole list * (eg to scroll from top to bottom). The most common case is to scroll * down 1-3 items at a time. * * While this is not as efficient as it could be, optimizing it gives no noticeable * benefit. We would have to use a new memory-intensive data structure for dimensions * to fully optimize it. */ getIndexForScrollValue: function(i, scrollValue) { var rect; //Scrolling up if (scrollValue <= this.dimensions[i].primaryPos) { while ( (rect = this.dimensions[i - 1]) && rect.primaryPos > scrollValue) { i--; } //Scrolling down } else { while ( (rect = this.dimensions[i + 1]) && rect.primaryPos < scrollValue) { i++; } } return i; }, /* * render: Figure out the scroll position, the index matching it, and then tell * the data source to render the correct items into the DOM. */ render: function(shouldRedrawAll) { var self = this; var i; var isOutOfBounds = ( this.currentIndex >= this.dataSource.getLength() ); // We want to remove all the items and redraw everything if we're out of bounds // or a flag is passed in. if (isOutOfBounds || shouldRedrawAll) { for (i in this.renderedItems) { this.removeItem(i); } // Just don't render anything if we're out of bounds if (isOutOfBounds) return; } var rect; var scrollValue = this.scrollValue(); // Scroll size = how many pixels are visible in the scroller at one time var scrollSize = this.scrollSize(); // We take the current scroll value and add it to the scrollSize to get // what scrollValue the current visible scroll area ends at. var scrollSizeEnd = scrollSize + scrollValue; // Get the new start index for scrolling, based on the current scrollValue and // the most recent known index var startIndex = this.getIndexForScrollValue(this.currentIndex, scrollValue); // If we aren't on the first item, add one row of items before so that when the user is // scrolling up he sees the previous item var renderStartIndex = Math.max(startIndex - 1, 0); // Keep adding items to the 'extra row above' until we get to a new row. // This is for the case where there are multiple items on one row above // the current item; we want to keep adding items above until // a new row is reached. while (renderStartIndex > 0 && (rect = this.dimensions[renderStartIndex]) && rect.primaryPos === this.dimensions[startIndex - 1].primaryPos) { renderStartIndex--; } // Keep rendering items, adding them until we are past the end of the visible scroll area i = renderStartIndex; while ((rect = this.dimensions[i]) && (rect.primaryPos - rect.primarySize < scrollSizeEnd)) { doRender(i, rect); i++; } // Render two extra items at the end as a buffer if (self.dimensions[i]) { doRender(i, self.dimensions[i]); i++; } if (self.dimensions[i]) { doRender(i, self.dimensions[i]); } var renderEndIndex = i; // Remove any items that were rendered and aren't visible anymore for (var renderIndex in this.renderedItems) { if (renderIndex < renderStartIndex || renderIndex > renderEndIndex) { this.removeItem(renderIndex); } } this.setCurrentIndex(startIndex); function doRender(dataIndex, rect) { if (dataIndex < self.dataSource.dataStartIndex) { // do nothing } else { self.renderItem(dataIndex, rect.primaryPos - self.beforeSize, rect.secondaryPos); } } }, renderItem: function(dataIndex, primaryPos, secondaryPos) { // Attach an item, and set its transform position to the required value var item = this.dataSource.attachItemAtIndex(dataIndex); //console.log(dataIndex, item); if (item && item.element) { if (item.primaryPos !== primaryPos || item.secondaryPos !== secondaryPos) { item.element.css(ionic.CSS.TRANSFORM, this.transformString( primaryPos, secondaryPos )); item.primaryPos = primaryPos; item.secondaryPos = secondaryPos; } // Save the item in rendered items this.renderedItems[dataIndex] = item; } else { // If an item at this index doesn't exist anymore, be sure to delete // it from rendered items delete this.renderedItems[dataIndex]; } }, removeItem: function(dataIndex) { // Detach a given item var item = this.renderedItems[dataIndex]; if (item) { item.primaryPos = item.secondaryPos = null; this.dataSource.detachItem(item); delete this.renderedItems[dataIndex]; } } }; return CollectionRepeatManager; }]); /** * @ngdoc service * @name $ionicGesture * @module ionic * @description An angular service exposing ionic * {@link ionic.utility:ionic.EventController}'s gestures. */ IonicModule .factory('$ionicGesture', [function() { return { /** * @ngdoc method * @name $ionicGesture#on * @description Add an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#onGesture}. * @param {string} eventType The gesture event to listen for. * @param {function(e)} callback The function to call when the gesture * happens. * @param {element} $element The angular element to listen for the event on. * @param {object} options object. * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on). */ on: function(eventType, cb, $element, options) { return window.ionic.onGesture(eventType, cb, $element[0], options); }, /** * @ngdoc method * @name $ionicGesture#off * @description Remove an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#offGesture}. * @param {ionic.Gesture} gesture The gesture that should be removed. * @param {string} eventType The gesture event to remove the listener for. * @param {function(e)} callback The listener to remove. */ off: function(gesture, eventType, cb) { return window.ionic.offGesture(gesture, eventType, cb); } }; }]); /** * @ngdoc service * @name $ionicHistory * @module ionic * @description * $ionicHistory keeps track of views as the user navigates through an app. Similar to the way a * browser behaves, an Ionic app is able to keep track of the previous view, the current view, and * the forward view (if there is one). However, a typical web browser only keeps track of one * history stack in a linear fashion. * * Unlike a traditional browser environment, apps and webapps have parallel independent histories, * such as with tabs. Should a user navigate few pages deep on one tab, and then switch to a new * tab and back, the back button relates not to the previous tab, but to the previous pages * visited within _that_ tab. * * `$ionicHistory` facilitates this parallel history architecture. */ IonicModule .factory('$ionicHistory', [ '$rootScope', '$state', '$location', '$window', '$timeout', '$ionicViewSwitcher', '$ionicNavViewDelegate', function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ionicNavViewDelegate) { // history actions while navigating views var ACTION_INITIAL_VIEW = 'initialView'; var ACTION_NEW_VIEW = 'newView'; var ACTION_MOVE_BACK = 'moveBack'; var ACTION_MOVE_FORWARD = 'moveForward'; // direction of navigation var DIRECTION_BACK = 'back'; var DIRECTION_FORWARD = 'forward'; var DIRECTION_ENTER = 'enter'; var DIRECTION_EXIT = 'exit'; var DIRECTION_SWAP = 'swap'; var DIRECTION_NONE = 'none'; var stateChangeCounter = 0; var lastStateId, nextViewOptions, nextViewExpireTimer, forcedNav; var viewHistory = { histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1 } }, views: {}, backView: null, forwardView: null, currentView: null }; var View = function() {}; View.prototype.initialize = function(data) { if (data) { for (var name in data) this[name] = data[name]; return this; } return null; }; View.prototype.go = function() { if (this.stateName) { return $state.go(this.stateName, this.stateParams); } if (this.url && this.url !== $location.url()) { if (viewHistory.backView === this) { return $window.history.go(-1); } else if (viewHistory.forwardView === this) { return $window.history.go(1); } $location.url(this.url); return; } return null; }; View.prototype.destroy = function() { if (this.scope) { this.scope.$destroy && this.scope.$destroy(); this.scope = null; } }; function getViewById(viewId) { return (viewId ? viewHistory.views[ viewId ] : null); } function getBackView(view) { return (view ? getViewById(view.backViewId) : null); } function getForwardView(view) { return (view ? getViewById(view.forwardViewId) : null); } function getHistoryById(historyId) { return (historyId ? viewHistory.histories[ historyId ] : null); } function getHistory(scope) { var histObj = getParentHistoryObj(scope); if (!viewHistory.histories[ histObj.historyId ]) { // this history object exists in parent scope, but doesn't // exist in the history data yet viewHistory.histories[ histObj.historyId ] = { historyId: histObj.historyId, parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId, stack: [], cursor: -1 }; } return getHistoryById(histObj.historyId); } function getParentHistoryObj(scope) { var parentScope = scope; while (parentScope) { if (parentScope.hasOwnProperty('$historyId')) { // this parent scope has a historyId return { historyId: parentScope.$historyId, scope: parentScope }; } // nothing found keep climbing up parentScope = parentScope.$parent; } // no history for the parent, use the root return { historyId: 'root', scope: $rootScope }; } function setNavViews(viewId) { viewHistory.currentView = getViewById(viewId); viewHistory.backView = getBackView(viewHistory.currentView); viewHistory.forwardView = getForwardView(viewHistory.currentView); } function getCurrentStateId() { var id; if ($state && $state.current && $state.current.name) { id = $state.current.name; if ($state.params) { for (var key in $state.params) { if ($state.params.hasOwnProperty(key) && $state.params[key]) { id += "_" + key + "=" + $state.params[key]; } } } return id; } // if something goes wrong make sure its got a unique stateId return ionic.Utils.nextUid(); } function getCurrentStateParams() { var rtn; if ($state && $state.params) { for (var key in $state.params) { if ($state.params.hasOwnProperty(key)) { rtn = rtn || {}; rtn[key] = $state.params[key]; } } } return rtn; } return { register: function(parentScope, viewLocals) { var currentStateId = getCurrentStateId(), hist = getHistory(parentScope), currentView = viewHistory.currentView, backView = viewHistory.backView, forwardView = viewHistory.forwardView, viewId = null, action = null, direction = DIRECTION_NONE, historyId = hist.historyId, url = $location.url(), tmp, x, ele; if (lastStateId !== currentStateId) { lastStateId = currentStateId; stateChangeCounter++; } if (forcedNav) { // we've previously set exactly what to do viewId = forcedNav.viewId; action = forcedNav.action; direction = forcedNav.direction; forcedNav = null; } else if (backView && backView.stateId === currentStateId) { // they went back one, set the old current view as a forward view viewId = backView.viewId; historyId = backView.historyId; action = ACTION_MOVE_BACK; if (backView.historyId === currentView.historyId) { // went back in the same history direction = DIRECTION_BACK; } else if (currentView) { direction = DIRECTION_EXIT; tmp = getHistoryById(backView.historyId); if (tmp && tmp.parentHistoryId === currentView.historyId) { direction = DIRECTION_ENTER; } else { tmp = getHistoryById(currentView.historyId); if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { direction = DIRECTION_SWAP; } } } } else if (forwardView && forwardView.stateId === currentStateId) { // they went to the forward one, set the forward view to no longer a forward view viewId = forwardView.viewId; historyId = forwardView.historyId; action = ACTION_MOVE_FORWARD; if (forwardView.historyId === currentView.historyId) { direction = DIRECTION_FORWARD; } else if (currentView) { direction = DIRECTION_EXIT; if (currentView.historyId === hist.parentHistoryId) { direction = DIRECTION_ENTER; } else { tmp = getHistoryById(currentView.historyId); if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { direction = DIRECTION_SWAP; } } } tmp = getParentHistoryObj(parentScope); if (forwardView.historyId && tmp.scope) { // if a history has already been created by the forward view then make sure it stays the same tmp.scope.$historyId = forwardView.historyId; historyId = forwardView.historyId; } } else if (currentView && currentView.historyId !== historyId && hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length && hist.stack[hist.cursor].stateId === currentStateId) { // they just changed to a different history and the history already has views in it var switchToView = hist.stack[hist.cursor]; viewId = switchToView.viewId; historyId = switchToView.historyId; action = ACTION_MOVE_BACK; direction = DIRECTION_SWAP; tmp = getHistoryById(currentView.historyId); if (tmp && tmp.parentHistoryId === historyId) { direction = DIRECTION_EXIT; } else { tmp = getHistoryById(historyId); if (tmp && tmp.parentHistoryId === currentView.historyId) { direction = DIRECTION_ENTER; } } // if switching to a different history, and the history of the view we're switching // to has an existing back view from a different history than itself, then // it's back view would be better represented using the current view as its back view tmp = getViewById(switchToView.backViewId); if (tmp && switchToView.historyId !== tmp.historyId) { hist.stack[hist.cursor].backViewId = currentView.viewId; } } else { // create an element from the viewLocals template ele = $ionicViewSwitcher.createViewEle(viewLocals); if (this.isAbstractEle(ele, viewLocals)) { void 0; 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 tmp.stack[x].destroy(); tmp.stack.splice(x); } historyId = forwardView.historyId; } } // its only moving forward if its in the same history if (hist.historyId === currentView.historyId) { direction = DIRECTION_FORWARD; } else if (currentView.historyId !== hist.historyId) { direction = DIRECTION_ENTER; tmp = getHistoryById(currentView.historyId); if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { direction = DIRECTION_SWAP; } else { tmp = getHistoryById(tmp.parentHistoryId); if (tmp && tmp.historyId === hist.historyId) { direction = DIRECTION_EXIT; } } } } else { // there's no current view, so this must be the initial view action = ACTION_INITIAL_VIEW; } if (stateChangeCounter < 2) { // views that were spun up on the first load should not animate direction = DIRECTION_NONE; } // add the new view viewHistory.views[viewId] = this.createView({ viewId: viewId, index: hist.stack.length, historyId: hist.historyId, backViewId: (currentView && currentView.viewId ? currentView.viewId : null), forwardViewId: null, stateId: currentStateId, stateName: this.currentStateName(), stateParams: getCurrentStateParams(), url: url }); // add the new view to this history's stack hist.stack.push(viewHistory.views[viewId]); } $timeout.cancel(nextViewExpireTimer); if (nextViewOptions) { if (nextViewOptions.disableAnimate) direction = DIRECTION_NONE; if (nextViewOptions.disableBack) viewHistory.views[viewId].backViewId = null; if (nextViewOptions.historyRoot) { for (x = 0; x < hist.stack.length; x++) { if (hist.stack[x].viewId === viewId) { hist.stack[x].index = 0; hist.stack[x].backViewId = hist.stack[x].forwardViewId = null; } else { delete viewHistory.views[hist.stack[x].viewId]; } } hist.stack = [viewHistory.views[viewId]]; } nextViewOptions = null; } setNavViews(viewId); if (viewHistory.backView && historyId == viewHistory.backView.historyId && currentStateId == viewHistory.backView.stateId && url == viewHistory.backView.url) { for (x = 0; x < hist.stack.length; x++) { if (hist.stack[x].viewId == viewId) { action = 'dupNav'; direction = DIRECTION_NONE; hist.stack[x - 1].forwardViewId = 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; } } } void 0; hist.cursor = viewHistory.currentView.index; return { viewId: viewId, action: action, direction: direction, historyId: historyId, enableBack: !!(viewHistory.backView && viewHistory.backView.historyId === viewHistory.currentView.historyId), 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() { if (viewHistory.backView) { return viewHistory.backView.title; } }, /** * @ngdoc method * @name $ionicHistory#forwardView * @description Returns the view that was in front of the current view in the history stack. * A forward view would exist if the user navigated from View A to View B, then * navigated back to View A. At this point then View B would be the forward view, and View * A would be the current view. * @returns {object} Returns the forward view. */ forwardView: function(view) { if (arguments.length) { viewHistory.forwardView = view; } return viewHistory.forwardView; }, /** * @ngdoc method * @name $ionicHistory#currentStateName * @description Returns the current state name. * @returns {string} */ currentStateName: function() { return ($state && $state.current ? $state.current.name : null); }, isCurrentStateNavView: function(navView) { return !!($state && $state.current && $state.current.views && $state.current.views[navView]); }, goToHistoryRoot: function(historyId) { if (historyId) { var hist = getHistoryById(historyId); if (hist && hist.stack.length) { if (viewHistory.currentView && viewHistory.currentView.viewId === hist.stack[0].viewId) { return; } forcedNav = { viewId: hist.stack[0].viewId, action: ACTION_MOVE_BACK, direction: DIRECTION_BACK }; hist.stack[0].go(); } } }, /** * @ngdoc method * @name $ionicHistory#goBack * @description Navigates the app to the back view, if a back view exists. */ goBack: function() { viewHistory.backView && viewHistory.backView.go(); }, /** * @ngdoc method * @name $ionicHistory#clearHistory * @description Clears out the app's entire history, except for the current view. */ clearHistory: function() { var histories = viewHistory.histories, currentView = viewHistory.currentView; if (histories) { for (var historyId in histories) { if (histories[historyId].stack) { histories[historyId].stack = []; histories[historyId].cursor = -1; } if (currentView && currentView.historyId === historyId) { currentView.backViewId = currentView.forwardViewId = null; histories[historyId].stack.push(currentView); } else if (histories[historyId].destroy) { histories[historyId].destroy(); } } } for (var viewId in viewHistory.views) { if (viewId !== currentView.viewId) { delete viewHistory.views[viewId]; } } if (currentView) { setNavViews(currentView.viewId); } }, /** * @ngdoc method * @name $ionicHistory#clearCache * @description Removes all cached views within every {@link ionic.directive:ionNavView}. * This both removes the view element from the DOM, and destroy it's scope. */ clearCache: function() { $ionicNavViewDelegate._instances.forEach(function(instance) { instance.clearCache(); }); }, /** * @ngdoc method * @name $ionicHistory#nextViewOptions * @description Sets options for the next view. This method can be useful to override * certain view/transition defaults right before a view transition happens. For example, * the {@link ionic.directive:menuClose} directive uses this method internally to ensure * an animated view transition does not happen when a side menu is open, and also sets * the next view as the root of its history stack. After the transition these options * are set back to null. * * Available options: * * * `disableAnimate`: Do not animate the next transition. * * `disableBack`: The next view should forget its back view, and set it to null. * * `historyRoot`: The next view should become the root view in its history stack. * * ```js * $ionicHistory.nextViewOptions({ * disableAnimate: true, * disableBack: true * }); * ``` */ nextViewOptions: function(opts) { if (arguments.length) { $timeout.cancel(nextViewExpireTimer); if (opts === null) { nextViewOptions = opts; } else { nextViewOptions = nextViewOptions || {}; extend(nextViewOptions, opts); if (nextViewOptions.expire) { nextViewExpireTimer = $timeout(function(){ nextViewOptions = null; }, nextViewOptions.expire); } } } return nextViewOptions; }, isAbstractEle: function(ele, viewLocals) { if (viewLocals && viewLocals.$$state && viewLocals.$$state.self.abstract) { return true; } return !!(ele && (isAbstractTag(ele) || isAbstractTag(ele.children()))); }, isActiveScope: function(scope) { if (!scope) return false; var climbScope = scope; var currentHistoryId = this.currentHistoryId(); var foundHistoryId; while (climbScope) { if (climbScope.$$disconnected) { return false; } if (!foundHistoryId && climbScope.hasOwnProperty('$historyId')) { foundHistoryId = true; } if (currentHistoryId) { if (climbScope.hasOwnProperty('$historyId') && currentHistoryId == climbScope.$historyId) { return true; } if (climbScope.hasOwnProperty('$activeHistoryId')) { if (currentHistoryId == climbScope.$activeHistoryId) { if (climbScope.hasOwnProperty('$historyId')) { return true; } if (!foundHistoryId) { return true; } } } } if (foundHistoryId && climbScope.hasOwnProperty('$activeHistoryId')) { foundHistoryId = false; } climbScope = climbScope.$parent; } return currentHistoryId ? currentHistoryId == 'root' : true; } }; function isAbstractTag(ele) { return ele && ele.length && /ion-side-menus|ion-tabs/i.test(ele[0].tagName); } }]) .run([ '$rootScope', '$state', '$location', '$document', '$ionicPlatform', '$ionicHistory', function($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory) { // always reset the keyboard state when change stage $rootScope.$on('$ionicView.beforeEnter', function() { ionic.keyboard && ionic.keyboard.hide && ionic.keyboard.hide(); }); $rootScope.$on('$ionicHistory.change', function(e, data) { if (!data) return; var viewHistory = $ionicHistory.viewHistory(); var hist = (data.historyId ? viewHistory.histories[ data.historyId ] : null); if (hist && hist.cursor > -1 && hist.cursor < hist.stack.length) { // the history they're going to already exists // go to it's last view in its stack var view = hist.stack[ hist.cursor ]; return view.go(data); } // this history does not have a URL, but it does have a uiSref // figure out its URL from the uiSref if (!data.url && data.uiSref) { data.url = $state.href(data.uiSref); } if (data.url) { // don't let it start with a #, messes with $location.url() if (data.url.indexOf('#') === 0) { data.url = data.url.replace('#', ''); } if (data.url !== $location.url()) { // we've got a good URL, ready GO! $location.url(data.url); } } }); $rootScope.$ionicGoBack = function() { $ionicHistory.goBack(); }; // Set the document title when a new view is shown $rootScope.$on('$ionicView.afterEnter', function(ev, data) { if (data && data.title) { $document[0].title = data.title; } }); // Triggered when devices with a hardware back button (Android) is clicked by the user // This is a Cordova/Phonegap platform specifc method function onHardwareBackButton(e) { var backView = $ionicHistory.backView(); if (backView) { // there is a back view, go to it backView.go(); } else { // there is no back view, so close the app instead ionic.Platform.exitApp(); } e.preventDefault(); return false; } $ionicPlatform.registerBackButtonAction( onHardwareBackButton, PLATFORM_BACK_BUTTON_PRIORITY_VIEW ); }]); /** * @ngdoc provider * @name $ionicConfigProvider * @module ionic * @description * Ionic automatically takes platform configurations into account to adjust things like what * transition style to use and whether tab icons should show on the top or bottom. For example, * iOS will move forward by transitioning the entering view from right to center and the leaving * view from center to left. However, Android will transition with the entering view going from * bottom to center, covering the previous view, which remains stationary. It should be noted * that when a platform is not iOS or Android, then it'll default to iOS. So if you are * developing on a desktop browser, it's going to take on iOS default configs. * * These configs can be changed using the `$ionicConfigProvider` during the configuration phase * of your app. Additionally, `$ionicConfig` can also set and get config values during the run * phase and within the app itself. * * By default, all base config variables are set to `'platform'`, which means it'll take on the * default config of the platform on which it's running. Config variables can be set at this * level so all platforms follow the same setting, rather than its platform config. * The following code would set the same config variable for all platforms: * * ```js * $ionicConfigProvider.views.maxCache(10); * ``` * * Additionally, each platform can have it's own config within the `$ionicConfigProvider.platform` * property. The config below would only apply to Android devices. * * ```js * $ionicConfigProvider.platform.android.views.maxCache(5); * ``` * * @usage * ```js * var myApp = angular.module('reallyCoolApp', ['ionic']); * * myApp.config(function($ionicConfigProvider) { * $ionicConfigProvider.views.maxCache(5); * * // note that you can also chain configs * $ionicConfigProvider.backButton.text('Go Back').icon('ion-chevron-left'); * }); * ``` */ /** * @ngdoc method * @name $ionicConfigProvider#views.transition * @description Animation style when transitioning between views. Default `platform`. * * @param {string} transition Which style of view transitioning to use. * * * `platform`: Dynamically choose the correct transition style depending on the platform * the app is running from. If the platform is not `ios` or `android` then it will default * to `ios`. * * `ios`: iOS style transition. * * `android`: Android style transition. * * `none`: Do not preform animated transitions. * * @returns {string} value */ /** * @ngdoc method * @name $ionicConfigProvider#views.maxCache * @description Maximum number of view elements to cache in the DOM. When the max number is * exceeded, the view with the longest time period since it was accessed is removed. Views that * stay in the DOM cache the view's scope, current state, and scroll position. The scope is * disconnected from the `$watch` cycle when it is cached and reconnected when it enters again. * When the maximum cache is `0`, the leaving view's element will be removed from the DOM after * each view transition, and the next time the same view is shown, it will have to re-compile, * attach to the DOM, and link the element again. This disables caching, in effect. * @param {number} maxNumber Maximum number of views to retain. Default `10`. * @returns {number} How many views Ionic will hold onto until the a view is removed. */ /** * @ngdoc method * @name $ionicConfigProvider#views.forwardCache * @description By default, when navigating, views that were recently visited are cached, and * the same instance data and DOM elements are referenced when navigating back. However, when * navigating back in the history, the "forward" views are removed from the cache. If you * navigate forward to the same view again, it'll create a new DOM element and controller * instance. Basically, any forward views are reset each time. Set this config to `true` to have * forward views cached and not reset on each load. * @param {boolean} value * @returns {boolean} */ /** * @ngdoc method * @name $ionicConfigProvider#backButton.icon * @description Back button icon. * @param {string} value * @returns {string} */ /** * @ngdoc method * @name $ionicConfigProvider#backButton.text * @description Back button text. * @param {string} value Defaults to `Back`. * @returns {string} */ /** * @ngdoc method * @name $ionicConfigProvider#backButton.previousTitleText * @description If the previous title text should become the back button text. This * is the default for iOS. * @param {boolean} value * @returns {boolean} */ /** * @ngdoc method * @name $ionicConfigProvider#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 }, navBar: { alignTitle: PLATFORM, positionPrimaryButtons: PLATFORM, positionSecondaryButtons: PLATFORM, transition: PLATFORM }, backButton: { icon: PLATFORM, text: PLATFORM, previousTitleText: PLATFORM }, form: { checkbox: PLATFORM }, tabs: { style: PLATFORM, position: PLATFORM }, templates: { maxPrefetch: PLATFORM }, platform: {} }; createConfig(configProperties, provider, ''); // Default // ------------------------- setPlatformConfig('default', { views: { maxCache: 10, forwardCache: false, transition: 'ios' }, navBar: { alignTitle: 'center', positionPrimaryButtons: 'left', positionSecondaryButtons: 'right', transition: 'view' }, backButton: { icon: 'ion-ios7-arrow-back', text: 'Back', previousTitleText: true }, form: { checkbox: 'circle' }, tabs: { style: 'standard', position: 'bottom' }, templates: { maxPrefetch: 30 } }); // iOS (it is the default already) // ------------------------- setPlatformConfig('ios', {}); // Android // ------------------------- setPlatformConfig('android', { views: { transition: 'android' }, navBar: { alignTitle: 'left', positionPrimaryButtons: 'right', positionSecondaryButtons: 'right' }, backButton: { icon: 'ion-arrow-left-c', text: false, previousTitleText: false }, form: { checkbox: 'square' }, tabs: { style: 'striped', position: 'top' } }); provider.transitions = { views: {}, navBar: {} }; // iOS Transitions // ----------------------- provider.transitions.views.ios = function(enteringEle, leavingEle, direction, shouldAnimate) { shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); function setStyles(ele, opacity, x) { var css = {}; css[ionic.CSS.TRANSITION_DURATION] = shouldAnimate ? '' : 0; css.opacity = opacity; css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)'; ionic.DomUtil.cachedStyles(ele, css); } return { run: function(step) { if (direction == 'forward') { setStyles(enteringEle, 1, (1 - step) * 99); // starting at 98% prevents a flicker setStyles(leavingEle, (1 - 0.1 * step), step * -33); } else if (direction == 'back') { setStyles(enteringEle, (1 - 0.1 * (1 - step)), (1 - step) * -33); setStyles(leavingEle, 1, step * 100); } else { // swap, enter, exit setStyles(enteringEle, 1, 0); setStyles(leavingEle, 0, 0); } }, shouldAnimate: shouldAnimate }; }; provider.transitions.navBar.ios = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) { shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); function setStyles(ctrl, opacity, titleX, backTextX) { var css = {}; css[ionic.CSS.TRANSITION_DURATION] = shouldAnimate ? '' : 0; css.opacity = opacity === 1 ? '' : opacity; ctrl.setCss('buttons-left', css); ctrl.setCss('buttons-right', css); ctrl.setCss('back-button', css); css[ionic.CSS.TRANSFORM] = 'translate3d(' + backTextX + 'px,0,0)'; ctrl.setCss('back-text', css); css[ionic.CSS.TRANSFORM] = 'translate3d(' + titleX + 'px,0,0)'; ctrl.setCss('title', css); } function enter(ctrlA, ctrlB, step) { if (!ctrlA) 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) return; var titleX = (-(ctrlA.titleTextX() - ctrlB.backButtonTextLeft()) - (ctrlA.titleLeftRight())) * step; setStyles(ctrlA, 1 - step, titleX, 0); } return { run: function(step) { var enteringHeaderCtrl = enteringHeaderBar.controller(); var leavingHeaderCtrl = leavingHeaderBar && leavingHeaderBar.controller(); if (direction == 'back') { leave(enteringHeaderCtrl, leavingHeaderCtrl, 1 - step); enter(leavingHeaderCtrl, enteringHeaderCtrl, 1 - step); } else { enter(enteringHeaderCtrl, leavingHeaderCtrl, step); leave(leavingHeaderCtrl, enteringHeaderCtrl, step); } }, shouldAnimate: shouldAnimate }; }; // Android Transitions // ----------------------- provider.transitions.views.android = function(enteringEle, leavingEle, direction, shouldAnimate) { shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); function setStyles(ele, x) { var css = {}; css[ionic.CSS.TRANSITION_DURATION] = shouldAnimate ? '' : 0; css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)'; ionic.DomUtil.cachedStyles(ele, css); } return { run: function(step) { if (direction == 'forward') { setStyles(enteringEle, (1 - step) * 99); // starting at 98% prevents a flicker setStyles(leavingEle, step * -100); } else if (direction == 'back') { setStyles(enteringEle, (1 - step) * -100); setStyles(leavingEle, step * 100); } else { // swap, enter, exit setStyles(enteringEle, 0); setStyles(leavingEle, 0); } }, shouldAnimate: shouldAnimate }; }; provider.transitions.navBar.android = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) { shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); 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: true }; }; // No Transition // ----------------------- provider.transitions.views.none = function(enteringEle, leavingEle) { return { run: function(step) { provider.transitions.views.android(enteringEle, leavingEle, false, false).run(step); } }; }; 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); } }; }; // private: used to set platform configs function setPlatformConfig(platformName, platformConfigs) { configProperties.platform[platformName] = platformConfigs; provider.platform[platformName] = {}; addConfig(configProperties, configProperties.platform[platformName]); createConfig(configProperties.platform[platformName], provider.platform[platformName], ''); } // private: used to recursively add new platform configs function addConfig(configObj, platformObj) { for (var n in configObj) { if (n != PLATFORM && configObj.hasOwnProperty(n)) { if (angular.isObject(configObj[n])) { if (!isDefined(platformObj[n])) { platformObj[n] = {}; } addConfig(configObj[n], platformObj[n]); } else if (!isDefined(platformObj[n])) { platformObj[n] = null; } } } } // private: create methods for each config to get/set function createConfig(configObj, providerObj, platformPath) { forEach(configObj, function(value, namespace) { if (angular.isObject(configObj[namespace])) { // recursively drill down the config object so we can create a method for each one providerObj[namespace] = {}; createConfig(configObj[namespace], providerObj[namespace], platformPath + '.' + namespace); } else { // create a method for the provider/config methods that will be exposed providerObj[namespace] = function(newValue) { if (arguments.length) { configObj[namespace] = newValue; return providerObj; } if (configObj[namespace] == PLATFORM) { // if the config is set to 'platform', then get this config's platform value var platformConfig = stringObj(configProperties.platform, ionic.Platform.platform() + platformPath + '.' + namespace); if (platformConfig || platformConfig === false) { return platformConfig; } // didnt find a specific platform config, now try the default return stringObj(configProperties.platform, 'default' + platformPath + '.' + namespace); } return configObj[namespace]; }; } }); } function stringObj(obj, str) { str = str.split("."); for (var i = 0; i < str.length; i++) { if (obj && isDefined(obj[str[i]])) { obj = obj[str[i]]; } else { return null; } } return obj; } provider.setPlatformConfig = setPlatformConfig; // private: Service definition for internal Ionic use /** * @ngdoc service * @name $ionicConfig * @module ionic * @private */ provider.$get = function() { return provider; }; }); var LOADING_TPL = '
' + '
' + '
' + '
'; var LOADING_HIDE_DEPRECATED = '$ionicLoading instance.hide() has been deprecated. Use $ionicLoading.hide().'; var LOADING_SHOW_DEPRECATED = '$ionicLoading instance.show() has been deprecated. Use $ionicLoading.show().'; var LOADING_SET_DEPRECATED = '$ionicLoading instance.setContent() has been deprecated. Use $ionicLoading.show({ template: \'my content\' }).'; /** * @ngdoc service * @name $ionicLoading * @module ionic * @description * An overlay that can be used to indicate activity while blocking user * interaction. * * @usage * ```js * angular.module('LoadingApp', ['ionic']) * .controller('LoadingCtrl', function($scope, $ionicLoading) { * $scope.show = function() { * $ionicLoading.show({ * template: 'Loading...' * }); * }; * $scope.hide = function(){ * $ionicLoading.hide(); * }; * }); * ``` */ /** * @ngdoc object * @name $ionicLoadingConfig * @module ionic * @description * Set the default options to be passed to the {@link ionic.service:$ionicLoading} service. * * @usage * ```js * var app = angular.module('myApp', ['ionic']) * app.constant('$ionicLoadingConfig', { * template: 'Default Loading Template...' * }); * app.controller('AppCtrl', function($scope, $ionicLoading) { * $scope.showLoading = function() { * $ionicLoading.show(); //options default to values in $ionicLoadingConfig * }; * }); * ``` */ IonicModule .constant('$ionicLoadingConfig', { template: '' }) .factory('$ionicLoading', [ '$ionicLoadingConfig', '$ionicBody', '$ionicTemplateLoader', '$ionicBackdrop', '$timeout', '$q', '$log', '$compile', '$ionicPlatform', '$rootScope', function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform, $rootScope) { var loaderInstance; //default values var deregisterBackAction = angular.noop; var deregisterStateListener = angular.noop; var loadingShowDelay = $q.when(); return { /** * @ngdoc method * @name $ionicLoading#show * @description Shows a loading indicator. If the indicator is already shown, * it will set the options given and keep the indicator shown. * @param {object} opts The options for the loading indicator. Available properties: * - `{string=}` `template` The html content of the indicator. * - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator. * - `{object=}` `scope` The scope to be a child of. Default: creates a child of $rootScope. * - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown. * - `{boolean=}` `hideOnStateChange` Whether to hide the loading spinner when navigating * to a new state. Default false. * - `{number=}` `delay` How many milliseconds to delay showing the indicator. By default there is no delay. * - `{number=}` `duration` How many milliseconds to wait until automatically * hiding the indicator. By default, the indicator will be shown until `.hide()` is called. */ show: showLoader, /** * @ngdoc method * @name $ionicLoading#hide * @description Hides the loading indicator, if shown. */ hide: hideLoader, /** * @private for testing */ _getLoader: getLoader }; function getLoader() { if (!loaderInstance) { loaderInstance = $ionicTemplateLoader.compile({ template: LOADING_TPL, appendTo: $ionicBody.get() }) .then(function(loader) { var self = loader; loader.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 (!this.isShown) { //options.showBackdrop: deprecated this.hasBackdrop = !options.noBackdrop && options.showBackdrop !== false; if (this.hasBackdrop) { $ionicBackdrop.retain(); $ionicBackdrop.getElement().addClass('backdrop-loading'); } } if (options.duration) { $timeout.cancel(this.durationTimeout); this.durationTimeout = $timeout( angular.bind(this, this.hide), +options.duration ); } deregisterBackAction(); //Disable hardware back button while loading deregisterBackAction = $ionicPlatform.registerBackButtonAction( angular.noop, PLATFORM_BACK_BUTTON_PRIORITY_LOADING ); templatePromise.then(function(html) { if (html) { var loading = self.element.children(); loading.html(html); $compile(loading.contents())(self.scope); } //Don't show until template changes if (self.isShown) { self.element.addClass('visible'); ionic.requestAnimationFrame(function() { if(self.isShown) { self.element.addClass('active'); $ionicBody.addClass('loading-active'); } }); } }); this.isShown = true; }; loader.hide = function() { deregisterBackAction(); if (this.isShown) { if (this.hasBackdrop) { $ionicBackdrop.release(); $ionicBackdrop.getElement().removeClass('backdrop-loading'); } self.element.removeClass('active'); $ionicBody.removeClass('loading-active'); setTimeout(function() { !self.isShown && self.element.removeClass('visible'); }, 200); } $timeout.cancel(this.durationTimeout); this.isShown = false; }; return loader; }); } return loaderInstance; } function showLoader(options) { options = extend({}, $ionicLoadingConfig || {}, options || {}); var delay = options.delay || options.showDelay || 0; //If loading.show() was called previously, cancel it and show with our new options loadingShowDelay && $timeout.cancel(loadingShowDelay); loadingShowDelay = $timeout(angular.noop, delay); loadingShowDelay.then(getLoader).then(function(loader) { if (options.hideOnStateChange) { deregisterStateListener = $rootScope.$on('$stateChangeSuccess', hideLoader); } return loader.show(options); }); return { hide: deprecated.method(LOADING_HIDE_DEPRECATED, $log.error, hideLoader), show: deprecated.method(LOADING_SHOW_DEPRECATED, $log.error, function() { showLoader(options); }), setContent: deprecated.method(LOADING_SET_DEPRECATED, $log.error, function(content) { getLoader().then(function(loader) { loader.show({ template: content }); }); }) }; } function hideLoader() { deregisterStateListener(); $timeout.cancel(loadingShowDelay); getLoader().then(function(loader) { loader.hide(); }); } }]); /** * @ngdoc service * @name $ionicModal * @module ionic * @description * * Related: {@link ionic.controller:ionicModal ionicModal controller}. * * The Modal is a content pane that can go over the user's main view * temporarily. Usually used for making a choice or editing an item. * * Put the content of the modal inside of an `` element. * * **Notes:** * - A modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating * scope, passing in itself as an event argument. Both the modal.removed and modal.hidden events are * called when the modal is removed. * * - This example assumes your modal is in your main index file or another template file. If it is in its own * template file, remove the script tags and call it by file name. * * @usage * ```html * * ``` * ```js * angular.module('testApp', ['ionic']) * .controller('MyController', function($scope, $ionicModal) { * $ionicModal.fromTemplateUrl('my-modal.html', { * scope: $scope, * animation: 'slide-in-up' * }).then(function(modal) { * $scope.modal = modal; * }); * $scope.openModal = function() { * $scope.modal.show(); * }; * $scope.closeModal = function() { * $scope.modal.hide(); * }; * //Cleanup the modal when we're done with it! * $scope.$on('$destroy', function() { * $scope.modal.remove(); * }); * // Execute action on hide modal * $scope.$on('modal.hidden', function() { * // Execute action * }); * // Execute action on remove modal * $scope.$on('modal.removed', function() { * // Execute action * }); * }); * ``` */ IonicModule .factory('$ionicModal', [ '$rootScope', '$ionicBody', '$compile', '$timeout', '$ionicPlatform', '$ionicTemplateLoader', '$q', '$log', function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $q, $log) { /** * @ngdoc controller * @name ionicModal * @module ionic * @description * Instantiated by the {@link ionic.service:$ionicModal} service. * * Be sure to call [remove()](#remove) when you are done with each modal * to clean it up and avoid memory leaks. * * Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating * scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are * called when the modal is removed. */ var ModalView = ionic.views.Modal.inherit({ /** * @ngdoc method * @name ionicModal#initialize * @description Creates a new modal controller instance. * @param {object} options An options object with the following properties: * - `{object=}` `scope` The scope to be a child of. * Default: creates a child of $rootScope. * - `{string=}` `animation` The animation to show & hide with. * Default: 'slide-in-up' * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of * the modal when shown. Default: false. * - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop. * Default: true. * - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware * back button on Android and similar devices. Default: true. */ initialize: function(opts) { ionic.views.Modal.prototype.initialize.call(this, opts); this.animation = opts.animation || 'slide-in-up'; }, /** * @ngdoc method * @name ionicModal#show * @description Show this modal instance. * @returns {promise} A promise which is resolved when the modal is finished animating in. */ show: function(target) { var self = this; if (self.scope.$$destroyed) { $log.error('Cannot call ' + self.viewType + '.show() after remove(). Please create a new ' + self.viewType + ' instance.'); return; } var modalEl = jqLite(self.modalEl); self.el.classList.remove('hide'); $timeout(function() { $ionicBody.addClass(self.viewType + '-open'); }, 400); if (!self.el.parentElement) { modalEl.addClass(self.animation); $ionicBody.append(self.el); } if (target && self.positionView) { self.positionView(target, modalEl); // set up a listener for in case the window size changes ionic.on('resize',function() { ionic.off('resize',null,window); self.positionView(target,modalEl); },window); } modalEl.addClass('ng-enter active') .removeClass('ng-leave ng-leave-active'); self._isShown = true; self._deregisterBackButton = $ionicPlatform.registerBackButtonAction( self.hardwareBackButtonClose ? angular.bind(self, self.hide) : angular.noop, PLATFORM_BACK_BUTTON_PRIORITY_MODAL ); self._isOpenPromise = $q.defer(); ionic.views.Modal.prototype.show.call(self); $timeout(function() { modalEl.addClass('ng-enter-active'); ionic.trigger('resize'); self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self); self.el.classList.add('active'); self.scope.$broadcast('$ionicHeader.align'); }, 20); return $timeout(function() { //After animating in, allow hide on backdrop click self.$el.on('click', function(e) { if (self.backdropClickToClose && e.target === self.el) { self.hide(); } }); }, 400); }, /** * @ngdoc method * @name ionicModal#hide * @description Hide this modal instance. * @returns {promise} A promise which is resolved when the modal is finished animating out. */ hide: function() { var self = this; var modalEl = jqLite(self.modalEl); self.el.classList.remove('active'); modalEl.addClass('ng-leave'); $timeout(function() { modalEl.addClass('ng-leave-active') .removeClass('ng-enter ng-enter-active active'); }, 20); self.$el.off('click'); self._isShown = false; self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self); self._deregisterBackButton && self._deregisterBackButton(); ionic.views.Modal.prototype.hide.call(self); // clean up event listeners if (self.positionView) { ionic.off('resize',null,window); } return $timeout(function() { $ionicBody.removeClass(self.viewType + '-open'); self.el.classList.add('hide'); }, self.hideDelay || 320); }, /** * @ngdoc method * @name ionicModal#remove * @description Remove this modal instance from the DOM and clean up. * @returns {promise} A promise which is resolved when the modal is finished animating out. */ remove: function() { var self = this; self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self); return self.hide().then(function() { self.scope.$destroy(); self.$el.remove(); }); }, /** * @ngdoc method * @name ionicModal#isShown * @returns boolean Whether this modal is currently shown. */ isShown: function() { return !!this._isShown; } }); var createModal = function(templateString, options) { // Create a new scope for the modal var scope = options.scope && options.scope.$new() || $rootScope.$new(true); options.viewType = options.viewType || 'modal'; extend(scope, { $hasHeader: false, $hasSubheader: false, $hasFooter: false, $hasSubfooter: false, $hasTabs: false, $hasTabsTop: false }); // Compile the template var element = $compile('' + templateString + '')(scope); options.$el = element; options.el = element[0]; options.modalEl = options.el.querySelector('.' + options.viewType); var modal = new ModalView(options); modal.scope = scope; // If this wasn't a defined scope, we can assign the viewType to the isolated scope // we created if (!options.scope) { scope[ options.viewType ] = modal; } return modal; }; return { /** * @ngdoc method * @name $ionicModal#fromTemplate * @param {string} templateString The template string to use as the modal's * content. * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method. * @returns {object} An instance of an {@link ionic.controller:ionicModal} * controller. */ fromTemplate: function(templateString, options) { var modal = createModal(templateString, options || {}); return modal; }, /** * @ngdoc method * @name $ionicModal#fromTemplateUrl * @param {string} templateUrl The url to load the template from. * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method. * options object. * @returns {promise} A promise that will be resolved with an instance of * an {@link ionic.controller:ionicModal} controller. */ fromTemplateUrl: function(url, options, _) { var cb; //Deprecated: allow a callback as second parameter. Now we return a promise. if (angular.isFunction(options)) { cb = options; options = _; } return $ionicTemplateLoader.load(url).then(function(templateString) { var modal = createModal(templateString, options || {}); cb && cb(modal); return modal; }); } }; }]); /** * @ngdoc service * @name $ionicNavBarDelegate * @module ionic * @description * Delegate for controlling the {@link ionic.directive:ionNavBar} directive. * * @usage * * ```html * * * * * * ``` * ```js * function MyCtrl($scope, $ionicNavBarDelegate) { * $scope.setNavTitle = function(title) { * $ionicNavBarDelegate.title(title); * } * } * ``` */ IonicModule .service('$ionicNavBarDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicNavBarDelegate#align * @description Aligns the title with the buttons in a given direction. * @param {string=} direction The direction to the align the title text towards. * Available: 'left', 'right', 'center'. Default: 'center'. */ 'align', /** * @ngdoc method * @name $ionicNavBarDelegate#showBackButton * @description * Set/get whether the {@link ionic.directive:ionNavBackButton} is shown * (if it exists and there is a previous view that can be navigated to). * @param {boolean=} show Whether to show the back button. * @returns {boolean} Whether the back button is shown. */ 'showBackButton', /** * @ngdoc method * @name $ionicNavBarDelegate#showBar * @description * Set/get whether the {@link ionic.directive:ionNavBar} is shown. * @param {boolean} show Whether to show the bar. * @returns {boolean} Whether the bar is shown. */ 'showBar', /** * @ngdoc method * @name $ionicNavBarDelegate#title * @description * Set the title for the {@link ionic.directive:ionNavBar}. * @param {string} title The new title to show. */ 'title', // DEPRECATED, as of v1.0.0-beta14 ------- 'changeTitle', 'setTitle', 'getTitle', 'back', 'getPreviousTitle' // END DEPRECATED ------- ])); IonicModule .service('$ionicNavViewDelegate', ionic.DelegateService([ 'clearCache' ])); var PLATFORM_BACK_BUTTON_PRIORITY_VIEW = 100; var PLATFORM_BACK_BUTTON_PRIORITY_SIDE_MENU = 150; var PLATFORM_BACK_BUTTON_PRIORITY_MODAL = 200; var PLATFORM_BACK_BUTTON_PRIORITY_ACTION_SHEET = 300; var PLATFORM_BACK_BUTTON_PRIORITY_POPUP = 400; var PLATFORM_BACK_BUTTON_PRIORITY_LOADING = 500; /** * @ngdoc service * @name $ionicPlatform * @module ionic * @description * An angular abstraction of {@link ionic.utility:ionic.Platform}. * * Used to detect the current platform, as well as do things like override the * Android back button in PhoneGap/Cordova. */ IonicModule .provider('$ionicPlatform', function() { return { $get: ['$q', '$rootScope', function($q, $rootScope) { var self = { /** * @ngdoc method * @name $ionicPlatform#onHardwareBackButton * @description * Some platforms have a hardware back button, so this is one way to * bind to it. * @param {function} callback the callback to trigger when this event occurs */ onHardwareBackButton: function(cb) { ionic.Platform.ready(function() { document.addEventListener('backbutton', cb, false); }); }, /** * @ngdoc method * @name $ionicPlatform#offHardwareBackButton * @description * Remove an event listener for the backbutton. * @param {function} callback The listener function that was * originally bound. */ offHardwareBackButton: function(fn) { ionic.Platform.ready(function() { document.removeEventListener('backbutton', fn); }); }, /** * @ngdoc method * @name $ionicPlatform#registerBackButtonAction * @description * Register a hardware back button action. Only one action will execute * when the back button is clicked, so this method decides which of * the registered back button actions has the highest priority. * * For example, if an actionsheet is showing, the back button should * close the actionsheet, but it should not also go back a page view * or close a modal which may be open. * * @param {function} callback Called when the back button is pressed, * if this listener is the highest priority. * @param {number} priority Only the highest priority will execute. * @param {*=} actionId The id to assign this action. Default: a * random unique id. * @returns {function} A function that, when called, will deregister * this backButtonAction. */ $backButtonActions: {}, registerBackButtonAction: function(fn, priority, actionId) { if (!self._hasBackButtonHandler) { // add a back button listener if one hasn't been setup yet self.$backButtonActions = {}; self.onHardwareBackButton(self.hardwareBackButtonClick); self._hasBackButtonHandler = true; } var action = { id: (actionId ? actionId : ionic.Utils.nextUid()), priority: (priority ? priority : 0), fn: fn }; self.$backButtonActions[action.id] = action; // return a function to de-register this back button action return function() { delete self.$backButtonActions[action.id]; }; }, /** * @private */ hardwareBackButtonClick: function(e) { // loop through all the registered back button actions // and only run the last one of the highest priority var priorityAction, actionId; for (actionId in self.$backButtonActions) { if (!priorityAction || self.$backButtonActions[actionId].priority >= priorityAction.priority) { priorityAction = self.$backButtonActions[actionId]; } } if (priorityAction) { priorityAction.fn(e); return priorityAction; } }, is: function(type) { return ionic.Platform.is(type); }, /** * @ngdoc method * @name $ionicPlatform#on * @description * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`, * `offline`, etc. More information about available event types can be found in * [Cordova's event documentation](https://cordova.apache.org/docs/en/edge/cordova_events_events.md.html#Events). * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/edge/cordova_events_events.md.html#Events). * @param {function} callback Called when the Cordova event is fired. * @returns {function} Returns a deregistration function to remove the event listener. */ on: function(type, cb) { ionic.Platform.ready(function() { document.addEventListener(type, cb, false); }); return function() { ionic.Platform.ready(function() { document.removeEventListener(type, cb); }); }; }, /** * @ngdoc method * @name $ionicPlatform#ready * @description * Trigger a callback once the device is ready, * or immediately if the device is already ready. * @param {function=} callback The function to call. * @returns {promise} A promise which is resolved when the device is ready. */ ready: function(cb) { var q = $q.defer(); ionic.Platform.ready(function() { q.resolve(); cb && cb(); }); return q.promise; } }; return self; }] }; }); /** * @ngdoc service * @name $ionicPopover * @module ionic * @description * * Related: {@link ionic.controller:ionicPopover ionicPopover controller}. * * The Popover is a view that floats above an app’s content. Popovers provide an * easy way to present or gather information from the user and are * commonly used in the following situations: * * - Show more info about the current view * - Select a commonly used tool or configuration * - Present a list of actions to perform inside one of your views * * Put the content of the popover inside of an `` element. * * @usage * ```html *

* *

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

My Popover Title

Hello!
'; * * $scope.popover = $ionicPopover.fromTemplate(template, { * scope: $scope, * }); * * // .fromTemplateUrl() method * $ionicPopover.fromTemplateUrl('my-popover.html', { * scope: $scope, * }).then(function(popover) { * $scope.popover = popover; * }); * * * $scope.openPopover = function($event) { * $scope.popover.show($event); * }; * $scope.closePopover = function() { * $scope.popover.hide(); * }; * //Cleanup the popover when we're done with it! * $scope.$on('$destroy', function() { * $scope.popover.remove(); * }); * // Execute action on hide popover * $scope.$on('popover.hidden', function() { * // Execute action * }); * // Execute action on remove popover * $scope.$on('popover.removed', function() { * // Execute action * }); * }); * ``` */ IonicModule .factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', '$window', function($ionicModal, $ionicPosition, $document, $window) { var POPOVER_BODY_PADDING = 6; var POPOVER_OPTIONS = { viewType: 'popover', hideDelay: 1, animation: 'none', positionView: positionView }; function positionView(target, popoverEle) { var targetEle = angular.element(target.target || target); var buttonOffset = $ionicPosition.offset(targetEle); var popoverWidth = popoverEle.prop('offsetWidth'); var popoverHeight = popoverEle.prop('offsetHeight'); var bodyWidth = $document[0].body.clientWidth; // clientHeight doesn't work on all platforms for body var bodyHeight = $window.innerHeight; var popoverCSS = { left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2 }; var arrowEle = jqLite(popoverEle[0].querySelector('.popover-arrow')); if (popoverCSS.left < POPOVER_BODY_PADDING) { popoverCSS.left = POPOVER_BODY_PADDING; } else if (popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) { popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING; } // If the popover when popped down stretches past bottom of screen, // make it pop up if (buttonOffset.top + buttonOffset.height + popoverHeight > bodyHeight) { popoverCSS.top = buttonOffset.top - popoverHeight; popoverEle.addClass('popover-bottom'); } else { popoverCSS.top = buttonOffset.top + buttonOffset.height; popoverEle.removeClass('popover-bottom'); } arrowEle.css({ left: buttonOffset.left + buttonOffset.width / 2 - arrowEle.prop('offsetWidth') / 2 - popoverCSS.left + 'px' }); popoverEle.css({ top: popoverCSS.top + 'px', left: popoverCSS.left + 'px', marginLeft: '0', opacity: '1' }); } /** * @ngdoc controller * @name ionicPopover * @module ionic * @description * Instantiated by the {@link ionic.service:$ionicPopover} service. * * Be sure to call [remove()](#remove) when you are done with each popover * to clean it up and avoid memory leaks. * * Note: a popover will broadcast 'popover.shown', 'popover.hidden', and 'popover.removed' events from its originating * scope, passing in itself as an event argument. Both the popover.removed and popover.hidden events are * called when the popover is removed. */ /** * @ngdoc method * @name ionicPopover#initialize * @description Creates a new popover controller instance. * @param {object} options An options object with the following properties: * - `{object=}` `scope` The scope to be a child of. * Default: creates a child of $rootScope. * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of * the popover when shown. Default: false. * - `{boolean=}` `backdropClickToClose` Whether to close the popover on clicking the backdrop. * Default: true. * - `{boolean=}` `hardwareBackButtonClose` Whether the popover can be closed using the hardware * back button on Android and similar devices. Default: true. */ /** * @ngdoc method * @name ionicPopover#show * @description Show this popover instance. * @param {$event} $event The $event or target element which the popover should align * itself next to. * @returns {promise} A promise which is resolved when the popover is finished animating in. */ /** * @ngdoc method * @name ionicPopover#hide * @description Hide this popover instance. * @returns {promise} A promise which is resolved when the popover is finished animating out. */ /** * @ngdoc method * @name ionicPopover#remove * @description Remove this popover instance from the DOM and clean up. * @returns {promise} A promise which is resolved when the popover is finished animating out. */ /** * @ngdoc method * @name ionicPopover#isShown * @returns boolean Whether this popover is currently shown. */ return { /** * @ngdoc method * @name $ionicPopover#fromTemplate * @param {string} templateString The template string to use as the popovers's * content. * @param {object} options Options to be passed to the initialize method. * @returns {object} An instance of an {@link ionic.controller:ionicPopover} * controller (ionicPopover is built on top of $ionicPopover). */ fromTemplate: function(templateString, options) { return $ionicModal.fromTemplate(templateString, ionic.Utils.extend(POPOVER_OPTIONS, options || {})); }, /** * @ngdoc method * @name $ionicPopover#fromTemplateUrl * @param {string} templateUrl The url to load the template from. * @param {object} options Options to be passed to the initialize method. * @returns {promise} A promise that will be resolved with an instance of * an {@link ionic.controller:ionicPopover} controller (ionicPopover is built on top of $ionicPopover). */ fromTemplateUrl: function(url, options) { return $ionicModal.fromTemplateUrl(url, ionic.Utils.extend(POPOVER_OPTIONS, options || {})); } }; }]); var POPUP_TPL = ''; /** * @ngdoc service * @name $ionicPopup * @module ionic * @restrict E * @codepen zkmhJ * @description * * The Ionic Popup service allows programmatically creating and showing popup * windows that require the user to respond in order to continue. * * The popup system has support for more flexible versions of the built in `alert()`, `prompt()`, * and `confirm()` functions that users are used to, in addition to allowing popups with completely * custom content and look. * * An input can be given an `autofocus` attribute so it automatically receives focus when * the popup first shows. However, depending on certain use-cases this can cause issues with * the tap/click system, which is why Ionic prefers using the `autofocus` attribute as * an opt-in feature and not the default. * * @usage * A few basic examples, see below for details about all of the options available. * * ```js *angular.module('mySuperApp', ['ionic']) *.controller('PopupCtrl',function($scope, $ionicPopup, $timeout) { * * // Triggered on a button click, or some other target * $scope.showPopup = function() { * $scope.data = {} * * // An elaborate, custom popup * var myPopup = $ionicPopup.show({ * template: '', * title: 'Enter Wi-Fi Password', * subTitle: 'Please use normal things', * scope: $scope, * buttons: [ * { text: 'Cancel' }, * { * text: 'Save', * type: 'button-positive', * onTap: function(e) { * if (!$scope.data.wifi) { * //don't allow the user to close unless he enters wifi password * e.preventDefault(); * } else { * return $scope.data.wifi; * } * } * } * ] * }); * myPopup.then(function(res) { * console.log('Tapped!', res); * }); * $timeout(function() { * myPopup.close(); //close the popup after 3 seconds for some reason * }, 3000); * }; * // A confirm dialog * $scope.showConfirm = function() { * var confirmPopup = $ionicPopup.confirm({ * title: 'Consume Ice Cream', * template: 'Are you sure you want to eat this ice cream?' * }); * confirmPopup.then(function(res) { * if(res) { * console.log('You are sure'); * } else { * console.log('You are not sure'); * } * }); * }; * * // An alert dialog * $scope.showAlert = function() { * var alertPopup = $ionicPopup.alert({ * title: 'Don\'t eat that!', * template: 'It might taste good' * }); * alertPopup.then(function(res) { * console.log('Thank you for not eating my delicious ice cream cone'); * }); * }; *}); *``` */ IonicModule .factory('$ionicPopup', [ '$ionicTemplateLoader', '$ionicBackdrop', '$q', '$timeout', '$rootScope', '$ionicBody', '$compile', '$ionicPlatform', function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicBody, $compile, $ionicPlatform) { //TODO allow this to be configured var config = { stackPushDelay: 75 }; var popupStack = []; var $ionicPopup = { /** * @ngdoc method * @description * Show a complex popup. This is the master show function for all popups. * * A complex popup has a `buttons` array, with each button having a `text` and `type` * field, in addition to an `onTap` function. The `onTap` function, called when * the corresponding button on the popup is tapped, will by default close the popup * and resolve the popup promise with its return value. If you wish to prevent the * default and keep the popup open on button tap, call `event.preventDefault()` on the * passed in tap event. Details below. * * @name $ionicPopup#show * @param {object} options The options for the new popup, of the form: * * ``` * { * title: '', // String. The title of the popup. * cssClass: '', // String, The custom CSS class name * subTitle: '', // String (optional). The sub-title of the popup. * template: '', // String (optional). The html template to place in the popup body. * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. * scope: null, // Scope (optional). A scope to link to the popup content. * buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer. * text: 'Cancel', * type: 'button-default', * onTap: function(e) { * // e.preventDefault() will stop the popup from closing when tapped. * e.preventDefault(); * } * }, { * text: 'OK', * type: 'button-positive', * onTap: function(e) { * // Returning a value will cause the promise to resolve with the given value. * return scope.data.response; * } * }] * } * ``` * * @returns {object} A promise which is resolved when the popup is closed. Has an additional * `close` function, which can be used to programmatically close the popup. */ show: showPopup, /** * @ngdoc method * @name $ionicPopup#alert * @description Show a simple alert popup with a message and one button that the user can * tap to close the popup. * * @param {object} options The options for showing the alert, of the form: * * ``` * { * title: '', // String. The title of the popup. * cssClass: '', // String, The custom CSS class name * subTitle: '', // String (optional). The sub-title of the popup. * template: '', // String (optional). The html template to place in the popup body. * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. * okText: '', // String (default: 'OK'). The text of the OK button. * okType: '', // String (default: 'button-positive'). The type of the OK button. * } * ``` * * @returns {object} A promise which is resolved when the popup is closed. Has one additional * function `close`, which can be called with any value to programmatically close the popup * with the given value. */ alert: showAlert, /** * @ngdoc method * @name $ionicPopup#confirm * @description * Show a simple confirm popup with a Cancel and OK button. * * Resolves the promise with true if the user presses the OK button, and false if the * user presses the Cancel button. * * @param {object} options The options for showing the confirm popup, of the form: * * ``` * { * title: '', // String. The title of the popup. * cssClass: '', // String, The custom CSS class name * subTitle: '', // String (optional). The sub-title of the popup. * template: '', // String (optional). The html template to place in the popup body. * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. * cancelText: '', // String (default: 'Cancel'). The text of the Cancel button. * cancelType: '', // String (default: 'button-default'). The type of the Cancel button. * okText: '', // String (default: 'OK'). The text of the OK button. * okType: '', // String (default: 'button-positive'). The type of the OK button. * } * ``` * * @returns {object} A promise which is resolved when the popup is closed. Has one additional * function `close`, which can be called with any value to programmatically close the popup * with the given value. */ confirm: showConfirm, /** * @ngdoc method * @name $ionicPopup#prompt * @description Show a simple prompt popup, which has an input, OK button, and Cancel button. * Resolves the promise with the value of the input if the user presses OK, and with undefined * if the user presses Cancel. * * ```javascript * $ionicPopup.prompt({ * title: 'Password Check', * template: 'Enter your secret password', * inputType: 'password', * inputPlaceholder: 'Your password' * }).then(function(res) { * console.log('Your password is', res); * }); * ``` * @param {object} options The options for showing the prompt popup, of the form: * * ``` * { * title: '', // String. The title of the popup. * cssClass: '', // String, The custom CSS class name * subTitle: '', // String (optional). The sub-title of the popup. * template: '', // String (optional). The html template to place in the popup body. * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. * inputType: // String (default: 'text'). The type of input to use * inputPlaceholder: // String (default: ''). A placeholder to use for the input. * cancelText: // String (default: 'Cancel'. The text of the Cancel button. * cancelType: // String (default: 'button-default'). The type of the Cancel button. * okText: // String (default: 'OK'). The text of the OK button. * okType: // String (default: 'button-positive'). The type of the OK button. * } * ``` * * @returns {object} A promise which is resolved when the popup is closed. Has one additional * function `close`, which can be called with any value to programmatically close the popup * with the given value. */ prompt: showPrompt, /** * @private for testing */ _createPopup: createPopup, _popupStack: popupStack }; return $ionicPopup; function createPopup(options) { options = extend({ scope: null, title: '', buttons: [] }, options || {}); var popupPromise = $ionicTemplateLoader.compile({ template: POPUP_TPL, scope: options.scope && options.scope.$new(), appendTo: $ionicBody.get() }); var contentPromise = options.templateUrl ? $ionicTemplateLoader.load(options.templateUrl) : $q.when(options.template || options.content || ''); return $q.all([popupPromise, contentPromise]) .then(function(results) { var self = results[0]; var content = results[1]; var responseDeferred = $q.defer(); self.responseDeferred = responseDeferred; //Can't ng-bind-html for popup-body because it can be insecure html //(eg an input in case of prompt) var body = jqLite(self.element[0].querySelector('.popup-body')); if (content) { body.html(content); $compile(body.contents())(self.scope); } else { body.remove(); } extend(self.scope, { title: options.title, buttons: options.buttons, subTitle: options.subTitle, cssClass: options.cssClass, $buttonTapped: function(button, event) { var result = (button.onTap || angular.noop)(event); event = event.originalEvent || event; //jquery events if (!event.defaultPrevented) { responseDeferred.resolve(result); } } }); self.show = function() { if (self.isShown) return; self.isShown = true; ionic.requestAnimationFrame(function() { //if hidden while waiting for raf, don't show if (!self.isShown) return; self.element.removeClass('popup-hidden'); self.element.addClass('popup-showing active'); focusInput(self.element); }); }; self.hide = function(callback) { callback = callback || angular.noop; if (!self.isShown) return callback(); self.isShown = false; self.element.removeClass('active'); self.element.addClass('popup-hidden'); $timeout(callback, 250); }; self.remove = function() { if (self.removed) return; self.hide(function() { self.element.remove(); self.scope.$destroy(); }); self.removed = true; }; return self; }); } function onHardwareBackButton(e) { popupStack[0] && popupStack[0].responseDeferred.resolve(); } function showPopup(options) { var popupPromise = $ionicPopup._createPopup(options); var previousPopup = popupStack[0]; if (previousPopup) { previousPopup.hide(); } var resultPromise = $timeout(angular.noop, previousPopup ? config.stackPushDelay : 0) .then(function() { return popupPromise; }) .then(function(popup) { if (!previousPopup) { //Add popup-open & backdrop if this is first popup $ionicBody.addClass('popup-open'); $ionicBackdrop.retain(); //only show the backdrop on the first popup $ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction( onHardwareBackButton, PLATFORM_BACK_BUTTON_PRIORITY_POPUP ); } popupStack.unshift(popup); popup.show(); //DEPRECATED: notify the promise with an object with a close method popup.responseDeferred.notify({ close: resultPromise.close }); return popup.responseDeferred.promise.then(function(result) { var index = popupStack.indexOf(popup); if (index !== -1) { popupStack.splice(index, 1); } popup.remove(); var previousPopup = popupStack[0]; if (previousPopup) { previousPopup.show(); } else { //Remove popup-open & backdrop if this is last popup $timeout(function() { // wait to remove this due to a 300ms delay native // click which would trigging whatever was underneath this $ionicBody.removeClass('popup-open'); }, 400); $timeout(function() { $ionicBackdrop.release(); }, config.stackPushDelay || 0); ($ionicPopup._backButtonActionDone || angular.noop)(); } return result; }); }); function close(result) { popupPromise.then(function(popup) { if (!popup.removed) { popup.responseDeferred.resolve(result); } }); } resultPromise.close = close; return resultPromise; } function focusInput(element) { var focusOn = element[0].querySelector('[autofocus]'); if (focusOn) { focusOn.focus(); } } function showAlert(opts) { return showPopup(extend({ buttons: [{ text: opts.okText || 'OK', type: opts.okType || 'button-positive', onTap: function(e) { return true; } }] }, opts || {})); } function showConfirm(opts) { return showPopup(extend({ buttons: [{ text: opts.cancelText || 'Cancel', type: opts.cancelType || 'button-default', onTap: function(e) { return false; } }, { text: opts.okText || 'OK', type: opts.okType || 'button-positive', onTap: function(e) { return true; } }] }, opts || {})); } function showPrompt(opts) { var scope = $rootScope.$new(true); scope.data = {}; var text = ''; if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) { text = '' + opts.template + ''; delete opts.template; } return showPopup(extend({ template: text + '', scope: scope, buttons: [{ text: opts.cancelText || 'Cancel', type: opts.cancelType || 'button-default', onTap: function(e) {} }, { text: opts.okText || 'OK', type: opts.okType || 'button-positive', onTap: function(e) { return scope.data.response || ''; } }] }, opts || {})); } }]); /** * @ngdoc service * @name $ionicPosition * @module ionic * @description * A set of utility methods that can be use to retrieve position of DOM elements. * It is meant to be used where we need to absolute-position DOM elements in * relation to other, existing elements (this is the case for tooltips, popovers, etc.). * * Adapted from [AngularUI Bootstrap](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js), * ([license](https://github.com/angular-ui/bootstrap/blob/master/LICENSE)) */ IonicModule .factory('$ionicPosition', ['$document', '$window', function($document, $window) { function getStyle(el, cssprop) { if (el.currentStyle) { //IE return el.currentStyle[cssprop]; } else if ($window.getComputedStyle) { return $window.getComputedStyle(el)[cssprop]; } // finally try and get inline style return el.style[cssprop]; } /** * Checks if a given element is statically positioned * @param element - raw DOM element */ function isStaticPositioned(element) { return (getStyle(element, 'position') || 'static') === 'static'; } /** * returns the closest, non-statically positioned parentOffset of a given element * @param element */ var parentOffsetEl = function(element) { var docDomEl = $document[0]; var offsetParent = element.offsetParent || docDomEl; while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) { offsetParent = offsetParent.offsetParent; } return offsetParent || docDomEl; }; return { /** * @ngdoc method * @name $ionicPosition#position * @description Get the current coordinates of the element, relative to the offset parent. * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/). * @param {element} element The element to get the position of. * @returns {object} Returns an object containing the properties top, left, width and height. */ position: function(element) { var elBCR = this.offset(element); var offsetParentBCR = { top: 0, left: 0 }; var offsetParentEl = parentOffsetEl(element[0]); if (offsetParentEl != $document[0]) { offsetParentBCR = this.offset(angular.element(offsetParentEl)); offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; } var boundingClientRect = element[0].getBoundingClientRect(); return { width: boundingClientRect.width || element.prop('offsetWidth'), height: boundingClientRect.height || element.prop('offsetHeight'), top: elBCR.top - offsetParentBCR.top, left: elBCR.left - offsetParentBCR.left }; }, /** * @ngdoc method * @name $ionicPosition#offset * @description Get the current coordinates of the element, relative to the document. * Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/). * @param {element} element The element to get the offset of. * @returns {object} Returns an object containing the properties top, left, width and height. */ offset: function(element) { var boundingClientRect = element[0].getBoundingClientRect(); return { width: boundingClientRect.width || element.prop('offsetWidth'), height: boundingClientRect.height || element.prop('offsetHeight'), top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) }; } }; }]); /** * @ngdoc service * @name $ionicScrollDelegate * @module ionic * @description * Delegate for controlling scrollViews (created by * {@link ionic.directive:ionContent} and * {@link ionic.directive:ionScroll} directives). * * Methods called directly on the $ionicScrollDelegate service will control all scroll * views. Use the {@link ionic.service:$ionicScrollDelegate#$getByHandle $getByHandle} * method to control specific scrollViews. * * @usage * * ```html * * * * * * ``` * ```js * function MainCtrl($scope, $ionicScrollDelegate) { * $scope.scrollTop = function() { * $ionicScrollDelegate.scrollTop(); * }; * } * ``` * * Example of advanced usage, with two scroll areas using `delegate-handle` * for fine control. * * ```html * * * * * * * * * ``` * ```js * function MainCtrl($scope, $ionicScrollDelegate) { * $scope.scrollMainToTop = function() { * $ionicScrollDelegate.$getByHandle('mainScroll').scrollTop(); * }; * $scope.scrollSmallToTop = function() { * $ionicScrollDelegate.$getByHandle('small').scrollTop(); * }; * } * ``` */ IonicModule .service('$ionicScrollDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicScrollDelegate#resize * @description Tell the scrollView to recalculate the size of its container. */ 'resize', /** * @ngdoc method * @name $ionicScrollDelegate#scrollTop * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollTop', /** * @ngdoc method * @name $ionicScrollDelegate#scrollBottom * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollBottom', /** * @ngdoc method * @name $ionicScrollDelegate#scrollTo * @param {number} left The x-value to scroll to. * @param {number} top The y-value to scroll to. * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollTo', /** * @ngdoc method * @name $ionicScrollDelegate#scrollBy * @param {number} left The x-offset to scroll by. * @param {number} top The y-offset to scroll by. * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'scrollBy', /** * @ngdoc method * @name $ionicScrollDelegate#zoomTo * @param {number} level Level to zoom to. * @param {boolean=} animate Whether to animate the zoom. * @param {number=} originLeft Zoom in at given left coordinate. * @param {number=} originTop Zoom in at given top coordinate. */ 'zoomTo', /** * @ngdoc method * @name $ionicScrollDelegate#zoomBy * @param {number} factor The factor to zoom by. * @param {boolean=} animate Whether to animate the zoom. * @param {number=} originLeft Zoom in at given left coordinate. * @param {number=} originTop Zoom in at given top coordinate. */ 'zoomBy', /** * @ngdoc method * @name $ionicScrollDelegate#getScrollPosition * @returns {object} The scroll position of this view, with the following properties: * - `{number}` `left` The distance the user has scrolled from the left (starts at 0). * - `{number}` `top` The distance the user has scrolled from the top (starts at 0). */ '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#getScrollView * @returns {object} The scrollView associated with this delegate. */ 'getScrollView', /** * @ngdoc method * @name $ionicScrollDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * scrollViews with `delegate-handle` matching the given handle. * * Example: `$ionicScrollDelegate.$getByHandle('my-handle').scrollTop();` */ ])); /** * @ngdoc service * @name $ionicSideMenuDelegate * @module ionic * * @description * Delegate for controlling the {@link ionic.directive:ionSideMenus} directive. * * Methods called directly on the $ionicSideMenuDelegate service will control all side * menus. Use the {@link ionic.service:$ionicSideMenuDelegate#$getByHandle $getByHandle} * method to control specific ionSideMenus instances. * * @usage * * ```html * * * * Content! * * * * Left Menu! * * * * ``` * ```js * function MainCtrl($scope, $ionicSideMenuDelegate) { * $scope.toggleLeftSideMenu = function() { * $ionicSideMenuDelegate.toggleLeft(); * }; * } * ``` */ IonicModule .service('$ionicSideMenuDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicSideMenuDelegate#toggleLeft * @description Toggle the left side menu (if it exists). * @param {boolean=} isOpen Whether to open or close the menu. * Default: Toggles the menu. */ 'toggleLeft', /** * @ngdoc method * @name $ionicSideMenuDelegate#toggleRight * @description Toggle the right side menu (if it exists). * @param {boolean=} isOpen Whether to open or close the menu. * Default: Toggles the menu. */ 'toggleRight', /** * @ngdoc method * @name $ionicSideMenuDelegate#getOpenRatio * @description Gets the ratio of open amount over menu width. For example, a * menu of width 100 that is opened by 50 pixels is 50% opened, and would return * a ratio of 0.5. * * @returns {float} 0 if nothing is open, between 0 and 1 if left menu is * opened/opening, and between 0 and -1 if right menu is opened/opening. */ 'getOpenRatio', /** * @ngdoc method * @name $ionicSideMenuDelegate#isOpen * @returns {boolean} Whether either the left or right menu is currently opened. */ 'isOpen', /** * @ngdoc method * @name $ionicSideMenuDelegate#isOpenLeft * @returns {boolean} Whether the left menu is currently opened. */ 'isOpenLeft', /** * @ngdoc method * @name $ionicSideMenuDelegate#isOpenRight * @returns {boolean} Whether the right menu is currently opened. */ 'isOpenRight', /** * @ngdoc method * @name $ionicSideMenuDelegate#canDragContent * @param {boolean=} canDrag Set whether the content can or cannot be dragged to open * side menus. * @returns {boolean} Whether the content can be dragged to open side menus. */ 'canDragContent', /** * @ngdoc method * @name $ionicSideMenuDelegate#edgeDragThreshold * @param {boolean|number=} value Set whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Accepts three different values: * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu. * - If true is given, the default number of pixels (25) is used as the maximum allowed distance. * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed. * @returns {boolean} Whether the drag can start only from within the edge of screen threshold. */ 'edgeDragThreshold', /** * @ngdoc method * @name $ionicSideMenuDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionSideMenus} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicSideMenuDelegate.$getByHandle('my-handle').toggleLeft();` */ ])); /** * @ngdoc service * @name $ionicSlideBoxDelegate * @module ionic * @description * Delegate that controls the {@link ionic.directive:ionSlideBox} directive. * * Methods called directly on the $ionicSlideBoxDelegate service will control all slide boxes. Use the {@link ionic.service:$ionicSlideBoxDelegate#$getByHandle $getByHandle} * method to control specific slide box instances. * * @usage * * ```html * * * *
* *
*
* *
* Slide 2! *
*
*
* * ``` * ```js * function MyCtrl($scope, $ionicSlideBoxDelegate) { * $scope.nextSlide = function() { * $ionicSlideBoxDelegate.next(); * } * } * ``` */ IonicModule .service('$ionicSlideBoxDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicSlideBoxDelegate#update * @description * Update the slidebox (for example if using Angular with ng-repeat, * resize it for the elements inside). */ 'update', /** * @ngdoc method * @name $ionicSlideBoxDelegate#slide * @param {number} to The index to slide to. * @param {number=} speed The number of milliseconds for the change to take. */ 'slide', 'select', /** * @ngdoc method * @name $ionicSlideBoxDelegate#enableSlide * @param {boolean=} shouldEnable Whether to enable sliding the slidebox. * @returns {boolean} Whether sliding is enabled. */ 'enableSlide', /** * @ngdoc method * @name $ionicSlideBoxDelegate#previous * @description Go to the previous slide. Wraps around if at the beginning. */ 'previous', /** * @ngdoc method * @name $ionicSlideBoxDelegate#next * @description Go to the next slide. Wraps around if at the end. */ 'next', /** * @ngdoc method * @name $ionicSlideBoxDelegate#stop * @description Stop sliding. The slideBox will not move again until * explicitly told to do so. */ 'stop', 'autoPlay', /** * @ngdoc method * @name $ionicSlideBoxDelegate#start * @description Start sliding again if the slideBox was stopped. */ 'start', /** * @ngdoc method * @name $ionicSlideBoxDelegate#currentIndex * @returns number The index of the current slide. */ 'currentIndex', 'selected', /** * @ngdoc method * @name $ionicSlideBoxDelegate#slidesCount * @returns number The number of slides there are currently. */ 'slidesCount', 'count', 'loop', /** * @ngdoc method * @name $ionicSlideBoxDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionSlideBox} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicSlideBoxDelegate.$getByHandle('my-handle').stop();` */ ])); /** * @ngdoc service * @name $ionicTabsDelegate * @module ionic * * @description * Delegate for controlling the {@link ionic.directive:ionTabs} directive. * * Methods called directly on the $ionicTabsDelegate service will control all ionTabs * directives. Use the {@link ionic.service:$ionicTabsDelegate#$getByHandle $getByHandle} * method to control specific ionTabs instances. * * @usage * * ```html * * * * * Hello tab 1! * * * Hello tab 2! * * * * ``` * ```js * function MyCtrl($scope, $ionicTabsDelegate) { * $scope.selectTabWithIndex = function(index) { * $ionicTabsDelegate.select(index); * } * } * ``` */ IonicModule .service('$ionicTabsDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicTabsDelegate#select * @description Select the tab matching the given index. * * @param {number} index Index of the tab to select. */ 'select', /** * @ngdoc method * @name $ionicTabsDelegate#selectedIndex * @returns `number` The index of the selected tab, or -1. */ 'selectedIndex' /** * @ngdoc method * @name $ionicTabsDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionTabs} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicTabsDelegate.$getByHandle('my-handle').select(0);` */ ])); // closure to keep things neat (function() { var templatesToCache = []; /** * @ngdoc service * @name $ionicTemplateCache * @module ionic * @description A service that preemptively caches template files to eliminate transition flicker and boost performance. * @usage * State templates are cached automatically, but you can optionally cache other templates. * * ```js * $ionicTemplateCache('myNgIncludeTemplate.html'); * ``` * * Optionally disable all preemptive caching with the `$ionicConfigProvider` or individual states by setting `prefetchTemplate` * in the `$state` definition * * ```js * angular.module('myApp', ['ionic']) * .config(function($stateProvider, $ionicConfigProvider) { * * // disable preemptive template caching globally * $ionicConfigProvider.templates.prefetch(false); * * // disable individual states * $stateProvider * .state('tabs', { * url: "/tab", * abstract: true, * prefetchTemplate: false, * templateUrl: "tabs-templates/tabs.html" * }) * .state('tabs.home', { * url: "/home", * views: { * 'home-tab': { * prefetchTemplate: false, * templateUrl: "tabs-templates/home.html", * controller: 'HomeTabCtrl' * } * } * }); * }); * ``` */ IonicModule .factory('$ionicTemplateCache', [ '$http', '$templateCache', '$timeout', function($http, $templateCache, $timeout) { var toCache = templatesToCache, hasRun; function $ionicTemplateCache(templates) { if (typeof templates === 'undefined') { return run(); } if (isString(templates)) { templates = [templates]; } forEach(templates, function(template) { toCache.push(template); }); if (hasRun) { run(); } } // run through methods - internal method function run() { $ionicTemplateCache._runCount++; hasRun = true; // ignore if race condition already zeroed out array if (toCache.length === 0) return; var i = 0; while (i < 4 && (template = toCache.pop())) { // note that inline templates are ignored by this request if (isString(template)) $http.get(template, { cache: $templateCache }); i++; } // only preload 3 templates a second if (toCache.length) { $timeout(run, 1000); } } // exposing for testing $ionicTemplateCache._runCount = 0; // default method return $ionicTemplateCache; }]) // Intercepts the $stateprovider.state() command to look for templateUrls that can be cached .config([ '$stateProvider', '$ionicConfigProvider', function($stateProvider, $ionicConfigProvider) { var stateProviderState = $stateProvider.state; $stateProvider.state = function(stateName, definition) { // don't even bother if it's disabled. note, another config may run after this, so it's not a catch-all if (typeof definition === 'object') { var enabled = definition.prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch(); if (enabled && isString(definition.templateUrl)) templatesToCache.push(definition.templateUrl); if (angular.isObject(definition.views)) { for (var key in definition.views) { enabled = definition.views[key].prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch(); if (enabled && isString(definition.views[key].templateUrl)) templatesToCache.push(definition.views[key].templateUrl); } } } return stateProviderState.call($stateProvider, stateName, definition); }; }]) // process the templateUrls collected by the $stateProvider, adding them to the cache .run(['$ionicTemplateCache', function($ionicTemplateCache) { $ionicTemplateCache(); }]); })(); IonicModule .factory('$ionicTemplateLoader', [ '$compile', '$controller', '$http', '$q', '$rootScope', '$templateCache', function($compile, $controller, $http, $q, $rootScope, $templateCache) { return { load: fetchTemplate, compile: loadAndCompile }; function fetchTemplate(url) { return $http.get(url, {cache: $templateCache}) .then(function(response) { return response.data && response.data.trim(); }); } function loadAndCompile(options) { options = extend({ template: '', templateUrl: '', scope: null, controller: null, locals: {}, appendTo: null }, options || {}); var templatePromise = options.templateUrl ? this.load(options.templateUrl) : $q.when(options.template); return templatePromise.then(function(template) { var controller; var scope = options.scope || $rootScope.$new(); //Incase template doesn't have just one root element, do this var element = jqLite('
').html(template).contents(); if (options.controller) { controller = $controller( options.controller, extend(options.locals, { $scope: scope }) ); element.children().data('$ngControllerController', controller); } if (options.appendTo) { jqLite(options.appendTo).append(element); } $compile(element)(scope); return { element: element, scope: scope }; }); } }]); /** * @private * DEPRECATED, as of v1.0.0-beta14 ------- */ IonicModule .factory('$ionicViewService', ['$ionicHistory', '$log', function($ionicHistory, $log) { function warn(oldMethod, newMethod) { $log.warn('$ionicViewService' + oldMethod + ' is deprecated, please use $ionicHistory' + newMethod + ' instead: http://ionicframework.com/docs/nightly/api/service/$ionicHistory/'); } warn('', ''); var methodsMap = { getCurrentView: 'currentView', getBackView: 'backView', getForwardView: 'forwardView', getCurrentStateName: 'currentStateName', nextViewOptions: 'nextViewOptions', clearHistory: 'clearHistory' }; forEach(methodsMap, function(newMethod, oldMethod) { methodsMap[oldMethod] = function() { warn('.' + oldMethod, '.' + newMethod); return $ionicHistory[newMethod].apply(this, arguments); }; }); return methodsMap; }]); /** * @private * TODO document */ IonicModule .factory('$ionicViewSwitcher',[ '$timeout', '$document', '$q', '$ionicClickBlock', '$ionicConfig', '$ionicNavBarDelegate', function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDelegate) { var TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; var DATA_NO_CACHE = '$noCache'; var DATA_DESTROY_ELE = '$destroyEle'; var DATA_ELE_IDENTIFIER = '$eleId'; var DATA_VIEW_ACCESSED = '$accessed'; var DATA_FALLBACK_TIMER = '$fallbackTimer'; var DATA_VIEW = '$viewData'; var NAV_VIEW_ATTR = 'nav-view'; var HISTORY_CURSOR_ATTR = 'history-cursor'; var VIEW_STATUS_ACTIVE = 'active'; var VIEW_STATUS_CACHED = 'cached'; var VIEW_STATUS_STAGED = 'stage'; var transitionCounter = 0; var nextTransition, nextDirection; ionic.transition = ionic.transition || {}; ionic.transition.isActive = false; var isActiveTimer; var cachedAttr = ionic.DomUtil.cachedAttr; var transitionPromises = []; var ionicViewSwitcher = { create: function(navViewCtrl, viewLocals, enteringView, leavingView) { // 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 viewEle, viewElements = navViewCtrl.getViewElements(); var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView); var navViewActiveEleId = navViewCtrl.activeEleId(); for (var 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 (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); } navViewCtrl.activeEleId(enteringEleIdentifier); registerData.ele = null; }, render: function(registerData, callback) { // disconnect the leaving scope before reconnecting or creating a scope for the entering view leavingEle && ionic.Utils.disconnectScope(leavingEle.scope()); 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) { var deferred = $q.defer(); transitionPromises.push(deferred.promise); 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; 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)); switcher.emit('before', enteringData, leavingData); // 1) 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); if (viewTransition.shouldAnimate) { // 2) attach transitionend events (and fallback timer) enteringEle.on(TRANSITIONEND_EVENT, transitionComplete); enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, 1000)); $ionicClickBlock.show(); } // 3) stage entering element, opacity 0, no transition duration navViewAttr(enteringEle, VIEW_STATUS_STAGED); // 4) place the elements in the correct step to begin viewTransition.run(0); // 5) wait a frame so the styles apply $timeout(onReflow, 16); function onReflow() { // 6) remove that we're staging the entering element so it can transition navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE); navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED); // 7) start the transition viewTransition.run(1); $ionicNavBarDelegate._instances.forEach(function(instance) { instance.triggerTransitionStart(transitionId); }); if (!viewTransition.shouldAnimate) { // no animated transition transitionComplete(); } } function transitionComplete() { if (transitionComplete.x) return; transitionComplete.x = true; enteringEle.off(TRANSITIONEND_EVENT, transitionComplete); $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER)); // 8) emit that the views have finished transitioning // each parent nav-view will update which views are active and cached switcher.emit('after', enteringData, leavingData); // 9) resolve that this one transition (there could be many w/ nested views) deferred.resolve(navViewCtrl); // 10) the most recent transition added has completed and all the active // transition promises should be added to the services array of promises if (transitionId === transitionCounter) { $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd); switcher.cleanup(enteringData); } $ionicNavBarDelegate._instances.forEach(function(instance) { instance.triggerTransitionEnd(); }); // remove any references that could cause memory issues nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null; } }, emit: function(step, enteringData, leavingData) { var scope = enteringEle.scope(); if (scope) { scope.$emit('$ionicView.' + step + 'Enter', enteringData); if (step == 'after') { scope.$emit('$ionicView.enter', enteringData); } } if (leavingEle) { scope = leavingEle.scope(); if (scope) { scope.$emit('$ionicView.' + step + 'Leave', leavingData); if (step == 'after') { scope.$emit('$ionicView.leave', leavingData); } } } }, cleanup: function(transData) { // check if any views should be removed if (leavingEle && transData.direction == 'back' && !$ionicConfig.views.forwardCache()) { // if they just navigated back we can destroy the forward view // do not remove forward views if cacheForwardViews config is true destroyViewEle(leavingEle); } var viewElements = navViewCtrl.getViewElements(); var viewElementsLength = viewElements.length; var x, viewElement; var removeOldestAccess = (viewElementsLength - 1) > $ionicConfig.views.maxCache(); var removableEle; var oldestAccess = Date.now(); for (x = 0; x < viewElementsLength; x++) { viewElement = viewElements.eq(x); if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) { // remember what was the oldest element to be accessed so it can be destroyed oldestAccess = viewElement.data(DATA_VIEW_ACCESSED); removableEle = viewElements.eq(x); } else if (viewElement.data(DATA_DESTROY_ELE) && navViewAttr(viewElement) != VIEW_STATUS_ACTIVE) { destroyViewEle(viewElement); } } destroyViewEle(removableEle); if (enteringEle.data(DATA_NO_CACHE)) { enteringEle.data(DATA_DESTROY_ELE, true); } }, enteringEle: function() { return enteringEle; }, leavingEle: function() { return leavingEle; } }; return switcher; }, transitionEnd: function(navViewCtrls) { forEach(navViewCtrls, function(navViewCtrl){ navViewCtrl.transitionEnd(); }); ionicViewSwitcher.isTransitioning(false); $ionicClickBlock.hide(); transitionPromises = []; }, nextTransition: function(val) { nextTransition = val; }, nextDirection: function(val) { nextDirection = val; }, isTransitioning: function(val) { if (arguments.length) { ionic.transition.isActive = !!val; $timeout.cancel(isActiveTimer); if (val) { isActiveTimer = $timeout(function() { ionicViewSwitcher.isTransitioning(false); }, 999); } } return ionic.transition.isActive; }, createViewEle: function(viewLocals) { var containerEle = $document[0].createElement('div'); if (viewLocals && viewLocals.$template) { containerEle.innerHTML = viewLocals.$template; if (containerEle.children.length === 1) { containerEle.children[0].classList.add('pane'); return jqLite(containerEle.children[0]); } } containerEle.className = "pane"; return jqLite(containerEle); }, viewEleIsActive: function(viewEle, isActiveAttr) { navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED); }, getTransitionData: getTransitionData, navViewAttr: navViewAttr, destroyViewEle: destroyViewEle }; return ionicViewSwitcher; function getViewElementIdentifier(locals, view) { if (viewState(locals).abstract) return viewState(locals).name; if (view) return view.stateId || view.viewId; return ionic.Utils.nextUid(); } function viewState(locals) { return locals && locals.$$state && locals.$$state.self || {}; } function getTransitionData(viewLocals, enteringEle, direction, view) { // Priority // 1) attribute directive on the button/link to this view // 2) entering element's attribute // 3) entering view's $state config property // 4) view registration data // 5) global config // 6) fallback value var state = viewState(viewLocals); var viewTransition = nextTransition || cachedAttr(enteringEle, 'view-transition') || state.viewTransition || $ionicConfig.views.transition() || 'ios'; var navBarTransition = $ionicConfig.navBar.transition(); direction = nextDirection || cachedAttr(enteringEle, 'view-direction') || state.viewDirection || direction || 'none'; return extend(getViewData(view), { transition: viewTransition, navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition, direction: direction, shouldAnimate: (viewTransition !== 'none' && direction !== 'none') }); } function getViewData(view) { view = view || {}; return { viewId: view.viewId, historyId: view.historyId, stateId: view.stateId, stateName: view.stateName, stateParams: view.stateParams }; } function navViewAttr(ele, value) { if (arguments.length > 1) { cachedAttr(ele, NAV_VIEW_ATTR, value); } else { return cachedAttr(ele, NAV_VIEW_ATTR); } } function destroyViewEle(ele) { // we found an element that should be removed // destroy its scope, then remove the element if (ele && ele.length) { var viewScope = ele.scope(); if (viewScope) { viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW)); viewScope.$destroy(); } ele.remove(); } } }]); /** * @private * Parts of Ionic requires that $scope data is attached to the element. * We do not want to disable adding $scope data to the $element when * $compileProvider.debugInfoEnabled(false) is used. */ IonicModule.config(['$provide', function($provide) { $provide.decorator('$compile', ['$delegate', function($compile) { $compile.$$addScopeInfo = function $$addScopeInfo($element, scope, isolated, noTemplate) { var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; $element.data(dataName, scope); }; return $compile; }]); }]); /** * @private */ IonicModule.config([ '$provide', function($provide) { function $LocationDecorator($location, $timeout) { $location.__hash = $location.hash; //Fix: when window.location.hash is set, the scrollable area //found nearest to body's scrollTop is set to scroll to an element //with that ID. $location.hash = function(value) { if (angular.isDefined(value)) { $timeout(function() { var scroll = document.querySelector('.scroll-content'); if (scroll) scroll.scrollTop = 0; }, 0, false); } return $location.__hash(value); }; return $location; } $provide.decorator('$location', ['$delegate', '$timeout', $LocationDecorator]); }]); IonicModule .controller('$ionicHeaderBar', [ '$scope', '$element', '$attrs', '$q', '$ionicConfig', '$ionicHistory', function($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) { var TITLE = 'title'; var BACK_TEXT = 'back-text'; var BACK_BUTTON = 'back-button'; var DEFAULT_TITLE = 'default-title'; var PREVIOUS_TITLE = 'previous-title'; var HIDE = 'hide'; var self = this; var titleText = ''; var previousTitleText = ''; var titleLeft = 0; var titleRight = 0; var titleCss = ''; var isBackEnabled = false; var isBackShown = true; var isNavBackShown = true; var isBackElementShown = false; var titleTextWidth = 0; self.beforeEnter = function(viewData) { $scope.$broadcast('$ionicView.beforeEnter', viewData); }; self.title = function(newTitleText) { if (arguments.length && newTitleText !== titleText) { getEle(TITLE).innerHTML = newTitleText; titleText = newTitleText; titleTextWidth = 0; } return titleText; }; self.enableBack = function(shouldEnable, disableReset) { // whether or not the back button show be visible, according // to the navigation and history if (arguments.length) { isBackEnabled = shouldEnable; if (!disableReset) self.updateBackButton(); } return isBackEnabled; }; self.showBack = function(shouldShow, disableReset) { // different from enableBack() because this will always have the back // visually hidden if false, even if the history says it should show if (arguments.length) { isBackShown = shouldShow; if (!disableReset) self.updateBackButton(); } return isBackShown; }; self.showNavBack = function(shouldShow) { // different from showBack() because this is for the entire nav bar's // setting for all of it's child headers. For internal use. isNavBackShown = shouldShow; self.updateBackButton(); }; self.updateBackButton = function() { if ((isBackShown && isNavBackShown && isBackEnabled) !== isBackElementShown) { isBackElementShown = isBackShown && isNavBackShown && isBackEnabled; var backBtnEle = getEle(BACK_BUTTON); backBtnEle && backBtnEle.classList[ isBackElementShown ? 'remove' : 'add' ](HIDE); } }; self.titleTextWidth = function() { if (!titleTextWidth) { var bounds = ionic.DomUtil.getTextBounds(getEle(TITLE)); titleTextWidth = Math.min(bounds && bounds.width || 30); } return titleTextWidth; }; self.titleWidth = function() { var titleWidth = self.titleTextWidth(); var offsetWidth = getEle(TITLE).offsetWidth; if (offsetWidth < titleWidth) { titleWidth = offsetWidth + (titleLeft - titleRight - 5); } return titleWidth; }; self.titleTextX = function() { return ($element[0].offsetWidth / 2) - (self.titleWidth() / 2); }; self.titleLeftRight = function() { return titleLeft - titleRight; }; self.backButtonTextLeft = function() { var offsetLeft = 0; var ele = getEle(BACK_TEXT); while (ele) { offsetLeft += ele.offsetLeft; ele = ele.parentElement; } return offsetLeft; }; self.resetBackButton = function() { if ($ionicConfig.backButton.previousTitleText()) { var previousTitleEle = getEle(PREVIOUS_TITLE); if (previousTitleEle) { previousTitleEle.classList.remove(HIDE); var newPreviousTitleText = $ionicHistory.backTitle(); 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; }); }]); /** * @ngdoc service * @name $ionicListDelegate * @module ionic * * @description * Delegate for controlling the {@link ionic.directive:ionList} directive. * * Methods called directly on the $ionicListDelegate service will control all lists. * Use the {@link ionic.service:$ionicListDelegate#$getByHandle $getByHandle} * method to control specific ionList instances. * * @usage * * ````html * * * * * {% raw %}Hello, {{i}}!{% endraw %} * * * * * ``` * ```js * function MyCtrl($scope, $ionicListDelegate) { * $scope.showDeleteButtons = function() { * $ionicListDelegate.showDelete(true); * }; * } * ``` */ IonicModule .service('$ionicListDelegate', ionic.DelegateService([ /** * @ngdoc method * @name $ionicListDelegate#showReorder * @param {boolean=} showReorder Set whether or not this list is showing its reorder buttons. * @returns {boolean} Whether the reorder buttons are shown. */ 'showReorder', /** * @ngdoc method * @name $ionicListDelegate#showDelete * @param {boolean=} showDelete Set whether or not this list is showing its delete buttons. * @returns {boolean} Whether the delete buttons are shown. */ 'showDelete', /** * @ngdoc method * @name $ionicListDelegate#canSwipeItems * @param {boolean=} canSwipeItems Set whether or not this list is able to swipe to show * option buttons. * @returns {boolean} Whether the list is able to swipe to show option buttons. */ 'canSwipeItems', /** * @ngdoc method * @name $ionicListDelegate#closeOptionButtons * @description Closes any option buttons on the list that are swiped open. */ 'closeOptionButtons', /** * @ngdoc method * @name $ionicListDelegate#$getByHandle * @param {string} handle * @returns `delegateInstance` A delegate instance that controls only the * {@link ionic.directive:ionList} directives with `delegate-handle` matching * the given handle. * * Example: `$ionicListDelegate.$getByHandle('my-handle').showReorder(true);` */ ])) .controller('$ionicList', [ '$scope', '$attrs', '$ionicListDelegate', '$ionicHistory', function($scope, $attrs, $ionicListDelegate, $ionicHistory) { var self = this; var isSwipeable = true; var isReorderShown = false; var isDeleteShown = false; var deregisterInstance = $ionicListDelegate._registerInstance( self, $attrs.delegateHandle, function() { return $ionicHistory.isActiveScope($scope); } ); $scope.$on('$destroy', deregisterInstance); self.showReorder = function(show) { if (arguments.length) { isReorderShown = !!show; } return isReorderShown; }; self.showDelete = function(show) { if (arguments.length) { isDeleteShown = !!show; } return isDeleteShown; }; self.canSwipeItems = function(can) { if (arguments.length) { isSwipeable = !!can; } return isSwipeable; }; self.closeOptionButtons = function() { self.listView && self.listView.clearDragEffects(); }; }]); IonicModule .controller('$ionicNavBar', [ '$scope', '$element', '$attrs', '$compile', '$timeout', '$ionicNavBarDelegate', '$ionicConfig', '$ionicHistory', function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $ionicConfig, $ionicHistory) { var CSS_HIDE = 'hide'; var DATA_NAV_BAR_CTRL = '$ionNavBarController'; var PRIMARY_BUTTONS = 'primaryButtons'; var SECONDARY_BUTTONS = 'secondaryButtons'; var BACK_BUTTON = 'backButton'; var ITEM_TYPES = 'primaryButtons secondaryButtons leftButtons rightButtons title'.split(' '); var self = this; var headerBars = []; var navElementHtml = {}; var isVisible = true; var queuedTransitionStart, queuedTransitionEnd, latestTransitionId; $element.parent().data(DATA_NAV_BAR_CTRL, self); var delegateHandle = $attrs.delegateHandle || 'navBar' + ionic.Utils.nextUid(); var deregisterInstance = $ionicNavBarDelegate._registerInstance(self, delegateHandle); self.init = function() { $element.addClass('nav-bar-container'); ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', $ionicConfig.views.transition()); // create two nav bar blocks which will trade out which one is shown self.createHeaderBar(false); self.createHeaderBar(true); $scope.$emit('ionNavBar.init', delegateHandle); }; self.createHeaderBar = function(isActive, navBarClass) { var containerEle = jqLite('