Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
feat(virtualRepeat): allow for scrolling areas larger than the browser's
Browse files Browse the repository at this point in the history
maximum element size.
  • Loading branch information
jelbourn committed Jun 25, 2015
1 parent a0fd73e commit 98e91ae
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 18 deletions.
67 changes: 51 additions & 16 deletions src/components/virtualRepeat/virtualRepeater.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,30 @@ function VirtualRepeatContainerDirective() {
}


function virtualRepeatContainerTemplate($element, $attrs) {
function virtualRepeatContainerTemplate($element) {
return '<div class="md-virtual-repeat-scroller">' +
'<div class="md-virtual-repeat-sizer"></div>' +
'<div class="md-virtual-repeat-offsetter">' +
$element[0].innerHTML +
'</div></div>';
}

/**
* 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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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)';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
};
26 changes: 24 additions & 2 deletions src/components/virtualRepeat/virtualRepeater.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,6 @@ describe('<md-virtual-repeat>', 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);
Expand Down Expand Up @@ -227,6 +225,30 @@ describe('<md-virtual-repeat>', 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)
Expand Down

0 comments on commit 98e91ae

Please sign in to comment.