diff --git a/js/angular/controller/scrollController.js b/js/angular/controller/scrollController.js
index b84689253f..c9efbde5a4 100644
--- a/js/angular/controller/scrollController.js
+++ b/js/angular/controller/scrollController.js
@@ -85,6 +85,7 @@ function($scope,
$timeout(function() {
scrollView && scrollView.run && scrollView.run();
+ $element.triggerHandler('scroll.init');
});
self.getScrollView = function() {
diff --git a/js/angular/directive/collectionRepeat.js b/js/angular/directive/collectionRepeat.js
index 88d5bf62af..38e9b8fb40 100644
--- a/js/angular/directive/collectionRepeat.js
+++ b/js/angular/directive/collectionRepeat.js
@@ -1,309 +1,902 @@
+
/**
* @ngdoc directive
- * @module ionic
- * @name collectionRepeat
* @restrict A
+ * @name collectionRepeat
+ * @module ionic
+ * @description
* @codepen mFygh
* @description
- * `collection-repeat` is a directive that allows you to render lists with
- * thousands of items in them, and experience little to no performance penalty.
+ * `collection-repeat` allows an app to show huge lists of items much more performantly than
+ * `ng-repeat`.
*
- * Demo:
+ * It renders into the DOM only as many items as are currently visible.
*
- * The directive renders onto the screen only the items that should be currently visible.
- * So if you have 1,000 items in your list but only ten fit on your screen,
- * collection-repeat will only render into the DOM the ten that are in the current
- * scroll position.
+ * This means that on a phone screen that can fit eight items, only the eight items matching
+ * the current scroll position will be rendered.
*
- * Here are a few things to keep in mind while using collection-repeat:
+ * **Basics**:
*
- * 1. The data supplied to collection-repeat must be an array.
- * 2. You must explicitly tell the directive what size your items will be in the DOM, using directive attributes. Pixel amounts or percentages are allowed (see usage examples below).
- * 3. Each collection-repeat list will take up all of its parent scrollView's space.
- * If you wish to have multiple lists on one page, put each list within its own
- * {@link ionic.directive:ionScroll ionScroll} container.
- * 4. You should not use the ng-show and ng-hide directives on your ion-content/ion-scroll elements that
- * have a collection-repeat inside. ng-show and ng-hide apply the `display: none` css rule to the content's
- * style, causing the scrollView to read the width and height of the content as 0. Resultingly,
- * collection-repeat will render elements that have just been un-hidden incorrectly.
+ * - The data given to collection-repeat must be an array.
+ * - If the `item-height` and `item-width` attributes are not supplied, it will be assumed that
+ * every item in the list's dimensions are the same as the first item's dimensions.
*
+ * **Performance Tips**:
+ *
+ * - The iOS webview has a performance bottleneck when switching out `` attributes.
+ * To increase performance of images on iOS, cache your images in advance and,
+ * if possible, lower the number of unique images. Check out [this codepen]().
*
* @usage
- *
- * #### Basic Usage (single rows of items)
- *
- * Notice two things here: we use ng-style to set the height of the item to match
- * what the repeater thinks our item height is. Additionally, we add a css rule
- * to make our item stretch to fit the full screen (since it will be absolutely
- * positioned).
- *
- * ```html
- *
- *
- *
- * {% raw %}{{item}}{% endraw %}
- *
- *
- *
- * ```
- * ```js
- * function ContentCtrl($scope) {
- * $scope.items = [];
- * for (var i = 0; i < 1000; i++) {
- * $scope.items.push('Item ' + i);
- * }
- *
- * $scope.getItemHeight = function(item, index) {
- * //Make evenly indexed items be 10px taller, for the sake of example
- * return (index % 2) === 0 ? 50 : 60;
- * };
- * }
- * ```
- *
- * #### Grid Usage (three items per row)
- *
+ * #### Basic Item List (codepen)
* ```html
*
- *
- *
- *
+ *
+ * {% raw %}{{item}}{% endraw %}
+ *
*
* ```
- * This example shows a 3 by 3 matrix that fits on the screen (3 rows and 3 colums).
*
- * @param {expression} collection-repeat The expression indicating how to enumerate a collection. These
- * formats are currently supported:
+ * #### Grid of Images (codepen)
+ * ```html
+ *
+ *
+ *
+ * ```
*
- * * `variable in expression` – where variable is the user defined loop variable and `expression`
- * is a scope expression giving the collection to enumerate.
- *
- * For example: `album in artist.albums`.
- *
- * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
- * which can be used to associate the objects in the collection with the DOM elements. If no tracking function
- * is specified the collection-repeat associates elements by identity in the collection. It is an error to have
- * more than one tracking function to resolve to the same key. (This would mean that two distinct objects are
- * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression,
- * before specifying a tracking expression.
- *
- * For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements
- * will be associated by item identity in the array.
- *
- * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
- * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
- * with the corresponding item in the array by identity. Moving the same object in array would move the DOM
- * element in the same way in the DOM.
- *
- * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this
- * case the object identity does not matter. Two objects are considered equivalent as long as their `id`
- * property is same.
- *
- * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
- * to items in conjunction with a tracking expression.
- *
- * @param {expression} collection-item-width The width of the repeated element. Can be a number (in pixels) or a percentage.
- * @param {expression} collection-item-height The height of the repeated element. Can be a number (in pixels), or a percentage.
- * @param {number=} collection-buffer-size The number of rows (or columns in a vertical scroll view) to load above and below the visible items. Default 2. This is good to set higher if you have lots of images to preload. Warning: the larger the buffer size, the worse performance will be. After ten or so you will see a difference.
- * @param {boolean=} collection-refresh-images Whether to force images to refresh their `src` when an item's element is recycled. If provided, this stops problems with images still showing their old src when item's elements are recycled.
- * If set to true, this comes with a small performance loss. Default false.
+ * #### Horizontal Scroller, Dynamic Item Width (codepen)
+ * ```html
+ *
+ *
+ *
+ * ```
*
+ * @param {expression} collection-repeat The expression indicating how to enumerate a collection,
+ * of the format `variable in expression` – where variable is the user defined loop variable
+ * and `expression` is a scope expression giving the collection to enumerate.
+ * For example: `album in artist.albums` or `album in artist.albums | orderBy:'name'`.
+ * @param {expression=} item-width The width of the repeated element. The expression must return
+ * a number (pixels) or a percentage. Defaults to the width of the first item in the list.
+ * @param {expression=} item-height The height of the repeated element. The expression must return
+ * a number (pixels) or a percentage. Defaults to the height of the first item in the list.
+ * @param {number=} item-render-buffer The number of items to load before and after the visible
+ * items in the list. Default 10. This is good to set higher if you have lots of images to preload.
+ * @param {boolean=} force-refresh-images Force images to refresh as you scroll. This fixes a problem
+ * where, when an element is interchanged as scrolling, its image will still have the old src
+ * while the new src loads. Setting this to true comes with a small performance loss.
*/
-var COLLECTION_REPEAT_SCROLLVIEW_XY_ERROR = "Cannot create a collection-repeat within a scrollView that is scrollable on both x and y axis. Choose either x direction or y direction.";
-var COLLECTION_REPEAT_ATTR_HEIGHT_ERROR = "collection-repeat expected attribute collection-item-height to be a an expression that returns a number (in pixels) or percentage.";
-var COLLECTION_REPEAT_ATTR_WIDTH_ERROR = "collection-repeat expected attribute collection-item-width to be a an expression that returns a number (in pixels) or percentage.";
-var COLLECTION_REPEAT_ATTR_REPEAT_ERROR = "collection-repeat expected expression in form of '_item_ in _collection_[ track by _id_]' but got '%'";
IonicModule
-.directive('collectionRepeat', [
- '$collectionRepeatManager',
- '$collectionDataSource',
- '$parse',
-function($collectionRepeatManager, $collectionDataSource, $parse) {
+.directive('collectionRepeat', CollectionRepeatDirective)
+.factory('$ionicCollectionManager', RepeatManagerFactory);
+
+var ONE_PX_TRANSPARENT_IMG_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
+var WIDTH_HEIGHT_REGEX = /height:.*?px;\s*width:.*?px/;
+var DEFAULT_RENDER_BUFFER = 10;
+
+CollectionRepeatDirective.$inject = ['$ionicCollectionManager', '$parse', '$window'];
+function CollectionRepeatDirective($ionicCollectionManager, $parse, $window) {
return {
+ restrict: 'A',
priority: 1000,
transclude: 'element',
- terminal: true,
$$tlb: true,
- require: ['^$ionicScroll', '^?ionNavView'],
- controller: [function() {}],
- link: function($scope, $element, $attr, ctrls, $transclude) {
- var scrollCtrl = ctrls[0];
- var navViewCtrl = ctrls[1];
- var wrap = jqLite('
');
- $element.parent()[0].insertBefore(wrap[0], $element[0]);
- wrap.append($element);
-
- var scrollView = scrollCtrl.scrollView;
- if (scrollView.options.scrollingX && scrollView.options.scrollingY) {
- throw new Error(COLLECTION_REPEAT_SCROLLVIEW_XY_ERROR);
- }
-
- var isVertical = !!scrollView.options.scrollingY;
- if (isVertical && !$attr.collectionItemHeight) {
- throw new Error(COLLECTION_REPEAT_ATTR_HEIGHT_ERROR);
- } else if (!isVertical && !$attr.collectionItemWidth) {
- throw new Error(COLLECTION_REPEAT_ATTR_WIDTH_ERROR);
- }
-
- var heightParsed = $parse($attr.collectionItemHeight || '"100%"');
- var widthParsed = $parse($attr.collectionItemWidth || '"100%"');
-
- var heightGetter = function(scope, locals) {
- var result = heightParsed(scope, locals);
- if (isString(result) && result.indexOf('%') > -1) {
- return Math.floor(parseInt(result) / 100 * scrollView.__clientHeight);
- }
- return parseInt(result);
- };
- var widthGetter = function(scope, locals) {
- var result = widthParsed(scope, locals);
- if (isString(result) && result.indexOf('%') > -1) {
- return Math.floor(parseInt(result) / 100 * scrollView.__clientWidth);
- }
- return parseInt(result);
- };
-
- var match = $attr.collectionRepeat.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
- if (!match) {
- throw new Error(COLLECTION_REPEAT_ATTR_REPEAT_ERROR
- .replace('%', $attr.collectionRepeat));
- }
- var keyExpr = match[1];
- var listExpr = match[2];
- var trackByExpr = match[3];
-
- var dataSource = new $collectionDataSource({
- scope: $scope,
- transcludeFn: $transclude,
- transcludeParent: $element.parent(),
- keyExpr: keyExpr,
- listExpr: listExpr,
- trackByExpr: trackByExpr,
- heightGetter: heightGetter,
- widthGetter: widthGetter,
- shouldRefreshImages: angular.isDefined($attr.collectionRefreshImages) &&
- $attr.collectionRefreshImages !== 'false'
- });
- var collectionRepeatManager = new $collectionRepeatManager({
- dataSource: dataSource,
- element: scrollCtrl.$element,
- scrollView: scrollCtrl.scrollView,
- bufferSize: parseInt($attr.collectionBufferSize)
- });
-
- var listExprParsed = $parse(listExpr);
- $scope.$watchCollection(listExprParsed, function(value) {
- if (value && !angular.isArray(value)) {
- throw new Error("collection-repeat expects an array to repeat over, but instead got '" + typeof value + "'.");
- }
- rerender(value);
- });
-
- // Find every sibling before and after the repeated items, and pass them
- // to the dataSource
- var scrollViewContent = scrollCtrl.scrollView.__content;
- function rerender(value) {
- var beforeSiblings = [];
- var afterSiblings = [];
- var collectionRepeatNode = $element[0];
- var before = true;
- var children = scrollViewContent.children;
- var width, height, el, child;
-
- // Loop through all of the children of scrollViewContent. Put every child BEFORE
- // the collectionRepeatNode in `beforeSiblings`, and all of the children AFTER
- // the collectionRepeatNode in `afterSiblings`.
- for (var i = 0; (child = children[i]); i++) {
- if (child.hasAttribute('collection-repeat-ignore')) continue;
- if (child.contains(collectionRepeatNode)) {
- // Once we reach the collectionRepeatNode, we're now counting siblings AFTER the repeater.
- before = false;
- continue;
- }
- if ( (width = child.offsetWidth) && (height = child.offsetHeight) ) {
- (before ? beforeSiblings : afterSiblings).push({
- width: width,
- height: height,
- element: (el = jqLite(child)),
- scope: el.isolateScope() || el.scope(),
- isOutside: true
- });
- }
- }
-
- scrollView.resize();
- dataSource.setData(value, beforeSiblings, afterSiblings);
- collectionRepeatManager.resize();
- }
-
- var requiresRerender;
- var lastDim = {};
- var newDim = {};
- function rerenderOnResize() {
- newDim = {
- width: scrollViewContent.clientWidth,
- height: scrollViewContent.clientHeight
- };
- if (!angular.equals(lastDim, newDim) && !$scope.$$disconnected) {
- rerender(listExprParsed($scope));
- }
- requiresRerender = (!newDim.width && !newDim.height);
- lastDim = newDim;
- }
-
- function viewEnter() {
- if (requiresRerender) {
- rerenderOnResize();
- }
- }
-
- scrollCtrl.$element.on('scroll.resize', rerenderOnResize);
- ionic.on('resize', rerenderOnResize, window);
- var deregisterViewListener;
- if (navViewCtrl) {
- deregisterViewListener = navViewCtrl.scope.$on('$ionicView.afterEnter', viewEnter);
- }
-
- $scope.$on('$destroy', function() {
- collectionRepeatManager.destroy();
- dataSource.destroy();
- ionic.off('resize', rerenderOnResize, window);
- (deregisterViewListener || noop)();
- });
- }
+ require: '^$ionicScroll',
+ link: postLink
};
-}])
-.directive({
- ngSrc: collectionRepeatSrcDirective('ngSrc', 'src'),
- ngSrcset: collectionRepeatSrcDirective('ngSrcset', 'srcset'),
- ngHref: collectionRepeatSrcDirective('ngHref', 'href')
-});
-// Fix for #1674
-// Problem: if an ngSrc or ngHref expression evaluates to a falsy value, it will
-// not erase the previous truthy value of the href.
-// In collectionRepeat, we re-use elements from before. So if the ngHref expression
-// evaluates to truthy for item 1 and then falsy for item 2, if an element changes
-// from representing item 1 to representing item 2, item 2 will still have
-// item 1's href value.
-// Solution: erase the href or src attribute if ngHref/ngSrc are falsy.
-function collectionRepeatSrcDirective(ngAttrName, attrName) {
- return [function() {
- return {
- priority: '99', // it needs to run after the attributes are interpolated
- link: function(scope, element, attr) {
- attr.$observe(ngAttrName, function(value) {
- if (!value) {
- element[0].removeAttribute(attrName);
- }
+ function postLink(scope, element, attr, scrollCtrl, transclude) {
+
+ var scrollView = scrollCtrl.scrollView;
+ var node = element[0];
+ var containerNode = angular.element('
')[0];
+ node.parentNode.replaceChild(containerNode, node);
+
+ if (scrollView.options.scrollingX && scrollView.options.scrollingY) {
+ throw new Error("collection-repeat expected a parent x or y scrollView, not " +
+ "an xy scrollView.");
+ }
+
+ var match = attr.collectionRepeat.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
+ if (!match) {
+ throw new Error("collection-repeat expected expression in form of '_item_ in " +
+ "_collection_[ track by _id_]' but got '" + attr.collectionRepeat + "'.");
+ }
+ var keyExpr = match[1];
+ var listExpr = match[2];
+ var heightData = {};
+ var widthData = {};
+ var computedStyleDimensions = {};
+ var repeatManager;
+
+ // attr.collectionBufferSize is deprecated
+ var renderBufferExpr = attr.itemRenderBuffer || attr.collectionBufferSize;
+ var renderBuffer = angular.isDefined(renderBufferExpr) ?
+ parseInt(renderBufferExpr) :
+ DEFAULT_RENDER_BUFFER;
+
+ // attr.collectionItemHeight is deprecated
+ var heightExpr = attr.itemHeight || attr.collectionItemHeight;
+ // attr.collectionItemWidth is deprecated
+ var widthExpr = attr.itemWidth || attr.collectionItemWidth;
+
+ //Height and width have four 'modes':
+ //1) Computed Mode
+ // - Nothing is supplied, so we getComputedStyle() on one element in the list and use
+ // that width and height value for the width and height of every item. This is re-computed
+ // every resize.
+ //2) Constant Mode, Static Integer
+ // - The user provides a constant number for width or height, in pixels. We parse it,
+ // store it on the `value` field, and it never changes
+ //3) Constant Mode, Percent
+ // - The user provides a percent string for width or height. The getter for percent is
+ // stored on the `getValue()` field, and is re-evaluated once every resize. The result
+ // is stored on the `value` field.
+ //4) Dynamic Mode
+ // - The user provides a dynamic expression for the width or height. This is re-evaluated
+ // for every item, stored on the `.getValue()` field.
+ if (!heightExpr && !widthExpr) {
+ heightData.computed = widthData.computed = true;
+ } else {
+ if (heightExpr) {
+ parseDimensionAttr(heightExpr, heightData);
+ } else {
+ heightData.computed = true;
+ }
+ if (!widthExpr) widthExpr = '"100%"';
+ parseDimensionAttr(widthExpr, widthData);
+ }
+
+ var afterItemsContainer = angular.element(
+ scrollView.__content.querySelector('.collection-repeat-after-container')
+ );
+ if (!afterItemsContainer.length) {
+ var elementIsAfterRepeater = false;
+ var afterNodes = [].filter.call(scrollView.__content.childNodes, function(node) {
+ if (node.contains(containerNode)) {
+ elementIsAfterRepeater = true;
+ return false;
+ }
+ return elementIsAfterRepeater;
+ });
+ afterItemsContainer = angular.element('');
+ if (scrollView.options.scrollingX) {
+ afterItemsContainer.addClass('horizontal');
+ }
+ afterItemsContainer.append(afterNodes);
+ scrollView.__content.appendChild(afterItemsContainer[0]);
+ }
+
+ scrollCtrl.$element.one('scroll.init', refreshDimensions);
+
+ var onWindowResize = ionic.animationFrameThrottle(validateResize);
+ angular.element($window).on('resize', onWindowResize);
+
+ scope.$on('$destroy', function() {
+ angular.element($window).off('resize', onWindowResize);
+ scrollCtrl.$element && scrollCtrl.$element.off('scroll.init', refreshDimensions);
+
+ computedStyleNode && computedStyleNode.parentNode &&
+ computedStyleNode.parentNode.removeChild(computedStyleNode);
+ computedStyleScope && computedStyleScope.$destroy();
+ computedStyleScope = computedStyleNode = null;
+
+ repeatManager && repeatManager.destroy();
+ repeatManager = null;
+ });
+ scope.$on('$ionic.reconnectScope', function() {
+ if (refreshDimensions.queued) refreshDimensions();
+ });
+
+ // Make sure this resize actually changed the size of the screen
+ function validateResize() {
+ var h = window.innerHeight || screen.height, w = window.innerWidth || screen.width;
+ if (validateResize.height !== h || validateResize.width !== w) {
+ refreshDimensions();
+ }
+ validateResize.height = h;
+ validateResize.width = w;
+ }
+ function refreshDimensions() {
+ // If we're disconnected, don't refresh the dimensions. But mark that we need to once
+ // the scope reconnects.
+ if (scope.$$disconnected) return (refreshDimensions.queued = true);
+ refreshDimensions.queued = false;
+
+ if (heightData.computed || widthData.computed) {
+ computeStyleDimensions();
+ }
+
+ if (heightData.computed) {
+ heightData.value = computedStyleDimensions.height;
+ } else if (!heightData.dynamic && heightData.getValue) {
+ // If it's a constant with a getter (eg percent), we just refresh .value after resize
+ heightData.value = heightData.getValue();
+ }
+ if (widthData.computed) {
+ widthData.value = computedStyleDimensions.width;
+ } else if (!widthData.dynamic && widthData.getValue) {
+ // If it's a constant with a getter (eg percent), we just refresh .value after resize
+ widthData.value = widthData.getValue();
+ }
+ // Dynamic dimensions aren't updated on resize. Since they're already dynamic anyway,
+ // .getValue() will be used.
+
+ if (!repeatManager) {
+ repeatManager = new $ionicCollectionManager({
+ afterItemsNode: afterItemsContainer[0],
+ containerNode: containerNode,
+ heightData: heightData,
+ widthData: widthData,
+ forceRefreshImages: !!(isDefined(attr.forceRefreshImages) && attr.forceRefreshImages !== 'false'),
+ keyExpression: keyExpr,
+ listExpression: listExpr,
+ renderBuffer: renderBuffer,
+ scope: scope,
+ scrollView: scrollCtrl.scrollView,
+ transclude: transclude,
});
}
- };
- }];
+ repeatManager.refreshLayout();
+ }
+
+ function parseDimensionAttr(attrValue, dimensionData) {
+ if (!attrValue) return;
+
+ var parsedValue;
+ // Try to just parse the plain attr value
+ try {
+ parsedValue = $parse(attrValue);
+ } catch(e) {
+ // If the parse fails and the value has `px` or `%` in it, surround the attr in
+ // quotes, to attempt to let the user provide a simple `attr="100%"` or `attr="100px"`
+ if (attrValue.indexOf('%') !== -1 || attrValue.indexOf('px') !== -1) {
+ attrValue = '"' + attrValue + '"';
+ }
+ parsedValue = $parse(attrValue);
+ }
+
+ dimensionData.attrValue = attrValue;
+
+ // If it's a constant, it's either a percent or just a constant pixel number.
+ if (parsedValue.constant) {
+ var intValue = parseInt(parsedValue());
+
+ // For percents, store the percent getter on .getValue()
+ if (attrValue.indexOf('%') > -1) {
+ var decimalValue = intValue / 100;
+ dimensionData.getValue = dimensionData === heightData ?
+ function() { return Math.floor(decimalValue * scrollView.__clientHeight); } :
+ function() { return Math.floor(decimalValue * scrollView.__clientWidth); };
+ } else {
+ // For static constants, just store the static constant.
+ dimensionData.value = intValue;
+ }
+
+ } else {
+ dimensionData.dynamic = true;
+ dimensionData.getValue = dimensionData === heightData ?
+ function heightGetter(scope, locals) {
+ var result = parsedValue(scope, locals);
+ if (result.charAt && result.charAt(result.length - 1) === '%')
+ return Math.floor(parseInt(result) / 100 * scrollView.__clientHeight);
+ return parseInt(result);
+ } :
+ function widthGetter(scope, locals) {
+ var result = parsedValue(scope, locals);
+ if (result.charAt && result.charAt(result.length - 1) === '%')
+ return Math.floor(parseInt(result) / 100 * scrollView.__clientWidth);
+ return parseInt(result);
+ };
+ }
+ }
+
+ var computedStyleNode;
+ var computedStyleScope;
+ function computeStyleDimensions() {
+ if (!computedStyleNode) {
+ transclude(computedStyleScope = scope.$new(), function(clone) {
+ clone[0].removeAttribute('collection-repeat'); // remove absolute position styling
+ computedStyleNode = clone[0];
+ });
+ }
+ containerNode.appendChild(computedStyleNode);
+
+ var style = $window.getComputedStyle(computedStyleNode);
+ computedStyleDimensions.width = parseInt(style.width);
+ computedStyleDimensions.height = parseInt(style.height);
+
+ containerNode.removeChild(computedStyleNode);
+ }
+
+ }
+
}
+
+RepeatManagerFactory.$inject = ['$rootScope', '$window', '$$rAF'];
+function RepeatManagerFactory($rootScope, $window, $$rAF) {
+ var EMPTY_DIMENSION = { primaryPos: 0, secondaryPos: 0, primarySize: 0, secondarySize: 0 };
+
+ return function RepeatController(options) {
+ var afterItemsNode = options.afterItemsNode;
+ var containerNode = options.containerNode;
+ var forceRefreshImages = options.forceRefreshImages;
+ var heightData = options.heightData;
+ var widthData = options.widthData;
+ var keyExpression = options.keyExpression;
+ var listExpression = options.listExpression;
+ var renderBuffer = options.renderBuffer;
+ var scope = options.scope;
+ var scrollView = options.scrollView;
+ var transclude = options.transclude;
+
+ var data = [];
+
+ var getterLocals = {};
+ var heightFn = heightData.getValue || function() { return heightData.value; };
+ var heightGetter = function(index, value) {
+ getterLocals[keyExpression] = value;
+ getterLocals.$index = index;
+ return heightFn(scope, getterLocals);
+ };
+
+ var widthFn = widthData.getValue || function() { return widthData.value; };
+ var widthGetter = function(index, value) {
+ getterLocals[keyExpression] = value;
+ getterLocals.$index = index;
+ return widthFn(scope, getterLocals);
+ };
+
+ var isVertical = !!scrollView.options.scrollingY;
+
+ // We say it's a grid view if we're either dynamic or not 100% width
+ var isGridView = isVertical ?
+ (widthData.dynamic || widthData.value !== scrollView.__clientWidth) :
+ (heightData.dynamic || heightData.value !== scrollView.__clientHeight);
+
+ var isStaticView = !heightData.dynamic && !widthData.dynamic;
+
+ var PRIMARY = 'PRIMARY';
+ var SECONDARY = 'SECONDARY';
+ var TRANSLATE_TEMPLATE_STR = isVertical ?
+ 'translate3d(SECONDARYpx,PRIMARYpx,0)' :
+ 'translate3d(PRIMARYpx,SECONDARYpx,0)';
+ var WIDTH_HEIGHT_TEMPLATE_STR = isVertical ?
+ 'height: PRIMARYpx; width: SECONDARYpx;' :
+ 'height: SECONDARYpx; width: PRIMARYpx;';
+
+ var estimatedHeight;
+ var estimatedWidth;
+
+ var repeaterBeforeSize = 0;
+ var repeaterAfterSize = 0;
+
+ var renderStartIndex = -1;
+ var renderEndIndex = -1;
+ var renderAfterBoundary = -1;
+ var renderBeforeBoundary = -1;
+
+ var itemsPool = [];
+ var itemsLeaving = [];
+ var itemsEntering = [];
+ var itemsShownMap = {};
+ var nextItemId = 0;
+ var estimatedItemsAcross;
+
+ // view is a mix of list/grid methods + static/dynamic methods.
+ // See bottom for implementations. Available methods:
+ //
+ // getEstimatedPrimaryPos(i), getEstimatedSecondaryPos(i), getEstimatedIndex(scrollTop),
+ // calculateDimensions(toIndex), getDimensions(index),
+ // updateRenderRange(scrollTop, scrollValueEnd), onRefreshLayout(), onRefreshData()
+ var view = isVertical ? new VerticalViewType() : new HorizontalViewType();
+ (isGridView ? GridViewType : ListViewType).call(view);
+ (isStaticView ? StaticViewType : DynamicViewType).call(view);
+
+ var isLayoutReady = false;
+ var isDataReady = false;
+ this.refreshLayout = function(itemsAfterRepeater) {
+ estimatedHeight = heightGetter(0, data[0]);
+ estimatedWidth = widthGetter(0, data[0]);
+
+ // Get the size of every element AFTER the repeater. We have to get the margin before and
+ // after the first/last element to fix a browser bug with getComputedStyle() not counting
+ // the first/last child's margins into height.
+ var style = getComputedStyle(afterItemsNode);
+ var firstStyle = getComputedStyle(afterItemsNode.firstElementChild);
+ var lastStyle = getComputedStyle(afterItemsNode.lastElementChild);
+ repeaterAfterSize = (parseInt(style[isVertical ? 'height' : 'width']) || 0) +
+ (firstStyle && parseInt(firstStyle[isVertical ? 'marginTop' : 'marginLeft']) || 0) +
+ (lastStyle && parseInt(lastStyle[isVertical ? 'marginBottom' : 'marginRight']) || 0);
+
+ // Get the offsetTop of the repeater.
+ repeaterBeforeSize = 0;
+ var current = containerNode;
+ do {
+ repeaterBeforeSize += current[isVertical ? 'offsetTop' : 'offsetLeft'];
+ } while ( scrollView.__content.contains(current = current.offsetParent) );
+
+ (view.onRefreshLayout || angular.noop)();
+ view.refreshDirection();
+
+ // Create the pool of items for reuse, setting the size to (estimatedItemsOnScreen) * 2,
+ // plus the size of the renderBuffer.
+ if (!isLayoutReady) {
+ var poolSize = 2 * view.scrollPrimarySize /
+ view.estimatedPrimarySize * view.estimatedItemsAcross + (renderBuffer * 2);
+ for (var i = 0; i < poolSize; i++) {
+ itemsPool.push(new RepeatItem());
+ }
+ }
+
+ isLayoutReady = true;
+ if (isLayoutReady && isDataReady) {
+ forceRerender();
+ }
+ };
+
+ this.refreshData = function(newData) {
+ newData || (newData = []);
+
+ if (!angular.isArray(newData)) {
+ throw new Error("collection-repeat expected an array for '" + listExpression + "', " +
+ "but got a " + typeof value);
+ }
+
+ data = newData;
+ (view.onRefreshData || angular.noop)();
+
+ isDataReady = true;
+ if (isLayoutReady && isDataReady) {
+ forceRerender();
+ }
+ };
+
+ var unwatch = scope.$watchCollection(listExpression, angular.bind(this, this.refreshData));
+ this.destroy = function() {
+ render.destroyed = true;
+ unwatch();
+
+ scrollView.__calback = scrollView.__$callback;
+ itemsPool.forEach(function(item) {
+ item.scope.$destroy();
+ item.scope = item.element = item.node = item.images = null;
+ });
+ itemsPool.length = itemsEntering.length = itemsLeaving.length = 0;
+ itemsShownMap = {};
+
+ (view.onDestroy || angular.noop)();
+ };
+
+ scrollView.options[isVertical ? 'getContentHeight' : 'getContentWidth'] =
+ angular.bind(view, view.getContentSize);
+
+ scrollView.__$callback = scrollView.__callback;
+ scrollView.__callback = function(transformLeft, transformTop, zoom, wasResize) {
+ var scrollValue = view.getScrollValue();
+ if (renderStartIndex === -1 ||
+ scrollValue + view.scrollPrimarySize > renderAfterBoundary ||
+ scrollValue < renderBeforeBoundary) {
+ render();
+ }
+ scrollView.__$callback(transformLeft, transformTop, zoom, wasResize);
+ };
+
+
+ function forceRerender() {
+ return render(true);
+ }
+ function render(forceRerender) {
+ if (render.destroyed) return;
+ var i;
+ var item;
+ var dim;
+ var scope;
+ var scrollValue = view.getScrollValue();
+ var scrollValueEnd = scrollValue + view.scrollPrimarySize;
+
+ view.updateRenderRange(scrollValue, scrollValueEnd);
+
+ renderStartIndex = Math.max(0, renderStartIndex - renderBuffer);
+ renderEndIndex = Math.min(data.length - 1, renderEndIndex + renderBuffer);
+
+ for (i in itemsShownMap) {
+ if (i < renderStartIndex || i > renderEndIndex) {
+ item = itemsShownMap[i];
+ delete itemsShownMap[i];
+ itemsLeaving.push(item);
+ item.isShown = false;
+ }
+ }
+
+ // Render indicies that aren't shown yet
+ //
+ // NOTE(ajoslin): this may sound crazy, but calling any other functions during this render
+ // loop will often push the render time over the edge from less than one frame to over
+ // one frame, causing visible jank.
+ // DON'T call any other functions inside this loop unless it's vital.
+ for (i = renderStartIndex; i <= renderEndIndex; i++) {
+ // If the item at this index is already shown, skip
+ if (i >= data.length || itemsShownMap[i] && !forceRerender) continue;
+
+ item = itemsShownMap[i] || (itemsShownMap[i] = getNextItem());
+ itemsEntering.push(item);
+ item.isShown = true;
+
+ scope = item.scope;
+ scope.$index = i;
+ scope[keyExpression] = data[i];
+ scope.$first = (i === 0);
+ scope.$last = (i === (data.length - 1));
+ scope.$middle = !(scope.$first || scope.$last);
+ scope.$odd = !(scope.$even = (i&1) === 0);
+
+ if (scope.$$disconnected) ionic.Utils.reconnectScope(item.scope);
+
+ dim = view.getDimensions(i);
+ if (item.secondaryPos !== dim.secondaryPos || item.primaryPos !== dim.primaryPos) {
+ item.node.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
+ .replace(PRIMARY, (item.primaryPos = dim.primaryPos))
+ .replace(SECONDARY, (item.secondaryPos = dim.secondaryPos));
+ }
+ if (item.secondarySize !== dim.secondarySize || item.primarySize !== dim.primarySize) {
+ item.node.style.cssText = item.node.style.cssText
+ .replace(WIDTH_HEIGHT_REGEX, WIDTH_HEIGHT_TEMPLATE_STR
+ .replace(PRIMARY, 1 + (item.primarySize = dim.primarySize))
+ .replace(SECONDARY, (item.secondarySize = dim.secondarySize))
+ );
+ }
+
+ }
+
+ // If we reach the end of the list, render the afterItemsNode - this contains all the
+ // elements the developer placed after the collection-repeat
+ if (renderEndIndex === data.length - 1) {
+ dim = view.getDimensions(data.length - 1) || EMPTY_DIMENSION;
+ afterItemsNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
+ .replace(PRIMARY, dim.primaryPos + dim.primarySize)
+ .replace(SECONDARY, 0);
+ }
+
+ while (itemsLeaving.length) {
+ item = itemsLeaving.pop();
+ ionic.Utils.disconnectScope(item.scope);
+ itemsPool.push(item);
+ item.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
+ item.primaryPos = item.secondaryPos = null;
+ }
+
+ if (forceRefreshImages) {
+ for (i = 0, ii = itemsEntering.length; i < ii && (item = itemsEntering[i]); i++) {
+ if (!item.images) continue;
+ for (var j = 0, jj = item.images.length, img; j < jj && (img = item.images[j]); j++) {
+ var src = img.src;
+ img.src = ONE_PX_TRANSPARENT_IMG_SRC;
+ img.src = src;
+ }
+ }
+ }
+ if (forceRerender) {
+ var rootScopePhase = $rootScope.$$phase;
+ while (itemsEntering.length) {
+ item = itemsEntering.pop();
+ if (!rootScopePhase) item.scope.$digest();
+ }
+ } else {
+ digestEnteringItems();
+ }
+ }
+
+ function getNextItem() {
+ if (itemsLeaving.length)
+ return itemsLeaving.pop();
+ else if (itemsPool.length)
+ return itemsPool.shift();
+ return new RepeatItem();
+ }
+
+ function digestEnteringItems() {
+ var item;
+ var scope;
+ var len;
+ if (digestEnteringItems.running) return;
+ digestEnteringItems.running = true;
+
+ $$rAF(function process() {
+ if( (len = itemsEntering.length) ) {
+ var count = Math.floor(len / 1.5) || 1;
+ var rootScopePhase = $rootScope.$$phase;
+ while (count && itemsEntering.length) {
+ item = itemsEntering.pop();
+ if (item.isShown) {
+ count--;
+ if (!$rootScope.$$phase) item.scope.$digest();
+ }
+ }
+ $$rAF(process);
+ } else {
+ digestEnteringItems.running = false;
+ }
+ });
+ }
+
+ function RepeatItem() {
+ var self = this;
+ this.scope = scope.$new();
+ this.id = 'item_'+ (nextItemId++);
+ transclude(this.scope, function(clone) {
+ self.element = clone;
+ self.element.data('$$collectionRepeatItem', self);
+ // TODO destroy
+ self.node = clone[0];
+ // Batch style setting to lower repaints
+ self.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
+ self.node.style.cssText += ' height: 0px; width: 0px;';
+ ionic.Utils.disconnectScope(self.scope);
+ containerNode.appendChild(self.node);
+ self.images = clone[0].getElementsByTagName('img');
+ });
+ }
+
+ function VerticalViewType() {
+ this.getItemPrimarySize = heightGetter;
+ this.getItemSecondarySize = widthGetter;
+
+ this.getScrollValue = function() {
+ return Math.max(0, Math.min(scrollView.__scrollTop - repeaterBeforeSize,
+ scrollView.__maxScrollTop - repeaterBeforeSize - repeaterAfterSize));
+ };
+
+ this.refreshDirection = function() {
+ this.scrollPrimarySize = scrollView.__clientHeight;
+ this.scrollSecondarySize = scrollView.__clientWidth;
+
+ this.estimatedPrimarySize = estimatedHeight;
+ this.estimatedSecondarySize = estimatedWidth;
+ this.estimatedItemsAcross = isGridView &&
+ Math.floor(scrollView.__clientWidth / estimatedWidth) ||
+ 1;
+ };
+ }
+ function HorizontalViewType() {
+ this.getItemPrimarySize = widthGetter;
+ this.getItemSecondarySize = heightGetter;
+
+ this.getScrollValue = function() {
+ return Math.max(0, Math.min(scrollView.__scrollLeft - repeaterBeforeSize,
+ scrollView.__maxScrollLeft - repeaterBeforeSize - repeaterAfterSize));
+ };
+
+ this.refreshDirection = function() {
+ this.scrollPrimarySize = scrollView.__clientWidth;
+ this.scrollSecondarySize = scrollView.__clientHeight;
+
+ this.estimatedPrimarySize = estimatedWidth;
+ this.estimatedSecondarySize = estimatedHeight;
+ this.estimatedItemsAcross = isGridView &&
+ Math.floor(scrollView.__clientHeight / estimatedHeight) ||
+ 1;
+ };
+ }
+
+ function GridViewType() {
+ this.getEstimatedSecondaryPos = function(index) {
+ return (index % this.estimatedItemsAcross) * this.estimatedPrimarySize;
+ };
+ this.getEstimatedPrimaryPos = function(index) {
+ return Math.floor(index / this.estimatedItemsAcross) * this.estimatedPrimarySize;
+ };
+ this.getEstimatedIndex = function(scrollValue) {
+ return Math.floor(scrollValue / this.estimatedPrimarySize) *
+ this.estimatedItemsAcross;
+ };
+ }
+
+ function ListViewType() {
+ this.getEstimatedSecondaryPos = function() {
+ return 0;
+ };
+ this.getEstimatedPrimaryPos = function(index) {
+ return index * this.estimatedPrimarySize;
+ };
+ this.getEstimatedIndex = function(scrollValue) {
+ return Math.floor((scrollValue) / this.estimatedPrimarySize);
+ };
+ }
+
+ function StaticViewType() {
+ this.getContentSize = function() {
+ return this.getEstimatedPrimaryPos(data.length - 1) + this.estimatedPrimarySize +
+ repeaterBeforeSize + repeaterAfterSize;
+ };
+ // static view always returns the same object for getDimensions, to avoid memory allocation
+ // while scrolling. This could be dangerous if this was a public function, but it's not.
+ // Only we use it.
+ var dim = {};
+ this.getDimensions = function(index) {
+ dim.primaryPos = this.getEstimatedPrimaryPos(index);
+ dim.secondaryPos = this.getEstimatedSecondaryPos(index);
+ dim.primarySize = this.estimatedPrimarySize;
+ dim.secondarySize = this.estimatedSecondarySize;
+ return dim;
+ };
+ this.updateRenderRange = function(scrollValue, scrollValueEnd) {
+ renderStartIndex = Math.max(0, this.getEstimatedIndex(scrollValue));
+
+ // Make sure the renderEndIndex takes into account all the items on the row
+ renderEndIndex = Math.min(data.length - 1,
+ this.getEstimatedIndex(scrollValueEnd) + this.estimatedItemsAcross - 1);
+
+ renderBeforeBoundary = Math.max(0,
+ this.getEstimatedPrimaryPos(renderStartIndex));
+ renderAfterBoundary = this.getEstimatedPrimaryPos(renderEndIndex) +
+ this.estimatedPrimarySize;
+ };
+ }
+
+ function DynamicViewType() {
+ var self = this;
+ var scrollViewSetDimensions = isVertical ?
+ function() {
+ scrollView.setDimensions(null, null, null, self.getContentSize(), true);
+ } :
+ function() {
+ scrollView.setDimensions(null, null, self.getContentSize(), null, true);
+ };
+ var debouncedScrollViewSetDimensions = ionic.debounce(scrollViewSetDimensions, 25, true);
+ var calculateDimensions = isGridView ? calculateDimensionsGrid : calculateDimensionsList;
+ var dimensionsIndex;
+ var dimensions = [];
+
+ // Get the dimensions at index. {width, height, left, top}.
+ // We start with no dimensions calculated, then any time dimensions are asked for at an
+ // index we calculate dimensions up to there.
+ function calculateDimensionsList(toIndex) {
+ var i, prevDimension, dim;
+ for (i = Math.max(0, dimensionsIndex); i <= toIndex && (dim = dimensions[i]); i++) {
+ prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
+ dim.primarySize = self.getItemPrimarySize(i, data[i]);
+ dim.secondarySize = self.scrollSecondarySize;
+ dim.primaryPos = prevDimension.primaryPos + prevDimension.primarySize;
+ dim.secondaryPos = 0;
+ }
+ }
+ function calculateDimensionsGrid(toIndex) {
+ var i, prevDimension, dim;
+ for (i = Math.max(dimensionsIndex, 0); i <= toIndex && (dim = dimensions[i]); i++) {
+ prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
+ dim.secondarySize = Math.min(
+ self.getItemSecondarySize(i, data[i]),
+ self.scrollSecondarySize
+ );
+ dim.secondaryPos = prevDimension.secondaryPos + prevDimension.secondarySize;
+
+ if (i === 0 || dim.secondaryPos + dim.secondarySize > self.scrollSecondarySize) {
+ dim.rowStartIndex = i;
+ dim.secondaryPos = 0;
+ dim.primarySize = self.getItemPrimarySize(i, data[i]);
+ dim.primaryPos = prevDimension.primaryPos + prevDimension.primarySize;
+ } else {
+ dim.rowStartIndex = prevDimension.rowStartIndex;
+ dim.primarySize = prevDimension.primarySize;
+ dim.primaryPos = prevDimension.primaryPos;
+ }
+ }
+ }
+
+ this.getContentSize = function() {
+ var dim = dimensions[dimensionsIndex] || EMPTY_DIMENSION;
+ return ((dim.primaryPos + dim.primarySize) || 0) +
+ this.getEstimatedPrimaryPos(data.length - dimensionsIndex - 1) +
+ repeaterBeforeSize + repeaterAfterSize;
+ };
+ this.onDestroy = function() {
+ dimensions.length = 0;
+ };
+
+ this.onRefreshData = function() {
+ // Make sure dimensions has as many items as data.length.
+ // This is to be sure we don't have to allocate objects while scrolling.
+ for (i = dimensions.length, len = data.length; i < len; i++) {
+ dimensions.push({});
+ }
+ dimensionsIndex = -1;
+ };
+ this.onRefreshLayout = function() {
+ dimensionsIndex = -1;
+ };
+ this.getDimensions = function(index) {
+ index = Math.min(index, data.length - 1);
+
+ if (dimensionsIndex < index) {
+ // Once we start asking for dimensions near the end of the list, go ahead and calculate
+ // everything. This is to make sure when the user gets to the end of the list, the
+ // scroll height of the list is 100% accurate (not estimated anymore).
+ if (index > data.length * 0.9) {
+ calculateDimensions(data.length - 1);
+ dimensionsIndex = data.length - 1;
+ scrollViewSetDimensions();
+ } else {
+ calculateDimensions(index);
+ dimensionsIndex = index;
+ debouncedScrollViewSetDimensions();
+ }
+
+ }
+ return dimensions[index];
+ };
+
+ var oldRenderStartIndex = -1;
+ var oldScrollValue = -1;
+ this.updateRenderRange = function(scrollValue, scrollValueEnd) {
+ var i;
+ var len;
+ var dim;
+
+ // Calculate more dimensions than we estimate we'll need, to be sure.
+ this.getDimensions( this.getEstimatedIndex(scrollValueEnd) * 2 );
+
+ // -- Calculate renderStartIndex
+ // base case: start at 0
+ if (oldRenderStartIndex === -1 || scrollValue === 0) {
+ i = 0;
+ // scrolling down
+ } else if (scrollValue >= oldScrollValue) {
+ for (i = oldRenderStartIndex, len = data.length; i < len; i++) {
+ if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.primarySize >= scrollValue) {
+ break;
+ }
+ }
+ // scrolling up
+ } else {
+ for (i = oldRenderStartIndex; i >= 0; i--) {
+ if ((dim = this.getDimensions(i)) && dim.primaryPos <= scrollValue) {
+ // when grid view, make sure the render starts at the beginning of a row.
+ i = isGridView ? dim.rowStartIndex : i;
+ break;
+ }
+ }
+ }
+
+ renderStartIndex = Math.min(Math.max(0, i), data.length - 1);
+ renderBeforeBoundary = renderStartIndex !== -1 ? this.getDimensions(renderStartIndex).primaryPos : -1;
+
+ // -- Calculate renderEndIndex
+ var lastRowDim;
+ for (i = renderStartIndex + 1, len = data.length; i < len; i++) {
+ if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.primarySize > scrollValueEnd) {
+
+ // Go all the way to the end of the row if we're in a grid
+ if (isGridView) {
+ lastRowDim = dim;
+ while (i < len - 1 &&
+ (dim = this.getDimensions(i + 1)).primaryPos === lastRowDim.primaryPos) {
+ i++;
+ }
+ }
+ break;
+ }
+ }
+
+ renderEndIndex = Math.min(i, data.length - 1);
+ renderAfterBoundary = renderEndIndex !== -1 ?
+ ((dim = this.getDimensions(renderEndIndex)).primaryPos + dim.primarySize) :
+ -1;
+
+ oldScrollValue = scrollValue;
+ oldRenderStartIndex = renderStartIndex;
+ };
+
+ }
+
+
+ };
+
+}
+
+
diff --git a/js/angular/service/collectionRepeatDataSource.js b/js/angular/service/collectionRepeatDataSource.js
deleted file mode 100644
index 803aba374c..0000000000
--- a/js/angular/service/collectionRepeatDataSource.js
+++ /dev/null
@@ -1,169 +0,0 @@
-IonicModule
-.factory('$collectionDataSource', [
- '$cacheFactory',
- '$parse',
- '$rootScope',
-function($cacheFactory, $parse, $rootScope) {
- var ONE_PX_TRANSPARENT_IMG_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
- 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.shouldRefreshImages = options.shouldRefreshImages;
-
- 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(el) {
- item.element = el;
- item.images = el[0].getElementsByTagName('img');
- });
- 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.data.length];
- }
-
- 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.shouldRefreshImages && refreshImages(item.images);
- }
- }
- 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;
-
- function refreshImages(imgNodes) {
- var i, len, img, src;
- for (i = 0, len = imgNodes.length; i < len; i++) {
- img = imgNodes[i];
- src = img.src;
- img.src = ONE_PX_TRANSPARENT_IMG_SRC;
- img.src = src;
- }
- }
-}]);
-
diff --git a/js/angular/service/collectionRepeatManager.js b/js/angular/service/collectionRepeatManager.js
deleted file mode 100644
index 3a5dceca7e..0000000000
--- a/js/angular/service/collectionRepeatManager.js
+++ /dev/null
@@ -1,366 +0,0 @@
-
-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.bufferSize = options.bufferSize || 2;
- this.bufferItems = Math.max(this.bufferSize * 10, 50);
-
- 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 = noop;
- this.calculateDimensions = 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 self = this;
- var primaryPos = 0;
- var secondaryPos = 0;
- var secondaryScrollSize = self.secondaryScrollSize();
- var previousItem;
- var i, len;
-
- // Skip past every beforeSibling, we want our list to start after those.
- for (i = 0, len = (self.dataSource.beforeSiblings || []).length; i < len; i++) {
- calculateSize(self.dataSource.beforeSiblings[i]);
- }
- var beforeSize = primaryPos + (previousItem && previousItem.primarySize || 0);
-
- primaryPos = secondaryPos = 0;
- previousItem = null;
-
- var dimensions = [];
- for (i = 0, len = self.dataSource.dimensions.length; i < len; i++) {
- dimensions.push( calculateSize(self.dataSource.dimensions[i]) );
- }
- 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: self.primaryDimension(dim),
- //Max out the item's width to the width of the scrollview
- secondarySize: Math.min(self.secondaryDimension(dim), secondaryScrollSize)
- };
-
- //If self 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.transcludeParent[0].style.height = result.totalSize + "px";
- 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 dimensions = this.dimensions;
- var rect;
- //Scrolling up
- if (scrollValue <= dimensions[i].primaryPos) {
- while ( (rect = dimensions[i - 1]) && rect.primaryPos > scrollValue) {
- i--;
- }
- //Scrolling down
- } else {
- while ((rect = 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 self = this;
- 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;
- // The bottom of the viewport
- var scrollValue = this.scrollValue();
- var viewportBottom = scrollValue + this.scrollSize();
-
- // 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);
-
- // Add two extra rows above the visible area
- renderStartIndex = this.addRowsToIndex(startIndex, -this.bufferSize);
-
- // 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 < viewportBottom) &&
- this.dimensions[i + 1]) {
- i++;
- }
-
- var renderEndIndex = this.addRowsToIndex(i, this.bufferSize);
-
- for (i = renderStartIndex; i <= renderEndIndex; i++) {
- rect = this.dimensions[i];
- self.renderItem(i, rect.primaryPos - self.beforeSize, rect.secondaryPos);
- }
-
- // 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);
- var itemDimensions = this.dimensions[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;
- }
-
- var width = this.isVertical ? itemDimensions.secondarySize : itemDimensions.primarySize;
- var height = this.isVertical ? itemDimensions.primarySize : itemDimensions.secondarySize;
- if (item.cssWidth !== width) {
- item.element[0].style.width = width + 'px';
- item.cssWidth = width;
- }
- if (item.cssHeight !== height) {
- item.element[0].style.height = height + 'px';
- item.cssHeight = height;
- }
- // 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];
- }
- },
- /*
- * Given an index, how many items do we have to change to get `rowDelta` number of rows up or down?
- * Eg if we are at index 0 and there are 2 items on the first row and 3 items on the second row,
- * to move forward two rows we have to go to index 5.
- * In that case, addRowsToIndex(dim, 0, 2) == 5.
- */
- addRowsToIndex: function(index, rowDelta) {
- var dimensions = this.dimensions;
- var direction = rowDelta > 0 ? 1 : -1;
- var rect;
- var positionOfRow;
- rowDelta = Math.abs(rowDelta);
- do {
- positionOfRow = dimensions[index] && dimensions[index].primaryPos;
- while ((rect = dimensions[index]) && rect.primaryPos === positionOfRow &&
- dimensions[index + direction]) {
- index += direction;
- }
- } while (rowDelta--);
- return index;
- }
- };
-
- return CollectionRepeatManager;
-}]);
-
diff --git a/js/views/scrollView.js b/js/views/scrollView.js
index 31b29e8fd2..f0cf73f2d8 100644
--- a/js/views/scrollView.js
+++ b/js/views/scrollView.js
@@ -1179,7 +1179,7 @@ ionic.views.Scroll = ionic.views.View.inherit({
this.__fadeScrollbars('out');
},
- resize: function() {
+ resize: function(continueScrolling) {
var self = this;
if (!self.__container || !self.options) return;
@@ -1189,7 +1189,8 @@ ionic.views.Scroll = ionic.views.View.inherit({
self.__container.clientWidth,
self.__container.clientHeight,
self.options.getContentWidth(),
- self.options.getContentHeight()
+ self.options.getContentHeight(),
+ continueScrolling
);
},
/*
@@ -1282,7 +1283,7 @@ ionic.views.Scroll = ionic.views.View.inherit({
* @param contentWidth {Integer} Outer width of inner element
* @param contentHeight {Integer} Outer height of inner element
*/
- setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight) {
+ setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight, continueScrolling) {
var self = this;
if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) {
@@ -1312,7 +1313,9 @@ ionic.views.Scroll = ionic.views.View.inherit({
self.__resizeScrollbars();
// Refresh scroll position
- self.scrollTo(self.__scrollLeft, self.__scrollTop, true, null, true);
+ if (!continueScrolling) {
+ self.scrollTo(self.__scrollLeft, self.__scrollTop, true, null, true);
+ }
},
@@ -2174,15 +2177,11 @@ ionic.views.Scroll = ionic.views.View.inherit({
clearTimeout(self.__sizerTimeout);
var sizer = function() {
- self.resize();
-
- // if ((self.options.scrollingX && !self.__maxScrollLeft) || (self.options.scrollingY && !self.__maxScrollTop)) {
- // //self.__sizerTimeout = setTimeout(sizer, 1000);
- // }
+ self.resize(true);
};
sizer();
- self.__sizerTimeout = setTimeout(sizer, 1000);
+ self.__sizerTimeout = setTimeout(sizer, 500);
},
/*
diff --git a/package.json b/package.json
index 3fd9b4c379..94c449f9ad 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"htmlparser2": "3.7.0",
"ircb": "^0.3.1",
"jasmine-node": "^1.14.3",
+ "js-string-escape": "^1.0.0",
"js-yaml": "^3.0.2",
"jshint-summary": "^0.3.0",
"karma": "^0.12.23",
diff --git a/scss/_util.scss b/scss/_util.scss
index 273c6a8258..eb85c0d626 100644
--- a/scss/_util.scss
+++ b/scss/_util.scss
@@ -270,5 +270,16 @@
[collection-repeat] {
/* Position is set by transforms */
left: 0 !important;
+ top: 0 !important;
position: absolute !important;
+ z-index: 1;
+}
+.collection-repeat-container {
+ position: relative;
+}
+.collection-repeat-after-container {
+ z-index: 0;
+ &.horizontal {
+ display: inline-block;
+ }
}
diff --git a/test/html/collection-repeat/basic-grid.html b/test/html/collection-repeat/basic-grid.html
new file mode 100644
index 0000000000..c9afbc411d
--- /dev/null
+++ b/test/html/collection-repeat/basic-grid.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ Sample UL
+
+
+
+
+
+
+
+
+