perf(img): load through webworkers, lazy load viewable imgs

This commit is contained in:
Adam Bradley
2016-12-05 18:02:14 -06:00
parent b70d13dd6f
commit a1542abbe4
11 changed files with 1431 additions and 138 deletions

View File

@ -0,0 +1,224 @@
import { removeArrayItem } from '../../util/util';
export class ImgLoader {
private wkr: Worker;
private callbacks: Function[] = [];
private ids = 0;
load(src: string, cache: boolean, callback: Function) {
if (src) {
(<any>callback).id = this.ids++;
this.callbacks.push(callback);
this.worker().postMessage(JSON.stringify({
id: (<any>callback).id,
src: src,
cache: cache
}));
}
}
cancelLoad(callback: Function) {
removeArrayItem(this.callbacks, callback);
}
abort(src: string) {
if (src) {
this.worker().postMessage(JSON.stringify({
src: src,
type: 'abort'
}));
}
}
private worker() {
if (!this.wkr) {
// create a blob from the inline worker string
const workerBlob = new Blob([INLINE_WORKER]);
// obtain a blob URL reference to our worker 'file'.
const blobURL = window.URL.createObjectURL(workerBlob);
// create the worker
this.wkr = new Worker(blobURL);
// create worker onmessage handler
this.wkr.onmessage = (ev: MessageEvent) => {
// we got something back from the web worker
// let's emit this out to everyone listening
const msg: ImgResponseMessage = JSON.parse(ev.data);
const callback = this.callbacks.find(cb => (<any>cb).id === msg.id);
if (callback) {
callback(msg);
removeArrayItem(this.callbacks, callback);
}
};
// create worker onerror handler
this.wkr.onerror = (ev: ErrorEvent) => {
console.error(`ImgLoader, worker ${ev.type} ${ev.message ? ev.message : ''}`);
this.callbacks.length = 0;
this.wkr.terminate();
this.wkr = null;
};
}
// return that hard worker
return this.wkr;
}
}
const INLINE_WORKER = `/** minify-start **/
(function(){
var imgs = [];
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var cacheLimit = 1381855 * 10;
onmessage = function(msg) {
var msgData = JSON.parse(msg.data);
var id = msgData.id;
var src = msgData.src;
var imgData;
for (var i = 0; i < imgs.length; i++) {
if (imgs[i].s === src) {
imgData = imgs[i];
break;
}
}
if (msgData.type === 'abort') {
if (imgData && imgData.x) {
imgData.x.abort();
imgData.x = null;
}
} else if (msgData.cache && imgData && imgData.d) {
postMessage(JSON.stringify({
id: id,
src: src,
status: 200,
data: imgData.d,
len: imgData.l
}));
} else {
if (imgData && imgData.x && imgData.x.readyState !== 4) {
imgData.x.addEventListener('load', function(ev) {
onXhrLoad(id, src, imgData, ev);
});
imgData.x.addEventListener('error', function(e) {
onXhrError(id, src, imgData, e);
});
return;
}
if (!imgData) {
imgData = { s: src, c: msgData.cache };
imgs.push(imgData);
}
imgData.x = new XMLHttpRequest();
imgData.x.open('GET', src, true);
imgData.x.responseType = 'arraybuffer';
imgData.x.addEventListener('load', function(ev) {
onXhrLoad(id, src, imgData, ev);
});
imgData.x.addEventListener('error', function(e) {
onXhrError(id, src, imgData, e);
});
imgData.x.send();
}
};
function onXhrLoad(id, src, imgData, ev) {
var rsp = {
id: id,
src: src,
status: ev.target.status,
data: null,
len: 0
};
if (ev.target.status === 200) {
setData(rsp, ev.target.getResponseHeader('Content-Type'), ev.target.response);
rsp.len = rsp.data.length;
}
postMessage(JSON.stringify(rsp));
if (imgData.x.status === 200 && imgData.c) {
imgData.d = rsp.data;
imgData.l = rsp.len;
var cacheSize = 0;
for (var i = imgs.length - 1; i >= 0; i--) {
cacheSize += imgs[i].l;
if (cacheSize > cacheLimit) {
imgs.splice(i, 1);
}
}
}
};
function onXhrError(id, src, imgData, e) {
postMessage(JSON.stringify({
id: id,
src: src,
status: 510,
msg: e.message + '' + e.stack
}));
imgData.x = null;
};
function setData(rsp, contentType, arrayBuffer) {
rsp.data = 'data:' + contentType + ';base64,';
var bytes = new Uint8Array(arrayBuffer);
var byteLength = bytes.byteLength;
var byteRemainder = byteLength % 3;
var mainLength = byteLength - byteRemainder;
var i, a, b, c, d, chunk;
for (i = 0; i < mainLength; i = i + 3) {
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
a = (chunk & 16515072) >> 18;
b = (chunk & 258048) >> 12;
c = (chunk & 4032) >> 6;
d = chunk & 63;
rsp.data += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
if (byteRemainder === 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2;
b = (chunk & 3) << 4;
rsp.data += encodings[a] + encodings[b] + '==';
} else if (byteRemainder === 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10;
b = (chunk & 1008) >> 4;
c = (chunk & 15) << 2;
rsp.data += encodings[a] + encodings[b] + encodings[c] + '=';
}
}
})();
/** minify-end **/`;
export interface ImgResponseMessage {
id: number;
src: string;
status?: number;
data?: string;
msg?: string;
}

