@logo-elements/component-base
Version:
A set of mixins used by Logo Elements which is extended from Vaadin components.
952 lines (865 loc) • 30.1 kB
JavaScript
/**
* @license
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
import { animationFrame, idlePeriod, microTask } from './async.js';
import { Debouncer, enqueueDebouncer, flush } from './debounce.js';
const IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
const IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
const DEFAULT_PHYSICAL_COUNT = 3;
/**
* @private
*/
export const ironList = {
/**
* The ratio of hidden tiles that should remain in the scroll direction.
* Recommended value ~0.5, so it will distribute tiles evenly in both
* directions.
*/
_ratio: 0.5,
/**
* The padding-top value for the list.
*/
_scrollerPaddingTop: 0,
/**
* This value is a cached value of `scrollTop` from the last `scroll` event.
*/
_scrollPosition: 0,
/**
* The sum of the heights of all the tiles in the DOM.
*/
_physicalSize: 0,
/**
* The average `offsetHeight` of the tiles observed till now.
*/
_physicalAverage: 0,
/**
* The number of tiles which `offsetHeight` > 0 observed until now.
*/
_physicalAverageCount: 0,
/**
* The Y position of the item rendered in the `_physicalStart`
* tile relative to the scrolling list.
*/
_physicalTop: 0,
/**
* The number of items in the list.
*/
_virtualCount: 0,
/**
* The estimated scroll height based on `_physicalAverage`
*/
_estScrollHeight: 0,
/**
* The scroll height of the dom node
*/
_scrollHeight: 0,
/**
* The height of the list. This is referred as the viewport in the context of
* list.
*/
_viewportHeight: 0,
/**
* The width of the list. This is referred as the viewport in the context of
* list.
*/
_viewportWidth: 0,
/**
* An array of DOM nodes that are currently in the tree
* @type {?Array<!HTMLElement>}
*/
_physicalItems: null,
/**
* An array of heights for each item in `_physicalItems`
* @type {?Array<number>}
*/
_physicalSizes: null,
/**
* A cached value for the first visible index.
* See `firstVisibleIndex`
* @type {?number}
*/
_firstVisibleIndexVal: null,
/**
* A cached value for the last visible index.
* See `lastVisibleIndex`
* @type {?number}
*/
_lastVisibleIndexVal: null,
/**
* The max number of pages to render. One page is equivalent to the height of
* the list.
*/
_maxPages: 2,
/**
* The maximum items per row
*/
_itemsPerRow: 1,
/**
* The width of each grid item
*/
_itemWidth: 0,
/**
* The height of the row in grid layout.
*/
_rowHeight: 0,
/**
* The cost of stamping a template in ms.
*/
_templateCost: 0,
/**
* Needed to pass event.model property to declarative event handlers -
* see polymer/polymer#4339.
*/
_parentModel: true,
/**
* The bottom of the physical content.
*/
get _physicalBottom() {
return this._physicalTop + this._physicalSize;
},
/**
* The bottom of the scroll.
*/
get _scrollBottom() {
return this._scrollPosition + this._viewportHeight;
},
/**
* The n-th item rendered in the last physical item.
*/
get _virtualEnd() {
return this._virtualStart + this._physicalCount - 1;
},
/**
* The height of the physical content that isn't on the screen.
*/
get _hiddenContentSize() {
var size = this.index ? this._physicalRows * this._rowHeight : this._physicalSize;
return size - this._viewportHeight;
},
/**
* The maximum scroll top value.
*/
get _maxScrollTop() {
return this._estScrollHeight - this._viewportHeight + this._scrollOffset;
},
/**
* The largest n-th value for an item such that it can be rendered in
* `_physicalStart`.
*/
get _maxVirtualStart() {
var virtualCount = this._convertIndexToCompleteRow(this._virtualCount);
return Math.max(0, virtualCount - this._physicalCount);
},
set _virtualStart(val) {
val = this._clamp(val, 0, this._maxVirtualStart);
if (this.index) {
val = val - (val % this._itemsPerRow);
}
this._virtualStartVal = val;
},
get _virtualStart() {
return this._virtualStartVal || 0;
},
/**
* The k-th tile that is at the top of the scrolling list.
*/
set _physicalStart(val) {
val = val % this._physicalCount;
if (val < 0) {
val = this._physicalCount + val;
}
if (this.index) {
val = val - (val % this._itemsPerRow);
}
this._physicalStartVal = val;
},
get _physicalStart() {
return this._physicalStartVal || 0;
},
/**
* The k-th tile that is at the bottom of the scrolling list.
*/
get _physicalEnd() {
return (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
},
set _physicalCount(val) {
this._physicalCountVal = val;
},
get _physicalCount() {
return this._physicalCountVal || 0;
},
/**
* An optimal physical size such that we will have enough physical items
* to fill up the viewport and recycle when the user scrolls.
*
* This default value assumes that we will at least have the equivalent
* to a viewport of physical items above and below the user's viewport.
*/
get _optPhysicalSize() {
return this._viewportHeight === 0 ? Infinity : this._viewportHeight * this._maxPages;
},
/**
* True if the current list is visible.
*/
get _isVisible() {
return Boolean(this.offsetWidth || this.offsetHeight);
},
/**
* Gets the index of the first visible item in the viewport.
*
* @type {number}
*/
get firstVisibleIndex() {
var idx = this._firstVisibleIndexVal;
if (idx == null) {
var physicalOffset = this._physicalTop + this._scrollOffset;
idx =
this._iterateItems(function (pidx, vidx) {
physicalOffset += this._getPhysicalSizeIncrement(pidx);
if (physicalOffset > this._scrollPosition) {
return this.index ? vidx - (vidx % this._itemsPerRow) : vidx;
}
// Handle a partially rendered final row in grid mode
if (this.index && this._virtualCount - 1 === vidx) {
return vidx - (vidx % this._itemsPerRow);
}
}) || 0;
this._firstVisibleIndexVal = idx;
}
return idx;
},
/**
* Gets the index of the last visible item in the viewport.
*
* @type {number}
*/
get lastVisibleIndex() {
var idx = this._lastVisibleIndexVal;
if (idx == null) {
if (this.index) {
idx = Math.min(this._virtualCount, this.firstVisibleIndex + this._estRowsInView * this._itemsPerRow - 1);
} else {
var physicalOffset = this._physicalTop + this._scrollOffset;
this._iterateItems(function (pidx, vidx) {
if (physicalOffset < this._scrollBottom) {
idx = vidx;
}
physicalOffset += this._getPhysicalSizeIncrement(pidx);
});
}
this._lastVisibleIndexVal = idx;
}
return idx;
},
get _defaultScrollTarget() {
return this;
},
get _virtualRowCount() {
return Math.ceil(this._virtualCount / this._itemsPerRow);
},
get _estRowsInView() {
return Math.ceil(this._viewportHeight / this._rowHeight);
},
get _physicalRows() {
return Math.ceil(this._physicalCount / this._itemsPerRow);
},
get _scrollOffset() {
return this._scrollerPaddingTop + this.scrollOffset;
},
/**
* Recycles the physical items when needed.
*/
_scrollHandler: function () {
var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop));
var delta = scrollTop - this._scrollPosition;
var isScrollingDown = delta >= 0;
// Track the current scroll position.
this._scrollPosition = scrollTop;
// Clear indexes for first and last visible indexes.
this._firstVisibleIndexVal = null;
this._lastVisibleIndexVal = null;
// Random access.
if (Math.abs(delta) > this._physicalSize && this._physicalSize > 0) {
delta = delta - this._scrollOffset;
var idxAdjustment = Math.round(delta / this._physicalAverage) * this._itemsPerRow;
this._virtualStart = this._virtualStart + idxAdjustment;
this._physicalStart = this._physicalStart + idxAdjustment;
// Estimate new physical offset based on the virtual start index.
// adjusts the physical start position to stay in sync with the clamped
// virtual start index. It's critical not to let this value be
// more than the scroll position however, since that would result in
// the physical items not covering the viewport, and leading to
// _increasePoolIfNeeded to run away creating items to try to fill it.
this._physicalTop = Math.min(
Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage,
this._scrollPosition
);
this._update();
} else if (this._physicalCount > 0) {
var reusables = this._getReusables(isScrollingDown);
if (isScrollingDown) {
this._physicalTop = reusables.physicalTop;
this._virtualStart = this._virtualStart + reusables.indexes.length;
this._physicalStart = this._physicalStart + reusables.indexes.length;
} else {
this._virtualStart = this._virtualStart - reusables.indexes.length;
this._physicalStart = this._physicalStart - reusables.indexes.length;
}
this._update(reusables.indexes, isScrollingDown ? null : reusables.indexes);
this._debounce('_increasePoolIfNeeded', this._increasePoolIfNeeded.bind(this, 0), microTask);
}
},
/**
* Returns an object that contains the indexes of the physical items
* that might be reused and the physicalTop.
*
* @param {boolean} fromTop If the potential reusable items are above the scrolling region.
*/
_getReusables: function (fromTop) {
var ith, lastIth, offsetContent, physicalItemHeight;
var idxs = [];
var protectedOffsetContent = this._hiddenContentSize * this._ratio;
var virtualStart = this._virtualStart;
var virtualEnd = this._virtualEnd;
var physicalCount = this._physicalCount;
var top = this._physicalTop + this._scrollOffset;
var bottom = this._physicalBottom + this._scrollOffset;
// This may be called outside of a scrollHandler, so use last cached position
var scrollTop = this._scrollPosition;
var scrollBottom = this._scrollBottom;
if (fromTop) {
ith = this._physicalStart;
lastIth = this._physicalEnd;
offsetContent = scrollTop - top;
} else {
ith = this._physicalEnd;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
lastIth = this._physicalStart;
offsetContent = bottom - scrollBottom;
}
// eslint-disable-next-line no-constant-condition
while (true) {
physicalItemHeight = this._getPhysicalSizeIncrement(ith);
offsetContent = offsetContent - physicalItemHeight;
if (idxs.length >= physicalCount || offsetContent <= protectedOffsetContent) {
break;
}
if (fromTop) {
// Check that index is within the valid range.
if (virtualEnd + idxs.length + 1 >= this._virtualCount) {
break;
}
// Check that the index is not visible.
if (top + physicalItemHeight >= scrollTop - this._scrollOffset) {
break;
}
idxs.push(ith);
top = top + physicalItemHeight;
ith = (ith + 1) % physicalCount;
} else {
// Check that index is within the valid range.
if (virtualStart - idxs.length <= 0) {
break;
}
// Check that the index is not visible.
if (top + this._physicalSize - physicalItemHeight <= scrollBottom) {
break;
}
idxs.push(ith);
top = top - physicalItemHeight;
ith = ith === 0 ? physicalCount - 1 : ith - 1;
}
}
return { indexes: idxs, physicalTop: top - this._scrollOffset };
},
/**
* Update the list of items, starting from the `_virtualStart` item.
* @param {!Array<number>=} itemSet
* @param {!Array<number>=} movingUp
*/
_update: function (itemSet, movingUp) {
if ((itemSet && itemSet.length === 0) || this._physicalCount === 0) {
return;
}
this._manageFocus();
this._assignModels(itemSet);
this._updateMetrics(itemSet);
// Adjust offset after measuring.
if (movingUp) {
while (movingUp.length) {
var idx = movingUp.pop();
this._physicalTop -= this._getPhysicalSizeIncrement(idx);
}
}
this._positionItems();
this._updateScrollerSize();
},
_isClientFull: function () {
return (
this._scrollBottom != 0 &&
this._physicalBottom - 1 >= this._scrollBottom &&
this._physicalTop <= this._scrollPosition
);
},
/**
* Increases the pool size.
*/
_increasePoolIfNeeded: function (count) {
var nextPhysicalCount = this._clamp(
this._physicalCount + count,
DEFAULT_PHYSICAL_COUNT,
this._virtualCount - this._virtualStart
);
nextPhysicalCount = this._convertIndexToCompleteRow(nextPhysicalCount);
if (this.index) {
var correction = nextPhysicalCount % this._itemsPerRow;
if (correction && nextPhysicalCount - correction <= this._physicalCount) {
nextPhysicalCount += this._itemsPerRow;
}
nextPhysicalCount -= correction;
}
var delta = nextPhysicalCount - this._physicalCount;
var nextIncrease = Math.round(this._physicalCount * 0.5);
if (delta < 0) {
return;
}
if (delta > 0) {
var ts = window.performance.now();
// Concat arrays in place.
[].push.apply(this._physicalItems, this._createPool(delta));
// Push 0s into physicalSizes. Can't use Array.fill because IE11 doesn't
// support it.
for (var i = 0; i < delta; i++) {
this._physicalSizes.push(0);
}
this._physicalCount = this._physicalCount + delta;
// Update the physical start if it needs to preserve the model of the
// focused item. In this situation, the focused item is currently rendered
// and its model would have changed after increasing the pool if the
// physical start remained unchanged.
if (
this._physicalStart > this._physicalEnd &&
this._isIndexRendered(this._focusedVirtualIndex) &&
this._getPhysicalIndex(this._focusedVirtualIndex) < this._physicalEnd
) {
this._physicalStart = this._physicalStart + delta;
}
this._update();
this._templateCost = (window.performance.now() - ts) / delta;
nextIncrease = Math.round(this._physicalCount * 0.5);
}
// The upper bounds is not fixed when dealing with a grid that doesn't
// fill it's last row with the exact number of items per row.
if (this._virtualEnd >= this._virtualCount - 1 || nextIncrease === 0) {
// Do nothing.
} else if (!this._isClientFull()) {
this._debounce('_increasePoolIfNeeded', this._increasePoolIfNeeded.bind(this, nextIncrease), microTask);
} else if (this._physicalSize < this._optPhysicalSize) {
// Yield and increase the pool during idle time until the physical size is
// optimal.
this._debounce(
'_increasePoolIfNeeded',
this._increasePoolIfNeeded.bind(this, this._clamp(Math.round(50 / this._templateCost), 1, nextIncrease)),
idlePeriod
);
}
},
/**
* Renders the a new list.
*/
_render: function () {
if (!this.isAttached || !this._isVisible) {
return;
}
if (this._physicalCount !== 0) {
var reusables = this._getReusables(true);
this._physicalTop = reusables.physicalTop;
this._virtualStart = this._virtualStart + reusables.indexes.length;
this._physicalStart = this._physicalStart + reusables.indexes.length;
this._update(reusables.indexes);
this._update();
this._increasePoolIfNeeded(0);
} else if (this._virtualCount > 0) {
// Initial render
this.updateViewportBoundaries();
this._increasePoolIfNeeded(DEFAULT_PHYSICAL_COUNT);
}
},
_gridChanged: function (newGrid, oldGrid) {
if (typeof oldGrid === 'undefined') return;
this.notifyResize();
flush();
newGrid && this._updateGridMetrics();
},
/**
* Called when the items have changed. That is, reassignments
* to `items`, splices or updates to a single item.
*/
_itemsChanged: function (change) {
if (change.path === 'items') {
this._virtualStart = 0;
this._physicalTop = 0;
this._virtualCount = this.items ? this.items.length : 0;
this._physicalIndexForKey = {};
this._firstVisibleIndexVal = null;
this._lastVisibleIndexVal = null;
this._physicalCount = this._physicalCount || 0;
this._physicalItems = this._physicalItems || [];
this._physicalSizes = this._physicalSizes || [];
this._physicalStart = 0;
if (this._scrollTop > this._scrollOffset) {
this._resetScrollPosition(0);
}
this._removeFocusedItem();
this._debounce('_render', this._render, animationFrame);
} else if (change.path === 'items.splices') {
this._adjustVirtualIndex(change.value.indexSplices);
this._virtualCount = this.items ? this.items.length : 0;
// Only blur if at least one item is added or removed.
var itemAddedOrRemoved = change.value.indexSplices.some(function (splice) {
return splice.addedCount > 0 || splice.removed.length > 0;
});
if (itemAddedOrRemoved) {
// Only blur activeElement if it is a descendant of the list (#505,
// #507).
var activeElement = this._getActiveElement();
if (this.contains(activeElement)) {
activeElement.blur();
}
}
// Render only if the affected index is rendered.
var affectedIndexRendered = change.value.indexSplices.some(function (splice) {
return splice.index + splice.addedCount >= this._virtualStart && splice.index <= this._virtualEnd;
}, this);
if (!this._isClientFull() || affectedIndexRendered) {
this._debounce('_render', this._render, animationFrame);
}
} else if (change.path !== 'items.length') {
this._forwardItemPath(change.path, change.value);
}
},
/**
* Executes a provided function per every physical index in `itemSet`
* `itemSet` default value is equivalent to the entire set of physical
* indexes.
*
* @param {!function(number, number)} fn
* @param {!Array<number>=} itemSet
*/
_iterateItems: function (fn, itemSet) {
var pidx, vidx, rtn, i;
if (arguments.length === 2 && itemSet) {
for (i = 0; i < itemSet.length; i++) {
pidx = itemSet[i];
vidx = this._computeVidx(pidx);
if ((rtn = fn.call(this, pidx, vidx)) != null) {
return rtn;
}
}
} else {
pidx = this._physicalStart;
vidx = this._virtualStart;
for (; pidx < this._physicalCount; pidx++, vidx++) {
if ((rtn = fn.call(this, pidx, vidx)) != null) {
return rtn;
}
}
for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
if ((rtn = fn.call(this, pidx, vidx)) != null) {
return rtn;
}
}
}
},
/**
* Returns the virtual index for a given physical index
*
* @param {number} pidx Physical index
* @return {number}
*/
_computeVidx: function (pidx) {
if (pidx >= this._physicalStart) {
return this._virtualStart + (pidx - this._physicalStart);
}
return this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
},
/**
* Updates the height for a given set of items.
*
* @param {!Array<number>=} itemSet
*/
_updateMetrics: function (itemSet) {
// Make sure we distributed all the physical items
// so we can measure them.
flush();
var newPhysicalSize = 0;
var oldPhysicalSize = 0;
var prevAvgCount = this._physicalAverageCount;
var prevPhysicalAvg = this._physicalAverage;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._iterateItems(function (pidx, vidx) {
oldPhysicalSize += this._physicalSizes[pidx];
this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
newPhysicalSize += this._physicalSizes[pidx];
this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
}, itemSet);
if (this.index) {
this._updateGridMetrics();
this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
} else {
oldPhysicalSize =
this._itemsPerRow === 1
? oldPhysicalSize
: Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize;
this._itemsPerRow = 1;
}
// Update the average if it measured something.
if (this._physicalAverageCount !== prevAvgCount) {
this._physicalAverage = Math.round(
(prevPhysicalAvg * prevAvgCount + newPhysicalSize) / this._physicalAverageCount
);
}
},
_updateGridMetrics: function () {
this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoundingClientRect().width : 200;
this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetHeight : 200;
this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / this._itemWidth) : this._itemsPerRow;
},
/**
* Updates the position of the physical items.
*/
_positionItems: function () {
this._adjustScrollPosition();
var y = this._physicalTop;
if (this.index) {
var totalItemWidth = this._itemsPerRow * this._itemWidth;
var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
this._iterateItems(function (pidx, vidx) {
var modulus = vidx % this._itemsPerRow;
var x = Math.floor(modulus * this._itemWidth + rowOffset);
if (this._isRTL) {
x = x * -1;
}
this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
if (this._shouldRenderNextRow(vidx)) {
y += this._rowHeight;
}
});
} else {
const order = [];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._iterateItems(function (pidx, vidx) {
const item = this._physicalItems[pidx];
this.translate3d(0, y + 'px', 0, item);
y += this._physicalSizes[pidx];
const itemId = item.id;
if (itemId) {
order.push(itemId);
}
});
if (order.length) {
this.setAttribute('aria-owns', order.join(' '));
}
}
},
_getPhysicalSizeIncrement: function (pidx) {
if (!this.index) {
return this._physicalSizes[pidx];
}
if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
return 0;
}
return this._rowHeight;
},
/**
* Returns, based on the current index,
* whether or not the next index will need
* to be rendered on a new row.
*
* @param {number} vidx Virtual index
* @return {boolean}
*/
_shouldRenderNextRow: function (vidx) {
return vidx % this._itemsPerRow === this._itemsPerRow - 1;
},
/**
* Adjusts the scroll position when it was overestimated.
*/
_adjustScrollPosition: function () {
var deltaHeight =
this._virtualStart === 0 ? this._physicalTop : Math.min(this._scrollPosition + this._physicalTop, 0);
// Note: the delta can be positive or negative.
if (deltaHeight !== 0) {
this._physicalTop = this._physicalTop - deltaHeight;
// This may be called outside of a scrollHandler, so use last cached position
var scrollTop = this._scrollPosition;
// juking scroll position during interial scrolling on iOS is no bueno
if (!IOS_TOUCH_SCROLLING && scrollTop > 0) {
this._resetScrollPosition(scrollTop - deltaHeight);
}
}
},
/**
* Sets the position of the scroll.
*/
_resetScrollPosition: function (pos) {
if (this.scrollTarget && pos >= 0) {
this._scrollTop = pos;
this._scrollPosition = this._scrollTop;
}
},
/**
* Sets the scroll height, that's the height of the content,
*
* @param {boolean=} forceUpdate If true, updates the height no matter what.
*/
_updateScrollerSize: function (forceUpdate) {
if (this.index) {
this._estScrollHeight = this._virtualRowCount * this._rowHeight;
} else {
this._estScrollHeight =
this._physicalBottom +
Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage;
}
forceUpdate = forceUpdate || this._scrollHeight === 0;
forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
forceUpdate = forceUpdate || (this.index && this.$.items.style.height < this._estScrollHeight);
// Amortize height adjustment, so it won't trigger large repaints too often.
if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._viewportHeight) {
this.$.items.style.height = this._estScrollHeight + 'px';
this._scrollHeight = this._estScrollHeight;
}
},
/**
* Scroll to a specific index in the virtual list regardless
* of the physical items in the DOM tree.
*
* @method scrollToIndex
* @param {number} idx The index of the item
*/
scrollToIndex: function (idx) {
if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
return;
}
flush();
// Items should have been rendered prior scrolling to an index.
if (this._physicalCount === 0) {
return;
}
idx = this._clamp(idx, 0, this._virtualCount - 1);
// Update the virtual start only when needed.
if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
this._virtualStart = this.index ? idx - this._itemsPerRow * 2 : idx - 1;
}
this._manageFocus();
this._assignModels();
this._updateMetrics();
// Estimate new physical offset.
this._physicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage;
var currentTopItem = this._physicalStart;
var currentVirtualItem = this._virtualStart;
var targetOffsetTop = 0;
var hiddenContentSize = this._hiddenContentSize;
// scroll to the item as much as we can.
while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(currentTopItem);
currentTopItem = (currentTopItem + 1) % this._physicalCount;
currentVirtualItem++;
}
this._updateScrollerSize(true);
this._positionItems();
this._resetScrollPosition(this._physicalTop + this._scrollOffset + targetOffsetTop);
this._increasePoolIfNeeded(0);
// clear cached visible index.
this._firstVisibleIndexVal = null;
this._lastVisibleIndexVal = null;
},
/**
* Reset the physical average and the average count.
*/
_resetAverage: function () {
this._physicalAverage = 0;
this._physicalAverageCount = 0;
},
/**
* A handler for the `iron-resize` event triggered by `IronResizableBehavior`
* when the element is resized.
*/
_resizeHandler: function () {
this._debounce(
'_render',
function () {
// clear cached visible index.
this._firstVisibleIndexVal = null;
this._lastVisibleIndexVal = null;
if (this._isVisible) {
this.updateViewportBoundaries();
// Reinstall the scroll event listener.
this.toggleScrollListener(true);
this._resetAverage();
this._render();
} else {
// Uninstall the scroll event listener.
this.toggleScrollListener(false);
}
},
animationFrame
);
},
/**
* Updates the size of a given list item.
*
* @method updateSizeForItem
* @param {Object} item The item instance.
*/
updateSizeForItem: function (item) {
return this.updateSizeForIndex(this.items.indexOf(item));
},
/**
* Updates the size of the item at the given index in the items array.
*
* @method updateSizeForIndex
* @param {number} index The index of the item in the items array.
*/
updateSizeForIndex: function (index) {
if (!this._isIndexRendered(index)) {
return null;
}
this._updateMetrics([this._getPhysicalIndex(index)]);
this._positionItems();
return null;
},
/**
* Converts a random index to the index of the item that completes it's row.
* Allows for better order and fill computation when grid == true.
*/
_convertIndexToCompleteRow: function (idx) {
// when grid == false _itemPerRow can be unset.
this._itemsPerRow = this._itemsPerRow || 1;
return this.index ? Math.ceil(idx / this._itemsPerRow) * this._itemsPerRow : idx;
},
_isIndexRendered: function (idx) {
return idx >= this._virtualStart && idx <= this._virtualEnd;
},
_isIndexVisible: function (idx) {
return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
},
_getPhysicalIndex: function (vidx) {
return (this._physicalStart + (vidx - this._virtualStart)) % this._physicalCount;
},
_clamp: function (v, min, max) {
return Math.min(max, Math.max(min, v));
},
_debounce: function (name, cb, asyncModule) {
this._debouncers = this._debouncers || {};
this._debouncers[name] = Debouncer.debounce(this._debouncers[name], asyncModule, cb.bind(this));
enqueueDebouncer(this._debouncers[name]);
}
};