2
// Copyright Kamil Pękala http://github.com/kamilkp
3
// Angular Virtual Scroll Repeat v1.1.7 2016/03/08
6
(function(window, angular) {
8
/* jshint eqnull:true */
12
// vsRepeat directive stands for Virtual Scroll Repeat. It turns a standard ngRepeated set of elements in a scrollable container
13
// into a component, where the user thinks he has all the elements rendered and all he needs to do is scroll (without any kind of
14
// pagination - which most users loath) and at the same time the browser isn't overloaded by that many elements/angular bindings etc.
15
// The directive renders only so many elements that can fit into current container's clientHeight/clientWidth.
18
// - current version only supports an Array as a right-hand-side object for ngRepeat
19
// - all rendered elements must have the same height/width or the sizes of the elements must be known up front
22
// In order to use the vsRepeat directive you need to place a vs-repeat attribute on a direct parent of an element with ng-repeat
25
// <div ng-repeat="item in someArray">
32
// <div ng-repeat-start="item in someArray">
36
// <!-- something in the middle -->
38
// <div ng-repeat-end>
43
// You can also measure the single element's height/width (including all paddings and margins), and then speficy it as a value
44
// of the attribute 'vs-repeat'. This can be used if one wants to override the automatically computed element size.
46
// <div vs-repeat="50"> <!-- the specified element height is 50px -->
47
// <div ng-repeat="item in someArray">
54
// - the vsRepeat directive must be applied to a direct parent of an element with ngRepeat
55
// - the value of vsRepeat attribute is the single element's height/width measured in pixels. If none provided, the directive
56
// will compute it automatically
58
// OPTIONAL PARAMETERS (attributes):
59
// vs-repeat-container="selector" - selector for element containing ng-repeat. (defaults to the current element)
60
// vs-scroll-parent="selector" - selector to the scrollable container. The directive will look for a closest parent matching
61
// the given selector (defaults to the current element)
62
// vs-horizontal - stack repeated elements horizontally instead of vertically
63
// vs-offset-before="value" - top/left offset in pixels (defaults to 0)
64
// vs-offset-after="value" - bottom/right offset in pixels (defaults to 0)
65
// vs-excess="value" - an integer number representing the number of elements to be rendered outside of the current container's viewport
67
// vs-size - a property name of the items in collection that is a number denoting the element size (in pixels)
68
// vs-autoresize - use this attribute without vs-size and without specifying element's size. The automatically computed element style will
69
// readjust upon window resize if the size is dependable on the viewport size
70
// vs-scrolled-to-end="callback" - callback will be called when the last item of the list is rendered
71
// vs-scrolled-to-end-offset="integer" - set this number to trigger the scrolledToEnd callback n items before the last gets rendered
72
// vs-scrolled-to-beginning="callback" - callback will be called when the first item of the list is rendered
73
// vs-scrolled-to-beginning-offset="integer" - set this number to trigger the scrolledToBeginning callback n items before the first gets rendered
76
// - 'vsRepeatTrigger' - an event the directive listens for to manually trigger reinitialization
77
// - 'vsRepeatReinitialized' - an event the directive emits upon reinitialization done
79
var dde = document.documentElement,
80
matchingFunction = dde.matches ? 'matches' :
81
dde.matchesSelector ? 'matchesSelector' :
82
dde.webkitMatches ? 'webkitMatches' :
83
dde.webkitMatchesSelector ? 'webkitMatchesSelector' :
84
dde.msMatches ? 'msMatches' :
85
dde.msMatchesSelector ? 'msMatchesSelector' :
86
dde.mozMatches ? 'mozMatches' :
87
dde.mozMatchesSelector ? 'mozMatchesSelector' : null;
89
var closestElement = angular.element.prototype.closest || function (selector) {
90
var el = this[0].parentNode;
91
while (el !== document.documentElement && el != null && !el[matchingFunction](selector)) {
95
if (el && el[matchingFunction](selector)) {
96
return angular.element(el);
99
return angular.element();
103
function getWindowScroll() {
104
if ('pageYOffset' in window) {
106
scrollTop: pageYOffset,
107
scrollLeft: pageXOffset
111
var sx, sy, d = document, r = d.documentElement, b = d.body;
112
sx = r.scrollLeft || b.scrollLeft || 0;
113
sy = r.scrollTop || b.scrollTop || 0;
121
function getClientSize(element, sizeProp) {
122
if (element === window) {
123
return sizeProp === 'clientWidth' ? window.innerWidth : window.innerHeight;
126
return element[sizeProp];
130
function getScrollPos(element, scrollProp) {
131
return element === window ? getWindowScroll()[scrollProp] : element[scrollProp];
134
function getScrollOffset(vsElement, scrollElement, isHorizontal) {
135
var vsPos = vsElement.getBoundingClientRect()[isHorizontal ? 'left' : 'top'];
136
var scrollPos = scrollElement === window ? 0 : scrollElement.getBoundingClientRect()[isHorizontal ? 'left' : 'top'];
137
var correction = vsPos - scrollPos +
138
(scrollElement === window ? getWindowScroll() : scrollElement)[isHorizontal ? 'scrollLeft' : 'scrollTop'];
143
var vsRepeatModule = angular.module('vs-repeat', []).directive('vsRepeat', ['$compile', '$parse', function($compile, $parse) {
147
compile: function($element, $attrs) {
148
var repeatContainer = angular.isDefined($attrs.vsRepeatContainer) ? angular.element($element[0].querySelector($attrs.vsRepeatContainer)) : $element,
149
ngRepeatChild = repeatContainer.children().eq(0),
151
childCloneHtml = ngRepeatChild[0].outerHTML,
156
originalNgRepeatAttr,
157
collectionName = '$vs_collection',
158
isNgRepeatStart = false,
159
attributesDictionary = {
160
'vsRepeat': 'elementSize',
161
'vsOffsetBefore': 'offsetBefore',
162
'vsOffsetAfter': 'offsetAfter',
163
'vsScrolledToEndOffset': 'scrolledToEndOffset',
164
'vsScrolledToBeginningOffset': 'scrolledToBeginningOffset',
168
if (ngRepeatChild.attr('ng-repeat')) {
169
originalNgRepeatAttr = 'ng-repeat';
170
ngRepeatExpression = ngRepeatChild.attr('ng-repeat');
172
else if (ngRepeatChild.attr('data-ng-repeat')) {
173
originalNgRepeatAttr = 'data-ng-repeat';
174
ngRepeatExpression = ngRepeatChild.attr('data-ng-repeat');
176
else if (ngRepeatChild.attr('ng-repeat-start')) {
177
isNgRepeatStart = true;
178
originalNgRepeatAttr = 'ng-repeat-start';
179
ngRepeatExpression = ngRepeatChild.attr('ng-repeat-start');
181
else if (ngRepeatChild.attr('data-ng-repeat-start')) {
182
isNgRepeatStart = true;
183
originalNgRepeatAttr = 'data-ng-repeat-start';
184
ngRepeatExpression = ngRepeatChild.attr('data-ng-repeat-start');
187
throw new Error('angular-vs-repeat: no ng-repeat directive on a child element');
190
expressionMatches = /^\s*(\S+)\s+in\s+([\S\s]+?)(track\s+by\s+\S+)?$/.exec(ngRepeatExpression);
191
lhs = expressionMatches[1];
192
rhs = expressionMatches[2];
193
rhsSuffix = expressionMatches[3];
195
if (isNgRepeatStart) {
197
var repeaterElement = repeatContainer.children().eq(0);
198
while(repeaterElement.attr('ng-repeat-end') == null && repeaterElement.attr('data-ng-repeat-end') == null) {
200
repeaterElement = repeatContainer.children().eq(index);
201
childCloneHtml += repeaterElement[0].outerHTML;
205
repeatContainer.empty();
207
pre: function($scope, $element, $attrs) {
208
var repeatContainer = angular.isDefined($attrs.vsRepeatContainer) ? angular.element($element[0].querySelector($attrs.vsRepeatContainer)) : $element,
209
childClone = angular.element(childCloneHtml),
210
childTagName = childClone[0].tagName.toLowerCase(),
211
originalCollection = [],
213
$$horizontal = typeof $attrs.vsHorizontal !== 'undefined',
214
$beforeContent = angular.element('<' + childTagName + ' class="vs-repeat-before-content"></' + childTagName + '>'),
215
$afterContent = angular.element('<' + childTagName + ' class="vs-repeat-after-content"></' + childTagName + '>'),
216
autoSize = !$attrs.vsRepeat,
217
sizesPropertyExists = !!$attrs.vsSize || !!$attrs.vsSizeProperty,
218
$scrollParent = $attrs.vsScrollParent ?
219
$attrs.vsScrollParent === 'window' ? angular.element(window) :
220
closestElement.call(repeatContainer, $attrs.vsScrollParent) : repeatContainer,
221
$$options = 'vsOptions' in $attrs ? $scope.$eval($attrs.vsOptions) : {},
222
clientSize = $$horizontal ? 'clientWidth' : 'clientHeight',
223
offsetSize = $$horizontal ? 'offsetWidth' : 'offsetHeight',
224
scrollPos = $$horizontal ? 'scrollLeft' : 'scrollTop';
226
$scope.totalSize = 0;
227
if (!('vsSize' in $attrs) && 'vsSizeProperty' in $attrs) {
228
console.warn('vs-size-property attribute is deprecated. Please use vs-size attribute which also accepts angular expressions.');
231
if ($scrollParent.length === 0) {
232
throw 'Specified scroll parent selector did not match any element';
234
$scope.$scrollParent = $scrollParent;
236
if (sizesPropertyExists) {
237
$scope.sizesCumulative = [];
241
$scope.elementSize = (+$attrs.vsRepeat) || getClientSize($scrollParent[0], clientSize) || 50;
242
$scope.offsetBefore = 0;
243
$scope.offsetAfter = 0;
247
$beforeContent.css('height', '100%');
248
$afterContent.css('height', '100%');
251
$beforeContent.css('width', '100%');
252
$afterContent.css('width', '100%');
255
Object.keys(attributesDictionary).forEach(function(key) {
257
$attrs.$observe(key, function(value) {
258
// '+' serves for getting a number from the string as the attributes are always strings
259
$scope[attributesDictionary[key]] = +value;
266
$scope.$watchCollection(rhs, function(coll) {
267
originalCollection = coll || [];
272
if (!originalCollection || originalCollection.length < 1) {
273
$scope[collectionName] = [];
275
$scope.sizesCumulative = [0];
278
originalLength = originalCollection.length;
279
if (sizesPropertyExists) {
280
$scope.sizes = originalCollection.map(function(item) {
281
var s = $scope.$new(false);
282
angular.extend(s, item);
284
var size = ($attrs.vsSize || $attrs.vsSizeProperty) ?
285
s.$eval($attrs.vsSize || $attrs.vsSizeProperty) :
291
$scope.sizesCumulative = $scope.sizes.map(function(size) {
296
$scope.sizesCumulative.push(sum);
306
function setAutoSize() {
308
$scope.$$postDigest(function() {
309
if (repeatContainer[0].offsetHeight || repeatContainer[0].offsetWidth) { // element is visible
310
var children = repeatContainer.children(),
312
gotSomething = false,
313
insideStartEndSequence = false;
315
while (i < children.length) {
316
if (children[i].attributes[originalNgRepeatAttr] != null || insideStartEndSequence) {
318
$scope.elementSize = 0;
322
if (children[i][offsetSize]) {
323
$scope.elementSize += children[i][offsetSize];
326
if (isNgRepeatStart) {
327
if (children[i].attributes['ng-repeat-end'] != null || children[i].attributes['data-ng-repeat-end'] != null) {
331
insideStartEndSequence = true;
344
if ($scope.$root && !$scope.$root.$$phase) {
350
var dereg = $scope.$watch(function() {
351
if (repeatContainer[0].offsetHeight || repeatContainer[0].offsetWidth) {
361
function getLayoutProp() {
362
var layoutPropPrefix = childTagName === 'tr' ? '' : 'min-';
363
var layoutProp = $$horizontal ? layoutPropPrefix + 'width' : layoutPropPrefix + 'height';
367
childClone.eq(0).attr(originalNgRepeatAttr, lhs + ' in ' + collectionName + (rhsSuffix ? ' ' + rhsSuffix : ''));
368
childClone.addClass('vs-repeat-repeated-element');
370
repeatContainer.append($beforeContent);
371
repeatContainer.append(childClone);
372
$compile(childClone)($scope);
373
repeatContainer.append($afterContent);
375
$scope.startIndex = 0;
378
function scrollHandler() {
379
if (updateInnerCollection()) {
384
$scrollParent.on('scroll', scrollHandler);
386
function onWindowResize() {
387
if (typeof $attrs.vsAutoresize !== 'undefined') {
390
if ($scope.$root && !$scope.$root.$$phase) {
394
if (updateInnerCollection()) {
399
angular.element(window).on('resize', onWindowResize);
400
$scope.$on('$destroy', function() {
401
angular.element(window).off('resize', onWindowResize);
402
$scrollParent.off('scroll', scrollHandler);
405
$scope.$on('vsRepeatTrigger', refresh);
407
$scope.$on('vsRepeatResize', function() {
417
$scope.$on('vsRenderAll', function() {//e , quantum) {
418
if($$options.latch) {
419
setTimeout(function() {
420
// var __endIndex = Math.min($scope.endIndex + (quantum || 1), originalLength);
421
var __endIndex = originalLength;
422
_maxEndIndex = Math.max(__endIndex, _maxEndIndex);
423
$scope.endIndex = $$options.latch ? _maxEndIndex : __endIndex;
424
$scope[collectionName] = originalCollection.slice($scope.startIndex, $scope.endIndex);
425
_prevEndIndex = $scope.endIndex;
427
$scope.$$postDigest(function() {
428
$beforeContent.css(getLayoutProp(), 0);
429
$afterContent.css(getLayoutProp(), 0);
432
$scope.$apply(function() {
433
$scope.$emit('vsRenderAllDone');
439
function reinitialize() {
440
_prevStartIndex = void 0;
441
_prevEndIndex = void 0;
442
_minStartIndex = originalLength;
444
updateTotalSize(sizesPropertyExists ?
445
$scope.sizesCumulative[originalLength] :
446
$scope.elementSize * originalLength
448
updateInnerCollection();
450
$scope.$emit('vsRepeatReinitialized', $scope.startIndex, $scope.endIndex);
453
function updateTotalSize(size) {
454
$scope.totalSize = $scope.offsetBefore + size + $scope.offsetAfter;
458
function reinitOnClientHeightChange() {
459
var ch = getClientSize($scrollParent[0], clientSize);
460
if (ch !== _prevClientSize) {
462
if ($scope.$root && !$scope.$root.$$phase) {
466
_prevClientSize = ch;
469
$scope.$watch(function() {
470
if (typeof window.requestAnimationFrame === 'function') {
471
window.requestAnimationFrame(reinitOnClientHeightChange);
474
reinitOnClientHeightChange();
478
function updateInnerCollection() {
479
var $scrollPosition = getScrollPos($scrollParent[0], scrollPos);
480
var $clientSize = getClientSize($scrollParent[0], clientSize);
482
var scrollOffset = repeatContainer[0] === $scrollParent[0] ? 0 : getScrollOffset(
488
var __startIndex = $scope.startIndex;
489
var __endIndex = $scope.endIndex;
491
if (sizesPropertyExists) {
493
while ($scope.sizesCumulative[__startIndex] < $scrollPosition - $scope.offsetBefore - scrollOffset) {
496
if (__startIndex > 0) { __startIndex--; }
498
// Adjust the start index according to the excess
499
__startIndex = Math.max(
500
Math.floor(__startIndex - $scope.excess / 2),
504
__endIndex = __startIndex;
505
while ($scope.sizesCumulative[__endIndex] < $scrollPosition - $scope.offsetBefore - scrollOffset + $clientSize) {
509
// Adjust the end index according to the excess
510
__endIndex = Math.min(
511
Math.ceil(__endIndex + $scope.excess / 2),
516
__startIndex = Math.max(
518
($scrollPosition - $scope.offsetBefore - scrollOffset) / $scope.elementSize
519
) - $scope.excess / 2,
523
__endIndex = Math.min(
524
__startIndex + Math.ceil(
525
$clientSize / $scope.elementSize
531
_minStartIndex = Math.min(__startIndex, _minStartIndex);
532
_maxEndIndex = Math.max(__endIndex, _maxEndIndex);
534
$scope.startIndex = $$options.latch ? _minStartIndex : __startIndex;
535
$scope.endIndex = $$options.latch ? _maxEndIndex : __endIndex;
537
var digestRequired = false;
538
if (_prevStartIndex == null) {
539
digestRequired = true;
541
else if (_prevEndIndex == null) {
542
digestRequired = true;
545
if (!digestRequired) {
546
if ($$options.hunked) {
547
if (Math.abs($scope.startIndex - _prevStartIndex) >= $scope.excess / 2 ||
548
($scope.startIndex === 0 && _prevStartIndex !== 0)) {
549
digestRequired = true;
551
else if (Math.abs($scope.endIndex - _prevEndIndex) >= $scope.excess / 2 ||
552
($scope.endIndex === originalLength && _prevEndIndex !== originalLength)) {
553
digestRequired = true;
557
digestRequired = $scope.startIndex !== _prevStartIndex ||
558
$scope.endIndex !== _prevEndIndex;
562
if (digestRequired) {
563
$scope[collectionName] = originalCollection.slice($scope.startIndex, $scope.endIndex);
566
$scope.$emit('vsRepeatInnerCollectionUpdated', $scope.startIndex, $scope.endIndex, _prevStartIndex, _prevEndIndex);
568
if ($attrs.vsScrolledToEnd) {
569
triggerIndex = originalCollection.length - ($scope.scrolledToEndOffset || 0);
570
if (($scope.endIndex >= triggerIndex && _prevEndIndex < triggerIndex) || (originalCollection.length && $scope.endIndex === originalCollection.length)) {
571
$scope.$eval($attrs.vsScrolledToEnd);
574
if ($attrs.vsScrolledToBeginning) {
575
triggerIndex = $scope.scrolledToBeginningOffset || 0;
576
if (($scope.startIndex <= triggerIndex && _prevStartIndex > $scope.startIndex)) {
577
$scope.$eval($attrs.vsScrolledToBeginning);
581
_prevStartIndex = $scope.startIndex;
582
_prevEndIndex = $scope.endIndex;
584
var offsetCalculationString = sizesPropertyExists ?
585
'(sizesCumulative[$index + startIndex] + offsetBefore)' :
586
'(($index + startIndex) * elementSize + offsetBefore)';
588
var parsed = $parse(offsetCalculationString);
589
var o1 = parsed($scope, {$index: 0});
590
var o2 = parsed($scope, {$index: $scope[collectionName].length});
591
var total = $scope.totalSize;
593
$beforeContent.css(getLayoutProp(), o1 + 'px');
594
$afterContent.css(getLayoutProp(), (total - o2) + 'px');
597
return digestRequired;
605
if (typeof module !== 'undefined' && module.exports) {
606
module.exports = vsRepeatModule.name;
608
})(window, window.angular);