View File

@ -3,32 +3,29 @@
// Img // Img
// -------------------------------------------------- // --------------------------------------------------
ion-img { /// @prop - Color of the image when it hasn't fully loaded yet
position: relative; $img-placeholder-background: #eee !default;
display: flex;
overflow: hidden;
align-items: center;
justify-content: center; ion-img {
display: inline-block;
min-width: 20px;
min-height: 20px;
background: $img-placeholder-background;
contain: strict;
} }
ion-img img { ion-img img {
flex-shrink: 0; object-fit: cover;
} }
ion-img .img-placeholder { ion-img.img-unloaded img {
position: absolute; display: none;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #eee;
transition: opacity 200ms;
} }
ion-img.img-loaded .img-placeholder { ion-img.img-loaded img {
opacity: 0; display: block;
} }

View File

@ -1,7 +1,9 @@
import { Component, Input, HostBinding, ElementRef, ChangeDetectionStrategy, ViewEncapsulation, NgZone } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, Input, NgZone, OnDestroy, Optional, Renderer, ViewEncapsulation } from '@angular/core';
import { nativeRaf } from '../../util/dom'; import { Content } from '../content/content';
import { isPresent } from '../../util/util'; import { DomController } from '../../util/dom-controller';
import { ImgLoader, ImgResponseMessage } from './img-loader';
import { isPresent, isTrueProperty } from '../../util/util';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
@ -10,128 +12,268 @@ import { Platform } from '../../platform/platform';
*/ */
@Component({ @Component({
selector: 'ion-img', selector: 'ion-img',
template: template: '<img>',
'<div class="img-placeholder" [style.height]="_h" [style.width]="_w"></div>',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class Img { export class Img implements OnDestroy {
_src: string = ''; /** @internal */
_normalizeSrc: string = ''; _src: string;
_imgs: HTMLImageElement[] = []; /** @internal */
_w: string; _requestingSrc: string;
_h: string; /** @internal */
_enabled: boolean = true; _renderedSrc: string;
_init: boolean; /** @internal */
_tmpDataUri: string;
/** @internal */
_cache: boolean = true;
/** @internal */
_lazy: boolean = true;
/** @internal */
_ww: boolean = true;
/** @internal */
_cb: Function;
/** @internal */
_bounds: any;
/** @internal */
_rect: any;
/** @internal */
_w: string = '';
/** @internal */
_h: string = '';
/** @internal */
_wQ: string = '';
/** @internal */
_hQ: string = '';
constructor(private _elementRef: ElementRef, private _platform: Platform, private _zone: NgZone) {} /** @private */
canRequest: boolean;
/** @private */
canRender: boolean;
constructor(
private _ldr: ImgLoader,
private _elementRef: ElementRef,
private _renderer: Renderer,
private _platform: Platform,
private _zone: NgZone,
@Optional() private _content: Content,
private _dom: DomController
) {
if (!this._content) {
console.warn(`ion-img can only be used within an ion-content`);
} else {
this._content.addImg(this);
}
this._isLoaded(false);
}
@Input() @Input()
set src(val: string) { get src(): string {
let tmpImg = new Image(); return this._src;
tmpImg.src = isPresent(val) ? val : ''; }
set src(newSrc: string) {
if (newSrc !== this._src) {
this.reset();
this._src = isPresent(val) ? val : ''; // update to the new src
this._normalizeSrc = tmpImg.src; this._src = newSrc;
if (this._init) { // reset any existing datauri we might be holding onto
this._update(); this._tmpDataUri = null;
this.update();
} }
} }
ngOnInit() { reset() {
this._init = true; if (this._requestingSrc) {
this._update(); // abort any active requests
console.debug(`abortRequest ${this._requestingSrc} ${Date.now()}`);
this._ldr.abort(this._requestingSrc);
this._requestingSrc = null;
}
if (this._renderedSrc) {
// clear out the currently rendered img
console.debug(`clearRender ${this._renderedSrc} ${Date.now()}`);
this._renderedSrc = null;
this._isLoaded(false);
}
} }
_update() { update() {
if (this._enabled && this._src !== '') { if (this._src && this._content.isImgsRefreshable()) {
// actively update the image if (this.canRequest && (this._src !== this._renderedSrc && this._src !== this._requestingSrc) && !this._tmpDataUri) {
console.debug(`request ${this._src} ${Date.now()}`);
this._requestingSrc = this._src;
for (var i = this._imgs.length - 1; i >= 0; i--) { this._cb = (msg: ImgResponseMessage) => {
if (this._imgs[i].src === this._normalizeSrc) { this._loadResponse(msg);
// this is the active image };
if (this._imgs[i].complete) {
this._loaded(true); this._ldr.load(this._src, this._cache, this._cb);
this._setDims();
} }
if (this.canRender && this._tmpDataUri && this._src !== this._renderedSrc) {
// we can render and we have a datauri to render
this._renderedSrc = this._src;
this._setDims();
this._dom.write(() => {
if (this._tmpDataUri) {
console.debug(`render ${this._src} ${Date.now()}`);
this._isLoaded(true);
this._srcAttr(this._tmpDataUri);
this._tmpDataUri = null;
}
});
}
}
}
/**
* @internal
*/
_loadResponse(msg: ImgResponseMessage) {
this._requestingSrc = null;
if (msg.status === 200) {
// success :)
this._tmpDataUri = msg.data;
this.update();
} else { } else {
// no longer the active image // error :(
if (this._imgs[i].parentElement) { console.error(`img, status: ${msg.status} ${msg.msg}`);
this._imgs[i].parentElement.removeChild(this._imgs[i]); this._renderedSrc = this._tmpDataUri = null;
} this._dom.write(() => {
this._imgs.splice(i, 1); this._isLoaded(false);
}
}
if (!this._imgs.length) {
this._zone.runOutsideAngular(() => {
let img = new Image();
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) {
this._elementRef.nativeElement.appendChild(img);
nativeRaf(() => {
this._update();
}); });
} }
});
img.src = this._src;
this._imgs.push(img);
this._loaded(false);
});
} }
} else { /**
// do not actively update the image * @internal
if (!this._imgs.some(img => img.src === this._normalizeSrc)) { */
this._loaded(false); _isLoaded(isLoaded: boolean) {
const renderer = this._renderer;
const ele = this._elementRef.nativeElement;
renderer.setElementClass(ele, 'img-loaded', isLoaded);
renderer.setElementClass(ele, 'img-unloaded', !isLoaded);
} }
/**
* @internal
*/
_srcAttr(srcAttr: string) {
const imgEle = this._elementRef.nativeElement.firstChild;
const renderer = this._renderer;
renderer.setElementAttribute(imgEle, 'src', srcAttr);
renderer.setElementAttribute(imgEle, 'alt', this.alt);
}
/**
* @private
*/
get top(): number {
const bounds = this._getBounds();
return bounds && bounds.top || 0;
}
/**
* @private
*/
get bottom(): number {
const bounds = this._getBounds();
return bounds && bounds.bottom || 0;
}
private _getBounds() {
if (this._bounds) {
return this._bounds;
}
if (!this._rect) {
this._rect = (<HTMLElement>this._elementRef.nativeElement).getBoundingClientRect();
console.debug(`img, ${this._src}, read, ${this._rect.top} - ${this._rect.bottom}`);
}
return this._rect;
}
@Input()
set bounds(b: any) {
if (isPresent(b)) {
this._bounds = b;
} }
} }
_loaded(isLoaded: boolean) { @Input()
this._elementRef.nativeElement.classList[isLoaded ? 'add' : 'remove']('img-loaded'); get lazyLoad(): boolean {
return this._lazy;
}
set lazyLoad(val: boolean) {
this._lazy = isTrueProperty(val);
} }
enable(shouldEnable: boolean) { @Input()
this._enabled = shouldEnable; get webWorker(): boolean {
this._update(); return this._ww;
}
set webWorker(val: boolean) {
this._ww = isTrueProperty(val);
}
@Input()
get cache(): boolean {
return this._cache;
}
set cache(val: boolean) {
this._cache = isTrueProperty(val);
} }
@Input() @Input()
set width(val: string | number) { set width(val: string | number) {
this._w = getUnitValue(val); this._wQ = getUnitValue(val);
this._setDims();
} }
@Input() @Input()
set height(val: string | number) { set height(val: string | number) {
this._h = getUnitValue(val); this._hQ = getUnitValue(val);
this._setDims();
} }
@Input() alt: string; _setDims() {
if (this.canRender && (this._w !== this._wQ || this._h !== this._hQ)) {
var wrapperEle: HTMLImageElement = this._elementRef.nativeElement;
var renderer = this._renderer;
@Input() title: string; this._dom.write(() => {
if (this._w !== this._wQ) {
@HostBinding('style.width') this._w = this._wQ;
get _width(): string { renderer.setElementStyle(wrapperEle, 'width', this._w);
return isPresent(this._w) ? this._w : ''; }
if (this._h !== this._hQ) {
this._h = this._hQ;
renderer.setElementStyle(wrapperEle, 'height', this._h);
}
});
}
} }
@HostBinding('style.height') /**
get _height(): string { * Set the `alt` attribute on the inner `img` element.
return isPresent(this._h) ? this._h : ''; */
@Input() alt: string = '';
/**
* @private
*/
ngOnDestroy() {
this._ldr.cancelLoad(this._cb);
this._cb = null;
this._content && this._content.removeImg(this);
} }
} }

