` to ensure the component is rendered
+correctly. Since each custom component's implementation and internals can be
+quite different, wrapping within a `
` is a safe way to make sure
+dimensions are measured correctly.
+
+```html
+
+
+
+ {% raw %} {{ item }}{% endraw %}
+
+
+
+```
+
+## Virtual Scroll Performance Tips
+
+#### iOS Cordova WKWebView
+
+When deploying to iOS with Cordova, it's highly recommended to use the
+[WKWebView plugin](http://blog.ionic.io/cordova-ios-performance-improvements-drop-in-speed-with-wkwebview/)
+in order to take advantage of iOS's higher performimg webview. Additionally,
+WKWebView is superior at scrolling efficiently in comparision to the older
+UIWebView.
+
+#### Lock in element dimensions and locations
+
+In order for virtual scroll to efficiently size and locate every item, it's
+very important every element within each virtual item does not dynamically
+change its dimensions or location. The best way to ensure size and location
+does not change, it's recommended each virtual item has locked in its size
+via CSS.
+
+#### Use `ion-img` for images
+
+When including images within Virtual Scroll, be sure to use
+[`ion-img`](../img/Img/) rather than the standard `
![]()
` HTML element.
+With `ion-img`, images are lazy loaded so only the viewable ones are
+rendered, and HTTP requests are efficiently controlled while scrolling.
+
+#### Set Approximate Widths and Heights
+
+As mentioned above, all elements should lock in their dimensions. However,
+virtual scroll isn't aware of the dimensions until after they have been
+rendered. For the initial render, virtual scroll still needs to set
+how many items should be built. With "approx" property inputs, such as
+`approxItemHeight`, we're able to give virtual scroll an approximate size,
+therefore allowing virtual scroll to decide how many items should be
+created.
+
+#### Changing dataset should use `virtualTrackBy`
+
+It is possible for the identities of elements in the iterator to change
+while the data does not. This can happen, for example, if the iterator
+produced from an RPC to the server, and that RPC is re-run. Even if the
+"data" hasn't changed, the second response will produce objects with
+different identities, and Ionic will tear down the entire DOM and rebuild
+it. This is an expensive operation and should be avoided if possible.
+
+#### Efficient headers and footer functions
+Each virtual item must stay extremely efficient, but one way to really
+kill its performance is to perform any DOM operations within section header
+and footer functions. These functions are called for every record in the
+dataset, so please make sure they're performant.
+
+
+
+
+
+## Properties
+
+#### approxFooterHeight
+
+number
+
+The approximate width of each footer template's cell.
+This dimension is used to help determine how many cells should
+be created when initialized, and to help calculate the height of
+the scrollable area. This value can use either `px` or `%` units.
+Note that the actual rendered size of each cell comes from the
+app's CSS, whereas this approximation is used to help calculate
+initial dimensions before the item has been rendered. Default is `100%`.
+
+
+#### approxHeaderHeight
+
+number
+
+The approximate height of each header template's cell.
+This dimension is used to help determine how many cells should
+be created when initialized, and to help calculate the height of
+the scrollable area. This height value can only use `px` units.
+Note that the actual rendered size of each cell comes from the
+app's CSS, whereas this approximation is used to help calculate
+initial dimensions before the item has been rendered. Default is `40px`.
+
+
+#### approxItemHeight
+
+number
+
+It is important to provide this
+if virtual item height will be significantly larger than the default
+The approximate height of each virtual item template's cell.
+This dimension is used to help determine how many cells should
+be created when initialized, and to help calculate the height of
+the scrollable area. This height value can only use `px` units.
+Note that the actual rendered size of each cell comes from the
+app's CSS, whereas this approximation is used to help calculate
+initial dimensions before the item has been rendered. Default is
+`45`.
+
+
+#### domRender
+
+
+
+
+#### footerFn
+
+
+
+Section footers and the data used within its given
+template can be dynamically created by passing a function to `footerFn`.
+The logic within the footer function can decide if the footer template
+should be used, and what data to give to the footer template. The function
+must return `null` if a footer cell shouldn't be created.
+
+
+#### headerFn
+
+
+
+Section headers and the data used within its given
+template can be dynamically created by passing a function to `headerFn`.
+For example, a large list of contacts usually has dividers between each
+letter in the alphabet. App's can provide their own custom `headerFn`
+which is called with each record within the dataset. The logic within
+the header function can decide if the header template should be used,
+and what data to give to the header template. The function must return
+`null` if a header cell shouldn't be created.
+
+
+#### itemHeight
+
+
+
+
+#### itemRender
+
+
+
+
+#### items
+
+
+
+The data that builds the templates within the virtual scroll.
+This is the same data that you'd pass to `*ngFor`. It's important to note
+that when this data has changed, then the entire virtual scroll is reset,
+which is an expensive operation and should be avoided if possible.
+
+
+#### nodeHeight
+
+
+
+
+## Attributes
+
+#### approxFooterHeight
+
+number
+
+The approximate width of each footer template's cell.
+This dimension is used to help determine how many cells should
+be created when initialized, and to help calculate the height of
+the scrollable area. This value can use either `px` or `%` units.
+Note that the actual rendered size of each cell comes from the
+app's CSS, whereas this approximation is used to help calculate
+initial dimensions before the item has been rendered. Default is `100%`.
+
+
+#### approxHeaderHeight
+
+number
+
+The approximate height of each header template's cell.
+This dimension is used to help determine how many cells should
+be created when initialized, and to help calculate the height of
+the scrollable area. This height value can only use `px` units.
+Note that the actual rendered size of each cell comes from the
+app's CSS, whereas this approximation is used to help calculate
+initial dimensions before the item has been rendered. Default is `40px`.
+
+
+#### approxItemHeight
+
+number
+
+It is important to provide this
+if virtual item height will be significantly larger than the default
+The approximate height of each virtual item template's cell.
+This dimension is used to help determine how many cells should
+be created when initialized, and to help calculate the height of
+the scrollable area. This height value can only use `px` units.
+Note that the actual rendered size of each cell comes from the
+app's CSS, whereas this approximation is used to help calculate
+initial dimensions before the item has been rendered. Default is
+`45`.
+
+
+#### domRender
+
+
+
+
+#### footerFn
+
+
+
+Section footers and the data used within its given
+template can be dynamically created by passing a function to `footerFn`.
+The logic within the footer function can decide if the footer template
+should be used, and what data to give to the footer template. The function
+must return `null` if a footer cell shouldn't be created.
+
+
+#### headerFn
+
+
+
+Section headers and the data used within its given
+template can be dynamically created by passing a function to `headerFn`.
+For example, a large list of contacts usually has dividers between each
+letter in the alphabet. App's can provide their own custom `headerFn`
+which is called with each record within the dataset. The logic within
+the header function can decide if the header template should be used,
+and what data to give to the header template. The function must return
+`null` if a header cell shouldn't be created.
+
+
+#### itemHeight
+
+
+
+
+#### itemRender
+
+
+
+
+#### items
+
+
+
+The data that builds the templates within the virtual scroll.
+This is the same data that you'd pass to `*ngFor`. It's important to note
+that when this data has changed, then the entire virtual scroll is reset,
+which is an expensive operation and should be avoided if possible.
+
+
+#### nodeHeight
+
+
+
+
+## Methods
+
+#### positionForItem()
+
+
+
+----------------------------------------------
+
+*Built by [StencilJS](https://stenciljs.com/)*
diff --git a/packages/core/src/components/virtual-scroll/test/basic.html b/packages/core/src/components/virtual-scroll/test/basic.html
new file mode 100644
index 0000000000..621e6a9463
--- /dev/null
+++ b/packages/core/src/components/virtual-scroll/test/basic.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+
Ionic Item Sliding
+
+
+
+
+
+
+
+
+
+
+ Ionic CDN demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/components/virtual-scroll/test/cards.html b/packages/core/src/components/virtual-scroll/test/cards.html
new file mode 100644
index 0000000000..ee7c5f74b3
--- /dev/null
+++ b/packages/core/src/components/virtual-scroll/test/cards.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
Ionic Item Sliding
+
+
+
+
+
+
+
+
+
+
+ Ionic CDN demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/components/virtual-scroll/virtual-scroll-utils.tsx b/packages/core/src/components/virtual-scroll/virtual-scroll-utils.tsx
new file mode 100644
index 0000000000..99e11d76e1
--- /dev/null
+++ b/packages/core/src/components/virtual-scroll/virtual-scroll-utils.tsx
@@ -0,0 +1,193 @@
+
+export const enum CellType {
+ Item,
+ Header,
+ Footer
+}
+
+export interface Cell {
+ type: CellType;
+ value: any;
+ i: number;
+ index: number;
+ height: number;
+ reads: number;
+ visible: boolean;
+}
+
+export interface VirtualNode {
+ cell: Cell;
+ top: number;
+ change: number;
+ _d: boolean;
+}
+
+export type NodeHeightFn = (node: VirtualNode, index: number) => number;
+export type HeaderFn = (item: any, index: number, items: any[]) => string | null;
+export type ItemHeightFn = (item: any, index?: number) => number;
+export type ItemRenderFn = (el: HTMLElement|null, item: any, type: CellType, index?: number) => HTMLElement;
+export type DomRenderFn = (dom: VirtualNode[], height: number) => void;
+
+export function updateVDom(dom: VirtualNode[], heightIndex: Uint32Array, cells: Cell[], top: number, bottom: number) {
+ // reset dom
+ for (const node of dom) {
+ node.top = -9999;
+ node.change = 0;
+ node._d = true;
+ }
+
+ // try to match into exisiting dom
+ const toMutate = [];
+ const end = bottom + 1;
+
+ for (let i = top; i < end; i++) {
+ const cell = cells[i];
+ const node = dom.find((n) => n._d && n.cell === cell);
+ if (node) {
+ node._d = false;
+ node.change = 1;
+ node.top = heightIndex[i];
+ } else {
+ toMutate.push(cell);
+ }
+ }
+
+ // needs to append
+ const pool = dom.filter((n) => n._d);
+
+ // console.log('toMutate', toMutate.length);
+ for (const cell of toMutate) {
+ const node = pool.find(n => n._d && n.cell.type === cell.type);
+ const index = cell.index;
+ if (node) {
+ node._d = false;
+ node.change = 2;
+ node.cell = cell;
+ node.top = heightIndex[index];
+ } else {
+ dom.push({
+ _d: false,
+ change: 2,
+ cell: cell,
+ top: heightIndex[index],
+ });
+ }
+ }
+}
+
+export function doRender(el: HTMLElement, itemRender: ItemRenderFn, dom: VirtualNode[], updateCellHeight: Function, total: number) {
+ const children = el.children;
+ let child: HTMLElement;
+ for (let i = 0; i < dom.length; i++) {
+ const node = dom[i];
+ const cell = node.cell;
+ if (node.change === 2) {
+ if (i < children.length) {
+ child = children[i] as HTMLElement;
+ itemRender(child, cell.value, cell.type, cell.index);
+ } else {
+ child = itemRender(null, cell.value, cell.type, cell.index);
+ child.classList.add('virtual-item');
+ el.appendChild(child);
+ }
+ } else {
+ child = children[i] as HTMLElement;
+ }
+ (child as any)['$ionCell'] = cell;
+ if (node.change !== 0) {
+ child.style.transform = `translate3d(0,${node.top}px,0)`;
+ }
+ if (cell.visible) {
+ child.classList.remove('virtual-loading');
+ } else {
+ child.classList.add('virtual-loading');
+ }
+ if (cell.reads > 0) {
+ updateCellHeight(cell, child);
+ }
+ }
+ el.style.height = total + 'px';
+}
+
+export function doHeight(el: HTMLElement, index: number) {
+ const e = (el.children[index] as HTMLElement);
+ // const style = window.getComputedStyle(e);
+ return e.offsetHeight;
+}
+
+export function getTotalHeight(heightIndex: Uint32Array) {
+ return heightIndex[heightIndex.length - 1];
+}
+
+export interface Viewport {
+ top: number;
+ bottom: number;
+}
+
+export function getViewport(scrollTop: number, vierportHeight: number, margin: number): Viewport {
+ return {
+ top: scrollTop - margin,
+ bottom: scrollTop + vierportHeight + margin
+ };
+}
+
+export function getBounds(heightIndex: Uint32Array, viewport: Viewport, buffer: number) {
+ const topPos = viewport.top;
+ const bottomPos = viewport.bottom;
+
+ // find top index
+ let i = 0;
+ for (; i < heightIndex.length; i++) {
+ if (heightIndex[i] > topPos) {
+ break;
+ }
+ }
+ const top = Math.max(i - buffer, 0);
+
+ // find bottom index
+ for (; i < heightIndex.length; i++) {
+ if (heightIndex[i] > bottomPos) {
+ break;
+ }
+ }
+ const bottom = Math.min(i + buffer, heightIndex.length - 1);
+ return { top, bottom };
+}
+
+export function getShouldUpdate(dirtyIndex: number, currentTop: number, currentBottom: number, top: number, bottom: number) {
+ return (
+ dirtyIndex < bottom ||
+ currentTop !== top ||
+ currentBottom !== bottom
+ );
+}
+
+
+export function calcHeightIndex(buf: Uint32Array, cells: Cell[], index: number, bottom: number) {
+ if (!cells) {
+ return buf;
+ }
+ buf = resizeBuffer(buf, cells.length);
+
+ let acum = buf[index];
+ for (; index < buf.length; index++) {
+ buf[index] = acum;
+ acum += cells[index].height;
+ // if (acum > bottom) {
+ // break;
+ // }
+ }
+ return buf;
+}
+
+
+export function resizeBuffer(buf: Uint32Array, len: number) {
+ if (!buf) {
+ return new Uint32Array(len);
+ }
+ if (buf.length === len) {
+ return buf;
+ }
+ return buf;
+}
+
diff --git a/packages/core/src/components/virtual-scroll/virtual-scroll.scss b/packages/core/src/components/virtual-scroll/virtual-scroll.scss
new file mode 100644
index 0000000000..c3795d9b4a
--- /dev/null
+++ b/packages/core/src/components/virtual-scroll/virtual-scroll.scss
@@ -0,0 +1,21 @@
+@import "../../themes/ionic.globals";
+
+ion-virtual-scroll {
+ display: block;
+ position: relative;
+ width: 100%;
+
+ // contain: strict;
+}
+
+.virtual-loading {
+ opacity: 0;
+}
+
+.virtual-item {
+ @include position(0, 0, null, 0);
+
+ will-change: transform;
+ position: absolute;
+ // contain: strict;
+}
diff --git a/packages/core/src/components/virtual-scroll/virtual-scroll.tsx b/packages/core/src/components/virtual-scroll/virtual-scroll.tsx
new file mode 100644
index 0000000000..399c651b5e
--- /dev/null
+++ b/packages/core/src/components/virtual-scroll/virtual-scroll.tsx
@@ -0,0 +1,329 @@
+import { Component, Element, EventListenerEnable, Listen, Method, Prop, Watch } from '@stencil/core';
+import { DomController } from '../../index';
+import { Cell, CellType, DomRenderFn, HeaderFn, ItemHeightFn, ItemRenderFn, NodeHeightFn,
+ Viewport, VirtualNode, calcHeightIndex, doRender, getBounds, getShouldUpdate, getViewport, updateVDom } from './virtual-scroll-utils';
+
+
+const MIN_READS = 2;
+
+@Component({
+ tag: 'ion-virtual-scroll',
+ styleUrl: 'virtual-scroll.scss'
+})
+export class VirtualScroll {
+
+ private scrollEl: HTMLElement;
+ private topIndex = -100;
+ private bottomIndex = -100;
+ private timerUpdate: any;
+ private heightIndex: Uint32Array;
+ private viewportHeight: number;
+ private cells: Cell[] = [];
+ private virtualDom: VirtualNode[] = [];
+ private isEnabled = false;
+ private currentScrollTop = 0;
+ private indexDirty = 0;
+ private totalHeight = 0;
+
+ @Element() el: HTMLElement;
+
+ @Prop({context: 'dom'}) dom: DomController;
+ @Prop({context: 'enableListener'}) enableListener: EventListenerEnable;
+
+
+ /**
+ * It is important to provide this
+ * if virtual item height will be significantly larger than the default
+ * The approximate height of each virtual item template's cell.
+ * This dimension is used to help determine how many cells should
+ * be created when initialized, and to help calculate the height of
+ * the scrollable area. This height value can only use `px` units.
+ * Note that the actual rendered size of each cell comes from the
+ * app's CSS, whereas this approximation is used to help calculate
+ * initial dimensions before the item has been rendered. Default is
+ * `45`.
+ */
+ @Prop() approxItemHeight = 45;
+
+ /**
+ * The approximate height of each header template's cell.
+ * This dimension is used to help determine how many cells should
+ * be created when initialized, and to help calculate the height of
+ * the scrollable area. This height value can only use `px` units.
+ * Note that the actual rendered size of each cell comes from the
+ * app's CSS, whereas this approximation is used to help calculate
+ * initial dimensions before the item has been rendered. Default is `40px`.
+ */
+ @Prop() approxHeaderHeight = 40;
+
+ /**
+ * The approximate width of each footer template's cell.
+ * This dimension is used to help determine how many cells should
+ * be created when initialized, and to help calculate the height of
+ * the scrollable area. This value can use either `px` or `%` units.
+ * Note that the actual rendered size of each cell comes from the
+ * app's CSS, whereas this approximation is used to help calculate
+ * initial dimensions before the item has been rendered. Default is `100%`.
+ */
+ @Prop() approxFooterHeight = 40;
+
+ /**
+ * Section headers and the data used within its given
+ * template can be dynamically created by passing a function to `headerFn`.
+ * For example, a large list of contacts usually has dividers between each
+ * letter in the alphabet. App's can provide their own custom `headerFn`
+ * which is called with each record within the dataset. The logic within
+ * the header function can decide if the header template should be used,
+ * and what data to give to the header template. The function must return
+ * `null` if a header cell shouldn't be created.
+ */
+ @Prop() headerFn: HeaderFn;
+
+ /**
+ * Section footers and the data used within its given
+ * template can be dynamically created by passing a function to `footerFn`.
+ * The logic within the footer function can decide if the footer template
+ * should be used, and what data to give to the footer template. The function
+ * must return `null` if a footer cell shouldn't be created.
+ */
+ @Prop() footerFn: HeaderFn;
+
+ /**
+ * The data that builds the templates within the virtual scroll.
+ * This is the same data that you'd pass to `*ngFor`. It's important to note
+ * that when this data has changed, then the entire virtual scroll is reset,
+ * which is an expensive operation and should be avoided if possible.
+ */
+ @Prop() items: any[];
+
+ @Prop() nodeHeight: NodeHeightFn;
+ @Prop() itemHeight: ItemHeightFn;
+ @Prop() itemRender: ItemRenderFn;
+ @Prop() domRender: DomRenderFn;
+
+ @Watch('itemHeight')
+ @Watch('items')
+ itemsChanged() {
+ this.calcCells();
+ }
+
+ componentDidLoad() {
+ this.scrollEl = this.el.closest('ion-scroll') as HTMLElement;
+ if (!this.scrollEl) {
+ console.error('virtual-scroll must be used inside ion-scroll/ion-content');
+ return;
+ }
+ this.calcDimensions();
+ this.calcCells();
+ this.updateState();
+ }
+
+ componentDidUpdate() {
+ this.updateState();
+ }
+
+ componentDidUnload() {
+ this.scrollEl = null;
+ }
+
+ @Listen('scroll', {enabled: false, passive: false})
+ onScroll() {
+ this.updateVirtualScroll();
+ }
+
+ @Listen('window:resize')
+ onResize() {
+ this.indexDirty = 0;
+ this.calcDimensions();
+ this.calcCells();
+ this.updateVirtualScroll();
+ }
+
+ @Method()
+ positionForItem(index: number): number {
+ const cell = this.cells.find(cell => cell.type === CellType.Item && cell.index === index);
+ if (cell) {
+ return this.heightIndex[cell.i];
+ }
+ return -1;
+ }
+
+ private updateVirtualScroll() {
+ // do nothing if there is a scheduled update
+ if (!this.isEnabled) {
+ return;
+ }
+ if (this.timerUpdate) {
+ clearTimeout(this.timerUpdate);
+ this.timerUpdate = null;
+ }
+
+ this.dom.read(() => {
+ this.currentScrollTop = this.scrollEl.scrollTop;
+ });
+
+ this.dom.write(() => {
+ const dirtyIndex = this.indexDirty;
+
+ // get visible viewport
+ const viewport = getViewport(this.currentScrollTop, this.viewportHeight, 100);
+
+ // compute lazily the height index
+ const heightIndex = this.getHeightIndex(viewport);
+
+ // get array bounds of visible cells base in the viewport
+ const {top, bottom} = getBounds(heightIndex, viewport, 2);
+
+ // fast path, do nothing
+ const shouldUpdate = getShouldUpdate(dirtyIndex, this.topIndex, this.bottomIndex, top, bottom);
+ if (!shouldUpdate) {
+ return;
+ }
+ this.topIndex = top;
+ this.bottomIndex = bottom;
+
+ // in place mutation of the virtual DOM
+ updateVDom(
+ this.virtualDom,
+ heightIndex,
+ this.cells,
+ top,
+ bottom);
+
+ this.fireDomUpdate();
+ });
+ }
+
+ private fireDomUpdate() {
+ if (this.itemRender) {
+ doRender(this.el, this.itemRender, this.virtualDom, this.updateCellHeight.bind(this), this.totalHeight);
+ } else if (this.domRender) {
+ this.domRender(this.virtualDom, this.totalHeight);
+ }
+ }
+
+ updateCellHeight(cell: Cell, node: HTMLElement) {
+ (node as any).componentOnReady(() => {
+ // let's give some additional time to read the height size
+ setTimeout(() => this.dom.read(() => {
+ if ((node as any)['$ionCell'] === cell) {
+ const style = window.getComputedStyle(node);
+ const height = node.offsetHeight + parseFloat(style.getPropertyValue('margin-bottom'));
+ this.setCellHeight(cell, height);
+ }
+ }));
+ });
+ }
+
+ setCellHeight(cell: Cell, height: number) {
+ const index = cell.i;
+ // the cell might changed since the height update was scheduled
+ if (cell !== this.cells[index]) {
+ return;
+ }
+ cell.visible = true;
+ cell.reads--;
+ if (cell.height !== height) {
+ console.debug(`[${cell.reads}] cell size ${cell.height} -> ${height}`);
+ cell.height = height;
+ clearTimeout(this.timerUpdate);
+ this.indexDirty = Math.min(this.indexDirty, index);
+ this.timerUpdate = setTimeout(() => this.updateVirtualScroll(), 100);
+ }
+ }
+
+ private updateState() {
+ const shouldEnable = !!(
+ this.scrollEl &&
+ this.items &&
+ (this.itemRender || this.domRender) &&
+ this.viewportHeight > 1
+ );
+ if (shouldEnable !== this.isEnabled) {
+ this.enableScrollEvents(shouldEnable);
+ if (shouldEnable) {
+ this.updateVirtualScroll();
+ }
+ }
+ }
+
+
+ private calcCells() {
+ if (!this.items) {
+ return;
+ }
+ const items = this.items;
+ const cells = this.cells;
+ const headerFn = this.headerFn;
+ const footerFn = this.footerFn;
+
+ cells.length = 0;
+ this.indexDirty = 0;
+ let j = 0;
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (headerFn) {
+ const value = headerFn(item, i, this.items);
+ if (value != null) {
+ cells.push({
+ i: j++,
+ type: CellType.Header,
+ value: value,
+ index: i,
+ height: this.approxHeaderHeight,
+ reads: MIN_READS,
+ visible: false,
+ });
+ }
+ }
+
+ cells.push({
+ i: j++,
+ type: CellType.Item,
+ value: item,
+ index: i,
+ height: this.itemHeight ? this.itemHeight(item, i) : this.approxItemHeight,
+ reads: this.itemHeight ? 0 : MIN_READS,
+ visible: !!this.itemHeight,
+ });
+
+ if (footerFn) {
+ const value = footerFn(item, i, this.items);
+ if (value != null) {
+ cells.push({
+ i: j++,
+ type: CellType.Footer,
+ value: value,
+ index: i,
+ height: this.approxFooterHeight,
+ reads: 2,
+ visible: false,
+ });
+ }
+ }
+ }
+ }
+
+ private getHeightIndex(viewport: Viewport): Uint32Array {
+ if (this.indexDirty !== Infinity) {
+ this.calcHeightIndex(this.indexDirty, viewport.bottom);
+ }
+ return this.heightIndex;
+ }
+
+ private calcHeightIndex(index = 0, bottom = Infinity) {
+ this.heightIndex = calcHeightIndex(this.heightIndex, this.cells, index, bottom);
+ this.totalHeight = this.heightIndex[this.heightIndex.length - 1];
+ this.indexDirty = Infinity;
+ }
+
+ private calcDimensions() {
+ this.viewportHeight = this.scrollEl.offsetHeight;
+ }
+
+ private enableScrollEvents(shouldListen: boolean) {
+ this.isEnabled = shouldListen;
+ this.enableListener(this, 'scroll', shouldListen, this.scrollEl);
+ }
+}