mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
test(virtual-scroll): adds unit tests (part 1)
This commit is contained in:
@@ -40,16 +40,24 @@
|
||||
function renderItem(el, item) {
|
||||
if (!el) {
|
||||
el = document.createElement('ion-item');
|
||||
const text = document.createTextNode(item);
|
||||
el['$content'] = text;
|
||||
el.appendChild(text);
|
||||
} else {
|
||||
el['$content'].nodeValue = item;
|
||||
}
|
||||
el.textContent = item;
|
||||
return el;
|
||||
}
|
||||
|
||||
function renderHeader(el, item) {
|
||||
if (!el) {
|
||||
el = document.createElement('ion-item-divider');
|
||||
const text = document.createTextNode(item);
|
||||
el['$content'] = text;
|
||||
el.appendChild(text);
|
||||
} else {
|
||||
el['$content'].nodeValue = item;
|
||||
}
|
||||
el.textContent = item;
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,14 +67,6 @@
|
||||
};
|
||||
|
||||
|
||||
const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
|
||||
|
||||
const images = [
|
||||
"http://images.all-free-download.com/images/graphiclarge/travel_icons_6813629.jpg",
|
||||
"https://images.unsplash.com/photo-1500531279542-fc8490c8ea4d?auto=format&fit=crop&w=1502&q=60&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D",
|
||||
"https://images.unsplash.com/photo-1483354483454-4cd359948304?dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D",
|
||||
];
|
||||
|
||||
function getImgSrc() {
|
||||
let src = images[rotateImg];
|
||||
rotateImg++;
|
||||
@@ -94,7 +86,6 @@
|
||||
content: lorem.substring(0, (Math.random() * (lorem.length - 100)) + 100)
|
||||
});
|
||||
rotateImg++;
|
||||
if (rotateImg === images.length) rotateImg = 0;
|
||||
}
|
||||
virtual.items = items;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
import { CellType, HeaderFn, ItemHeightFn, VirtualNode, calcCells, calcHeightIndex, getRange, getViewport, resizeBuffer, updateVDom, ItemRenderFn, Range } from '../virtual-scroll-utils';
|
||||
|
||||
|
||||
describe('getViewport', () => {
|
||||
it('should return viewport without margin', () => {
|
||||
expect(getViewport(0, 100, 0)).toEqual({
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return viewport with margin', () => {
|
||||
expect(getViewport(0, 100, 150)).toEqual({
|
||||
top: 0,
|
||||
bottom: 250,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should return viewport with margin and scrollTop', () => {
|
||||
expect(getViewport(150, 100, 150)).toEqual({
|
||||
top: 0,
|
||||
bottom: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return viewport with margin and scrollTop 2', () => {
|
||||
expect(getViewport(100, 100, 10)).toEqual({
|
||||
top: 90,
|
||||
bottom: 210,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('getRange', () => {
|
||||
it('should return initial bounds without buffer', () => {
|
||||
const heightBuffer = mockHeightBuffer(20, () => 10);
|
||||
const bounds = getRange(heightBuffer, {top: 0, bottom: 100}, 0);
|
||||
|
||||
expect(bounds).toEqual({
|
||||
offset: 0,
|
||||
length: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return initial bounds with buffer', () => {
|
||||
const heightBuffer = mockHeightBuffer(20, () => 10);
|
||||
const bounds = getRange(heightBuffer, {top: 0, bottom: 100}, 4);
|
||||
|
||||
expect(bounds).toEqual({
|
||||
offset: 0,
|
||||
length: 14,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return initial bounds truncked', () => {
|
||||
const heightBuffer = mockHeightBuffer(5, () => 10);
|
||||
const bounds = getRange(heightBuffer, {top: 0, bottom: 100}, 4);
|
||||
|
||||
expect(bounds).toEqual({
|
||||
offset: 0,
|
||||
length: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return just first component', () => {
|
||||
const heightBuffer = mockHeightBuffer(5, () => 100);
|
||||
expect(getRange(heightBuffer, {top: 0, bottom: 100}, 0)).toEqual({
|
||||
offset: 0,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
expect(getRange(heightBuffer, {top: 50, bottom: 100}, 0)).toEqual({
|
||||
offset: 0,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
expect(getRange(heightBuffer, {top: 100, bottom: 200}, 0)).toEqual({
|
||||
offset: 1,
|
||||
length: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return just two components', () => {
|
||||
const heightBuffer = mockHeightBuffer(5, () => 100);
|
||||
expect(getRange(heightBuffer, {top: 1, bottom: 101}, 0)).toEqual({
|
||||
offset: 0,
|
||||
length: 2,
|
||||
});
|
||||
|
||||
expect(getRange(heightBuffer, {top: 99, bottom: 200}, 0)).toEqual({
|
||||
offset: 0,
|
||||
length: 2,
|
||||
});
|
||||
|
||||
expect(getRange(heightBuffer, {top: 100, bottom: 201}, 0)).toEqual({
|
||||
offset: 1,
|
||||
length: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return three components', () => {
|
||||
const heightBuffer = mockHeightBuffer(5, () => 100);
|
||||
expect(getRange(heightBuffer, {top: 99, bottom: 201}, 0)).toEqual({
|
||||
offset: 0,
|
||||
length: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resizeBuffer', () => {
|
||||
it('should allocate a buffer', () => {
|
||||
const buf = resizeBuffer(null, 10);
|
||||
expect(buf.length).toEqual(10);
|
||||
});
|
||||
|
||||
it('should not allocate a buffer', () => {
|
||||
const buf = new Uint32Array(10);
|
||||
const buf2 = resizeBuffer(buf, 10);
|
||||
expect(buf).toBe(buf2);
|
||||
expect(buf.length).toEqual(10);
|
||||
});
|
||||
|
||||
it('should grow a buffer', () => {
|
||||
const buf = new Uint32Array(10);
|
||||
buf[0] = 100;
|
||||
buf[9] = 123;
|
||||
|
||||
const buf2 = resizeBuffer(buf, 12);
|
||||
expect(buf2.length).toEqual(12);
|
||||
expect(buf2[0]).toEqual(100);
|
||||
expect(buf2[9]).toEqual(123);
|
||||
});
|
||||
|
||||
it('should shrink a buffer', () => {
|
||||
const buf = new Uint32Array(10);
|
||||
buf[0] = 100;
|
||||
buf[9] = 123;
|
||||
|
||||
const buf2 = resizeBuffer(buf, 5);
|
||||
expect(buf2.length).toEqual(5);
|
||||
expect(buf2[0]).toEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('calcCells', () => {
|
||||
it('should calculate cells without headers and itemHeight', () => {
|
||||
const items = ['0', 2, 'hola', {data: 'hello'}];
|
||||
const cells = calcCells(items, null, null, null, 10, 20, 30);
|
||||
expect(cells).toEqual([
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: '0',
|
||||
i: 0,
|
||||
index: 0,
|
||||
height: 30,
|
||||
reads: 2,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: 2,
|
||||
i: 1,
|
||||
index: 1,
|
||||
height: 30,
|
||||
reads: 2,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: 'hola',
|
||||
i: 2,
|
||||
index: 2,
|
||||
height: 30,
|
||||
reads: 2,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: {data: 'hello'},
|
||||
i: 3,
|
||||
index: 3,
|
||||
height: 30,
|
||||
reads: 2,
|
||||
visible: false,
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should calculate cells with itemHeight', () => {
|
||||
const items = [10, 9, 8];
|
||||
let called = 0;
|
||||
const itemHeight: ItemHeightFn = (item: any, index?: number) => {
|
||||
expect(item).toEqual(items[index]);
|
||||
called++;
|
||||
return index * 20 + 20;
|
||||
};
|
||||
const cells = calcCells(items, itemHeight, null, null, 10, 20, 30);
|
||||
|
||||
expect(called).toEqual(3);
|
||||
expect(cells).toEqual([
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: 10,
|
||||
i: 0,
|
||||
index: 0,
|
||||
height: 20,
|
||||
reads: 0,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: 9,
|
||||
i: 1,
|
||||
index: 1,
|
||||
height: 40,
|
||||
reads: 0,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: 8,
|
||||
i: 2,
|
||||
index: 2,
|
||||
height: 60,
|
||||
reads: 0,
|
||||
visible: true,
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should calculate cells with header and footer', () => {
|
||||
let headerCalled = 0;
|
||||
let footerCalled = 0;
|
||||
let called = 0;
|
||||
const items = ['10', '9', '8'];
|
||||
const headerFn: HeaderFn = (item, index, allItems) => {
|
||||
expect(item).toEqual(items[index]);
|
||||
expect(items).toBe(allItems);
|
||||
headerCalled++;
|
||||
return (index === 0) ? 'my header' : null;
|
||||
};
|
||||
const footerFn: HeaderFn = (item, index, allItems) => {
|
||||
expect(item).toEqual(items[index]);
|
||||
expect(items).toBe(allItems);
|
||||
footerCalled++;
|
||||
return (index === 2) ? 'my footer' : null;
|
||||
};
|
||||
const itemHeight: ItemHeightFn = (item: any, index?: number) => {
|
||||
expect(item).toEqual(items[index]);
|
||||
called++;
|
||||
return index * 20 + 20;
|
||||
};
|
||||
const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 20, 30);
|
||||
expect(cells).toHaveLength(5);
|
||||
expect(called).toEqual(3);
|
||||
expect(headerCalled).toEqual(3);
|
||||
expect(footerCalled).toEqual(3);
|
||||
expect(cells).toEqual([
|
||||
{
|
||||
type: CellType.Header,
|
||||
value: 'my header',
|
||||
i: 0,
|
||||
index: 0,
|
||||
height: 10,
|
||||
reads: 2,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: '10',
|
||||
i: 1,
|
||||
index: 0,
|
||||
height: 20,
|
||||
reads: 0,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: '9',
|
||||
i: 2,
|
||||
index: 1,
|
||||
height: 40,
|
||||
reads: 0,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
type: CellType.Item,
|
||||
value: '8',
|
||||
i: 3,
|
||||
index: 2,
|
||||
height: 60,
|
||||
reads: 0,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
type: CellType.Footer,
|
||||
value: 'my footer',
|
||||
i: 4,
|
||||
index: 2,
|
||||
height: 20,
|
||||
reads: 2,
|
||||
visible: false,
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calcHeightIndex', () => {
|
||||
it('should generate height index', () => {
|
||||
const items = [1, 2, 3, 4, 5];
|
||||
const headerFn: HeaderFn = (_, index) => {
|
||||
return (index === 0) ? 'my header' : null;
|
||||
};
|
||||
const footerFn: HeaderFn = (_, index) => {
|
||||
return (index === 2) ? 'my footer' : null;
|
||||
};
|
||||
const cells = calcCells(items, null, headerFn, footerFn, 10, 20, 50);
|
||||
const buf = resizeBuffer(null, cells.length);
|
||||
const totalHeight = calcHeightIndex(buf, cells, 0);
|
||||
expect(buf.length).toEqual(7);
|
||||
expect(buf[0]).toEqual(0);
|
||||
expect(buf[1]).toEqual(10);
|
||||
expect(buf[2]).toEqual(60);
|
||||
expect(buf[3]).toEqual(110);
|
||||
expect(buf[4]).toEqual(160);
|
||||
expect(buf[5]).toEqual(180);
|
||||
expect(buf[6]).toEqual(230);
|
||||
expect(totalHeight).toEqual(280);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateVDom', () => {
|
||||
it('should initialize empty VDOM', () => {
|
||||
const vdom: VirtualNode[] = [];
|
||||
const items = [1, 2, 3, 4, 5];
|
||||
const {heightIndex, cells} = mockVirtualScroll(items, () => 20);
|
||||
const range: Range = {offset: 1, length: 4};
|
||||
|
||||
updateVDom(vdom, heightIndex, cells, range);
|
||||
expect(vdom).toEqual([
|
||||
{
|
||||
cell: cells[1],
|
||||
change: 2,
|
||||
d: false,
|
||||
top: 20,
|
||||
},
|
||||
{
|
||||
cell: cells[2],
|
||||
change: 2,
|
||||
d: false,
|
||||
top: 40,
|
||||
},
|
||||
{
|
||||
cell: cells[3],
|
||||
change: 2,
|
||||
d: false,
|
||||
top: 60,
|
||||
},
|
||||
{
|
||||
cell: cells[4],
|
||||
change: 2,
|
||||
d: false,
|
||||
top: 80,
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function mockVirtualScroll(
|
||||
items: any[],
|
||||
itemHeight: ItemHeightFn = null,
|
||||
headerFn: HeaderFn = null,
|
||||
footerFn: HeaderFn = null
|
||||
) {
|
||||
const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 20, 30);
|
||||
const heightIndex = resizeBuffer(null, cells.length);
|
||||
calcHeightIndex(heightIndex, cells, 0);
|
||||
return { items, heightIndex, cells };
|
||||
}
|
||||
|
||||
function mockHeightBuffer(size: number, step: Function) {
|
||||
const buf = new Uint32Array(size);
|
||||
let acum = 0;
|
||||
for (let i = 0; i < size; i++) {
|
||||
buf[i] = acum;
|
||||
acum += step(i);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
@@ -1,3 +1,12 @@
|
||||
export interface Viewport {
|
||||
top: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
offset: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
export const enum CellType {
|
||||
Item,
|
||||
@@ -5,6 +14,12 @@ export const enum CellType {
|
||||
Footer
|
||||
}
|
||||
|
||||
export const enum NodeChange {
|
||||
NoChange,
|
||||
Position,
|
||||
Cell,
|
||||
}
|
||||
|
||||
export interface Cell {
|
||||
type: CellType;
|
||||
value: any;
|
||||
@@ -18,9 +33,11 @@ export interface Cell {
|
||||
export interface VirtualNode {
|
||||
cell: Cell;
|
||||
top: number;
|
||||
change: number;
|
||||
_d: boolean;
|
||||
change: NodeChange;
|
||||
d: boolean;
|
||||
}
|
||||
const MIN_READS = 2;
|
||||
|
||||
|
||||
export type NodeHeightFn = (node: VirtualNode, index: number) => number;
|
||||
export type HeaderFn = (item: any, index: number, items: any[]) => string | null;
|
||||
@@ -28,47 +45,50 @@ export type ItemHeightFn = (item: any, index?: number) => number;
|
||||
export type ItemRenderFn = (el: HTMLElement|null, cell: Cell, domIndex?: number) => HTMLElement;
|
||||
export type DomRenderFn = (dom: VirtualNode[], height: number) => void;
|
||||
|
||||
export function updateVDom(dom: VirtualNode[], heightIndex: Uint32Array, cells: Cell[], top: number, bottom: number) {
|
||||
export function updateVDom(dom: VirtualNode[], heightIndex: Uint32Array, cells: Cell[], range: Range) {
|
||||
// reset dom
|
||||
for (const node of dom) {
|
||||
node.top = -9999;
|
||||
node.change = 0;
|
||||
node._d = true;
|
||||
// node.top = -9999;
|
||||
node.change = NodeChange.NoChange;
|
||||
node.d = true;
|
||||
}
|
||||
|
||||
// try to match into exisiting dom
|
||||
const toMutate = [];
|
||||
const end = bottom + 1;
|
||||
const end = range.offset + range.length;
|
||||
|
||||
for (let i = top; i < end; i++) {
|
||||
for (let i = range.offset; i < end; i++) {
|
||||
const cell = cells[i];
|
||||
const node = dom.find((n) => n._d && n.cell === cell);
|
||||
const node = dom.find((n) => n.d && n.cell === cell);
|
||||
if (node) {
|
||||
node._d = false;
|
||||
node.change = 1;
|
||||
node.top = heightIndex[i];
|
||||
const top = heightIndex[i];
|
||||
if (top !== node.top) {
|
||||
node.top = top;
|
||||
node.change = NodeChange.Position;
|
||||
}
|
||||
node.d = false;
|
||||
} else {
|
||||
toMutate.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
// needs to append
|
||||
const pool = dom.filter((n) => n._d);
|
||||
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 node = pool.find(n => n.d && n.cell.type === cell.type);
|
||||
const index = cell.index;
|
||||
if (node) {
|
||||
node._d = false;
|
||||
node.change = 2;
|
||||
node.d = false;
|
||||
node.change = NodeChange.Cell;
|
||||
node.cell = cell;
|
||||
node.top = heightIndex[index];
|
||||
} else {
|
||||
dom.push({
|
||||
_d: false,
|
||||
change: 2,
|
||||
d: false,
|
||||
cell: cell,
|
||||
change: NodeChange.Cell,
|
||||
top: heightIndex[index],
|
||||
});
|
||||
}
|
||||
@@ -81,7 +101,9 @@ export function doRender(el: HTMLElement, itemRender: ItemRenderFn, dom: Virtual
|
||||
for (let i = 0; i < dom.length; i++) {
|
||||
const node = dom[i];
|
||||
const cell = node.cell;
|
||||
if (node.change === 2) {
|
||||
|
||||
// the cell change, the content must be updated
|
||||
if (node.change === NodeChange.Cell) {
|
||||
if (i < children.length) {
|
||||
child = children[i] as HTMLElement;
|
||||
itemRender(child, cell, i);
|
||||
@@ -90,11 +112,13 @@ export function doRender(el: HTMLElement, itemRender: ItemRenderFn, dom: Virtual
|
||||
child.classList.add('virtual-item');
|
||||
el.appendChild(child);
|
||||
}
|
||||
(child as any)['$ionCell'] = cell;
|
||||
} else {
|
||||
child = children[i] as HTMLElement;
|
||||
}
|
||||
(child as any)['$ionCell'] = cell;
|
||||
if (node.change !== 0) {
|
||||
|
||||
// only update position when it changes
|
||||
if (node.change !== NodeChange.NoChange) {
|
||||
child.style.transform = `translate3d(0,${node.top}px,0)`;
|
||||
}
|
||||
if (cell.visible) {
|
||||
@@ -109,29 +133,18 @@ export function doRender(el: HTMLElement, itemRender: ItemRenderFn, dom: Virtual
|
||||
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,
|
||||
top: Math.max(scrollTop - margin, 0),
|
||||
bottom: scrollTop + vierportHeight + margin
|
||||
};
|
||||
}
|
||||
|
||||
export function getBounds(heightIndex: Uint32Array, viewport: Viewport, buffer: number) {
|
||||
export function getRange(heightIndex: Uint32Array, viewport: Viewport, buffer: number): Range {
|
||||
const topPos = viewport.top;
|
||||
const bottomPos = viewport.bottom;
|
||||
|
||||
@@ -142,42 +155,96 @@ export function getBounds(heightIndex: Uint32Array, viewport: Viewport, buffer:
|
||||
break;
|
||||
}
|
||||
}
|
||||
const top = Math.max(i - buffer, 0);
|
||||
const offset = Math.max(i - buffer - 1, 0);
|
||||
|
||||
// find bottom index
|
||||
for (; i < heightIndex.length; i++) {
|
||||
if (heightIndex[i] > bottomPos) {
|
||||
if (heightIndex[i] >= bottomPos) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const bottom = Math.min(i + buffer, heightIndex.length - 1);
|
||||
return { top, bottom };
|
||||
const end = Math.min(i + buffer, heightIndex.length - 1);
|
||||
const length = end - offset;
|
||||
return { offset, length };
|
||||
}
|
||||
|
||||
export function getShouldUpdate(dirtyIndex: number, currentTop: number, currentBottom: number, top: number, bottom: number) {
|
||||
export function getShouldUpdate(dirtyIndex: number, currentRange: Range, range: Range) {
|
||||
const end = range.offset + range.length;
|
||||
return (
|
||||
dirtyIndex < bottom ||
|
||||
currentTop !== top ||
|
||||
currentBottom !== bottom
|
||||
dirtyIndex < end ||
|
||||
currentRange.offset !== range.offset ||
|
||||
currentRange.length !== range.length
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function calcHeightIndex(buf: Uint32Array, cells: Cell[], index: number, bottom: number) {
|
||||
if (!cells) {
|
||||
return buf;
|
||||
}
|
||||
buf = resizeBuffer(buf, cells.length);
|
||||
export function calcCells(
|
||||
items: any[],
|
||||
|
||||
itemHeight: ItemHeightFn,
|
||||
headerFn: HeaderFn,
|
||||
footerFn: HeaderFn,
|
||||
|
||||
approxHeaderHeight: number,
|
||||
approxFooterHeight: number,
|
||||
approxItemHeight: number
|
||||
): Cell[] {
|
||||
const cells = [];
|
||||
let j = 0;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (headerFn) {
|
||||
const value = headerFn(item, i, items);
|
||||
if (value != null) {
|
||||
cells.push({
|
||||
i: j++,
|
||||
type: CellType.Header,
|
||||
value: value,
|
||||
index: i,
|
||||
height: approxHeaderHeight,
|
||||
reads: MIN_READS,
|
||||
visible: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cells.push({
|
||||
i: j++,
|
||||
type: CellType.Item,
|
||||
value: item,
|
||||
index: i,
|
||||
height: itemHeight ? itemHeight(item, i) : approxItemHeight,
|
||||
reads: itemHeight ? 0 : MIN_READS,
|
||||
visible: !!itemHeight,
|
||||
});
|
||||
|
||||
if (footerFn) {
|
||||
const value = footerFn(item, i, items);
|
||||
if (value != null) {
|
||||
cells.push({
|
||||
i: j++,
|
||||
type: CellType.Footer,
|
||||
value: value,
|
||||
index: i,
|
||||
height: approxFooterHeight,
|
||||
reads: 2,
|
||||
visible: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
|
||||
export function calcHeightIndex(buf: Uint32Array, cells: Cell[], index: number): number {
|
||||
let acum = buf[index];
|
||||
for (; index < buf.length; index++) {
|
||||
buf[index] = acum;
|
||||
acum += cells[index].height;
|
||||
// if (acum > bottom) {
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
return buf;
|
||||
return acum;
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +254,19 @@ export function resizeBuffer(buf: Uint32Array, len: number) {
|
||||
}
|
||||
if (buf.length === len) {
|
||||
return buf;
|
||||
} else if (len > buf.length) {
|
||||
const newBuf = new Uint32Array(len);
|
||||
newBuf.set(buf);
|
||||
return newBuf;
|
||||
} else {
|
||||
return buf.subarray(0, len);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
export function positionForIndex(index: number, cells: Cell[], heightIndex: Uint32Array): number {
|
||||
const cell = cells.find(cell => cell.type === CellType.Item && cell.index === index);
|
||||
if (cell) {
|
||||
return heightIndex[cell.i];
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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';
|
||||
import { Cell, DomRenderFn, HeaderFn, ItemHeightFn, ItemRenderFn, NodeHeightFn, Range,
|
||||
Viewport, VirtualNode, calcCells, calcHeightIndex, doRender, getRange,
|
||||
getShouldUpdate, getViewport, positionForIndex, resizeBuffer, updateVDom } from './virtual-scroll-utils';
|
||||
|
||||
|
||||
const MIN_READS = 2;
|
||||
|
||||
@Component({
|
||||
tag: 'ion-virtual-scroll',
|
||||
styleUrl: 'virtual-scroll.scss'
|
||||
@@ -13,8 +12,7 @@ const MIN_READS = 2;
|
||||
export class VirtualScroll {
|
||||
|
||||
private scrollEl: HTMLElement;
|
||||
private topIndex = -100;
|
||||
private bottomIndex = -100;
|
||||
private range: Range;
|
||||
private timerUpdate: any;
|
||||
private heightIndex: Uint32Array;
|
||||
private viewportHeight: number;
|
||||
@@ -141,11 +139,7 @@ export class VirtualScroll {
|
||||
|
||||
@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;
|
||||
return positionForIndex(index, this.cells, this.heightIndex);
|
||||
}
|
||||
|
||||
private updateVirtualScroll() {
|
||||
@@ -172,23 +166,21 @@ export class VirtualScroll {
|
||||
const heightIndex = this.getHeightIndex(viewport);
|
||||
|
||||
// get array bounds of visible cells base in the viewport
|
||||
const {top, bottom} = getBounds(heightIndex, viewport, 2);
|
||||
const range = getRange(heightIndex, viewport, 2);
|
||||
|
||||
// fast path, do nothing
|
||||
const shouldUpdate = getShouldUpdate(dirtyIndex, this.topIndex, this.bottomIndex, top, bottom);
|
||||
const shouldUpdate = getShouldUpdate(dirtyIndex, this.range, range);
|
||||
if (!shouldUpdate) {
|
||||
return;
|
||||
}
|
||||
this.topIndex = top;
|
||||
this.bottomIndex = bottom;
|
||||
this.range = range;
|
||||
|
||||
// in place mutation of the virtual DOM
|
||||
updateVDom(
|
||||
this.virtualDom,
|
||||
heightIndex,
|
||||
this.cells,
|
||||
top,
|
||||
bottom);
|
||||
range);
|
||||
|
||||
this.fireDomUpdate();
|
||||
});
|
||||
@@ -202,7 +194,7 @@ export class VirtualScroll {
|
||||
}
|
||||
}
|
||||
|
||||
updateCellHeight(cell: Cell, node: HTMLElement) {
|
||||
private updateCellHeight(cell: Cell, node: HTMLElement) {
|
||||
(node as any).componentOnReady(() => {
|
||||
// let's give some additional time to read the height size
|
||||
setTimeout(() => this.dom.read(() => {
|
||||
@@ -215,7 +207,7 @@ export class VirtualScroll {
|
||||
});
|
||||
}
|
||||
|
||||
setCellHeight(cell: Cell, height: number) {
|
||||
private setCellHeight(cell: Cell, height: number) {
|
||||
const index = cell.i;
|
||||
// the cell might changed since the height update was scheduled
|
||||
if (cell !== this.cells[index]) {
|
||||
@@ -247,74 +239,32 @@ export class VirtualScroll {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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.cells = calcCells(
|
||||
this.items,
|
||||
this.itemHeight,
|
||||
this.headerFn,
|
||||
this.footerFn,
|
||||
this.approxHeaderHeight,
|
||||
this.approxFooterHeight,
|
||||
this.approxItemHeight
|
||||
);
|
||||
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 {
|
||||
private getHeightIndex(_: Viewport): Uint32Array {
|
||||
if (this.indexDirty !== Infinity) {
|
||||
this.calcHeightIndex(this.indexDirty, viewport.bottom);
|
||||
this.calcHeightIndex(this.indexDirty);
|
||||
}
|
||||
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];
|
||||
private calcHeightIndex(index = 0) {
|
||||
this.heightIndex = resizeBuffer(this.heightIndex, this.cells.length);
|
||||
this.totalHeight = calcHeightIndex(this.heightIndex, this.cells, index);
|
||||
this.indexDirty = Infinity;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user