View File

@ -0,0 +1,76 @@
<ion-header>
<ion-navbar>
<ion-title>Img: Basic</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-avatar item-left>
<ion-img></ion-img>
</ion-avatar>
Unloaded Avatar
<ion-avatar item-right>
<ion-img></ion-img>
</ion-avatar>
</ion-item>
<ion-item>
<ion-avatar item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/blues-brothers.jpg"></ion-img>
</ion-avatar>
Loaded Avatar
<ion-avatar item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bueller.jpg"></ion-img>
</ion-avatar>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img></ion-img>
</ion-thumbnail>
Unloaded Thumbnail
<ion-thumbnail item-right>
<ion-img></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Loaded Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
</ion-list>
<div padding>
Default ion-img w/in content, display: inline-block.
<ion-img alt="Smokey" width="220" height="165" src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
has width, height and alt set.
</div>
</ion-content>

View File

@ -0,0 +1,82 @@
import { ElementRef, Renderer } from '@angular/core';
import { Content } from '../../content/content';
import { Img } from '../img';
import { ImgLoader } from '../img-loader';
import { mockContent, MockDomController, mockElementRef, mockPlatform, mockRenderer, mockZone } from '../../../util/mock-providers';
import { Platform } from '../../../platform/platform';
fdescribe('Img', () => {
describe('reset', () => {
it('should clear rendering src', () => {
spyOn(img, '_isLoaded');
img._renderedSrc = '_renderedSrc.jpg';
img.reset();
expect(img._isLoaded).toHaveBeenCalledWith(false);
expect(img._renderedSrc).toEqual(null);
});
it('should abort requesting src', () => {
spyOn(ldr, 'abort');
img._requestingSrc = '_requestingSrc.jpg';
img.reset();
expect(ldr.abort).toHaveBeenCalledWith('_requestingSrc.jpg');
expect(img._requestingSrc).toEqual(null);
});
});
describe('src setter', () => {
it('should abort request if already requesting', () => {
spyOn(img, 'reset');
img._requestingSrc = 'requesting.jpg';
img._tmpDataUri = 'tmpDatauri.jpg';
img.src = 'image.jpg';
expect(img.reset).toHaveBeenCalled();
expect(img.src).toEqual('image.jpg');
expect(img._tmpDataUri).toEqual(null);
});
it('should set src', () => {
spyOn(img, 'update');
img.src = 'image.jpg';
expect(img.src).toEqual('image.jpg');
expect(img.update).toHaveBeenCalled();
});
});
describe('src getter', () => {
it('should get src if set', () => {
img._src = 'loaded.jpg';
expect(img.src).toEqual('loaded.jpg');
});
});
let img: Img;
let ldr: ImgLoader;
let elementRef: ElementRef;
let renderer: Renderer;
let platform: Platform;
let content: Content;
let dom: MockDomController;
beforeEach(() => {
content = mockContent();
ldr = new ImgLoader();
elementRef = mockElementRef();
renderer = mockRenderer();
platform = mockPlatform();
dom = new MockDomController();
img = new Img(ldr, elementRef, renderer, platform, mockZone(), content, dom);
});
});

