UNPKG

cordova-plugin-progressindicator

Version:
1,601 lines (1,454 loc) 240 kB
/*! * Copyright 2014 Drifty Co. * http://drifty.com/ * * Ionic, v1.0.0-beta.9 * A powerful HTML5 mobile app framework. * http://ionicframework.com/ * * By @maxlynch, @benjsperry, @adamdbradley <3 * * Licensed under the MIT license. Please see LICENSE for more information. * */ (function() { /* * deprecated.js * https://github.com/wearefractal/deprecated/ * Copyright (c) 2014 Fractal <contact@wearefractal.com> * License MIT */ //Interval object var deprecated = { method: function(msg, log, fn) { var called = false; return function deprecatedMethod(){ if (!called) { called = true; log(msg); } return fn.apply(this, arguments); }; }, field: function(msg, log, parent, field, val) { var called = false; var getter = function(){ if (!called) { called = true; log(msg); } return val; }; var setter = function(v) { if (!called) { called = true; log(msg); } val = v; return v; }; Object.defineProperty(parent, field, { get: getter, set: setter, enumerable: true }); return; } }; var IonicModule = angular.module('ionic', ['ngAnimate', 'ngSanitize', 'ui.router']), extend = angular.extend, forEach = angular.forEach, isDefined = angular.isDefined, 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: '<b>Share</b> This' }, * { text: 'Move' } * ], * destructiveText: 'Delete', * titleText: 'Modify your album', * cancelText: 'Cancel', * buttonClicked: function(index) { * return true; * } * }); * * // For example's sake, hide the sheet after two seconds * $timeout(function() { * hideSheet(); * }, 2000); * * }; * }); * ``` * */ IonicModule .factory('$ionicActionSheet', [ '$rootScope', '$document', '$compile', '$animate', '$timeout', '$ionicTemplateLoader', '$ionicPlatform', function($rootScope, $document, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform) { 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. * * @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('<ion-action-sheet buttons="buttons"></ion-action-sheet>')(scope); // Grab the sheet element for animation var sheetEl = jqLite(element[0].querySelector('.action-sheet-wrapper')); var stateChangeListenDone = scope.cancelOnStateChange ? $rootScope.$on('$stateChangeSuccess', function() { scope.cancel(); }) : angular.noop; // removes the actionSheet from the screen scope.removeSheet = function(done) { if (scope.removed) return; scope.removed = true; sheetEl.removeClass('action-sheet-up'); $document[0].body.classList.remove('action-sheet-open'); scope.$deregisterBackButton(); stateChangeListenDone(); $animate.removeClass(element, 'active', function() { scope.$destroy(); element.remove(); // scope.cancel.$scope is defined near the bottom scope.cancel.$scope = null; (done || angular.noop)(); }); }; scope.showSheet = function(done) { if (scope.removed) return; $document[0].body.appendChild(element[0]); $document[0].body.classList.add('action-sheet-open'); $animate.addClass(element, 'active', 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( 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(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(cssClasses); } else { splitClasses = cssClasses.split(' '); for (y=0; y<splitClasses.length; y++) { cssClass = splitClasses[y]; el.setAttribute('class', ( (" " + (el.getAttribute('class') || '') + " ") .replace(/[\n\t]/g, " ") .replace(" " + cssClass.trim() + " ", " ")).trim() ); } } } } } return this; }; /** * @ngdoc service * @name $ionicAnimation * @module ionic * @description * * A powerful animation and transition system for Ionic apps. * * @usage * * ```js * angular.module('mySuperApp', ['ionic']) * .controller(function($scope, $ionicAnimation) { * var anim = $ionicAnimate({ * // A unique, reusable name * name: 'popIn', * * // The duration of an auto playthrough * duration: 0.5, * * // How long to wait before running the animation * delay: 0, * * // Whether to reverse after doing one run through * autoReverse: false, * * // How many times to repeat? -1 or null for infinite * repeat: -1, * * // Timing curve to use (same as CSS timing functions), or a function of time "t" to handle it yourself * curve: 'ease-in-out' * * onStart: function() { * // Callback on start * }, * onEnd: function() { * // Callback on end * }, * step: function(amt) { * * } * }) * }); * ``` * */ IonicModule .provider('$ionicAnimation', function() { var useSlowAnimations = false; this.setSlowAnimations = function(isSlow) { useSlowAnimations = isSlow; }; this.create = function(animation) { return ionic.Animation.create(animation); }; this.$get = [function() { return function(opts) { opts.useSlowAnimations = useSlowAnimations; return ionic.Animation.create(opts); }; }]; }); /** * @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', function($document) { var el = jqLite('<div class="backdrop">'); var backdropHolds = 0; $document[0].body.appendChild(el[0]); return { /** * @ngdoc method * @name $ionicBackdrop#retain * @description Retains the backdrop. */ retain: retain, /** * @ngdoc method * @name $ionicBackdrop#release * @description * Releases the backdrop. */ release: release, getElement: getElement, // exposed for testing _element: el }; function retain() { if ( (++backdropHolds) === 1 ) { el.addClass('visible'); ionic.requestAnimationFrame(function() { backdropHolds && el.addClass('active'); }); } } function release() { if ( (--backdropHolds) === 0 ) { el.removeClass('active'); setTimeout(function() { !backdropHolds && el.removeClass('visible'); }, 100); } } 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; } }); }; }]); IonicModule .factory('$collectionDataSource', [ '$cacheFactory', '$parse', '$rootScope', function($cacheFactory, $parse, $rootScope) { function CollectionRepeatDataSource(options) { var self = this; this.scope = options.scope; this.transcludeFn = options.transcludeFn; this.transcludeParent = options.transcludeParent; this.keyExpr = options.keyExpr; this.listExpr = options.listExpr; this.trackByExpr = options.trackByExpr; this.heightGetter = options.heightGetter; this.widthGetter = options.widthGetter; this.dimensions = []; this.data = []; if (this.trackByExpr) { var trackByGetter = $parse(this.trackByExpr); var hashFnLocals = {$id: hashKey}; this.itemHashGetter = function(index, value) { hashFnLocals[self.keyExpr] = value; hashFnLocals.$index = index; return trackByGetter(self.scope, hashFnLocals); }; } else { this.itemHashGetter = function(index, value) { return hashKey(value); }; } this.attachedItems = {}; this.BACKUP_ITEMS_LENGTH = 10; this.backupItemsArray = []; } CollectionRepeatDataSource.prototype = { setup: function() { 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); }, 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(hash) { window.AMOUNT = window.AMOUNT || 0; if ( (item = this.attachedItems[hash]) ) { //do nothing, the item is good } else if ( (item = this.backupItemsArray.pop()) ) { reconnectScope(item.scope); } else { AMOUNT++; item = this.createItem(); } return item; }, attachItemAtIndex: function(index) { var value = this.data[index]; var hash = this.itemHashGetter(index, value); var item = this.getItem(hash); if (item.scope.$index !== index || item.scope[this.keyExpr] !== value) { item.scope[this.keyExpr] = value; item.scope.$index = index; 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(); } } item.hash = hash; this.attachedItems[hash] = 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.hash]; // If we are at the limit of backup items, just get rid of the this element if (this.backupItemsArray.length >= this.BACKUP_ITEMS_LENGTH) { this.destroyItem(item); // Otherwise, add it to our backup items } else { this.backupItemsArray.push(item); item.element.css(ionic.CSS.TRANSFORM, 'translate3d(-2000px,-2000px,0)'); //Don't .$destroy(), just stop watchers and events firing disconnectScope(item.scope); } }, getLength: function() { return this.data && this.data.length || 0; }, setData: function(value) { this.data = value || []; this.calculateDataDimensions(); }, }; return CollectionRepeatDataSource; }]); /** * Computes a hash of an 'obj'. * Hash of a: * string is string * number is number as string * object is either result of calling $$hashKey function on the object or uniquely generated id, * that is also assigned to the $$hashKey property of the object. * * @param obj * @returns {string} hash string such that the same input will have the same hash string. * The resulting string key is in 'type:hashKey' format. */ function hashKey(obj) { var objType = typeof obj, key; if (objType == 'object' && obj !== null) { if (typeof (key = obj.$$hashKey) == 'function') { // must invoke on object to keep the right this key = obj.$$hashKey(); } else if (key === undefined) { key = obj.$$hashKey = ionic.Utils.nextUid(); } } else { key = obj; } return objType + ':' + key; } function disconnectScope(scope) { if (scope.$root === scope) { return; // we can't disconnect the root node; } var parent = scope.$parent; scope.$$disconnected = true; // See Scope.$destroy if (parent.$$childHead === scope) { parent.$$childHead = scope.$$nextSibling; } if (parent.$$childTail === scope) { parent.$$childTail = scope.$$prevSibling; } if (scope.$$prevSibling) { scope.$$prevSibling.$$nextSibling = scope.$$nextSibling; } if (scope.$$nextSibling) { scope.$$nextSibling.$$prevSibling = scope.$$prevSibling; } scope.$$nextSibling = scope.$$prevSibling = null; } function reconnectScope(scope) { if (scope.$root === scope) { return; // we can't disconnect the root node; } if (!scope.$$disconnected) { return; } var parent = scope.$parent; scope.$$disconnected = false; // See Scope.$new for this logic... scope.$$prevSibling = parent.$$childTail; if (parent.$$childHead) { parent.$$childTail.$$nextSibling = scope; parent.$$childTail = scope; } else { parent.$$childHead = parent.$$childTail = scope; } } 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; return this.dataSource.dimensions.map(function(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; }, this); }, resize: function() { this.dimensions = this.calculateDimensions(); var lastItem = this.dimensions[this.dimensions.length - 1]; this.viewportSize = lastItem ? lastItem.primaryPos + lastItem.primarySize : 0; this.setCurrentIndex(0); this.render(true); if (!this.dataSource.backupItemsArray.length) { 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 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)) { this.renderItem(i, rect.primaryPos, rect.secondaryPos); i++; } var renderEndIndex = i - 1; // Remove any items that were rendered and aren't visible anymore for (i in this.renderedItems) { if (i < renderStartIndex || i > renderEndIndex) { this.removeItem(i); } } this.setCurrentIndex(startIndex); }, renderItem: function(dataIndex, primaryPos, secondaryPos) { // Attach an item, and set its transform position to the required value var item = this.dataSource.attachItemAtIndex(dataIndex); 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; }]); function delegateService(methodNames) { return ['$log', function($log) { var delegate = this; var instances = this._instances = []; this._registerInstance = function(instance, handle) { instance.$$delegateHandle = handle; instances.push(instance); return function deregister() { var index = instances.indexOf(instance); if (index !== -1) { instances.splice(index, 1); } }; }; this.$getByHandle = function(handle) { if (!handle) { return delegate; } return new InstanceForHandle(handle); }; /* * Creates a new object that will have all the methodNames given, * and call them on the given the controller instance matching given * handle. * The reason we don't just let $getByHandle return the controller instance * itself is that the controller instance might not exist yet. * * We want people to be able to do * `var instance = $ionicScrollDelegate.$getByHandle('foo')` on controller * instantiation, but on controller instantiation a child directive * may not have been compiled yet! * * So this is our way of solving this problem: we create an object * that will only try to fetch the controller with given handle * once the methods are actually called. */ function InstanceForHandle(handle) { this.handle = handle; } methodNames.forEach(function(methodName) { InstanceForHandle.prototype[methodName] = function() { var handle = this.handle; var args = arguments; var matchingInstancesFound = 0; var finalResult; var result; //This logic is repeated below; we could factor some of it out to a function //but don't because it lets this method be more performant (one loop versus 2) instances.forEach(function(instance) { if (instance.$$delegateHandle === handle) { matchingInstancesFound++; result = instance[methodName].apply(instance, args); //Only return the value from the first call if (matchingInstancesFound === 1) { finalResult = result; } } }); if (!matchingInstancesFound) { return $log.warn( 'Delegate for handle "'+this.handle+'" could not find a ' + 'corresponding element with delegate-handle="'+this.handle+'"! ' + methodName + '() was not called!\n' + 'Possible cause: If you are calling ' + methodName + '() immediately, and ' + 'your element with delegate-handle="' + this.handle + '" is a child of your ' + 'controller, then your element may not be compiled yet. Put a $timeout ' + 'around your call to ' + methodName + '() and try again.' ); } return finalResult; }; delegate[methodName] = function() { var args = arguments; var finalResult; var result; //This logic is repeated above instances.forEach(function(instance, index) { result = instance[methodName].apply(instance, args); //Only return the value from the first call if (index === 0) { finalResult = result; } }); return finalResult; }; function callMethod(instancesToUse, methodName, args) { var finalResult; var result; instancesToUse.forEach(function(instance, index) { result = instance[methodName].apply(instance, args); //Make it so the first result is the one returned if (index === 0) { finalResult = result; } }); return finalResult; } }); }]; } /** * @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. */ on: function(eventType, cb, $element) { return window.ionic.onGesture(eventType, cb, $element[0]); }, /** * @ngdoc method * @name $ionicGesture#off * @description Remove an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#offGesture}. * @param {string} eventType The gesture event to remove the listener for. * @param {function(e)} callback The listener to remove. * @param {element} $element The angular element that was listening for the event. */ off: function(gesture, eventType, cb) { return window.ionic.offGesture(gesture, eventType, cb); } }; }]); var LOADING_TPL = '<div class="loading">' + '</div>'; var LOADING_HIDE_DEPRECATED = '$ionicLoading instance.hide() has been deprecated. Use $ionicLoading.hide().'; var LOADING_SHOW_DEPRECATED = '$ionicLoading instance.show() has been deprecated. Use $ionicLoading.show().'; var LOADING_SET_DEPRECATED = '$ionicLoading instance.setContent() has been deprecated. Use $ionicLoading.show({ template: \'my content\' }).'; /** * @ngdoc service * @name $ionicLoading * @module ionic * @description * An overlay that can be used to indicate activity while blocking user * interaction. * * @usage * ```js * angular.module('LoadingApp', ['ionic']) * .controller('LoadingCtrl', function($scope, $ionicLoading) { * $scope.show = function() { * $ionicLoading.show({ * template: 'Loading...' * }); * }; * $scope.hide = function(){ * $ionicLoading.hide(); * }; * }); * ``` */ IonicModule .factory('$ionicLoading', [ '$document', '$ionicTemplateLoader', '$ionicBackdrop', '$timeout', '$q', '$log', '$compile', '$ionicPlatform', function($document, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform) { var loaderInstance; //default values var deregisterBackAction = 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. * - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown. * - `{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: $document[0].body }) .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 || ''); 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 ); } templatePromise.then(function(html) { if (html) { self.element.html(html); $compile(self.element.contents())(self.scope); } //Don't show until template changes if (self.isShown) { self.element.addClass('visible'); ionic.DomUtil.centerElementByMarginTwice(self.element[0]); ionic.requestAnimationFrame(function() { self.isShown && self.element.addClass('active'); ionic.DomUtil.centerElementByMarginTwice(self.element[0]); }); } }); this.isShown = true; }; loader.hide = function() { if (this.isShown) { if (this.hasBackdrop) { $ionicBackdrop.release(); $ionicBackdrop.getElement().removeClass('backdrop-loading'); } self.element.removeClass('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 || (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) { deregisterBackAction(); //Disable hardware back button while loading deregisterBackAction = $ionicPlatform.registerBackButtonAction( angular.noop, PLATFORM_BACK_BUTTON_PRIORITY_LOADING ); 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() { deregisterBackAction(); $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. * Note that you need to put the content of the modal inside a div with the class `modal`. * * Note: 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. * * @usage * ```html * <script id="my-modal.html" type="text/ng-template"> * <div class="modal"> * <ion-header-bar> * <h1 class="title">My Modal title</h1> * </ion-header-bar> * <ion-content> * Hello! * </ion-content> * </div> * </script> * ``` * ```js * angular.module('testApp', ['ionic']) * .controller('MyController', function($scope, $ionicModal) { * $ionicModal.fromTemplateUrl('my-modal.html', { * scope: $scope, * animation: 'slide-in-up' * }).then(function(modal) { * $scope.modal = modal; * }); * $scope.openModal = function() { * $scope.modal.show(); * }; * $scope.closeModal = function() { * $scope.modal.hide(); * }; * //Cleanup the modal when we're done with it! * $scope.$on('$destroy', function() { * $scope.modal.remove(); * }); * // Execute action on hide modal * $scope.$on('modal.hidden', function() { * // Execute action * }); * // Execute action on remove modal * $scope.$on('modal.removed', function() { * // Execute action * }); * }); * ``` */ IonicModule .factory('$ionicModal', [ '$rootScope', '$document', '$compile', '$timeout', '$ionicPlatform', '$ionicTemplateLoader', '$q', '$log', function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $q, $log) { /** * @ngdoc controller * @name ionicModal * @module ionic * @description * Instantiated by the {@link ionic.service:$ionicModal} service. * * Hint: 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() { var self = this; if(self.scope.$$destroyed) { $log.error('Cannot call modal.show() after remove(). Please create a new modal instance using $ionicModal.'); return; } var modalEl = jqLite(self.modalEl); self.el.classList.remove('hide'); $timeout(function(){ $document[0].body.classList.add('modal-open'); }, 400); if(!self.el.parentElement) { modalEl.addClass(self.animation); $document[0].body.appendChild(self.el); } 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'); self.scope.$parent && self.scope.$parent.$broadcast('modal.shown', self); self.el.classList.add('active'); }, 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('modal.hidden', self); self._deregisterBackButton && self._deregisterBackButton(); ionic.views.Modal.prototype.hide.call(self); return $timeout(function(){ $document[0].body.classList.remove('modal-open'); self.el.classList.add('hide'); }, 500); }, /** * @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('modal.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(te