mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-21 13:01:01 +08:00
perf(virtual-scroll): improve virtual-scroll performance
This commit is contained in:
@ -500,10 +500,6 @@ export class Content extends Ion implements AfterViewInit, OnDestroy {
|
||||
removeArrayItem(this._imgs, img);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
/**
|
||||
* @private
|
||||
* DOM WRITE
|
||||
|
35
src/components/img/test/basic/app-module.ts
Normal file
35
src/components/img/test/basic/app-module.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Component, NgModule } from '@angular/core';
|
||||
import { IonicApp, IonicModule } from '../../../..';
|
||||
|
||||
|
||||
@Component({
|
||||
templateUrl: 'main.html'
|
||||
})
|
||||
export class E2EPage {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: '<ion-nav [root]="root"></ion-nav>'
|
||||
})
|
||||
export class E2EApp {
|
||||
root = E2EPage;
|
||||
}
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
E2EApp,
|
||||
E2EPage
|
||||
],
|
||||
imports: [
|
||||
IonicModule.forRoot(E2EApp)
|
||||
],
|
||||
bootstrap: [IonicApp],
|
||||
entryComponents: [
|
||||
E2EApp,
|
||||
E2EPage
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
@ -6,28 +6,47 @@ import { IonicApp, IonicModule } from '../../../..';
|
||||
templateUrl: 'main.html'
|
||||
})
|
||||
export class E2EPage {
|
||||
items: Array<{title: string; id: number}>;
|
||||
items: Array<{id: number, url: string, gif: string}> = [];
|
||||
imgDomain = 'http://localhost:8900';
|
||||
responseDelay = 1500;
|
||||
itemCount = 15;
|
||||
showGifs = false;
|
||||
|
||||
constructor() {
|
||||
// take a look at the gulp task: test.imageserver
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `${this.imgDomain}/reset`, true);
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
this.fillList();
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
fillList() {
|
||||
this.items = [];
|
||||
for (let i = 0; i < 500; i++) {
|
||||
this.items.length = 0;
|
||||
let gifIndex = Math.ceil(Math.random() * gifs.length) - 1;
|
||||
|
||||
for (let i = 0; i < this.itemCount; i++) {
|
||||
this.items.push({
|
||||
title: 'Item ' + i,
|
||||
id: i
|
||||
id: i,
|
||||
url: `${this.imgDomain}/?d=${this.responseDelay}&id=${i}`,
|
||||
gif: gifs[gifIndex]
|
||||
});
|
||||
gifIndex++;
|
||||
if (gifIndex >= gifs.length) {
|
||||
gifIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emptyList() {
|
||||
this.items = [];
|
||||
this.items.length = 0;
|
||||
}
|
||||
|
||||
itemTapped(ev: any, item: {title: string, date: string}) {
|
||||
console.log(`itemTapped: ${item.title}`);
|
||||
toggleGifs() {
|
||||
this.showGifs = !this.showGifs;
|
||||
}
|
||||
|
||||
reload() {
|
||||
@ -36,6 +55,52 @@ export class E2EPage {
|
||||
|
||||
}
|
||||
|
||||
const gifs = [
|
||||
'https://media.giphy.com/media/cFdHXXm5GhJsc/giphy.gif',
|
||||
'https://media.giphy.com/media/5JjLO6t0lNvLq/giphy.gif',
|
||||
'https://media.giphy.com/media/ZmdIZ8K4fKEEM/giphy.gif',
|
||||
'https://media.giphy.com/media/lKXEBR8m1jWso/giphy.gif',
|
||||
'https://media.giphy.com/media/PjplWH49v1FS0/giphy.gif',
|
||||
'https://media.giphy.com/media/SyVyFtBTTVb5m/giphy.gif',
|
||||
'https://media.giphy.com/media/LWqQ5glpSMjny/giphy.gif',
|
||||
'https://media.giphy.com/media/l396Dat26yQOdfWgw/giphy.gif',
|
||||
'https://media.giphy.com/media/zetsDd1oSNd96/giphy.gif',
|
||||
'https://media.giphy.com/media/F6PFPjc3K0CPe/giphy.gif',
|
||||
'https://media.giphy.com/media/L0GJP0ZxdnVbW/giphy.gif',
|
||||
'https://media.giphy.com/media/26ufbLWPFHkhwXcpW/giphy.gif',
|
||||
'https://media.giphy.com/media/r3jTnU6iEwpbO/giphy.gif',
|
||||
'https://media.giphy.com/media/6Xbr4pVmJW4wM/giphy.gif',
|
||||
'https://media.giphy.com/media/FPmzkXGFVhp2U/giphy.gif',
|
||||
'https://media.giphy.com/media/p3yU7Rno2PvvW/giphy.gif',
|
||||
'https://media.giphy.com/media/vbBmb51klyyB2/giphy.gif',
|
||||
'https://media.giphy.com/media/ZAfpXz6fGrlYY/giphy.gif',
|
||||
'https://media.giphy.com/media/3oGRFvVyUdGBZeQiAw/giphy.gif',
|
||||
'https://media.giphy.com/media/NJbeypFZCHj2g/giphy.gif',
|
||||
'https://media.giphy.com/media/WpNO2ZXjhJ85y/giphy.gif',
|
||||
'https://media.giphy.com/media/xaw15bdmMEkgg/giphy.gif',
|
||||
'https://media.giphy.com/media/tLwQSHQo6hjTa/giphy.gif',
|
||||
'https://media.giphy.com/media/3dcoLqDDjd9pC/giphy.gif',
|
||||
'https://media.giphy.com/media/QFfs8ubyDkluo/giphy.gif',
|
||||
'https://media.giphy.com/media/10hYVVSPrSpZS0/giphy.gif',
|
||||
'https://media.giphy.com/media/EYJz9cfMa7WAU/giphy.gif',
|
||||
'https://media.giphy.com/media/Q21vzIHyTtmaQ/giphy.gif',
|
||||
'https://media.giphy.com/media/pzmUOeqhzJTck/giphy.gif',
|
||||
'https://media.giphy.com/media/G6kt1Gb4Luxy0/giphy.gif',
|
||||
'https://media.giphy.com/media/13wjHxAz6B6E9i/giphy.gif',
|
||||
'https://media.giphy.com/media/ANbbM3IzH9Tna/giphy.gif',
|
||||
'https://media.giphy.com/media/EQ5I7NF4BDYA/giphy.gif',
|
||||
'https://media.giphy.com/media/L7gHewOS8GOWY/giphy.gif',
|
||||
'https://media.giphy.com/media/nO16UrmQh7khW/giphy.gif',
|
||||
'https://media.giphy.com/media/eGuk6gQM3Q29W/giphy.gif',
|
||||
'https://media.giphy.com/media/8dpPMMlxmDEJO/giphy.gif',
|
||||
'https://media.giphy.com/media/5ox090BjCB8ME/giphy.gif',
|
||||
'https://media.giphy.com/media/Hzm8c1eMSq3CM/giphy.gif',
|
||||
'https://media.giphy.com/media/2APlzZshLu3LO/giphy.gif',
|
||||
'https://media.giphy.com/media/dgygjvNe7jckw/giphy.gif',
|
||||
'https://media.giphy.com/media/5g0mypSSPupO0/giphy.gif',
|
||||
'https://media.giphy.com/media/10JmxORlA6dEFW/giphy.gif',
|
||||
];
|
||||
|
||||
|
||||
@Component({
|
||||
template: '<ion-nav [root]="root"></ion-nav>'
|
||||
|
@ -15,16 +15,25 @@
|
||||
<div padding>
|
||||
<button ion-button (click)="fillList()">Fill List</button>
|
||||
<button ion-button (click)="emptyList()">Empty List</button>
|
||||
<button ion-button (click)="toggleGifs()">Gifs</button>
|
||||
</div>
|
||||
|
||||
<div padding>
|
||||
<code>gulp test.imageserve</code>
|
||||
</div>
|
||||
|
||||
<ion-list [virtualScroll]="items">
|
||||
|
||||
<ion-item class="item-text-wrap" *virtualItem="let item" (click)="itemTapped(item)">
|
||||
<ion-item class="item-text-wrap" *virtualItem="let item; let itemBounds = bounds;">
|
||||
<ion-thumbnail item-left>
|
||||
<ion-img src="http://loremflickr.com/80/80/dog?{{item.id}}"></ion-img>
|
||||
<ion-img src="{{item.url}}" [bounds]="itemBounds"></ion-img>
|
||||
</ion-thumbnail>
|
||||
|
||||
<h2 class="text-wrap">{{item.title}}</h2>
|
||||
<h2 class="text-wrap">{{item.id}}, top: {{itemBounds.top}}, bottom: {{itemBounds.bottom}}, height: {{itemBounds.height}}</h2>
|
||||
|
||||
<!--<ion-thumbnail item-right>
|
||||
<ion-img src="{{item.gif}}" [bounds]="itemBounds"></ion-img>
|
||||
</ion-thumbnail>-->
|
||||
</ion-item>
|
||||
|
||||
</ion-list>
|
||||
|
@ -289,7 +289,7 @@ describe('VirtualScroll', () => {
|
||||
cells, records, nodes, viewContainer,
|
||||
itmTmp, hdrTmp, ftrTmp, true);
|
||||
|
||||
expect(nodes.length).toBe(6);
|
||||
expect(nodes.length).toBe(3);
|
||||
|
||||
expect(nodes[0].cell).toBe(2);
|
||||
expect(nodes[1].cell).toBe(3);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
import { VirtualContext } from './virtual-util';
|
||||
|
||||
|
||||
/**
|
||||
@ -6,7 +7,7 @@ import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
*/
|
||||
@Directive({selector: '[virtualHeader]'})
|
||||
export class VirtualHeader {
|
||||
constructor(public templateRef: TemplateRef<Object>) {}
|
||||
constructor(public templateRef: TemplateRef<VirtualContext>) {}
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +16,7 @@ export class VirtualHeader {
|
||||
*/
|
||||
@Directive({selector: '[virtualFooter]'})
|
||||
export class VirtualFooter {
|
||||
constructor(public templateRef: TemplateRef<Object>) {}
|
||||
constructor(public templateRef: TemplateRef<VirtualContext>) {}
|
||||
}
|
||||
|
||||
|
||||
@ -24,5 +25,5 @@ export class VirtualFooter {
|
||||
*/
|
||||
@Directive({selector: '[virtualItem]'})
|
||||
export class VirtualItem {
|
||||
constructor(public templateRef: TemplateRef<Object>, public viewContainer: ViewContainerRef) {}
|
||||
constructor(public templateRef: TemplateRef<VirtualContext>, public viewContainer: ViewContainerRef) {}
|
||||
}
|
||||
|
@ -2,6 +2,10 @@
|
||||
// Virtual Scroll
|
||||
// --------------------------------------------------
|
||||
|
||||
.virtual-loading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.virtual-scroll {
|
||||
position: relative;
|
||||
|
||||
@ -20,6 +24,6 @@
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.virtual-scroll .virtual-hidden {
|
||||
.virtual-scroll .virtual-last {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { AfterContentInit, ChangeDetectorRef, ContentChild, ContentChildren, Directive, DoCheck, ElementRef, Input, IterableDiffers, IterableDiffer, NgZone, OnDestroy, Optional, QueryList, Renderer, TrackByFn } from '@angular/core';
|
||||
import { AfterContentInit, ChangeDetectorRef, ContentChild, Directive, DoCheck, ElementRef, Input, IterableDiffers, IterableDiffer, NgZone, OnDestroy, Optional, Renderer, TrackByFn } from '@angular/core';
|
||||
|
||||
import { adjustRendered, calcDimensions, estimateHeight, initReadNodes, processRecords, populateNodeData, updateDimensions, writeToNodes } from './virtual-util';
|
||||
import { clearNativeTimeout, nativeRaf, nativeTimeout } from '../../util/dom';
|
||||
import { adjustRendered, calcDimensions, estimateHeight, initReadNodes, processRecords, populateNodeData, updateDimensions, updateNodeContext, writeToNodes } from './virtual-util';
|
||||
import { Config } from '../../config/config';
|
||||
import { Content } from '../content/content';
|
||||
import { Img } from '../img/img';
|
||||
import { Content, ScrollEvent } from '../content/content';
|
||||
import { DomController } from '../../util/dom-controller';
|
||||
import { isBlank, isFunction, isPresent } from '../../util/util';
|
||||
import { Platform } from '../../platform/platform';
|
||||
import { ViewController } from '../../navigation/view-controller';
|
||||
@ -186,10 +185,9 @@ import { VirtualFooter, VirtualHeader, VirtualItem } from './virtual-item';
|
||||
export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
_trackBy: TrackByFn;
|
||||
_differ: IterableDiffer;
|
||||
_unreg: Function;
|
||||
_scrollSub: any;
|
||||
_scrollEndSub: any;
|
||||
_init: boolean;
|
||||
_rafId: number;
|
||||
_tmId: number;
|
||||
_hdrFn: Function;
|
||||
_ftrFn: Function;
|
||||
_records: any[] = [];
|
||||
@ -200,13 +198,12 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
_data: VirtualData = {
|
||||
scrollTop: 0,
|
||||
};
|
||||
_eventAssist: boolean;
|
||||
_queue: number = null;
|
||||
_queue: ScrollQueue = null;
|
||||
|
||||
@ContentChild(VirtualItem) _itmTmp: VirtualItem;
|
||||
@ContentChild(VirtualHeader) _hdrTmp: VirtualHeader;
|
||||
@ContentChild(VirtualFooter) _ftrTmp: VirtualFooter;
|
||||
@ContentChildren(Img) _imgs: QueryList<Img>;
|
||||
|
||||
|
||||
/**
|
||||
* @input {array} The data that builds the templates within the virtual scroll.
|
||||
@ -338,6 +335,8 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
this._trackBy = val;
|
||||
}
|
||||
|
||||
private _hasUpdate = false;
|
||||
|
||||
constructor(
|
||||
private _iterableDiffers: IterableDiffers,
|
||||
private _elementRef: ElementRef,
|
||||
@ -347,8 +346,76 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
private _content: Content,
|
||||
private _platform: Platform,
|
||||
@Optional() private _ctrl: ViewController,
|
||||
config: Config) {
|
||||
this._eventAssist = config.getBoolean('virtualScrollEventAssist');
|
||||
config: Config,
|
||||
private _dom: DomController) {
|
||||
|
||||
// hide the virtual scroll element with opacity so we don't
|
||||
// see jank as it loads up, but we're still able to read
|
||||
// dimensions because it's still rendered and only opacity hidden
|
||||
this._renderer.setElementClass(_elementRef.nativeElement, 'virtual-loading', true);
|
||||
|
||||
// wait for the content to be rendered and has readable dimensions
|
||||
_content.readReady.subscribe(() => {
|
||||
this.readUpdate(true, true);
|
||||
|
||||
if (!this._scrollSub) {
|
||||
// listen for scroll events
|
||||
this.addScrollListener(config.getBoolean('virtualScrollEventAssist'));
|
||||
}
|
||||
});
|
||||
|
||||
// wait for the content to be writable
|
||||
_content.writeReady.subscribe(() => {
|
||||
this.writeUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
readUpdate(checkDataChanges: boolean, dimensionsUpdated: boolean) {
|
||||
if (!this._records) return;
|
||||
|
||||
if (checkDataChanges && !dimensionsUpdated) {
|
||||
if (isPresent(this._differ) && !isPresent(this._differ.diff(this._records))) {
|
||||
// no changes
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(`virtual-scroll, readUpdate, checkDataChanges: ${checkDataChanges}, dimensionsUpdated: ${dimensionsUpdated}`);
|
||||
|
||||
this._hasUpdate = true;
|
||||
|
||||
// reset everything
|
||||
this._cells.length = 0;
|
||||
this._nodes.length = 0;
|
||||
this._itmTmp.viewContainer.clear();
|
||||
|
||||
// ******** DOM READ ****************
|
||||
calcDimensions(this._data, this._elementRef.nativeElement,
|
||||
this.approxItemWidth, this.approxItemHeight,
|
||||
this.approxHeaderWidth, this.approxHeaderHeight,
|
||||
this.approxFooterWidth, this.approxFooterHeight,
|
||||
this.bufferRatio);
|
||||
|
||||
}
|
||||
|
||||
writeUpdate() {
|
||||
if (!this._hasUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`virtual-scroll, writeUpdate`);
|
||||
|
||||
processRecords(this._data.renderHeight,
|
||||
this._records,
|
||||
this._cells,
|
||||
this._hdrFn,
|
||||
this._ftrFn,
|
||||
this._data);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
this.renderVirtual();
|
||||
|
||||
this._hasUpdate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -356,7 +423,8 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
*/
|
||||
ngDoCheck() {
|
||||
if (this._init) {
|
||||
this.update(true);
|
||||
this.readUpdate(true, false);
|
||||
this.writeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,98 +445,32 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
console.warn('Virtual Scroll: Please provide an "approxItemHeight" input to ensure proper virtual scroll rendering');
|
||||
}
|
||||
|
||||
this.update(true);
|
||||
|
||||
this._platform.onResize(() => {
|
||||
console.debug('VirtualScroll, onResize');
|
||||
this.update(false);
|
||||
});
|
||||
// this.update(true);
|
||||
|
||||
// this._platform.onResize(() => {
|
||||
// console.debug('VirtualScroll, onResize');
|
||||
// this.update(false);
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* DOM READ THEN DOM WRITE
|
||||
*/
|
||||
update(checkChanges: boolean) {
|
||||
const self = this;
|
||||
|
||||
if (!self._records) return;
|
||||
|
||||
if (checkChanges) {
|
||||
if (isPresent(self._differ)) {
|
||||
let changes = self._differ.diff(self._records);
|
||||
if (!isPresent(changes)) return;
|
||||
}
|
||||
}
|
||||
|
||||
console.debug('VirtualScroll, update, records:', self._records.length);
|
||||
|
||||
// reset everything
|
||||
self._cells.length = 0;
|
||||
self._nodes.length = 0;
|
||||
self._itmTmp.viewContainer.clear();
|
||||
self._elementRef.nativeElement.parentElement.scrollTop = 0;
|
||||
|
||||
let attempts = 0;
|
||||
function readDimensions(done: Function/* cuz promises add unnecessary overhead here */) {
|
||||
if (self._data.valid) {
|
||||
// good to go, we already have good dimension data
|
||||
done();
|
||||
|
||||
} else {
|
||||
// ******** DOM READ ****************
|
||||
calcDimensions(self._data, self._elementRef.nativeElement.parentElement,
|
||||
self.approxItemWidth, self.approxItemHeight,
|
||||
self.approxHeaderWidth, self.approxHeaderHeight,
|
||||
self.approxFooterWidth, self.approxFooterHeight,
|
||||
self.bufferRatio);
|
||||
|
||||
if (self._data.valid) {
|
||||
// sweet, we got some good dimension data!
|
||||
done();
|
||||
|
||||
} else if (attempts < 30) {
|
||||
// oh no! the DOM doesn't have good data yet!
|
||||
// let's try again in XXms, and give up eventually if we never get data
|
||||
attempts++;
|
||||
nativeRaf(function() {
|
||||
readDimensions(done);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ******** DOM READ ****************
|
||||
readDimensions(function() {
|
||||
processRecords(self._data.renderHeight,
|
||||
self._records,
|
||||
self._cells,
|
||||
self._hdrFn,
|
||||
self._ftrFn,
|
||||
self._data);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
self.renderVirtual();
|
||||
|
||||
// list for scroll events
|
||||
self.addScrollListener();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* DOM WRITE
|
||||
*/
|
||||
renderVirtual() {
|
||||
// initialize nodes with the correct cell data
|
||||
this._data.topCell = 0;
|
||||
this._data.bottomCell = (this._cells.length - 1);
|
||||
const nodes = this._nodes;
|
||||
const cells = this._cells;
|
||||
const data = this._data;
|
||||
const records = this._records;
|
||||
|
||||
populateNodeData(0, this._data.bottomCell,
|
||||
this._data.viewWidth, true,
|
||||
this._cells, this._records, this._nodes,
|
||||
// initialize nodes with the correct cell data
|
||||
data.topCell = 0;
|
||||
data.bottomCell = (cells.length - 1);
|
||||
|
||||
populateNodeData(0, data.bottomCell,
|
||||
data.viewWidth, true,
|
||||
cells, records, nodes,
|
||||
this._itmTmp.viewContainer,
|
||||
this._itmTmp.templateRef,
|
||||
this._hdrTmp && this._hdrTmp.templateRef,
|
||||
@ -477,56 +479,93 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
// ******** DOM WRITE ****************
|
||||
this._cd.detectChanges();
|
||||
|
||||
|
||||
// at this point, this fn was called from within another
|
||||
// requestAnimationFrame, so the next dom reads/writes within the next frame
|
||||
// wait a frame before trying to read and calculate the dimensions
|
||||
nativeRaf(this.postRenderVirtual.bind(this));
|
||||
}
|
||||
this._dom.read(() => {
|
||||
// ******** DOM READ ****************
|
||||
initReadNodes(nodes, cells, data);
|
||||
});
|
||||
|
||||
/**
|
||||
* @private
|
||||
* DOM READ THEN DOM WRITE
|
||||
*/
|
||||
postRenderVirtual() {
|
||||
// ******** DOM READ THEN DOM WRITE ****************
|
||||
initReadNodes(this._nodes, this._cells, this._data);
|
||||
this._dom.write(() => {
|
||||
const ele = this._elementRef.nativeElement;
|
||||
const recordsLength = records.length;
|
||||
const renderer = this._renderer;
|
||||
|
||||
// update the bound context for each node
|
||||
updateNodeContext(nodes, cells, data);
|
||||
|
||||
// ******** DOM READS ABOVE / DOM WRITES BELOW ****************
|
||||
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, 'virtual-scroll', true);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
writeToNodes(this._nodes, this._cells, this._records.length);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
this.setVirtualHeight(
|
||||
estimateHeight(this._records.length, this._cells[this._cells.length - 1], this._vHeight, 0.25)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
scrollUpdate() {
|
||||
clearNativeTimeout(this._tmId);
|
||||
this._tmId = nativeTimeout(this.onScrollEnd.bind(this), SCROLL_END_TIMEOUT_MS);
|
||||
|
||||
let data = this._data;
|
||||
|
||||
if (this._queue === QUEUE_CHANGE_DETECTION) {
|
||||
// ******** DOM WRITE ****************
|
||||
this._cd.detectChanges();
|
||||
|
||||
// add an element at the end so :last-child css doesn't get messed up
|
||||
// ******** DOM WRITE ****************
|
||||
writeToNodes(this._nodes, this._cells, this._records.length);
|
||||
const lastEle: HTMLElement = renderer.createElement(ele, 'div');
|
||||
lastEle.className = 'virtual-last';
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
renderer.setElementClass(ele, 'virtual-scroll', true);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
renderer.setElementClass(ele, 'virtual-loading', false);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
writeToNodes(nodes, cells, recordsLength);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
this.setVirtualHeight(
|
||||
estimateHeight(this._records.length, this._cells[this._cells.length - 1], this._vHeight, 0.25)
|
||||
estimateHeight(recordsLength, cells[cells.length - 1], this._vHeight, 0.25)
|
||||
);
|
||||
|
||||
this._queue = null;
|
||||
this._content.imgsRefresh();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
scrollUpdate(ev: ScrollEvent) {
|
||||
// there is a queue system so that we can
|
||||
// spread out the work over multiple frames
|
||||
const data = this._data;
|
||||
const cells = this._cells;
|
||||
const nodes = this._nodes;
|
||||
|
||||
// set the scroll top from the scroll event
|
||||
data.scrollTop = ev.scrollTop;
|
||||
|
||||
if (this._queue === ScrollQueue.RequiresDomWrite) {
|
||||
|
||||
this._dom.write(() => {
|
||||
// ******** DOM WRITE ****************
|
||||
writeToNodes(nodes, cells, this._records.length);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
this.setVirtualHeight(
|
||||
estimateHeight(this._records.length, cells[cells.length - 1], this._vHeight, 0.25)
|
||||
);
|
||||
|
||||
// we're done here, good work
|
||||
this._queue = ScrollQueue.NoChanges;
|
||||
});
|
||||
|
||||
} else if (this._queue === ScrollQueue.RequiresChangeDetection) {
|
||||
|
||||
this._dom.write(() => {
|
||||
// we've got work painting do, let's throw it in the
|
||||
// domWrite callback so everyone plays nice
|
||||
// ******** DOM WRITE ****************
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].hasChanges) {
|
||||
(<any>nodes[i].view).detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
// on the next frame we need write to the dom nodes manually
|
||||
this._queue = ScrollQueue.RequiresDomWrite;
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
@ -538,36 +577,31 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
|
||||
if (data.scrollDiff > 0) {
|
||||
// load data we may not have processed yet
|
||||
let stopAtHeight = (data.scrollTop + data.renderHeight);
|
||||
var stopAtHeight = (data.scrollTop + data.renderHeight);
|
||||
|
||||
processRecords(stopAtHeight, this._records, this._cells,
|
||||
processRecords(stopAtHeight, this._records, cells,
|
||||
this._hdrFn, this._ftrFn, data);
|
||||
}
|
||||
|
||||
// ******** DOM READ ****************
|
||||
updateDimensions(this._nodes, this._cells, data, false);
|
||||
updateDimensions(nodes, cells, data, false);
|
||||
|
||||
adjustRendered(this._cells, data);
|
||||
adjustRendered(cells, data);
|
||||
|
||||
let madeChanges = populateNodeData(data.topCell, data.bottomCell,
|
||||
var hasChanges = populateNodeData(data.topCell, data.bottomCell,
|
||||
data.viewWidth, data.scrollDiff > 0,
|
||||
this._cells, this._records, this._nodes,
|
||||
cells, this._records, nodes,
|
||||
this._itmTmp.viewContainer,
|
||||
this._itmTmp.templateRef,
|
||||
this._hdrTmp && this._hdrTmp.templateRef,
|
||||
this._ftrTmp && this._ftrTmp.templateRef, false);
|
||||
|
||||
if (madeChanges) {
|
||||
// do not update images while scrolling
|
||||
this._imgs.forEach(img => {
|
||||
img.enable(false);
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
// queue making updates in the next frame
|
||||
this._queue = QUEUE_CHANGE_DETECTION;
|
||||
this._queue = ScrollQueue.RequiresChangeDetection;
|
||||
|
||||
} else {
|
||||
this._queue = null;
|
||||
// update the bound context for each node
|
||||
updateNodeContext(nodes, cells, data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -578,24 +612,35 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
* @private
|
||||
* DOM WRITE
|
||||
*/
|
||||
onScrollEnd() {
|
||||
// scrolling is done, allow images to be updated now
|
||||
this._imgs.forEach(img => {
|
||||
img.enable(true);
|
||||
});
|
||||
scrollEnd(ev: ScrollEvent) {
|
||||
const nodes = this._nodes;
|
||||
const cells = this._cells;
|
||||
const data = this._data;
|
||||
|
||||
// ******** DOM READ ****************
|
||||
updateDimensions(this._nodes, this._cells, this._data, false);
|
||||
updateDimensions(nodes, cells, data, false);
|
||||
|
||||
adjustRendered(this._cells, this._data);
|
||||
adjustRendered(cells, data);
|
||||
|
||||
// ******** DOM READS ABOVE / DOM WRITES BELOW ****************
|
||||
|
||||
this._dom.write(() => {
|
||||
// update the bound context for each node
|
||||
updateNodeContext(nodes, cells, data);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
this._cd.detectChanges();
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
writeToNodes(nodes, cells, this._records.length);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
this.setVirtualHeight(
|
||||
estimateHeight(this._records.length, this._cells[this._cells.length - 1], this._vHeight, 0.05)
|
||||
estimateHeight(this._records.length, cells[cells.length - 1], this._vHeight, 0.05)
|
||||
);
|
||||
|
||||
this._queue = ScrollQueue.NoChanges;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -616,34 +661,22 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
* @private
|
||||
* NO DOM
|
||||
*/
|
||||
addScrollListener() {
|
||||
let self = this;
|
||||
|
||||
if (!self._unreg) {
|
||||
self._zone.runOutsideAngular(() => {
|
||||
|
||||
function onScroll() {
|
||||
// ******** DOM READ ****************
|
||||
self._data.scrollTop = self._content.getScrollTop();
|
||||
|
||||
// ******** DOM READ THEN DOM WRITE ****************
|
||||
self.scrollUpdate();
|
||||
}
|
||||
|
||||
if (self._eventAssist) {
|
||||
addScrollListener(eventAssist: boolean) {
|
||||
if (eventAssist) {
|
||||
// use JS scrolling for iOS UIWebView
|
||||
// goal is to completely remove this when iOS
|
||||
// fully supports scroll events
|
||||
// listen to JS scroll events
|
||||
self._unreg = self._content.jsScroll(onScroll);
|
||||
|
||||
} else {
|
||||
// listen to native scroll events
|
||||
self._unreg = self._content.addScrollListener(onScroll);
|
||||
this._content.enableJsScroll();
|
||||
}
|
||||
|
||||
this._scrollSub = this._content.ionScroll.subscribe((ev: ScrollEvent) => {
|
||||
this.scrollUpdate(ev);
|
||||
});
|
||||
|
||||
this._scrollEndSub = this._content.ionScrollEnd.subscribe((ev: ScrollEvent) => {
|
||||
this.scrollEnd(ev);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -651,12 +684,16 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
* NO DOM
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._unreg && this._unreg();
|
||||
this._unreg = null;
|
||||
this._scrollSub && this._scrollSub.unsubscribe();
|
||||
this._scrollEndSub && this._scrollEndSub.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const SCROLL_END_TIMEOUT_MS = 140;
|
||||
const SCROLL_DIFFERENCE_MINIMUM = 20;
|
||||
const QUEUE_CHANGE_DETECTION = 0;
|
||||
|
||||
export const enum ScrollQueue {
|
||||
NoChanges,
|
||||
RequiresChangeDetection,
|
||||
RequiresDomWrite
|
||||
}
|
||||
|
@ -129,24 +129,22 @@ function addCell(previousCell: VirtualCell, recordIndex: number, tmpl: number, t
|
||||
*/
|
||||
export function populateNodeData(startCellIndex: number, endCellIndex: number, viewportWidth: number, scrollingDown: boolean,
|
||||
cells: VirtualCell[], records: any[], nodes: VirtualNode[], viewContainer: ViewContainerRef,
|
||||
itmTmp: TemplateRef<Object>, hdrTmp: TemplateRef<Object>, ftrTmp: TemplateRef<Object>,
|
||||
itmTmp: TemplateRef<VirtualContext>, hdrTmp: TemplateRef<VirtualContext>, ftrTmp: TemplateRef<VirtualContext>,
|
||||
initialLoad: boolean): boolean {
|
||||
|
||||
if (!records.length) {
|
||||
const recordsLength = records.length;
|
||||
if (!recordsLength) {
|
||||
nodes.length = 0;
|
||||
// made changes
|
||||
return true;
|
||||
}
|
||||
|
||||
let madeChanges = false;
|
||||
let hasChanges = false;
|
||||
let node: VirtualNode;
|
||||
let availableNode: VirtualNode;
|
||||
let cell: VirtualCell;
|
||||
let isAlreadyRendered: boolean;
|
||||
let lastRecordIndex = (records.length - 1);
|
||||
let viewInsertIndex: number = null;
|
||||
let totalNodes = nodes.length;
|
||||
let templateRef: TemplateRef<any>;
|
||||
let templateRef: TemplateRef<VirtualContext>;
|
||||
startCellIndex = Math.max(startCellIndex, 0);
|
||||
endCellIndex = Math.min(endCellIndex, cells.length - 1);
|
||||
|
||||
@ -165,16 +163,6 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v
|
||||
// first node can only be used by the first cell (css :first-child reasons)
|
||||
// this node is never available to be reused
|
||||
continue;
|
||||
|
||||
} else if (node.isLastRecord) {
|
||||
// very last record, but could be a header/item/footer
|
||||
if (cell.record === lastRecordIndex) {
|
||||
availableNode = nodes[i];
|
||||
availableNode.hidden = false;
|
||||
break;
|
||||
}
|
||||
// this node is for the last record, but not actually the last
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.cell === cellIndex) {
|
||||
@ -215,7 +203,7 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v
|
||||
viewInsertIndex = -1;
|
||||
for (var j = totalNodes - 1; j >= 0; j--) {
|
||||
node = nodes[j];
|
||||
if (node && !node.isLastRecord) {
|
||||
if (node) {
|
||||
viewInsertIndex = viewContainer.indexOf(node.view);
|
||||
break;
|
||||
}
|
||||
@ -231,7 +219,7 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v
|
||||
|
||||
availableNode = {
|
||||
tmpl: cell.tmpl,
|
||||
view: <EmbeddedViewRef<VirtualContext>>viewContainer.createEmbeddedView(
|
||||
view: viewContainer.createEmbeddedView(
|
||||
templateRef,
|
||||
new VirtualContext(null, null, null),
|
||||
viewInsertIndex
|
||||
@ -247,63 +235,31 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v
|
||||
// apply the cell's data to this node
|
||||
availableNode.view.context.$implicit = cell.data || records[cell.record];
|
||||
availableNode.view.context.index = cellIndex;
|
||||
availableNode.view.context.count = recordsLength;
|
||||
availableNode.hasChanges = true;
|
||||
availableNode.lastTransform = null;
|
||||
madeChanges = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (initialLoad) {
|
||||
// add nodes that go at the very end, and only represent the last record
|
||||
let lastNodeTempData: any = (records[lastRecordIndex] || {});
|
||||
addLastNodes(nodes, viewContainer, TEMPLATE_HEADER, hdrTmp, lastNodeTempData);
|
||||
addLastNodes(nodes, viewContainer, TEMPLATE_ITEM, itmTmp, lastNodeTempData);
|
||||
addLastNodes(nodes, viewContainer, TEMPLATE_FOOTER, ftrTmp, lastNodeTempData);
|
||||
}
|
||||
|
||||
return madeChanges;
|
||||
}
|
||||
|
||||
|
||||
function addLastNodes(nodes: VirtualNode[], viewContainer: ViewContainerRef,
|
||||
templateType: number, templateRef: TemplateRef<Object>, temporaryData: any) {
|
||||
if (templateRef) {
|
||||
let node: VirtualNode = {
|
||||
tmpl: templateType,
|
||||
view: <EmbeddedViewRef<VirtualContext>>viewContainer.createEmbeddedView(templateRef),
|
||||
isLastRecord: true,
|
||||
hidden: true,
|
||||
};
|
||||
node.view.context.$implicit = temporaryData;
|
||||
nodes.push(node);
|
||||
}
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DOM READ THEN DOM WRITE
|
||||
* DOM READ
|
||||
*/
|
||||
export function initReadNodes(nodes: VirtualNode[], cells: VirtualCell[], data: VirtualData) {
|
||||
if (nodes.length && cells.length) {
|
||||
// first node
|
||||
// ******** DOM READ ****************
|
||||
let firstEle = getElement(nodes[0]);
|
||||
cells[0].top = firstEle.clientTop;
|
||||
cells[0].left = firstEle.clientLeft;
|
||||
cells[0].row = 0;
|
||||
var ele = getElement(nodes[0]);
|
||||
var firstCell = cells[0];
|
||||
firstCell.top = ele.clientTop;
|
||||
firstCell.left = ele.clientLeft;
|
||||
firstCell.row = 0;
|
||||
|
||||
// ******** DOM READ ****************
|
||||
updateDimensions(nodes, cells, data, true);
|
||||
|
||||
|
||||
// ******** DOM READS ABOVE / DOM WRITES BELOW ****************
|
||||
|
||||
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].hidden) {
|
||||
// ******** DOM WRITE ****************
|
||||
getElement(nodes[i]).classList.add('virtual-hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,17 +269,17 @@ export function initReadNodes(nodes: VirtualNode[], cells: VirtualCell[], data:
|
||||
*/
|
||||
export function updateDimensions(nodes: VirtualNode[], cells: VirtualCell[], data: VirtualData, initialUpdate: boolean) {
|
||||
let node: VirtualNode;
|
||||
let element: HTMLElement;
|
||||
let totalCells = cells.length;
|
||||
let element: VirtualHtmlElement;
|
||||
let cell: VirtualCell;
|
||||
let previousCell: VirtualCell;
|
||||
const totalCells = cells.length;
|
||||
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
node = nodes[i];
|
||||
cell = cells[node.cell];
|
||||
|
||||
// read element dimensions if they haven't been checked enough times
|
||||
if (cell && cell.reads < REQUIRED_DOM_READS && !node.hidden) {
|
||||
if (cell && cell.reads < REQUIRED_DOM_READS) {
|
||||
element = getElement(node);
|
||||
|
||||
// ******** DOM READ ****************
|
||||
@ -353,10 +309,11 @@ export function updateDimensions(nodes: VirtualNode[], cells: VirtualCell[], dat
|
||||
|
||||
cell.reads++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// figure out which cells are currently viewable within the viewport
|
||||
let viewableBottom = (data.scrollTop + data.viewHeight);
|
||||
const viewableBottom = (data.scrollTop + data.viewHeight);
|
||||
data.topViewCell = totalCells;
|
||||
data.bottomViewCell = 0;
|
||||
|
||||
@ -386,15 +343,42 @@ export function updateDimensions(nodes: VirtualNode[], cells: VirtualCell[], dat
|
||||
data.bottomViewCell = i;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function updateNodeContext(nodes: VirtualNode[], cells: VirtualCell[], data: VirtualData) {
|
||||
// ensure each node has the correct bounds in its context
|
||||
let node: VirtualNode;
|
||||
let cell: VirtualCell;
|
||||
let bounds: VirtualBounds;
|
||||
|
||||
for (var i = 0, ilen = nodes.length; i < ilen; i++) {
|
||||
node = nodes[i];
|
||||
cell = cells[node.cell];
|
||||
|
||||
if (node && cell) {
|
||||
bounds = node.view.context.bounds;
|
||||
|
||||
bounds.top = cell.top + data.viewTop;
|
||||
bounds.bottom = bounds.top + cell.height;
|
||||
|
||||
bounds.left = cell.left + data.viewLeft;
|
||||
bounds.right = bounds.left + cell.width;
|
||||
|
||||
bounds.width = cell.width;
|
||||
bounds.height = cell.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DOM READ
|
||||
*/
|
||||
function readElements(cell: VirtualCell, element: HTMLElement) {
|
||||
function readElements(cell: VirtualCell, element: VirtualHtmlElement) {
|
||||
// ******** DOM READ ****************
|
||||
let styles = window.getComputedStyle(element);
|
||||
const styles = window.getComputedStyle(<any>element);
|
||||
|
||||
// ******** DOM READ ****************
|
||||
cell.left = (element.offsetLeft - parseFloat(styles.marginLeft));
|
||||
@ -412,15 +396,13 @@ function readElements(cell: VirtualCell, element: HTMLElement) {
|
||||
*/
|
||||
export function writeToNodes(nodes: VirtualNode[], cells: VirtualCell[], totalRecords: number) {
|
||||
let node: VirtualNode;
|
||||
let element: HTMLElement;
|
||||
let element: VirtualHtmlElement;
|
||||
let cell: VirtualCell;
|
||||
let totalCells = Math.max(totalRecords, cells.length).toString();
|
||||
let transform: string;
|
||||
const totalCells = Math.max(totalRecords, cells.length);
|
||||
|
||||
for (var i = 0, ilen = nodes.length; i < ilen; i++) {
|
||||
node = nodes[i];
|
||||
|
||||
if (!node.hidden) {
|
||||
cell = cells[node.cell];
|
||||
|
||||
transform = `translate3d(${cell.left}px,${cell.top}px,0px)`;
|
||||
@ -430,20 +412,14 @@ export function writeToNodes(nodes: VirtualNode[], cells: VirtualCell[], totalRe
|
||||
|
||||
if (element) {
|
||||
// ******** DOM WRITE ****************
|
||||
(<any>element.style)[CSS.transform] = node.lastTransform = transform;
|
||||
element.style[CSS.transform] = node.lastTransform = transform;
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
element.classList.add('virtual-position');
|
||||
|
||||
if (node.isLastRecord) {
|
||||
// its the last record, now with data and safe to show
|
||||
// ******** DOM WRITE ****************
|
||||
element.classList.remove('virtual-hidden');
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/wai-aria/states_and_properties#aria-posinset
|
||||
// ******** DOM WRITE ****************
|
||||
element.setAttribute('aria-posinset', (node.cell + 1).toString());
|
||||
element.setAttribute('aria-posinset', node.cell + 1);
|
||||
|
||||
// https://www.w3.org/TR/wai-aria/states_and_properties#aria-setsize
|
||||
// ******** DOM WRITE ****************
|
||||
@ -452,7 +428,6 @@ export function writeToNodes(nodes: VirtualNode[], cells: VirtualCell[], totalRe
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@ -555,19 +530,30 @@ export function estimateHeight(totalRecords: number, lastCell: VirtualCell, exis
|
||||
* DOM READ
|
||||
*/
|
||||
export function calcDimensions(data: VirtualData,
|
||||
viewportElement: HTMLElement,
|
||||
virtualScrollElement: HTMLElement,
|
||||
approxItemWidth: string, approxItemHeight: string,
|
||||
appoxHeaderWidth: string, approxHeaderHeight: string,
|
||||
approxFooterWidth: string, approxFooterHeight: string,
|
||||
bufferRatio: number) {
|
||||
|
||||
// get the parent container's viewport height
|
||||
// get the parent container's viewport bounds
|
||||
const viewportElement = virtualScrollElement.parentElement;
|
||||
|
||||
// ******** DOM READ ****************
|
||||
data.viewWidth = viewportElement.offsetWidth;
|
||||
|
||||
// ******** DOM READ ****************
|
||||
data.viewHeight = viewportElement.offsetHeight;
|
||||
|
||||
|
||||
// get the virtual scroll element's offset data
|
||||
// ******** DOM READ ****************
|
||||
data.viewTop = virtualScrollElement.offsetTop;
|
||||
|
||||
// ******** DOM READ ****************
|
||||
data.viewLeft = virtualScrollElement.offsetLeft;
|
||||
|
||||
|
||||
// the height we'd like to render, which is larger than viewable
|
||||
data.renderHeight = (data.viewHeight * bufferRatio);
|
||||
|
||||
@ -614,8 +600,8 @@ function calcHeight(viewportHeight: number, approxHeight: string): number {
|
||||
/**
|
||||
* NO DOM
|
||||
*/
|
||||
function getElement(node: VirtualNode): HTMLElement {
|
||||
let rootNodes = node.view.rootNodes;
|
||||
function getElement(node: VirtualNode): VirtualHtmlElement {
|
||||
const rootNodes = node.view.rootNodes;
|
||||
for (var i = 0; i < rootNodes.length; i++) {
|
||||
if (rootNodes[i].nodeType === 1) {
|
||||
return rootNodes[i];
|
||||
@ -625,6 +611,23 @@ function getElement(node: VirtualNode): HTMLElement {
|
||||
}
|
||||
|
||||
|
||||
export interface VirtualHtmlElement {
|
||||
clientTop: number;
|
||||
clientLeft: number;
|
||||
offsetTop: number;
|
||||
offsetLeft: number;
|
||||
offsetWidth: number;
|
||||
offsetHeight: number;
|
||||
style: any;
|
||||
classList: {
|
||||
add: {(name: string)};
|
||||
remove: {(name: string)};
|
||||
};
|
||||
setAttribute: {(name: string, value: any)};
|
||||
parentElement: VirtualHtmlElement;
|
||||
}
|
||||
|
||||
|
||||
// could be either record data or divider data
|
||||
export interface VirtualCell {
|
||||
record?: number;
|
||||
@ -644,13 +647,13 @@ export interface VirtualNode {
|
||||
cell?: number;
|
||||
tmpl: number;
|
||||
view: EmbeddedViewRef<VirtualContext>;
|
||||
isLastRecord?: boolean;
|
||||
hidden?: boolean;
|
||||
hasChanges?: boolean;
|
||||
lastTransform?: string;
|
||||
}
|
||||
|
||||
export class VirtualContext {
|
||||
bounds: VirtualBounds = {};
|
||||
|
||||
constructor(public $implicit: any, public index: number, public count: number) {}
|
||||
|
||||
get first(): boolean { return this.index === 0; }
|
||||
@ -660,12 +663,23 @@ export class VirtualContext {
|
||||
get even(): boolean { return this.index % 2 === 0; }
|
||||
|
||||
get odd(): boolean { return !this.even; }
|
||||
|
||||
}
|
||||
|
||||
export interface VirtualBounds {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface VirtualData {
|
||||
scrollTop?: number;
|
||||
scrollDiff?: number;
|
||||
viewTop?: number;
|
||||
viewLeft?: number;
|
||||
viewWidth?: number;
|
||||
viewHeight?: number;
|
||||
renderHeight?: number;
|
||||
|
Reference in New Issue
Block a user