View 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 {}

View File

@ -0,0 +1,686 @@
<ion-header>
<ion-navbar>
<ion-title>Img: Lazy Load</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/batmobile.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/delorean.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/eleanor.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/ghostbusters.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/knight-rider.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
<ion-item>
<ion-thumbnail item-left>
<ion-img src="http://localhost:8000/dist/e2e/img/img/mirth-mobile.jpg"></ion-img>
</ion-thumbnail>
Thumbnail
<ion-thumbnail item-right>
<ion-img src="http://localhost:8000/dist/e2e/img/img/bandit.jpg"></ion-img>
</ion-thumbnail>
</ion-item>
</ion-list>
</ion-content>

View File

@ -16,10 +16,10 @@ $item-ios-paragraph-font-size: 1.4rem !default;
$item-ios-paragraph-text-color: #8e9093 !default; $item-ios-paragraph-text-color: #8e9093 !default;
/// @prop - Size of the avatar in the item /// @prop - Size of the avatar in the item
$item-ios-avatar-size: 3.6rem !default; $item-ios-avatar-size: 36px !default;
/// @prop - Size of the thumbnail in the item /// @prop - Size of the thumbnail in the item
$item-ios-thumbnail-size: 5.6rem !default; $item-ios-thumbnail-size: 56px !default;
/// @prop - Shows the detail arrow icon on an item /// @prop - Shows the detail arrow icon on an item
$item-ios-detail-push-show: true !default; $item-ios-detail-push-show: true !default;
@ -133,16 +133,6 @@ $item-ios-sliding-content-background: $list-ios-background-color !default;
margin-left: 0; margin-left: 0;
} }
.item-ios ion-avatar[item-left],
.item-ios ion-thumbnail[item-left] {
margin: ($item-ios-padding-right / 2) $item-ios-padding-right ($item-ios-padding-right / 2) 0;
}
.item-ios ion-avatar[item-right],
.item-ios ion-thumbnail[item-right] {
margin: ($item-ios-padding-right / 2);
}
.item-ios .item-button { .item-ios .item-button {
padding: 0 .5em; padding: 0 .5em;
@ -156,26 +146,52 @@ $item-ios-sliding-content-background: $list-ios-background-color !default;
padding: 0 1px; padding: 0 1px;
} }
.item-ios ion-avatar[item-left],
.item-ios ion-thumbnail[item-left] {
margin: ($item-ios-padding-right / 2) $item-ios-padding-right ($item-ios-padding-right / 2) 0;
}
.item-ios ion-avatar[item-right],
.item-ios ion-thumbnail[item-right] {
margin: ($item-ios-padding-right / 2);
}
// iOS Item Avatar
// --------------------------------------------------
.item-ios ion-avatar { .item-ios ion-avatar {
min-width: $item-ios-avatar-size; min-width: $item-ios-avatar-size;
min-height: $item-ios-avatar-size; min-height: $item-ios-avatar-size;
} }
.item-ios ion-avatar img { .item-ios ion-avatar ion-img {
max-width: $item-ios-avatar-size; width: $item-ios-avatar-size;
max-height: $item-ios-avatar-size; height: $item-ios-avatar-size;
border-radius: $item-ios-avatar-size / 2; border-radius: $item-ios-avatar-size / 2;
overflow: hidden;
} }
.item-ios ion-avatar img {
width: $item-ios-avatar-size;
height: $item-ios-avatar-size;
}
// iOS Item Thumbnail
// --------------------------------------------------
.item-ios ion-thumbnail { .item-ios ion-thumbnail {
min-width: $item-ios-thumbnail-size; min-width: $item-ios-thumbnail-size;
min-height: $item-ios-thumbnail-size; min-height: $item-ios-thumbnail-size;
} }
.item-ios ion-thumbnail ion-img,
.item-ios ion-thumbnail img { .item-ios ion-thumbnail img {
max-width: $item-ios-thumbnail-size; width: $item-ios-thumbnail-size;
max-height: $item-ios-thumbnail-size; height: $item-ios-thumbnail-size;
} }

