Files

297 lines
7.3 KiB
TypeScript

import { Cell, HeaderFn, ItemHeightFn, ItemRenderFn, VirtualNode } from '../../interface';
import { CELL_TYPE_FOOTER, CELL_TYPE_HEADER, CELL_TYPE_ITEM, NODE_CHANGE_CELL, NODE_CHANGE_NONE, NODE_CHANGE_POSITION } from './constants';
import { CellType } from './virtual-scroll-interface';
export interface Viewport {
top: number;
bottom: number;
}
export interface Range {
offset: number;
length: number;
}
const MIN_READS = 2;
export function updateVDom(dom: VirtualNode[], heightIndex: Uint32Array, cells: Cell[], range: Range) {
// reset dom
for (const node of dom) {
node.change = NODE_CHANGE_NONE;
node.d = true;
}
// try to match into exisiting dom
const toMutate = [];
const end = range.offset + range.length;
for (let i = range.offset; i < end; i++) {
const cell = cells[i];
const node = dom.find(n => n.d && n.cell === cell);
if (node) {
const top = heightIndex[i];
if (top !== node.top) {
node.top = top;
node.change = NODE_CHANGE_POSITION;
}
node.d = false;
} else {
toMutate.push(cell);
}
}
// needs to append
const pool = dom.filter(n => n.d);
for (const cell of toMutate) {
const node = pool.find(n => n.d && n.cell.type === cell.type);
const index = cell.i;
if (node) {
node.d = false;
node.change = NODE_CHANGE_CELL;
node.cell = cell;
node.top = heightIndex[index];
} else {
dom.push({
d: false,
cell,
visible: true,
change: NODE_CHANGE_CELL,
top: heightIndex[index],
});
}
}
dom
.filter(n => n.d && n.top !== -9999)
.forEach(n => {
n.change = NODE_CHANGE_POSITION;
n.top = -9999;
});
}
export function doRender(
el: HTMLElement,
nodeRender: ItemRenderFn,
dom: VirtualNode[],
updateCellHeight: (cell: Cell, node: HTMLElement) => void
) {
const children = Array.from(el.children).filter(n => n.tagName !== 'TEMPLATE');
const childrenNu = children.length;
let child: HTMLElement;
for (let i = 0; i < dom.length; i++) {
const node = dom[i];
const cell = node.cell;
// the cell change, the content must be updated
if (node.change === NODE_CHANGE_CELL) {
if (i < childrenNu) {
child = children[i] as HTMLElement;
nodeRender(child, cell, i);
} else {
const newChild = createNode(el, cell.type);
child = nodeRender(newChild, cell, i) || newChild;
child.classList.add('virtual-item');
el.appendChild(child!);
}
(child as any)['$ionCell'] = cell;
} else {
child = children[i] as HTMLElement;
}
// only update position when it changes
if (node.change !== NODE_CHANGE_NONE) {
child.style.transform = `translate3d(0,${node.top}px,0)`;
}
// update visibility
const visible = cell.visible;
if (node.visible !== visible) {
if (visible) {
child.classList.remove('virtual-loading');
} else {
child.classList.add('virtual-loading');
}
node.visible = visible;
}
// dynamic height
if (cell.reads > 0) {
updateCellHeight(cell, child);
cell.reads--;
}
}
}
function createNode(el: HTMLElement, type: CellType): HTMLElement | null {
const template = getTemplate(el, type);
if (template && el.ownerDocument) {
return el.ownerDocument.importNode(template.content, true).children[0] as HTMLElement;
}
return null;
}
function getTemplate(el: HTMLElement, type: CellType): HTMLTemplateElement | null {
switch (type) {
case CELL_TYPE_ITEM: return el.querySelector('template:not([name])');
case CELL_TYPE_HEADER: return el.querySelector('template[name=header]');
case CELL_TYPE_FOOTER: return el.querySelector('template[name=footer]');
}
}
export function getViewport(scrollTop: number, vierportHeight: number, margin: number): Viewport {
return {
top: Math.max(scrollTop - margin, 0),
bottom: scrollTop + vierportHeight + margin
};
}
export function getRange(heightIndex: Uint32Array, viewport: Viewport, buffer: number): Range {
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 offset = Math.max(i - buffer - 1, 0);
// find bottom index
for (; i < heightIndex.length; i++) {
if (heightIndex[i] >= bottomPos) {
break;
}
}
const end = Math.min(i + buffer, heightIndex.length);
const length = end - offset;
return { offset, length };
}
export function getShouldUpdate(dirtyIndex: number, currentRange: Range, range: Range) {
const end = range.offset + range.length;
return (
dirtyIndex <= end ||
currentRange.offset !== range.offset ||
currentRange.length !== range.length
);
}
export function findCellIndex(cells: Cell[], index: number): number {
const max = cells.length > 0 ? cells[cells.length - 1].index : 0;
if (index === 0) {
return 0;
} else if (index === max + 1) {
return cells.length;
} else {
return cells.findIndex(c => c.index === index);
}
}
export function inplaceUpdate(dst: Cell[], src: Cell[], offset: number) {
if (offset === 0 && src.length >= dst.length) {
return src;
}
for (let i = 0; i < src.length; i++) {
dst[i + offset] = src[i];
}
return dst;
}
export function calcCells(
items: any[],
itemHeight: ItemHeightFn | undefined,
headerFn: HeaderFn | undefined,
footerFn: HeaderFn | undefined,
approxHeaderHeight: number,
approxFooterHeight: number,
approxItemHeight: number,
j: number,
offset: number,
len: number
): Cell[] {
const cells: Cell[] = [];
const end = len + offset;
for (let i = offset; i < end; i++) {
const item = items[i];
if (headerFn) {
const value = headerFn(item, i, items);
if (value != null) {
cells.push({
i: j++,
type: CELL_TYPE_HEADER,
value,
index: i,
height: approxHeaderHeight,
reads: MIN_READS,
visible: false,
});
}
}
cells.push({
i: j++,
type: CELL_TYPE_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: CELL_TYPE_FOOTER,
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 (let i = index; i < buf.length; i++) {
buf[i] = acum;
acum += cells[i].height;
}
return acum;
}
export function resizeBuffer(buf: Uint32Array | undefined, len: number) {
if (!buf) {
return new Uint32Array(len);
}
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);
}
}
export function positionForIndex(index: number, cells: Cell[], heightIndex: Uint32Array): number {
const cell = cells.find(c => c.type === CELL_TYPE_ITEM && c.index === index);
if (cell) {
return heightIndex[cell.i];
}
return -1;
}