From a1542abbe404df1e5115fb5940d28f1db45a4e31 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 5 Dec 2016 18:02:14 -0600 Subject: [PATCH] perf(img): load through webworkers, lazy load viewable imgs --- src/components/img/img-loader.ts | 224 ++++++ src/components/img/img.scss | 35 +- src/components/img/img.ts | 310 +++++--- src/components/img/test/basic/main.html | 76 ++ src/components/img/test/img.spec.ts | 82 +++ .../img/test/lazy-load/app-module.ts | 35 + src/components/img/test/lazy-load/main.html | 686 ++++++++++++++++++ src/components/item/item.ios.scss | 50 +- src/components/item/item.md.scss | 30 +- src/components/item/item.wp.scss | 30 +- src/module.ts | 3 + 11 files changed, 1427 insertions(+), 134 deletions(-) create mode 100644 src/components/img/img-loader.ts create mode 100644 src/components/img/test/basic/main.html create mode 100644 src/components/img/test/img.spec.ts create mode 100644 src/components/img/test/lazy-load/app-module.ts create mode 100644 src/components/img/test/lazy-load/main.html diff --git a/src/components/img/img-loader.ts b/src/components/img/img-loader.ts new file mode 100644 index 00000000000..805043cc306 --- /dev/null +++ b/src/components/img/img-loader.ts @@ -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) { + (callback).id = this.ids++; + this.callbacks.push(callback); + this.worker().postMessage(JSON.stringify({ + id: (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 => (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; +} diff --git a/src/components/img/img.scss b/src/components/img/img.scss index ecb1328e2b3..49bd07bd508 100644 --- a/src/components/img/img.scss +++ b/src/components/img/img.scss @@ -3,32 +3,29 @@ // Img // -------------------------------------------------- +/// @prop - Color of the image when it hasn't fully loaded yet +$img-placeholder-background: #eee !default; + + ion-img { - position: relative; - display: flex; - overflow: hidden; + display: inline-block; + + min-width: 20px; + min-height: 20px; + + background: $img-placeholder-background; - align-items: center; - justify-content: center; + contain: strict; } ion-img img { - flex-shrink: 0; + object-fit: cover; } -ion-img .img-placeholder { - position: absolute; - top: 0; - left: 0; - - width: 100%; - height: 100%; - - background: #eee; - - transition: opacity 200ms; +ion-img.img-unloaded img { + display: none; } -ion-img.img-loaded .img-placeholder { - opacity: 0; +ion-img.img-loaded img { + display: block; } diff --git a/src/components/img/img.ts b/src/components/img/img.ts index 10892a2aa0f..b928f2da937 100644 --- a/src/components/img/img.ts +++ b/src/components/img/img.ts @@ -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 { isPresent } from '../../util/util'; +import { Content } from '../content/content'; +import { DomController } from '../../util/dom-controller'; +import { ImgLoader, ImgResponseMessage } from './img-loader'; +import { isPresent, isTrueProperty } from '../../util/util'; import { Platform } from '../../platform/platform'; @@ -10,128 +12,268 @@ import { Platform } from '../../platform/platform'; */ @Component({ selector: 'ion-img', - template: - '
', + template: '', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) -export class Img { - _src: string = ''; - _normalizeSrc: string = ''; - _imgs: HTMLImageElement[] = []; - _w: string; - _h: string; - _enabled: boolean = true; - _init: boolean; +export class Img implements OnDestroy { + /** @internal */ + _src: string; + /** @internal */ + _requestingSrc: string; + /** @internal */ + _renderedSrc: string; + /** @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() - set src(val: string) { - let tmpImg = new Image(); - tmpImg.src = isPresent(val) ? val : ''; + get src(): string { + return this._src; + } + set src(newSrc: string) { + if (newSrc !== this._src) { + this.reset(); + + // update to the new src + this._src = newSrc; - this._src = isPresent(val) ? val : ''; - this._normalizeSrc = tmpImg.src; + // reset any existing datauri we might be holding onto + this._tmpDataUri = null; - if (this._init) { - this._update(); + this.update(); } } - ngOnInit() { - this._init = true; - this._update(); + reset() { + if (this._requestingSrc) { + // 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() { - if (this._enabled && this._src !== '') { - // actively update the image + update() { + if (this._src && this._content.isImgsRefreshable()) { + 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--) { - if (this._imgs[i].src === this._normalizeSrc) { - // this is the active image - if (this._imgs[i].complete) { - this._loaded(true); - } + this._cb = (msg: ImgResponseMessage) => { + this._loadResponse(msg); + }; - } else { - // no longer the active image - if (this._imgs[i].parentElement) { - this._imgs[i].parentElement.removeChild(this._imgs[i]); + 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; } - this._imgs.splice(i, 1); - } + }); } + } + } - if (!this._imgs.length) { - this._zone.runOutsideAngular(() => { - let img = new Image(); - img.style.width = this._width; - img.style.height = this._height; + /** + * @internal + */ + _loadResponse(msg: ImgResponseMessage) { + this._requestingSrc = null; - if (isPresent(this.alt)) { - img.alt = this.alt; - } - if (isPresent(this.title)) { - img.title = this.title; - } + if (msg.status === 200) { + // success :) + this._tmpDataUri = msg.data; + this.update(); - img.addEventListener('load', () => { - if (img.src === this._normalizeSrc) { - this._elementRef.nativeElement.appendChild(img); - nativeRaf(() => { - this._update(); - }); - } - }); + } else { + // error :( + console.error(`img, status: ${msg.status} ${msg.msg}`); + this._renderedSrc = this._tmpDataUri = null; + this._dom.write(() => { + this._isLoaded(false); + }); + } + } - img.src = this._src; + /** + * @internal + */ + _isLoaded(isLoaded: boolean) { + const renderer = this._renderer; + const ele = this._elementRef.nativeElement; + renderer.setElementClass(ele, 'img-loaded', isLoaded); + renderer.setElementClass(ele, 'img-unloaded', !isLoaded); + } - this._imgs.push(img); - this._loaded(false); - }); - } + /** + * @internal + */ + _srcAttr(srcAttr: string) { + const imgEle = this._elementRef.nativeElement.firstChild; + const renderer = this._renderer; - } else { - // do not actively update the image - if (!this._imgs.some(img => img.src === this._normalizeSrc)) { - this._loaded(false); - } + 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 = (this._elementRef.nativeElement).getBoundingClientRect(); + console.debug(`img, ${this._src}, read, ${this._rect.top} - ${this._rect.bottom}`); + } + return this._rect; } - _loaded(isLoaded: boolean) { - this._elementRef.nativeElement.classList[isLoaded ? 'add' : 'remove']('img-loaded'); + @Input() + set bounds(b: any) { + if (isPresent(b)) { + this._bounds = b; + } } - enable(shouldEnable: boolean) { - this._enabled = shouldEnable; - this._update(); + @Input() + get lazyLoad(): boolean { + return this._lazy; + } + set lazyLoad(val: boolean) { + this._lazy = isTrueProperty(val); + } + + @Input() + get webWorker(): boolean { + 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() set width(val: string | number) { - this._w = getUnitValue(val); + this._wQ = getUnitValue(val); + this._setDims(); } @Input() set height(val: string | number) { - this._h = getUnitValue(val); + this._hQ = getUnitValue(val); + this._setDims(); } - @Input() alt: string; - - @Input() title: string; + _setDims() { + if (this.canRender && (this._w !== this._wQ || this._h !== this._hQ)) { + var wrapperEle: HTMLImageElement = this._elementRef.nativeElement; + var renderer = this._renderer; - @HostBinding('style.width') - get _width(): string { - return isPresent(this._w) ? this._w : ''; + this._dom.write(() => { + if (this._w !== this._wQ) { + this._w = this._wQ; + renderer.setElementStyle(wrapperEle, 'width', this._w); + } + if (this._h !== this._hQ) { + this._h = this._hQ; + renderer.setElementStyle(wrapperEle, 'height', this._h); + } + }); + } } - @HostBinding('style.height') - get _height(): string { - return isPresent(this._h) ? this._h : ''; + /** + * Set the `alt` attribute on the inner `img` element. + */ + @Input() alt: string = ''; + + /** + * @private + */ + ngOnDestroy() { + this._ldr.cancelLoad(this._cb); + this._cb = null; + this._content && this._content.removeImg(this); } } diff --git a/src/components/img/test/basic/main.html b/src/components/img/test/basic/main.html new file mode 100644 index 00000000000..e7538eb35d5 --- /dev/null +++ b/src/components/img/test/basic/main.html @@ -0,0 +1,76 @@ + + + Img: Basic + + + + + + + + + + + + + + + Unloaded Avatar + + + + + + + + + + + + + + Loaded Avatar + + + + + + + + + + + + + + Unloaded Thumbnail + + + + + + + + + + + + + + Loaded Thumbnail + + + + + + + + + +
+ Default ion-img w/in content, display: inline-block. + + has width, height and alt set. +
+ +
diff --git a/src/components/img/test/img.spec.ts b/src/components/img/test/img.spec.ts new file mode 100644 index 00000000000..c8535981f79 --- /dev/null +++ b/src/components/img/test/img.spec.ts @@ -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); + }); + +}); diff --git a/src/components/img/test/lazy-load/app-module.ts b/src/components/img/test/lazy-load/app-module.ts new file mode 100644 index 00000000000..559813021d6 --- /dev/null +++ b/src/components/img/test/lazy-load/app-module.ts @@ -0,0 +1,35 @@ +import { Component, NgModule } from '@angular/core'; +import { IonicApp, IonicModule } from '../../../..'; + + +@Component({ + templateUrl: 'main.html' +}) +export class E2EPage { + +} + + +@Component({ + template: '' +}) +export class E2EApp { + root = E2EPage; +} + + +@NgModule({ + declarations: [ + E2EApp, + E2EPage + ], + imports: [ + IonicModule.forRoot(E2EApp) + ], + bootstrap: [IonicApp], + entryComponents: [ + E2EApp, + E2EPage + ] +}) +export class AppModule {} diff --git a/src/components/img/test/lazy-load/main.html b/src/components/img/test/lazy-load/main.html new file mode 100644 index 00000000000..bcfccf310da --- /dev/null +++ b/src/components/img/test/lazy-load/main.html @@ -0,0 +1,686 @@ + + + Img: Lazy Load + + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + + + + + Thumbnail + + + + + + + + + + diff --git a/src/components/item/item.ios.scss b/src/components/item/item.ios.scss index 4645e95844c..e17b673e210 100644 --- a/src/components/item/item.ios.scss +++ b/src/components/item/item.ios.scss @@ -16,10 +16,10 @@ $item-ios-paragraph-font-size: 1.4rem !default; $item-ios-paragraph-text-color: #8e9093 !default; /// @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 -$item-ios-thumbnail-size: 5.6rem !default; +$item-ios-thumbnail-size: 56px !default; /// @prop - Shows the detail arrow icon on an item $item-ios-detail-push-show: true !default; @@ -133,16 +133,6 @@ $item-ios-sliding-content-background: $list-ios-background-color !default; 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 { padding: 0 .5em; @@ -156,26 +146,52 @@ $item-ios-sliding-content-background: $list-ios-background-color !default; 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 { min-width: $item-ios-avatar-size; min-height: $item-ios-avatar-size; } -.item-ios ion-avatar img { - max-width: $item-ios-avatar-size; - max-height: $item-ios-avatar-size; +.item-ios ion-avatar ion-img { + width: $item-ios-avatar-size; + height: $item-ios-avatar-size; 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 { min-width: $item-ios-thumbnail-size; min-height: $item-ios-thumbnail-size; } +.item-ios ion-thumbnail ion-img, .item-ios ion-thumbnail img { - max-width: $item-ios-thumbnail-size; - max-height: $item-ios-thumbnail-size; + width: $item-ios-thumbnail-size; + height: $item-ios-thumbnail-size; } diff --git a/src/components/item/item.md.scss b/src/components/item/item.md.scss index b12ae803d18..34acc43ed1f 100644 --- a/src/components/item/item.md.scss +++ b/src/components/item/item.md.scss @@ -16,10 +16,10 @@ $item-md-paragraph-text-color: #666 !default; $item-md-font-size: 1.6rem !default; /// @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 -$item-md-thumbnail-size: 8rem !default; +$item-md-thumbnail-size: 80px !default; /// @prop - Shows the detail arrow icon on an item $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); } + +// Material Design Item Avatar +// -------------------------------------------------- + .item-md ion-avatar { min-width: $item-md-avatar-size; min-height: $item-md-avatar-size; } -.item-md ion-avatar img { - max-width: $item-md-avatar-size; - max-height: $item-md-avatar-size; +.item-md ion-avatar ion-img { + width: $item-md-avatar-size; + height: $item-md-avatar-size; 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 { min-width: $item-md-thumbnail-size; min-height: $item-md-thumbnail-size; } +.item-md ion-thumbnail ion-img, .item-md ion-thumbnail img { - max-width: $item-md-thumbnail-size; - max-height: $item-md-thumbnail-size; + width: $item-md-thumbnail-size; + height: $item-md-thumbnail-size; } diff --git a/src/components/item/item.wp.scss b/src/components/item/item.wp.scss index 71e35b12a2c..7182399485b 100644 --- a/src/components/item/item.wp.scss +++ b/src/components/item/item.wp.scss @@ -22,10 +22,10 @@ $item-wp-paragraph-text-color: #666 !default; $item-wp-font-size: 1.6rem !default; /// @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 -$item-wp-thumbnail-size: 8rem !default; +$item-wp-thumbnail-size: 80px !default; /// @prop - Shows the detail arrow icon on an item $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); } + +// Windows Item Avatar +// -------------------------------------------------- + .item-wp ion-avatar { min-width: $item-wp-avatar-size; min-height: $item-wp-avatar-size; } -.item-wp ion-avatar img { - max-width: $item-wp-avatar-size; - max-height: $item-wp-avatar-size; +.item-wp ion-avatar ion-img { + overflow: hidden; + + width: $item-wp-avatar-size; + height: $item-wp-avatar-size; 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 { min-width: $item-wp-thumbnail-size; min-height: $item-wp-thumbnail-size; } +.item-wp ion-thumbnail ion-img, .item-wp ion-thumbnail img { - max-width: $item-wp-thumbnail-size; - max-height: $item-wp-thumbnail-size; + width: $item-wp-thumbnail-size; + height: $item-wp-thumbnail-size; } diff --git a/src/module.ts b/src/module.ts index e9d603ce264..b1702b93197 100644 --- a/src/module.ts +++ b/src/module.ts @@ -19,6 +19,7 @@ import { Events, setupProvideEvents } from './util/events'; import { Form } from './util/form'; import { GestureController } from './gestures/gesture-controller'; import { Haptic } from './util/haptic'; +import { ImgLoader } from './components/img/img-loader'; import { IonicGestureConfig } from './gestures/gesture-config'; import { Keyboard } from './util/keyboard'; import { LoadingController } from './components/loading/loading'; @@ -55,6 +56,7 @@ export { Config, setupConfig, ConfigToken } from './config/config'; export { DomController } from './util/dom-controller'; export { Platform, setupPlatform, UserAgentToken, DocumentDirToken, DocLangToken, NavigatorPlatformToken } from './platform/platform'; export { Haptic } from './util/haptic'; +export { ImgLoader } from './components/img/img-loader'; export { QueryParams, setupQueryParams, UrlToken } from './platform/query-params'; export { DeepLinker } from './navigation/deep-linker'; export { NavController } from './navigation/nav-controller'; @@ -174,6 +176,7 @@ export class IonicModule { Form, GestureController, Haptic, + ImgLoader, Keyboard, LoadingController, Location,