View File

@ -16,10 +16,10 @@ $item-md-paragraph-text-color: #666 !default;
$item-md-font-size: 1.6rem !default; $item-md-font-size: 1.6rem !default;
/// @prop - Size of the avatar in the item /// @prop - Size of the avatar in the item
$item-md-avatar-size: 4rem !default; $item-md-avatar-size: 40px !default;
/// @prop - Size of the thumbnail in the item /// @prop - Size of the thumbnail in the item
$item-md-thumbnail-size: 8rem !default; $item-md-thumbnail-size: 80px !default;
/// @prop - Shows the detail arrow icon on an item /// @prop - Shows the detail arrow icon on an item
$item-md-detail-push-show: false !default; $item-md-detail-push-show: false !default;
@ -178,26 +178,42 @@ $item-md-sliding-content-background: $list-md-background-color !default;
margin: ($item-md-padding-right / 2); margin: ($item-md-padding-right / 2);
} }
// Material Design Item Avatar
// --------------------------------------------------
.item-md ion-avatar { .item-md ion-avatar {
min-width: $item-md-avatar-size; min-width: $item-md-avatar-size;
min-height: $item-md-avatar-size; min-height: $item-md-avatar-size;
} }
.item-md ion-avatar img { .item-md ion-avatar ion-img {
max-width: $item-md-avatar-size; width: $item-md-avatar-size;
max-height: $item-md-avatar-size; height: $item-md-avatar-size;
border-radius: $item-md-avatar-size / 2; border-radius: $item-md-avatar-size / 2;
overflow: hidden;
} }
.item-ios ion-avatar img {
width: $item-md-avatar-size;
height: $item-md-avatar-size;
}
// Material Design Item Thumbnail
// --------------------------------------------------
.item-md ion-thumbnail { .item-md ion-thumbnail {
min-width: $item-md-thumbnail-size; min-width: $item-md-thumbnail-size;
min-height: $item-md-thumbnail-size; min-height: $item-md-thumbnail-size;
} }
.item-md ion-thumbnail ion-img,
.item-md ion-thumbnail img { .item-md ion-thumbnail img {
max-width: $item-md-thumbnail-size; width: $item-md-thumbnail-size;
max-height: $item-md-thumbnail-size; height: $item-md-thumbnail-size;
} }

