UNPKG

@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
/** * @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]); } };