diff --git a/src/components/virtualRepeat/virtualRepeater.js b/src/components/virtualRepeat/virtualRepeater.js index 006fbd12014..c058773179e 100644 --- a/src/components/virtualRepeat/virtualRepeater.js +++ b/src/components/virtualRepeat/virtualRepeater.js @@ -48,7 +48,7 @@ function VirtualRepeatContainerDirective() { } -function virtualRepeatContainerTemplate($element, $attrs) { +function virtualRepeatContainerTemplate($element) { return '
' + '
' + '
' + @@ -56,6 +56,22 @@ function virtualRepeatContainerTemplate($element, $attrs) { '
'; } +/** + * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies + * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could + * *compute* this value, but Firefox always reports an element to have a size of zero if it + * goes over the max, meaning that we'd have to binary search for the value. + * @const {number} + */ +var MAX_ELEMENT_SIZE = 1533917; + +/** + * Number of additional elements to render above and below the visible area inside + * of the virtual repeat container. A higher number results in less flicker when scrolling + * very quickly in Safari, but comes with a higher rendering and dirty-checking cost. + * @const {number} + */ +var NUM_EXTRA = 3; /** @ngInject */ function VirtualRepeatContainerController($$rAF, $scope, $element, $attrs) { @@ -131,11 +147,40 @@ VirtualRepeatContainerController.prototype.getScrollSize = function() { /** * Sets the scrollHeight or scrollWidth. Called by the repeater based on * its item count and item size. - * @param {number} The new size. + * @param {number} size The new size. */ VirtualRepeatContainerController.prototype.setScrollSize = function(size) { if (this.scrollSize !== size) { - this.sizer.style[this.isHorizontal() ? 'width' : 'height'] = size + 'px'; + var dimension = this.isHorizontal() ? 'width' : 'height'; + var crossDimension = this.isHorizontal() ? 'height' : 'width'; + + // If the size falls within the browser's maximum explicit size for a single element, we can + // set the size and be done. Otherwise, we have to create children that add up the the desired + // size. + if (size < MAX_ELEMENT_SIZE) { + this.sizer.style[dimension] = size + 'px'; + } else { + // Clear any existing dimensions. + this.sizer.innerHTML = ''; + this.sizer.style[dimension] = 'auto'; + this.sizer.style[crossDimension] = 'auto'; + + // Divide the total size we have to render into N max-size pieces. + var numChildren = Math.floor(size / MAX_ELEMENT_SIZE); + + // Element template to clone for each max-size piece. + var sizerChild = document.createElement('div'); + sizerChild.style[dimension] = MAX_ELEMENT_SIZE + 'px'; + sizerChild.style[crossDimension] = '1px'; + + for (var i = 0; i < numChildren; i++) { + this.sizer.appendChild(sizerChild.cloneNode(false)); + } + + // Re-use the element template for the remainder. + sizerChild.style[dimension] = (size - (numChildren * MAX_ELEMENT_SIZE)) + 'px'; + this.sizer.appendChild(sizerChild); + } } this.scrollSize = size; @@ -159,7 +204,7 @@ VirtualRepeatContainerController.prototype.handleScroll_ = function() { if (offset === this.scrollOffset) return; var itemSize = this.repeater.getItemSize(); - var numItems = Math.max(0, Math.floor(offset / itemSize) - VirtualRepeatController.NUM_EXTRA); + var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA); var transform = this.isHorizontal() ? 'translateX(' : 'translateY('; transform += (numItems * itemSize) + 'px)'; @@ -265,15 +310,6 @@ function VirtualRepeatController($scope, $element, $attrs, $browser, $document) VirtualRepeatController.Block; -/** - * Number of additional elements to render above and below the visible area inside - * of the virtual repeat container. A higher number results in less flicker when scrolling - * very quickly in Safari, but comes with a higher rendering and dirty-checking cost. - * @const {number} - */ -VirtualRepeatController.NUM_EXTRA = 3; - - /** * Called at startup by the md-virtual-repeat postLink function. * @param {!VirtualRepeatContainerController} container The container's controller. @@ -516,7 +552,6 @@ VirtualRepeatController.prototype.updateIndexes_ = function() { this.newStartIndex = Math.max(0, Math.min( itemsLength - containerLength, Math.floor(this.container.getScrollOffset() / this.itemSize))); - this.newEndIndex = Math.min(itemsLength, this.newStartIndex + containerLength + - VirtualRepeatController.NUM_EXTRA); - this.newStartIndex = Math.max(0, this.newStartIndex - VirtualRepeatController.NUM_EXTRA); + this.newEndIndex = Math.min(itemsLength, this.newStartIndex + containerLength + NUM_EXTRA); + this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA); }; diff --git a/src/components/virtualRepeat/virtualRepeater.spec.js b/src/components/virtualRepeat/virtualRepeater.spec.js index a4ca603e871..e6614d09e29 100644 --- a/src/components/virtualRepeat/virtualRepeater.spec.js +++ b/src/components/virtualRepeat/virtualRepeater.spec.js @@ -181,8 +181,6 @@ describe('', function() { expect(repeated[0].textContent.trim()).toBe('s184s 92'); }); - - it('should dirty-check only the swapped scope on scroll', function() { createRepeater(); scope.items = createItems(NUM_ITEMS); @@ -227,6 +225,30 @@ describe('', function() { expect(getRepeated()[0].textContent.trim()).toBe('a 0'); }); + it('should cap individual element size for the sizer in large item sets', function() { + // Copy max element size because we don't have a good way to reference it. + var maxElementSize = 1533917; + + // Create a much larger number of items than will fit in one maximum element size. + var numItems = 2000000; + createRepeater(); + scope.items = createItems(numItems); + scope.$apply(); + $$rAF.flush(); + + // Expect that the sizer as a whole is still exactly the height it should be. + expect(sizer[0].offsetHeight).toBe(numItems * ITEM_SIZE); + + // Expect that sizer only adds as many children as it needs to. + var numChildren = sizer[0].childNodes.length; + expect(numChildren).toBe(Math.ceil(numItems * ITEM_SIZE / maxElementSize)); + + // Expect that every child of sizer does not exceed the maximum element size. + for (var i = 0; i < numChildren; i++) { + expect(sizer[0].childNodes[i].offsetHeight).toBeLessThan(maxElementSize + 1); + } + }); + /** * Facade to access transform properly even when jQuery is used; * since jQuery's css function is obtaining the computed style (not wanted)