View File

@ -22,10 +22,10 @@ $item-wp-paragraph-text-color: #666 !default;
$item-wp-font-size: 1.6rem !default; $item-wp-font-size: 1.6rem !default;
/// @prop - Size of the avatar in the item /// @prop - Size of the avatar in the item
$item-wp-avatar-size: 4rem !default; $item-wp-avatar-size: 40px !default;
/// @prop - Size of the thumbnail in the item /// @prop - Size of the thumbnail in the item
$item-wp-thumbnail-size: 8rem !default; $item-wp-thumbnail-size: 80px !default;
/// @prop - Shows the detail arrow icon on an item /// @prop - Shows the detail arrow icon on an item
$item-wp-detail-push-show: false !default; $item-wp-detail-push-show: false !default;
@ -188,26 +188,42 @@ $item-wp-sliding-content-background: $list-wp-background-color !default;
margin: ($item-wp-padding-right / 2); margin: ($item-wp-padding-right / 2);
} }
// Windows Item Avatar
// --------------------------------------------------
.item-wp ion-avatar { .item-wp ion-avatar {
min-width: $item-wp-avatar-size; min-width: $item-wp-avatar-size;
min-height: $item-wp-avatar-size; min-height: $item-wp-avatar-size;
} }
.item-wp ion-avatar img { .item-wp ion-avatar ion-img {
max-width: $item-wp-avatar-size; overflow: hidden;
max-height: $item-wp-avatar-size;
width: $item-wp-avatar-size;
height: $item-wp-avatar-size;
border-radius: $item-wp-avatar-size / 2; border-radius: $item-wp-avatar-size / 2;
} }
.item-wp ion-avatar img {
width: $item-wp-avatar-size;
height: $item-wp-avatar-size;
}
// Windows Item Thumbnail
// --------------------------------------------------
.item-wp ion-thumbnail { .item-wp ion-thumbnail {
min-width: $item-wp-thumbnail-size; min-width: $item-wp-thumbnail-size;
min-height: $item-wp-thumbnail-size; min-height: $item-wp-thumbnail-size;
} }
.item-wp ion-thumbnail ion-img,
.item-wp ion-thumbnail img { .item-wp ion-thumbnail img {
max-width: $item-wp-thumbnail-size; width: $item-wp-thumbnail-size;
max-height: $item-wp-thumbnail-size; height: $item-wp-thumbnail-size;
} }

