cordova-plugin-progressindicator
Version:
cordova plugin to show a native progress indicator
1,601 lines (1,454 loc) • 240 kB
JavaScript
/*!
* 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.
*
* 
*
* @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