diff --git a/src/components/img/img.scss b/src/components/img/img.scss index 383661f4b8d..ce2f7ddda94 100644 --- a/src/components/img/img.scss +++ b/src/components/img/img.scss @@ -5,11 +5,18 @@ ion-img { position: relative; - display: block; + display: flex; + overflow: hidden; + + align-items: center; + justify-content: center; } ion-img img { - display: block; + flex-shrink: 0; + + min-width: 100%; + min-height: 100%; } ion-img .img-placeholder { diff --git a/src/components/img/img.ts b/src/components/img/img.ts index bba07bd926e..b4a49917ccb 100644 --- a/src/components/img/img.ts +++ b/src/components/img/img.ts @@ -1,4 +1,4 @@ -import {Component, Input, ElementRef, ChangeDetectionStrategy, ViewEncapsulation, NgZone} from '@angular/core'; +import {Component, Input, HostBinding, ElementRef, ChangeDetectionStrategy, ViewEncapsulation, NgZone} from '@angular/core'; import {nativeRaf} from '../../util/dom'; import {isPresent} from '../../util/util'; @@ -19,6 +19,7 @@ export class Img { private _w: string; private _h: string; private _enabled: boolean = true; + private _init: boolean; constructor(private _elementRef: ElementRef, private _platform: Platform, private _zone: NgZone) {} @@ -30,11 +31,18 @@ export class Img { this._src = isPresent(val) ? val : ''; this._normalizeSrc = tmpImg.src; + if (this._init) { + this._update(); + } + } + + ngOnInit() { + this._init = true; this._update(); } private _update() { - if (this._enabled && this._src !== '' && this.isVisible()) { + if (this._enabled && this._src !== '') { // actively update the image for (var i = this._imgs.length - 1; i >= 0; i--) { @@ -56,8 +64,15 @@ export class Img { if (!this._imgs.length) { this._zone.runOutsideAngular(() => { let img = new Image(); - img.style.width = this._w; - img.style.height = this._h; + img.style.width = this._width; + img.style.height = this._height; + + if (isPresent(this.alt)) { + img.alt = this.alt; + } + if (isPresent(this.title)) { + img.title = this.title; + } img.addEventListener('load', () => { if (img.src === this._normalizeSrc) { @@ -92,19 +107,45 @@ export class Img { this._update(); } - isVisible() { - let bounds = this._elementRef.nativeElement.getBoundingClientRect(); - return bounds.bottom > 0 && bounds.top < this._platform.height(); - } - @Input() set width(val: string | number) { - this._w = (typeof val === 'number') ? val + 'px' : val; + this._w = getUnitValue(val); } @Input() set height(val: string | number) { - this._h = (typeof val === 'number') ? val + 'px' : val; + this._h = getUnitValue(val); + } + + @Input() alt: string; + + @Input() title: string; + + @HostBinding('style.width') + get _width(): string { + return isPresent(this._w) ? this._w : ''; + } + + @HostBinding('style.height') + get _height(): string { + return isPresent(this._h) ? this._h : ''; } } + +function getUnitValue(val: any): string { + if (isPresent(val)) { + if (typeof val === 'string') { + if (val.indexOf('%') > -1 || val.indexOf('px') > -1) { + return val; + } + if (val.length) { + return val + 'px'; + } + + } else if (typeof val === 'number') { + return val + 'px'; + } + } + return ''; +} \ No newline at end of file diff --git a/src/components/virtual-scroll/test/basic/index.ts b/src/components/virtual-scroll/test/basic/index.ts index 0e2196f2d4e..49b8b08dbff 100644 --- a/src/components/virtual-scroll/test/basic/index.ts +++ b/src/components/virtual-scroll/test/basic/index.ts @@ -53,4 +53,6 @@ class E2EApp { root = E2EPage; } -ionicBootstrap(E2EApp); +ionicBootstrap(E2EApp, null, { + prodMode: true +}); diff --git a/src/components/virtual-scroll/test/cards/index.ts b/src/components/virtual-scroll/test/cards/index.ts index ce70d7bf3bf..1684c3ec5ed 100644 --- a/src/components/virtual-scroll/test/cards/index.ts +++ b/src/components/virtual-scroll/test/cards/index.ts @@ -7,14 +7,15 @@ import {ionicBootstrap} from '../../../../../src'; encapsulation: ViewEncapsulation.None }) class E2EPage { - items = []; + items: any[] = []; constructor() { - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 1000; i++) { this.items.push({ - imgSrc: `../../img/img/${images[rotateImg]}.jpg?${Math.random()}`, - imgHeight: Math.floor((Math.random() * 50) + 150), name: i + ' - ' + images[rotateImg], + imgSrc: getImgSrc(), + avatarSrc: getImgSrc(), + imgHeight: Math.floor((Math.random() * 50) + 150), content: lorem.substring(0, (Math.random() * (lorem.length - 100)) + 100) }); @@ -33,7 +34,9 @@ class E2EApp { root = E2EPage; } -ionicBootstrap(E2EApp); +ionicBootstrap(E2EApp, null, { + prodMode: true +}); 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.'; @@ -50,4 +53,11 @@ const images = [ 'mirth-mobile', ]; +function getImgSrc() { + let src = `../../img/img/${images[rotateImg]}.jpg?${Math.round(Math.random() * 10000000)}`; + rotateImg++; + if (rotateImg === images.length) rotateImg = 0; + return src; +} + let rotateImg = 0; diff --git a/src/components/virtual-scroll/test/cards/main.html b/src/components/virtual-scroll/test/cards/main.html index 400ebd042fe..9ed993421bc 100644 --- a/src/components/virtual-scroll/test/cards/main.html +++ b/src/components/virtual-scroll/test/cards/main.html @@ -7,12 +7,12 @@
- +
- +

{{ item.name }}

diff --git a/src/components/virtual-scroll/test/image-gallery/index.ts b/src/components/virtual-scroll/test/image-gallery/index.ts index 7dbd1c14194..d74059d606e 100644 --- a/src/components/virtual-scroll/test/image-gallery/index.ts +++ b/src/components/virtual-scroll/test/image-gallery/index.ts @@ -80,7 +80,9 @@ class E2EApp { root = E2EPage; } -ionicBootstrap(E2EApp); +ionicBootstrap(E2EApp, null, { + prodMode: true +}); var monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; diff --git a/src/components/virtual-scroll/test/variable-size/index.ts b/src/components/virtual-scroll/test/variable-size/index.ts index d2b84ac3edf..c4a4d589863 100644 --- a/src/components/virtual-scroll/test/variable-size/index.ts +++ b/src/components/virtual-scroll/test/variable-size/index.ts @@ -6,7 +6,7 @@ import {ionicBootstrap} from '../../../../../src'; templateUrl: 'main.html' }) class E2EPage { - items = []; + items: any[] = []; constructor() { @@ -21,7 +21,7 @@ class E2EPage { } } - headerFn(record, recordIndex) { + headerFn(record: any, recordIndex: number) { if (recordIndex > 0 && recordIndex % 100 === 0) { return recordIndex; } @@ -38,4 +38,6 @@ class E2EApp { root = E2EPage; } -ionicBootstrap(E2EApp); +ionicBootstrap(E2EApp, null, { + prodMode: true +}); diff --git a/src/components/virtual-scroll/test/virtual-scroll.spec.ts b/src/components/virtual-scroll/test/virtual-scroll.spec.ts index 5f8e85c434d..6d3b8a71225 100644 --- a/src/components/virtual-scroll/test/virtual-scroll.spec.ts +++ b/src/components/virtual-scroll/test/virtual-scroll.spec.ts @@ -75,11 +75,11 @@ describe('VirtualScroll', () => { data.hdrWidth = data.viewWidth; // 100%, 1 per row data.ftrWidth = data.viewWidth; // 100%, 1 per row - headerFn = function(record) { + headerFn = function(record: any) { return (record === 0) ? 'Header' : null; }; - footerFn = function(record) { + footerFn = function(record: any) { return (record === 4) ? 'Footer' : null; }; @@ -159,7 +159,7 @@ describe('VirtualScroll', () => { data.itmWidth = 90; // 2 per row data.hdrWidth = data.viewWidth; // 100%, 1 per row - headerFn = function(record) { + headerFn = function(record: any) { return (record === 0) ? 'Header' : null; }; @@ -267,10 +267,10 @@ describe('VirtualScroll', () => { let endCellIndex = 4; populateNodeData(startCellIndex, endCellIndex, data.viewWidth, true, - cells, records, nodes, viewContainer, - itmTmp, hdrTmp, ftrTmp, true); + cells, records, nodes, viewContainer, + itmTmp, hdrTmp, ftrTmp, true); - expect(nodes.length).toBe(3); + expect(nodes.length).toBe(6); expect(nodes[0].cell).toBe(2); expect(nodes[1].cell).toBe(3); @@ -522,9 +522,9 @@ describe('VirtualScroll', () => { let headerFn: Function; let footerFn: Function; let data: VirtualData; - let itmTmp = null; - let hdrTmp = null; - let ftrTmp = null; + let itmTmp: any = {}; + let hdrTmp: any = {}; + let ftrTmp: any = {}; let viewContainer: any = { createEmbeddedView: function() { return getView(); diff --git a/src/components/virtual-scroll/virtual-scroll.ts b/src/components/virtual-scroll/virtual-scroll.ts index 285a0fc8ddc..0a46ded0d2f 100644 --- a/src/components/virtual-scroll/virtual-scroll.ts +++ b/src/components/virtual-scroll/virtual-scroll.ts @@ -1,15 +1,15 @@ -import {Directive, ContentChild, ContentChildren, QueryList, IterableDiffers, IterableDiffer, TrackByFn, Input, Optional, Renderer, ElementRef, ChangeDetectorRef, NgZone, TemplateRef, ViewContainerRef, DoCheck, AfterContentInit, OnDestroy} from '@angular/core'; +import {Directive, ContentChild, ContentChildren, QueryList, IterableDiffers, IterableDiffer, TrackByFn, Input, Optional, Renderer, ElementRef, ChangeDetectorRef, NgZone, DoCheck, AfterContentInit, OnDestroy} from '@angular/core'; +import {adjustRendered, calcDimensions, estimateHeight, initReadNodes, processRecords, populateNodeData, updateDimensions, writeToNodes} from './virtual-util'; import {Config} from '../../config/config'; import {Content} from '../content/content'; +import {Img} from '../img/img'; +import {isBlank, isPresent, isFunction} from '../../util/util'; +import {nativeRaf, nativeTimeout, clearNativeTimeout} from '../../util/dom'; import {Platform} from '../../platform/platform'; import {ViewController} from '../nav/view-controller'; -import {VirtualItem, VirtualHeader, VirtualFooter} from './virtual-item'; -import {VirtualCell, VirtualNode, VirtualData} from './virtual-util'; -import {processRecords, populateNodeData, initReadNodes, writeToNodes, updateDimensions, adjustRendered, calcDimensions, estimateHeight} from './virtual-util'; -import {isBlank, isPresent, isFunction} from '../../util/util'; -import {rafFrames, nativeRaf, cancelRaf, pointerCoord, nativeTimeout, clearNativeTimeout} from '../../util/dom'; -import {Img} from '../img/img'; +import {VirtualCell, VirtualData, VirtualNode} from './virtual-util'; +import {VirtualFooter, VirtualHeader, VirtualItem} from './virtual-item'; /** @@ -410,9 +410,6 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { // ******** DOM WRITE **************** self.renderVirtual(); - // ******** DOM WRITE **************** - self._renderer.setElementClass(self._elementRef.nativeElement, 'virtual-scroll', true); - // list for scroll events self.addScrollListener(); }); @@ -436,7 +433,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { this._ftrTmp && this._ftrTmp.templateRef, true); // ******** DOM WRITE **************** - this.detectChanges(); + this._cd.detectChanges(); // wait a frame before trying to read and calculate the dimensions nativeRaf(this.postRenderVirtual.bind(this)); @@ -447,13 +444,6 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { * DOM READ THEN DOM WRITE */ postRenderVirtual() { - // ******** DOM READ **************** - calcDimensions(this._data, this._elementRef.nativeElement.parentElement, - this.approxItemWidth, this.approxItemHeight, - this.approxHeaderWidth, this.approxHeaderHeight, - this.approxFooterWidth, this.approxFooterHeight, - this.bufferRatio); - // ******** DOM READ THEN DOM WRITE **************** initReadNodes(this._nodes, this._cells, this._data); @@ -461,6 +451,9 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { // ******** 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); @@ -470,20 +463,6 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { ); } - /** - * @private - */ - detectChanges() { - let node: VirtualNode; - for (var i = 0; i < this._nodes.length; i++) { - node = this._nodes[i]; - if (node.hasChanges) { - node.view['detectChanges'](); - node.hasChanges = false; - } - } - } - /** * @private */ @@ -495,34 +474,23 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { if (this._queue === QUEUE_CHANGE_DETECTION) { // ******** DOM WRITE **************** - this.detectChanges(); - - if (this._eventAssist) { - // queue updating node positions in the next frame - this._queue = QUEUE_WRITE_TO_NODES; + this._cd.detectChanges(); - } else { - // update node positions right now - // ******** DOM WRITE **************** - writeToNodes(this._nodes, this._cells, this._records.length); - this._queue = null; - } + // ******** 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) ); - } else if (this._queue === QUEUE_WRITE_TO_NODES) { - // ******** DOM WRITE **************** - writeToNodes(this._nodes, this._cells, this._records.length); this._queue = null; } else { data.scrollDiff = (data.scrollTop - this._lastCheck); - if (Math.abs(data.scrollDiff) > 10) { + if (Math.abs(data.scrollDiff) > SCROLL_DIFFERENCE_MINIMUM) { // don't bother updating if the scrollTop hasn't changed much this._lastCheck = data.scrollTop; @@ -531,7 +499,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { let stopAtHeight = (data.scrollTop + data.renderHeight); processRecords(stopAtHeight, this._records, this._cells, - this._hdrFn, this._ftrFn, data); + this._hdrFn, this._ftrFn, data); } // ******** DOM READ **************** @@ -570,7 +538,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { */ onScrollEnd() { // scrolling is done, allow images to be updated now - this._imgs.toArray().forEach(img => { + this._imgs.forEach(img => { img.enable(true); }); @@ -580,13 +548,14 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { adjustRendered(this._cells, this._data); // ******** DOM WRITE **************** - this.detectChanges(); + this._cd.detectChanges(); // ******** DOM WRITE **************** this.setVirtualHeight( estimateHeight(this._records.length, this._cells[this._cells.length - 1], this._vHeight, 0.05) ); } + /** * @private * DOM WRITE @@ -595,6 +564,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { if (newVirtualHeight !== this._vHeight) { // ******** DOM WRITE **************** this._renderer.setElementStyle(this._elementRef.nativeElement, 'height', newVirtualHeight > 0 ? newVirtualHeight + 'px' : ''); + this._vHeight = newVirtualHeight; console.debug('VirtualScroll, height', newVirtualHeight); } @@ -646,6 +616,5 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy { } const SCROLL_END_TIMEOUT_MS = 140; - +const SCROLL_DIFFERENCE_MINIMUM = 20; const QUEUE_CHANGE_DETECTION = 0; -const QUEUE_WRITE_TO_NODES = 1; diff --git a/src/components/virtual-scroll/virtual-util.ts b/src/components/virtual-scroll/virtual-util.ts index dc37d49a9af..d0417966cf8 100644 --- a/src/components/virtual-scroll/virtual-util.ts +++ b/src/components/virtual-scroll/virtual-util.ts @@ -140,6 +140,7 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v let lastRecordIndex = (records.length - 1); let viewInsertIndex: number = null; let totalNodes = nodes.length; + let templateRef: TemplateRef; startCellIndex = Math.max(startCellIndex, 0); endCellIndex = Math.min(endCellIndex, cells.length - 1); @@ -216,12 +217,17 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v } } + // select which templateRef should be used for this cell + templateRef = cell.tmpl === TEMPLATE_HEADER ? hdrTmp : cell.tmpl === TEMPLATE_FOOTER ? ftrTmp : itmTmp; + if (!templateRef) { + console.error(`virtual${cell.tmpl === TEMPLATE_HEADER ? 'Header' : cell.tmpl === TEMPLATE_FOOTER ? 'Footer' : 'Item'} template required`); + continue; + } + availableNode = { tmpl: cell.tmpl, view: >viewContainer.createEmbeddedView( - cell.tmpl === TEMPLATE_HEADER ? hdrTmp : - cell.tmpl === TEMPLATE_FOOTER ? ftrTmp : - itmTmp, + templateRef, new VirtualContext(null, null, null), viewInsertIndex ) @@ -246,9 +252,10 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v if (initialLoad) { // add nodes that go at the very end, and only represent the last record - addLastNodes(nodes, viewContainer, TEMPLATE_HEADER, hdrTmp); - addLastNodes(nodes, viewContainer, TEMPLATE_ITEM, itmTmp); - addLastNodes(nodes, viewContainer, TEMPLATE_FOOTER, ftrTmp); + 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; @@ -256,7 +263,7 @@ export function populateNodeData(startCellIndex: number, endCellIndex: number, v function addLastNodes(nodes: VirtualNode[], viewContainer: ViewContainerRef, - templateType: number, templateRef: TemplateRef) { + templateType: number, templateRef: TemplateRef, temporaryData: any) { if (templateRef) { let node: VirtualNode = { tmpl: templateType, @@ -264,7 +271,7 @@ function addLastNodes(nodes: VirtualNode[], viewContainer: ViewContainerRef, isLastRecord: true, hidden: true, }; - node.view.context.$implicit = {}; + node.view.context.$implicit = temporaryData; nodes.push(node); } } @@ -409,40 +416,36 @@ export function writeToNodes(nodes: VirtualNode[], cells: VirtualCell[], totalRe for (var i = 0, ilen = nodes.length; i < ilen; i++) { node = nodes[i]; - if (node.hidden) { - continue; - } + if (!node.hidden) { + cell = cells[node.cell]; - cell = cells[node.cell]; + transform = `translate3d(${cell.left}px,${cell.top}px,0px)`; - transform = `translate3d(${cell.left}px,${cell.top}px,0px)`; + if (node.lastTransform !== transform) { + element = getElement(node); - if (node.lastTransform === transform) { - continue; - } + if (element) { + // ******** DOM WRITE **************** + element.style[CSS.transform] = node.lastTransform = transform; - element = getElement(node); + // ******** DOM WRITE **************** + element.classList.add('virtual-position'); - if (element) { - // ******** DOM WRITE **************** - element.style[CSS.transform] = node.lastTransform = transform; + if (node.isLastRecord) { + // its the last record, now with data and safe to show + // ******** DOM WRITE **************** + element.classList.remove('virtual-hidden'); + } - // ******** DOM WRITE **************** - element.classList.add('virtual-position'); + // https://www.w3.org/TR/wai-aria/states_and_properties#aria-posinset + // ******** DOM WRITE **************** + element.setAttribute('aria-posinset', (node.cell + 1).toString()); - 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-setsize + // ******** DOM WRITE **************** + element.setAttribute('aria-setsize', totalCells); + } } - - // https://www.w3.org/TR/wai-aria/states_and_properties#aria-posinset - // ******** DOM WRITE **************** - element.setAttribute('aria-posinset', (node.cell + 1).toString()); - - // https://www.w3.org/TR/wai-aria/states_and_properties#aria-setsize - // ******** DOM WRITE **************** - element.setAttribute('aria-setsize', totalCells); } } }