View File

@ -19,6 +19,7 @@ import { Events, setupProvideEvents } from './util/events';
import { Form } from './util/form'; import { Form } from './util/form';
import { GestureController } from './gestures/gesture-controller'; import { GestureController } from './gestures/gesture-controller';
import { Haptic } from './util/haptic'; import { Haptic } from './util/haptic';
import { ImgLoader } from './components/img/img-loader';
import { IonicGestureConfig } from './gestures/gesture-config'; import { IonicGestureConfig } from './gestures/gesture-config';
import { Keyboard } from './util/keyboard'; import { Keyboard } from './util/keyboard';
import { LoadingController } from './components/loading/loading'; import { LoadingController } from './components/loading/loading';
@ -55,6 +56,7 @@ export { Config, setupConfig, ConfigToken } from './config/config';
export { DomController } from './util/dom-controller'; export { DomController } from './util/dom-controller';
export { Platform, setupPlatform, UserAgentToken, DocumentDirToken, DocLangToken, NavigatorPlatformToken } from './platform/platform'; export { Platform, setupPlatform, UserAgentToken, DocumentDirToken, DocLangToken, NavigatorPlatformToken } from './platform/platform';
export { Haptic } from './util/haptic'; export { Haptic } from './util/haptic';
export { ImgLoader } from './components/img/img-loader';
export { QueryParams, setupQueryParams, UrlToken } from './platform/query-params'; export { QueryParams, setupQueryParams, UrlToken } from './platform/query-params';
export { DeepLinker } from './navigation/deep-linker'; export { DeepLinker } from './navigation/deep-linker';
export { NavController } from './navigation/nav-controller'; export { NavController } from './navigation/nav-controller';
@ -174,6 +176,7 @@ export class IonicModule {
Form, Form,
GestureController, GestureController,
Haptic, Haptic,
ImgLoader,
Keyboard, Keyboard,
LoadingController, LoadingController,
Location, Location,