source: trip-planner-front/node_modules/angular-material/modules/js/virtualRepeat/virtualRepeat.js@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 35.1 KB
Line 
1/*!
2 * AngularJS Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.2.3
6 */
7(function( window, angular, undefined ){
8"use strict";
9
10/**
11 * @ngdoc module
12 * @name material.components.virtualRepeat
13 */
14VirtualRepeatContainerController['$inject'] = ["$$rAF", "$mdUtil", "$mdConstant", "$parse", "$rootScope", "$window", "$scope", "$element", "$attrs"];
15VirtualRepeatController['$inject'] = ["$scope", "$element", "$attrs", "$browser", "$document", "$rootScope", "$$rAF", "$mdUtil"];
16VirtualRepeatDirective['$inject'] = ["$parse"];
17angular.module('material.components.virtualRepeat', [
18 'material.core',
19 'material.components.showHide'
20])
21.directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective)
22.directive('mdVirtualRepeat', VirtualRepeatDirective)
23.directive('mdForceHeight', ForceHeightDirective);
24
25
26/**
27 * @ngdoc directive
28 * @name mdVirtualRepeatContainer
29 * @module material.components.virtualRepeat
30 * @restrict E
31 * @description
32 * `md-virtual-repeat-container` provides the scroll container for
33 * <a ng-href="api/directive/mdVirtualRepeat">md-virtual-repeat</a>.
34 *
35 * VirtualRepeat is a limited substitute for `ng-repeat` that renders only
36 * enough DOM nodes to fill the container, recycling them as the user scrolls.
37 *
38 * Once an element is not visible anymore, the Virtual Repeat recycles the element and reuses it
39 * for another visible item by replacing the previous data set with the set of currently visible
40 * elements.
41 *
42 * ### Common Issues
43 *
44 * - When having one-time bindings inside of the view template, the Virtual Repeat will not properly
45 * update the bindings for new items, since the view will be recycled.
46 * - Directives inside of a Virtual Repeat will be only compiled (linked) once, because those
47 * items will be recycled and used for other items.
48 * The Virtual Repeat just updates the scope bindings.
49 *
50 *
51 * ### Notes
52 *
53 * > The VirtualRepeat is a similar implementation to the Android
54 * [RecyclerView](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html).
55 *
56 * <!-- This comment forces a break between blockquotes //-->
57 *
58 * > Please also review the <a ng-href="api/directive/mdVirtualRepeat">mdVirtualRepeat</a>
59 * documentation for more information.
60 *
61 *
62 * @usage
63 * <hljs lang="html">
64 *
65 * <md-virtual-repeat-container md-top-index="topIndex">
66 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
67 * </md-virtual-repeat-container>
68 * </hljs>
69 *
70 * @param {boolean=} md-auto-shrink When present and the container will shrink to fit
71 * the number of items in the `md-virtual-repeat`.
72 * @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink
73 * will shrink to. Default: `0`.
74 * @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally.
75 * The default is `false` which indicates vertical orientation and scrolling.
76 * @param {number=} md-top-index Binds the index of the item that is at the top of the scroll
77 * container to `$scope`. It can both read and set the scroll position.
78 */
79function VirtualRepeatContainerDirective() {
80 return {
81 controller: VirtualRepeatContainerController,
82 template: virtualRepeatContainerTemplate,
83 compile: function virtualRepeatContainerCompile($element, $attrs) {
84 $element
85 .addClass('md-virtual-repeat-container')
86 .addClass($attrs.hasOwnProperty('mdOrientHorizontal')
87 ? 'md-orient-horizontal'
88 : 'md-orient-vertical');
89 }
90 };
91}
92
93
94function virtualRepeatContainerTemplate($element) {
95 return '<div class="md-virtual-repeat-scroller" role="presentation">' +
96 '<div class="md-virtual-repeat-sizer" role="presentation"></div>' +
97 '<div class="md-virtual-repeat-offsetter" role="presentation">' +
98 $element[0].innerHTML +
99 '</div></div>';
100}
101
102/**
103 * Number of additional elements to render above and below the visible area inside
104 * of the virtual repeat container. A higher number results in less flicker when scrolling
105 * very quickly in Safari, but comes with a higher rendering and dirty-checking cost.
106 * @const {number}
107 */
108var NUM_EXTRA = 3;
109
110/** ngInject */
111function VirtualRepeatContainerController($$rAF, $mdUtil, $mdConstant, $parse, $rootScope, $window,
112 $scope, $element, $attrs) {
113 this.$rootScope = $rootScope;
114 this.$scope = $scope;
115 this.$element = $element;
116 this.$attrs = $attrs;
117
118 /** @type {number} The width or height of the container */
119 this.size = 0;
120 /** @type {number} The scroll width or height of the scroller */
121 this.scrollSize = 0;
122 /** @type {number} The scrollLeft or scrollTop of the scroller */
123 this.scrollOffset = 0;
124 /** @type {boolean} Whether the scroller is oriented horizontally */
125 this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal');
126 /** @type {!VirtualRepeatController} The repeater inside of this container */
127 this.repeater = null;
128 /** @type {boolean} Whether auto-shrink is enabled */
129 this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink');
130 /** @type {number} Minimum number of items to auto-shrink to */
131 this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0;
132 /** @type {?number} Original container size when shrank */
133 this.originalSize = null;
134 /** @type {number} Amount to offset the total scroll size by. */
135 this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0;
136 /** @type {?string} height or width element style on the container prior to auto-shrinking. */
137 this.oldElementSize = null;
138 /** @type {!number} Maximum amount of pixels allowed for a single DOM element */
139 this.maxElementPixels = $mdConstant.ELEMENT_MAX_PIXELS;
140 /** @type {string} Direction of the text */
141 this.ltr = !$mdUtil.isRtl(this.$attrs);
142
143 if (this.$attrs.mdTopIndex) {
144 /** @type {function(angular.Scope): number} Binds to topIndex on AngularJS scope */
145 this.bindTopIndex = $parse(this.$attrs.mdTopIndex);
146 /** @type {number} The index of the item that is at the top of the scroll container */
147 this.topIndex = this.bindTopIndex(this.$scope);
148
149 if (!angular.isDefined(this.topIndex)) {
150 this.topIndex = 0;
151 this.bindTopIndex.assign(this.$scope, 0);
152 }
153
154 this.$scope.$watch(this.bindTopIndex, angular.bind(this, function(newIndex) {
155 if (newIndex !== this.topIndex) {
156 this.scrollToIndex(newIndex);
157 }
158 }));
159 } else {
160 this.topIndex = 0;
161 }
162
163 this.scroller = $element[0].querySelector('.md-virtual-repeat-scroller');
164 this.sizer = this.scroller.querySelector('.md-virtual-repeat-sizer');
165 this.offsetter = this.scroller.querySelector('.md-virtual-repeat-offsetter');
166
167 // After the DOM stabilizes, measure the initial size of the container and
168 // make a best effort at re-measuring as it changes.
169 var boundUpdateSize = angular.bind(this, this.updateSize);
170
171 $$rAF(angular.bind(this, function() {
172 boundUpdateSize();
173
174 var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false);
175 var jWindow = angular.element($window);
176
177 // Make one more attempt to get the size if it is 0.
178 // This is not by any means a perfect approach, but there's really no
179 // silver bullet here.
180 if (!this.size) {
181 debouncedUpdateSize();
182 }
183
184 jWindow.on('resize', debouncedUpdateSize);
185 $scope.$on('$destroy', function() {
186 jWindow.off('resize', debouncedUpdateSize);
187 });
188
189 $scope.$emit('$md-resize-enable');
190 $scope.$on('$md-resize', boundUpdateSize);
191 }));
192}
193
194
195/** Called by the md-virtual-repeat inside of the container at startup. */
196VirtualRepeatContainerController.prototype.register = function(repeaterCtrl) {
197 this.repeater = repeaterCtrl;
198
199 angular.element(this.scroller)
200 .on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_));
201};
202
203
204/** @return {boolean} Whether the container is configured for horizontal scrolling. */
205VirtualRepeatContainerController.prototype.isHorizontal = function() {
206 return this.horizontal;
207};
208
209
210/** @return {number} The size (width or height) of the container. */
211VirtualRepeatContainerController.prototype.getSize = function() {
212 return this.size;
213};
214
215
216/**
217 * Resizes the container.
218 * @private
219 * @param {number} size The new size to set.
220 */
221VirtualRepeatContainerController.prototype.setSize_ = function(size) {
222 var dimension = this.getDimensionName_();
223
224 this.size = size;
225 this.$element[0].style[dimension] = size + 'px';
226};
227
228
229VirtualRepeatContainerController.prototype.unsetSize_ = function() {
230 this.$element[0].style[this.getDimensionName_()] = this.oldElementSize;
231 this.oldElementSize = null;
232};
233
234
235/** Instructs the container to re-measure its size. */
236VirtualRepeatContainerController.prototype.updateSize = function() {
237 // If the original size is already determined, we can skip the update.
238 if (this.originalSize) return;
239
240 var size = this.isHorizontal()
241 ? this.$element[0].clientWidth
242 : this.$element[0].clientHeight;
243
244 if (size) {
245 this.size = size;
246 }
247
248 // Recheck the scroll position after updating the size. This resolves
249 // problems that can result if the scroll position was measured while the
250 // element was display: none or detached from the document.
251 this.handleScroll_();
252
253 this.repeater && this.repeater.containerUpdated();
254};
255
256
257/** @return {number} The container's scrollHeight or scrollWidth. */
258VirtualRepeatContainerController.prototype.getScrollSize = function() {
259 return this.scrollSize;
260};
261
262/**
263 * @returns {string} either width or height dimension
264 * @private
265 */
266VirtualRepeatContainerController.prototype.getDimensionName_ = function() {
267 return this.isHorizontal() ? 'width' : 'height';
268};
269
270
271/**
272 * Sets the scroller element to the specified size.
273 * @private
274 * @param {number} size The new size.
275 */
276VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) {
277 var dimension = this.getDimensionName_();
278 var crossDimension = this.isHorizontal() ? 'height' : 'width';
279
280 // Clear any existing dimensions.
281 this.sizer.innerHTML = '';
282
283 // If the size falls within the browser's maximum explicit size for a single element, we can
284 // set the size and be done. Otherwise, we have to create children that add up the the desired
285 // size.
286 if (size < this.maxElementPixels) {
287 this.sizer.style[dimension] = size + 'px';
288 } else {
289 this.sizer.style[dimension] = 'auto';
290 this.sizer.style[crossDimension] = 'auto';
291
292 // Divide the total size we have to render into N max-size pieces.
293 var numChildren = Math.floor(size / this.maxElementPixels);
294
295 // Element template to clone for each max-size piece.
296 var sizerChild = document.createElement('div');
297 sizerChild.style[dimension] = this.maxElementPixels + 'px';
298 sizerChild.style[crossDimension] = '1px';
299
300 for (var i = 0; i < numChildren; i++) {
301 this.sizer.appendChild(sizerChild.cloneNode(false));
302 }
303
304 // Re-use the element template for the remainder.
305 sizerChild.style[dimension] = (size - (numChildren * this.maxElementPixels)) + 'px';
306 this.sizer.appendChild(sizerChild);
307 }
308};
309
310
311/**
312 * If auto-shrinking is enabled, shrinks or unshrinks as appropriate.
313 * @private
314 * @param {number} size The new size.
315 */
316VirtualRepeatContainerController.prototype.autoShrink_ = function(size) {
317 var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize());
318
319 if (this.autoShrink && shrinkSize !== this.size) {
320 if (this.oldElementSize === null) {
321 this.oldElementSize = this.$element[0].style[this.getDimensionName_()];
322 }
323
324 var currentSize = this.originalSize || this.size;
325
326 if (!currentSize || shrinkSize < currentSize) {
327 if (!this.originalSize) {
328 this.originalSize = this.size;
329 }
330
331 // Now we update the containers size, because shrinking is enabled.
332 this.setSize_(shrinkSize);
333 } else if (this.originalSize !== null) {
334 // Set the size back to our initial size.
335 this.unsetSize_();
336
337 var _originalSize = this.originalSize;
338 this.originalSize = null;
339
340 // We determine the repeaters size again, if the original size was zero.
341 // The originalSize needs to be null, to be able to determine the size.
342 if (!_originalSize) this.updateSize();
343
344 // Apply the original size or the determined size back to the container, because
345 // it has been overwritten before, in the shrink block.
346 this.setSize_(_originalSize || this.size);
347 }
348
349 this.repeater.containerUpdated();
350 }
351};
352
353
354/**
355 * Sets the scrollHeight or scrollWidth. Called by the repeater based on
356 * its item count and item size.
357 * @param {number} itemsSize The total size of the items.
358 */
359VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) {
360 var size = itemsSize + this.offsetSize;
361 if (this.scrollSize === size) return;
362
363 this.sizeScroller_(size);
364 this.autoShrink_(size);
365 this.scrollSize = size;
366};
367
368
369/** @return {number} The container's current scroll offset. */
370VirtualRepeatContainerController.prototype.getScrollOffset = function() {
371 return this.scrollOffset;
372};
373
374/**
375 * Scrolls to a given scrollTop position.
376 * @param {number} position
377 */
378VirtualRepeatContainerController.prototype.scrollTo = function(position) {
379 this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position;
380 this.handleScroll_();
381};
382
383/**
384 * Scrolls the item with the given index to the top of the scroll container.
385 * @param {number} index
386 */
387VirtualRepeatContainerController.prototype.scrollToIndex = function(index) {
388 var itemSize = this.repeater.getItemSize();
389 var itemsLength = this.repeater.itemsLength;
390 if (index > itemsLength) {
391 index = itemsLength - 1;
392 }
393 this.scrollTo(itemSize * index);
394};
395
396VirtualRepeatContainerController.prototype.resetScroll = function() {
397 this.scrollTo(0);
398};
399
400
401VirtualRepeatContainerController.prototype.handleScroll_ = function() {
402 if (!this.ltr && !this.maxSize) {
403 this.scroller.scrollLeft = this.scrollSize;
404 this.maxSize = this.scroller.scrollLeft;
405 }
406 var offset = this.isHorizontal() ?
407 (this.ltr ? this.scroller.scrollLeft : this.maxSize - this.scroller.scrollLeft)
408 : this.scroller.scrollTop;
409 if (this.scrollSize < this.size) return;
410 if (offset > this.scrollSize - this.size) {
411 offset = this.scrollSize - this.size;
412 }
413 if (offset === this.scrollOffset) return;
414
415 var itemSize = this.repeater.getItemSize();
416 if (!itemSize) return;
417
418 var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA);
419
420 var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') +
421 (!this.isHorizontal() || this.ltr ? (numItems * itemSize) : - (numItems * itemSize)) + 'px)';
422
423 this.scrollOffset = offset;
424 this.offsetter.style.webkitTransform = transform;
425 this.offsetter.style.transform = transform;
426
427 if (this.bindTopIndex) {
428 var topIndex = Math.floor(offset / itemSize);
429 if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) {
430 this.topIndex = topIndex;
431 this.bindTopIndex.assign(this.$scope, topIndex);
432 if (!this.$rootScope.$$phase) this.$scope.$digest();
433 }
434 }
435
436 this.repeater.containerUpdated();
437};
438
439
440/**
441 * @ngdoc directive
442 * @name mdVirtualRepeat
443 * @module material.components.virtualRepeat
444 * @restrict A
445 * @priority 1000
446 * @description
447 * The `md-virtual-repeat` attribute is applied to a template that is repeated using virtual
448 * scrolling. This provides smooth and performant scrolling through very large lists of elements.
449 *
450 * Virtual repeat is a limited substitute for `ng-repeat` that renders only
451 * enough DOM nodes to fill the container, recycling them as the user scrolls.
452 *
453 * ### Notes
454 *
455 * - Arrays are supported for iteration by default.
456 * - An object can used use if `md-on-demand` is specified and the object implements the interface
457 * described in the `md-on-demand` <a ng-href="#attributes">documentation</a>.
458 * - `trackBy`, `as` alias, and `(key, value)` syntax from `ng-repeat` are not supported.
459 *
460 * ### On-Demand Async Item Loading
461 *
462 * When using the `md-on-demand` attribute and loading some asynchronous data,
463 * the `getItemAtIndex` function will mostly return nothing.
464 *
465 * <hljs lang="js">
466 * DynamicItems.prototype.getItemAtIndex = function(index) {
467 * if (this.pages[index]) {
468 * return this.pages[index];
469 * } else {
470 * // This is an asynchronous action and does not return any value.
471 * this.loadPage(index);
472 * }
473 * };
474 * </hljs>
475 *
476 * This means that the Virtual Repeat will not have any value for the given index.<br/>
477 * After the data loading completes, the user expects the Virtual Repeat to recognize the change.
478 *
479 * To make sure that the Virtual Repeat properly detects any change, you need to run the operation
480 * in another digest.
481 *
482 * <hljs lang="js">
483 * DynamicItems.prototype.loadPage = function(index) {
484 * var self = this;
485 *
486 * // Trigger a new digest by using $timeout
487 * $timeout(function() {
488 * self.pages[index] = Data;
489 * });
490 * };
491 * </hljs>
492 *
493 * > <b>Note:</b> Please also review the
494 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation
495 * for more information.
496 *
497 * @usage
498 * <hljs lang="html">
499 * <md-virtual-repeat-container>
500 * <div md-virtual-repeat="i in items">Hello {{i}}!</div>
501 * </md-virtual-repeat-container>
502 *
503 * <md-virtual-repeat-container md-orient-horizontal>
504 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
505 * </md-virtual-repeat-container>
506 * </hljs>
507 *
508 * @param {expression=} md-extra-name Evaluates to an additional name to which the current iterated
509 * item can be assigned on the repeated scope (needed for use in `md-autocomplete`).
510 * @param {number=} md-item-size Optional height or width of the repeated elements (which **must be
511 * identical for each element**). Virtual repeat will attempt to read the size from the DOM,
512 * if missing, but it still assumes that all repeated nodes have the **same height**
513 * (when scrolling vertically) or **same width** (when scrolling horizontally).
514 * @param {boolean=} md-on-demand When present, treats the `md-virtual-repeat` argument as an object
515 * that can fetch rows rather than an array.
516 *
517 * **NOTE:** This object **must** implement the following interface with two methods:
518 *
519 * - `getItemAtIndex` - `{function(index): Object}`: The item at that `index` or `null` if it is
520 * not yet loaded (it should start downloading the item in that case).
521 * - `getLength` - `{function(): number}`: The data length to which the repeater container
522 * should be sized. Ideally, when the count is known, this method should return it.
523 * Otherwise, return a higher number than the currently loaded items to produce an
524 * infinite-scroll behavior.
525 */
526function VirtualRepeatDirective($parse) {
527 return {
528 controller: VirtualRepeatController,
529 priority: 1000,
530 require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'],
531 restrict: 'A',
532 terminal: true,
533 transclude: 'element',
534 compile: function VirtualRepeatCompile($element, $attrs) {
535 var expression = $attrs.mdVirtualRepeat;
536 var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);
537 var repeatName = match[1];
538 var repeatListExpression = $parse(match[2]);
539 var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName);
540
541 return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) {
542 ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName);
543 };
544 }
545 };
546}
547
548
549/** ngInject */
550function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope,
551 $$rAF, $mdUtil) {
552 this.$scope = $scope;
553 this.$element = $element;
554 this.$attrs = $attrs;
555 this.$browser = $browser;
556 this.$document = $document;
557 this.$mdUtil = $mdUtil;
558 this.$rootScope = $rootScope;
559 this.$$rAF = $$rAF;
560
561 /** @type {boolean} Whether we are in on-demand mode. */
562 this.onDemand = $mdUtil.parseAttributeBoolean($attrs.mdOnDemand);
563 /** @type {!Function} Backup reference to $browser.$$checkUrlChange */
564 this.browserCheckUrlChange = $browser.$$checkUrlChange;
565 /** @type {number} Most recent starting repeat index (based on scroll offset) */
566 this.newStartIndex = 0;
567 /** @type {number} Most recent ending repeat index (based on scroll offset) */
568 this.newEndIndex = 0;
569 /** @type {number} Most recent end visible index (based on scroll offset) */
570 this.newVisibleEnd = 0;
571 /** @type {number} Previous starting repeat index (based on scroll offset) */
572 this.startIndex = 0;
573 /** @type {number} Previous ending repeat index (based on scroll offset) */
574 this.endIndex = 0;
575 /** @type {?number} Height/width of repeated elements. */
576 this.itemSize = $scope.$eval($attrs.mdItemSize) || null;
577
578 /** @type {boolean} Whether this is the first time that items are rendered. */
579 this.isFirstRender = true;
580
581 /**
582 * @private {boolean} Whether the items in the list are already being updated. Used to prevent
583 * nested calls to virtualRepeatUpdate_.
584 */
585 this.isVirtualRepeatUpdating_ = false;
586
587 /** @type {number} Most recently seen length of items. */
588 this.itemsLength = 0;
589
590 /**
591 * @type {!Function} Unwatch callback for item size (when md-items-size is
592 * not specified), or angular.noop otherwise.
593 */
594 this.unwatchItemSize_ = angular.noop;
595
596 /**
597 * Presently rendered blocks by repeat index.
598 * @type {Object<number, !VirtualRepeatController.Block>}
599 */
600 this.blocks = {};
601 /** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */
602 this.pooledBlocks = [];
603
604 $scope.$on('$destroy', angular.bind(this, this.cleanupBlocks_));
605}
606
607
608/**
609 * An object representing a repeated item.
610 * @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}}
611 */
612VirtualRepeatController.Block;
613
614
615/**
616 * Called at startup by the md-virtual-repeat postLink function.
617 * @param {!VirtualRepeatContainerController} container The container's controller.
618 * @param {!Function} transclude The repeated element's bound transclude function.
619 * @param {string} repeatName The left hand side of the repeat expression, indicating
620 * the name for each item in the array.
621 * @param {!Function} repeatListExpression A compiled expression based on the right hand side
622 * of the repeat expression. Points to the array to repeat over.
623 * @param {string|undefined} extraName The optional extra repeatName.
624 */
625VirtualRepeatController.prototype.link_ =
626 function(container, transclude, repeatName, repeatListExpression, extraName) {
627 this.container = container;
628 this.transclude = transclude;
629 this.repeatName = repeatName;
630 this.rawRepeatListExpression = repeatListExpression;
631 this.extraName = extraName;
632 this.sized = false;
633
634 this.repeatListExpression = angular.bind(this, this.repeatListExpression_);
635
636 this.container.register(this);
637};
638
639
640/** @private Cleans up unused blocks. */
641VirtualRepeatController.prototype.cleanupBlocks_ = function() {
642 angular.forEach(this.pooledBlocks, function cleanupBlock(block) {
643 block.element.remove();
644 });
645};
646
647
648/** @private Attempts to set itemSize by measuring a repeated element in the dom */
649VirtualRepeatController.prototype.readItemSize_ = function() {
650 if (this.itemSize) {
651 // itemSize was successfully read in a different asynchronous call.
652 return;
653 }
654
655 this.items = this.repeatListExpression(this.$scope);
656 this.parentNode = this.$element[0].parentNode;
657 var block = this.getBlock_(0);
658 if (!block.element[0].parentNode) {
659 this.parentNode.appendChild(block.element[0]);
660 }
661
662 this.itemSize = block.element[0][
663 this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null;
664
665 this.blocks[0] = block;
666 this.poolBlock_(0);
667
668 if (this.itemSize) {
669 this.containerUpdated();
670 }
671};
672
673
674/**
675 * Returns the user-specified repeat list, transforming it into an array-like
676 * object in the case of infinite scroll/dynamic load mode.
677 * @param {!angular.Scope} scope The scope.
678 * @return {!Array|!Object} An array or array-like object for iteration.
679 */
680VirtualRepeatController.prototype.repeatListExpression_ = function(scope) {
681 var repeatList = this.rawRepeatListExpression(scope);
682
683 if (this.onDemand && repeatList) {
684 var virtualList = new VirtualRepeatModelArrayLike(repeatList);
685 virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd);
686 return virtualList;
687 } else {
688 return repeatList;
689 }
690};
691
692
693/**
694 * Called by the container. Informs us that the container's scroll or size has
695 * changed.
696 */
697VirtualRepeatController.prototype.containerUpdated = function() {
698 // If itemSize is unknown, attempt to measure it.
699 if (!this.itemSize) {
700 // Make sure to clean up watchers if we can (see #8178)
701 if (this.unwatchItemSize_ && this.unwatchItemSize_ !== angular.noop){
702 this.unwatchItemSize_();
703 }
704 this.unwatchItemSize_ = this.$scope.$watchCollection(
705 this.repeatListExpression,
706 angular.bind(this, function(items) {
707 if (items && items.length) {
708 this.readItemSize_();
709 }
710 }));
711 if (!this.$rootScope.$$phase) this.$scope.$digest();
712
713 return;
714 } else if (!this.sized) {
715 this.items = this.repeatListExpression(this.$scope);
716 }
717
718 if (!this.sized) {
719 this.unwatchItemSize_();
720 this.sized = true;
721 this.$scope.$watchCollection(this.repeatListExpression,
722 angular.bind(this, function(items, oldItems) {
723 if (!this.isVirtualRepeatUpdating_) {
724 this.virtualRepeatUpdate_(items, oldItems);
725 }
726 }));
727 }
728
729 this.updateIndexes_();
730
731 if (this.newStartIndex !== this.startIndex ||
732 this.newEndIndex !== this.endIndex ||
733 this.container.getScrollOffset() > this.container.getScrollSize()) {
734 if (this.items instanceof VirtualRepeatModelArrayLike) {
735 this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex);
736 }
737 this.virtualRepeatUpdate_(this.items, this.items);
738 }
739};
740
741
742/**
743 * Called by the container. Returns the size of a single repeated item.
744 * @return {?number} size of a repeated item.
745 */
746VirtualRepeatController.prototype.getItemSize = function() {
747 return this.itemSize;
748};
749
750
751/**
752 * Called by the container.
753 * @return {?number} the most recently seen length of items.
754 */
755VirtualRepeatController.prototype.getItemCount = function() {
756 return this.itemsLength;
757};
758
759
760/**
761 * Updates the order and visible offset of repeated blocks in response to scrolling
762 * or updates to `items`.
763 * @param {Array} items visible elements
764 * @param {Array} oldItems previously visible elements
765 * @private
766 */
767VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) {
768 this.isVirtualRepeatUpdating_ = true;
769
770 var itemsLength = items && items.length || 0;
771 var lengthChanged = false;
772
773 // If the number of items shrank, keep the scroll position.
774 if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) {
775 this.items = items;
776 var previousScrollOffset = this.container.getScrollOffset();
777 this.container.resetScroll();
778 this.container.scrollTo(previousScrollOffset);
779 }
780
781 if (itemsLength !== this.itemsLength) {
782 lengthChanged = true;
783 this.itemsLength = itemsLength;
784 }
785
786 this.items = items;
787 if (items !== oldItems || lengthChanged) {
788 this.updateIndexes_();
789 }
790
791 this.parentNode = this.$element[0].parentNode;
792
793 if (lengthChanged) {
794 this.container.setScrollSize(itemsLength * this.itemSize);
795 }
796
797 // Detach and pool any blocks that are no longer in the viewport.
798 Object.keys(this.blocks).forEach(function(blockIndex) {
799 var index = parseInt(blockIndex, 10);
800 if (index < this.newStartIndex || index >= this.newEndIndex) {
801 this.poolBlock_(index);
802 }
803 }, this);
804
805 // Add needed blocks.
806 // For performance reasons, temporarily block browser url checks as we digest
807 // the restored block scopes ($$checkUrlChange reads window.location to
808 // check for changes and trigger route change, etc, which we don't need when
809 // trying to scroll at 60fps).
810 this.$browser.$$checkUrlChange = angular.noop;
811
812 var i, block,
813 newStartBlocks = [],
814 newEndBlocks = [];
815
816 // Collect blocks at the top.
817 for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) {
818 block = this.getBlock_(i);
819 this.updateBlock_(block, i);
820 newStartBlocks.push(block);
821 }
822
823 // Update blocks that are already rendered.
824 for (; this.blocks[i] != null; i++) {
825 this.updateBlock_(this.blocks[i], i);
826 }
827 var maxIndex = i - 1;
828
829 // Collect blocks at the end.
830 for (; i < this.newEndIndex; i++) {
831 block = this.getBlock_(i);
832 this.updateBlock_(block, i);
833 newEndBlocks.push(block);
834 }
835
836 // Attach collected blocks to the document.
837 if (newStartBlocks.length) {
838 this.parentNode.insertBefore(
839 this.domFragmentFromBlocks_(newStartBlocks),
840 this.$element[0].nextSibling);
841 }
842 if (newEndBlocks.length) {
843 this.parentNode.insertBefore(
844 this.domFragmentFromBlocks_(newEndBlocks),
845 this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling);
846 }
847
848 // Restore $$checkUrlChange.
849 this.$browser.$$checkUrlChange = this.browserCheckUrlChange;
850
851 this.startIndex = this.newStartIndex;
852 this.endIndex = this.newEndIndex;
853
854 if (this.isFirstRender) {
855 this.isFirstRender = false;
856 var firstRenderStartIndex = this.$attrs.mdStartIndex ?
857 this.$scope.$eval(this.$attrs.mdStartIndex) :
858 this.container.topIndex;
859
860 // The first call to virtualRepeatUpdate_ may not be when the virtual repeater is ready.
861 // Introduce a slight delay so that the update happens when it is actually ready.
862 this.$mdUtil.nextTick(function() {
863 this.container.scrollToIndex(firstRenderStartIndex);
864 }.bind(this));
865 }
866
867 this.isVirtualRepeatUpdating_ = false;
868};
869
870
871/**
872 * @param {number} index Where the block is to be in the repeated list.
873 * @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index.
874 * @private
875 */
876VirtualRepeatController.prototype.getBlock_ = function(index) {
877 if (this.pooledBlocks.length) {
878 return this.pooledBlocks.pop();
879 }
880
881 var block;
882 this.transclude(angular.bind(this, function(clone, scope) {
883 block = {
884 element: clone,
885 new: true,
886 scope: scope
887 };
888
889 this.updateScope_(scope, index);
890 this.parentNode.appendChild(clone[0]);
891 }));
892
893 return block;
894};
895
896
897/**
898 * Updates and if not in a digest cycle, digests the specified block's scope to the data
899 * at the specified index.
900 * @param {!VirtualRepeatController.Block} block The block whose scope should be updated.
901 * @param {number} index The index to set.
902 * @private
903 */
904VirtualRepeatController.prototype.updateBlock_ = function(block, index) {
905 this.blocks[index] = block;
906
907 if (!block.new &&
908 (block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) {
909 return;
910 }
911 block.new = false;
912
913 // Update and digest the block's scope.
914 this.updateScope_(block.scope, index);
915
916 // Perform digest before reattaching the block.
917 // Any resulting synchronous DOM mutations should be much faster as a result.
918 // This might break some directives.
919 if (!this.$rootScope.$$phase) {
920 block.scope.$digest();
921 }
922};
923
924
925/**
926 * Updates scope to the data at the specified index.
927 * @param {!angular.Scope} scope The scope which should be updated.
928 * @param {number} index The index to set.
929 * @private
930 */
931VirtualRepeatController.prototype.updateScope_ = function(scope, index) {
932 scope.$index = index;
933 scope[this.repeatName] = this.items && this.items[index];
934 if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index];
935};
936
937
938/**
939 * Pools the block at the specified index (Pulls its element out of the dom and stores it).
940 * @param {number} index The index at which the block to pool is stored.
941 * @private
942 */
943VirtualRepeatController.prototype.poolBlock_ = function(index) {
944 this.pooledBlocks.push(this.blocks[index]);
945 this.parentNode.removeChild(this.blocks[index].element[0]);
946 delete this.blocks[index];
947};
948
949
950/**
951 * Produces a dom fragment containing the elements from the list of blocks.
952 * @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements
953 * should be added to the document fragment.
954 * @return {DocumentFragment}
955 * @private
956 */
957VirtualRepeatController.prototype.domFragmentFromBlocks_ = function(blocks) {
958 var fragment = this.$document[0].createDocumentFragment();
959 blocks.forEach(function(block) {
960 fragment.appendChild(block.element[0]);
961 });
962 return fragment;
963};
964
965
966/**
967 * Updates start and end indexes based on length of repeated items and container size.
968 * @private
969 */
970VirtualRepeatController.prototype.updateIndexes_ = function() {
971 var itemsLength = this.items ? this.items.length : 0;
972 var containerLength = Math.ceil(this.container.getSize() / this.itemSize);
973
974 this.newStartIndex = Math.max(0, Math.min(
975 itemsLength - containerLength,
976 Math.floor(this.container.getScrollOffset() / this.itemSize)));
977 this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA;
978 this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd);
979 this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA);
980};
981
982/**
983 * This VirtualRepeatModelArrayLike class enforces the interface requirements
984 * for infinite scrolling within a mdVirtualRepeatContainer.
985 *
986 * @param {Object} model An object with this interface must implement the following interface with
987 * two (2) methods:
988 *
989 * getItemAtIndex: function(index) -> item at that index or null if it is not yet
990 * loaded (It should start downloading the item in that case).
991 *
992 * getLength: function() -> number The data length to which the repeater container
993 * should be sized. Ideally, when the count is known, this method should return it.
994 * Otherwise, return a higher number than the currently loaded items to produce an
995 * infinite-scroll behavior.
996 *
997 * @usage
998 * <hljs lang="html">
999 * <md-virtual-repeat-container md-orient-horizontal>
1000 * <div md-virtual-repeat="i in items" md-on-demand>
1001 * Hello {{i}}!
1002 * </div>
1003 * </md-virtual-repeat-container>
1004 * </hljs>
1005 *
1006 */
1007function VirtualRepeatModelArrayLike(model) {
1008 if (!angular.isFunction(model.getItemAtIndex) ||
1009 !angular.isFunction(model.getLength)) {
1010 throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must ' +
1011 'implement functions getItemAtIndex() and getLength().');
1012 }
1013
1014 this.model = model;
1015}
1016
1017/**
1018 * @param {number} start
1019 * @param {number} end
1020 */
1021VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) {
1022 for (var i = start; i < end; i++) {
1023 if (!this.hasOwnProperty(i)) {
1024 this[i] = this.model.getItemAtIndex(i);
1025 }
1026 }
1027 this.length = this.model.getLength();
1028};
1029
1030/**
1031 * @ngdoc directive
1032 * @name mdForceHeight
1033 * @module material.components.virtualRepeat
1034 * @restrict A
1035 * @description
1036 *
1037 * Force an element to have a certain `px` height. This is used in place of a style tag in order to
1038 * conform to the
1039 * <a href="https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/script-src">
1040 * Content Security Policy</a> regarding `unsafe-inline` `<style>` tags.
1041 *
1042 * This directive is related to <a ng-href="api/directive/mdVirtualRepeat">mdVirtualRepeat</a>.
1043 *
1044 * @usage
1045 * <hljs lang="html">
1046 * <div md-force-height="'100px'"></div>
1047 * </hljs>
1048 */
1049function ForceHeightDirective($mdUtil) {
1050 return {
1051 restrict: 'A',
1052 link: function(scope, element, attrs) {
1053 var height = scope.$eval(attrs.mdForceHeight) || null;
1054
1055 if (height && element) {
1056 element[0].style.height = height;
1057 }
1058 }
1059 };
1060}
1061ForceHeightDirective['$inject'] = ['$mdUtil'];
1062
1063})(window, window.angular);
Note: See TracBrowser for help on using the repository browser.