ionic-cordova-gulp-seed
Version:
Ionic & Cordova & Gulp seed with organized code, tests, bower support and some other stuff. Originated from ionic-angular-cordova-seed.
307 lines (290 loc) • 12.6 kB
JavaScript
/**
* @ngdoc directive
* @module ionic
* @name collectionRepeat
* @restrict A
* @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.
*
* Demo:
*
* 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.
*
* Here are a few things to keep in mind while using collection-repeat:
*
* 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 below).
* 3. The elements rendered will be absolutely positioned: be sure to let your CSS work with
* this (see below).
* 4. 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.
* 5. 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.
*
*
* @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
* <ion-content ng-controller="ContentCtrl">
* <div class="list">
* <div class="item my-item"
* collection-repeat="item in items"
* collection-item-width="'100%'"
* collection-item-height="getItemHeight(item, $index)"
* ng-style="{height: getItemHeight(item, $index)}">
* {% raw %}{{item}}{% endraw %}
* </div>
* </div>
* </ion-content>
* ```
* ```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;
* };
* }
* ```
* ```css
* .my-item {
* left: 0;
* right: 0;
* }
* ```
*
* #### Grid Usage (three items per row)
*
* ```html
* <ion-content>
* <div class="item item-avatar my-image-item"
* collection-repeat="image in images"
* collection-item-width="'33%'"
* collection-item-height="'33%'">
* <img ng-src="{{image.src}}">
* </div>
* </ion-content>
* ```
* Percentage of total visible list dimensions. This example shows a 3 by 3 matrix that fits on the screen (3 rows and 3 colums). Note that dimensions are used in the creation of the element and therefore a measurement of the item cannnot be used as an input dimension.
* ```css
* .my-image-item img {
* height: 33%;
* width: 33%;
* }
* ```
*
* @param {expression} collection-repeat The expression indicating how to enumerate a collection. These
* formats are currently supported:
*
* * `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.
*
*/
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) {
return {
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('<div style="position:relative;">');
$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
});
var collectionRepeatManager = new $collectionRepeatManager({
dataSource: dataSource,
element: scrollCtrl.$element,
scrollView: scrollCtrl.scrollView,
});
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 before = true;
forEach(scrollViewContent.children, function(node, i) {
if ( ionic.DomUtil.elementIsDescendant($element[0], node, scrollViewContent) ) {
before = false;
} else {
if (node.hasAttribute('collection-repeat-ignore')) return;
var width = node.offsetWidth;
var height = node.offsetHeight;
if (width && height) {
var element = jqLite(node);
(before ? beforeSiblings : afterSiblings).push({
width: node.offsetWidth,
height: node.offsetHeight,
element: element,
scope: element.isolateScope() || element.scope(),
isOutside: true
});
}
}
});
scrollView.resize();
dataSource.setData(value, beforeSiblings, afterSiblings);
collectionRepeatManager.resize();
}
var requiresRerender;
function rerenderOnResize() {
rerender(listExprParsed($scope));
requiresRerender = (!scrollViewContent.clientWidth && !scrollViewContent.clientHeight);
}
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 || angular.noop)();
});
}
};
}])
.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);
}
});
}
};
}];
}