img.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import { ChangeDetectionStrategy, Component, ElementRef, Input, Optional, Renderer, ViewEncapsulation } from '@angular/core';
  2. import { Content } from '../content/content';
  3. import { DomController } from '../../platform/dom-controller';
  4. import { isPresent, isTrueProperty } from '../../util/util';
  5. import { Platform } from '../../platform/platform';
  6. /**
  7. * @name Img
  8. * @description
  9. * Two of the biggest cuprits of scroll jank is starting up a new HTTP
  10. * request, and rendering images. These two reasons is largely why
  11. * `ion-img` was created. The standard HTML `img` element is often a large
  12. * source of these problems, and what makes matters worse is that the app
  13. * does not have fine-grained control of requests and rendering for each
  14. * `img` element.
  15. *
  16. * The `ion-img` component is similar to the standard `img` element,
  17. * but it also adds features in order to provide improved performance.
  18. * Features include only loading images which are visible, using web workers
  19. * for HTTP requests, preventing jank while scrolling and in-memory caching.
  20. *
  21. * Note that `ion-img` also comes with a few more restrictions in comparison
  22. * to the standard `img` element. A good rule is, if there are only a few
  23. * images to be rendered on a page, then the standard `img` is probably
  24. * best. However, if a page has the potential for hundreds or even thousands
  25. * of images within a scrollable area, then `ion-img` would be better suited
  26. * for the job.
  27. *
  28. * > Note: `ion-img` is only meant to be used inside of [virtual-scroll](/docs/api/components/virtual-scroll/VirtualScroll/)
  29. *
  30. *
  31. * ### Lazy Loading
  32. *
  33. * Lazy loading images refers to only loading images which are actually
  34. * visible within the user's viewport. This also means that images which are
  35. * not viewable on the initial load would not be downloaded or rendered. Next,
  36. * as the user scrolls, each image which becomes visible is then requested
  37. * then rendered on-demand.
  38. *
  39. * The benefits of this approach is that unnecessary and resource intensive
  40. * HTTP requests are not started, valuable bandwidth isn't wasted, and this
  41. * allows the browser to free up resources which would be wasted on images
  42. * which are not even viewable. For example, animated GIFs are enourmous
  43. * performance drains, however, with `ion-img` the app is able to dedicate
  44. * resources to just the viewable images. But again, if the problems listed
  45. * above are not problems within your app, then the standard `img` element
  46. * may be best.
  47. *
  48. *
  49. * ### Image Dimensions
  50. *
  51. * By providing image dimensions up front, Ionic is able to accurately size
  52. * up the image's location within the viewport, which helps lazy load only
  53. * images which are viewable. Image dimensions can either by set as
  54. * properties, inline styles, or external stylesheets. It doesn't matter
  55. * which method of setting dimensions is used, but it's important that somehow
  56. * each `ion-img` has been given an exact size.
  57. *
  58. * For example, by default `<ion-avatar>` and `<ion-thumbnail>` already come
  59. * with exact sizes when placed within an `<ion-item>`. By giving each image
  60. * an exact size, this then further locks in the size of each `ion-item`,
  61. * which again helps improve scroll performance.
  62. *
  63. * ```html
  64. * <!-- dimensions set using attributes -->
  65. * <ion-img width="80" height="80" src="..."></ion-img>
  66. *
  67. * <!-- dimensions set using input properties -->
  68. * <ion-img [width]="imgWidth" [height]="imgHeight" src="..."></ion-img>
  69. *
  70. * <!-- dimensions set using inline styles -->
  71. * <ion-img style="width: 80px; height: 80px;" src="..."></ion-img>
  72. * ```
  73. *
  74. * Additionally, each `ion-img` uses the `object-fit: cover` CSS property.
  75. * What this means is that the actual rendered image will center itself within
  76. * it's container. Or to really get detailed: The image is sized to maintain
  77. * its aspect ratio while filling the containing element’s entire content box.
  78. * Its concrete object size is resolved as a cover constraint against the
  79. * element’s used width and height.
  80. *
  81. * ### Future Optimizations
  82. *
  83. * Future goals are to place image requests within web workers, and cache
  84. * images in-memory as datauris. This method has proven to be effective,
  85. * however there are some current limitations with Cordova which we are
  86. * currently working on.
  87. *
  88. */
  89. export class Img {
  90. constructor(_elementRef, _renderer, _plt, _content, _dom) {
  91. this._elementRef = _elementRef;
  92. this._renderer = _renderer;
  93. this._plt = _plt;
  94. this._content = _content;
  95. this._dom = _dom;
  96. /** @internal */
  97. this._cache = true;
  98. /** @internal */
  99. this._w = '';
  100. /** @internal */
  101. this._h = '';
  102. /** @internal */
  103. this._wQ = '';
  104. /** @internal */
  105. this._hQ = '';
  106. /**
  107. * @input {string} Set the `alt` attribute which gets assigned to
  108. * the inner `img` element.
  109. */
  110. this.alt = '';
  111. if (!this._content) {
  112. console.warn(`ion-img can only be used within an ion-content`);
  113. }
  114. else {
  115. this._content.addImg(this);
  116. }
  117. this._isLoaded(false);
  118. }
  119. /**
  120. * @input {string} The source of the image.
  121. */
  122. get src() {
  123. return this._src;
  124. }
  125. set src(newSrc) {
  126. // if the source hasn't changed, then um, let's not change it
  127. if (newSrc !== this._src) {
  128. // we're changing the source
  129. // so abort any active http requests
  130. // and render the image empty
  131. this.reset();
  132. // update to the new src
  133. this._src = newSrc;
  134. // Are they using an actual datauri already,
  135. // or reset any existing datauri we might be holding onto
  136. this._hasLoaded = newSrc.indexOf('data:') === 0;
  137. // run update to kick off requests or render if everything is good
  138. this.update();
  139. }
  140. }
  141. /**
  142. * @hidden
  143. */
  144. reset() {
  145. if (this._requestingSrc) {
  146. // abort any active requests
  147. (void 0) /* console.debug */;
  148. this._srcAttr('');
  149. this._requestingSrc = null;
  150. }
  151. if (this._renderedSrc) {
  152. // clear out the currently rendered img
  153. (void 0) /* console.debug */;
  154. this._renderedSrc = null;
  155. this._isLoaded(false);
  156. }
  157. }
  158. /**
  159. * @hidden
  160. */
  161. update() {
  162. // only attempt an update if there is an active src
  163. // and the content containing the image considers it updatable
  164. if (this._src && this._content.isImgsUpdatable()) {
  165. if (this.canRequest && (this._src !== this._renderedSrc && this._src !== this._requestingSrc) && !this._hasLoaded) {
  166. // only begin the request if we "can" request
  167. // begin the image request if the src is different from the rendered src
  168. // and if we don't already has a tmpDataUri
  169. (void 0) /* console.debug */;
  170. this._requestingSrc = this._src;
  171. this._isLoaded(false);
  172. this._srcAttr(this._src);
  173. // set the dimensions of the image if we do have different data
  174. this._setDims();
  175. }
  176. if (this.canRender && this._hasLoaded && this._src !== this._renderedSrc) {
  177. // we can render and we have a datauri to render
  178. this._renderedSrc = this._src;
  179. this._setDims();
  180. this._dom.write(() => {
  181. if (this._hasLoaded) {
  182. (void 0) /* console.debug */;
  183. this._isLoaded(true);
  184. this._srcAttr(this._src);
  185. }
  186. });
  187. }
  188. }
  189. }
  190. /**
  191. * @internal
  192. */
  193. _isLoaded(isLoaded) {
  194. const renderer = this._renderer;
  195. const ele = this._elementRef.nativeElement;
  196. renderer.setElementClass(ele, 'img-loaded', isLoaded);
  197. renderer.setElementClass(ele, 'img-unloaded', !isLoaded);
  198. }
  199. /**
  200. * @internal
  201. */
  202. _srcAttr(srcAttr) {
  203. const imgEle = this._img;
  204. const renderer = this._renderer;
  205. if (imgEle && imgEle.src !== srcAttr) {
  206. renderer.setElementAttribute(this._img, 'src', srcAttr);
  207. renderer.setElementAttribute(this._img, 'alt', this.alt);
  208. }
  209. }
  210. /**
  211. * @hidden
  212. */
  213. get top() {
  214. const bounds = this._getBounds();
  215. return bounds && bounds.top || 0;
  216. }
  217. /**
  218. * @hidden
  219. */
  220. get bottom() {
  221. const bounds = this._getBounds();
  222. return bounds && bounds.bottom || 0;
  223. }
  224. _getBounds() {
  225. if (this._bounds) {
  226. // we've been manually passed bounds data
  227. // this is probably from Virtual Scroll items
  228. return this._bounds;
  229. }
  230. if (!this._rect) {
  231. // we don't have bounds from virtual scroll
  232. // so let's do the raw DOM lookup w/ getBoundingClientRect
  233. this._rect = this._elementRef.nativeElement.getBoundingClientRect();
  234. (void 0) /* console.debug */;
  235. }
  236. return this._rect;
  237. }
  238. /**
  239. * @input {any} Sets the bounding rectangle of the element relative to the viewport.
  240. * When using `VirtualScroll`, each virtual item should pass its bounds to each
  241. * `ion-img`. The passed in data object should include `top` and `bottom` properties.
  242. */
  243. set bounds(b) {
  244. if (isPresent(b)) {
  245. this._bounds = b;
  246. }
  247. }
  248. /**
  249. * @input {boolean} After an image has been successfully downloaded, it can be cached
  250. * in-memory. This is useful for `VirtualScroll` by allowing image responses to be
  251. * cached, and not rendered, until after scrolling has completed, which allows for
  252. * smoother scrolling.
  253. */
  254. get cache() {
  255. return this._cache;
  256. }
  257. set cache(val) {
  258. this._cache = isTrueProperty(val);
  259. }
  260. /**
  261. * @input {string} Image width. If this property is not set it's important that
  262. * the dimensions are still set using CSS. If the dimension is just a number it
  263. * will assume the `px` unit.
  264. */
  265. set width(val) {
  266. this._wQ = getUnitValue(val);
  267. this._setDims();
  268. }
  269. /**
  270. * @input {string} Image height. If this property is not set it's important that
  271. * the dimensions are still set using CSS. If the dimension is just a number it
  272. * will assume the `px` unit.
  273. */
  274. set height(val) {
  275. this._hQ = getUnitValue(val);
  276. this._setDims();
  277. }
  278. _setDims() {
  279. // only set the dimensions if we can render
  280. // and only if the dimensions have changed from when we last set it
  281. if (this.canRender && (this._w !== this._wQ || this._h !== this._hQ)) {
  282. var wrapperEle = this._elementRef.nativeElement;
  283. var renderer = this._renderer;
  284. this._dom.write(() => {
  285. if (this._w !== this._wQ) {
  286. this._w = this._wQ;
  287. renderer.setElementStyle(wrapperEle, 'width', this._w);
  288. }
  289. if (this._h !== this._hQ) {
  290. this._h = this._hQ;
  291. renderer.setElementStyle(wrapperEle, 'height', this._h);
  292. }
  293. });
  294. }
  295. }
  296. /**
  297. * @hidden
  298. */
  299. ngAfterContentInit() {
  300. this._img = this._elementRef.nativeElement.firstChild;
  301. this._unreg = this._plt.registerListener(this._img, 'load', () => {
  302. this._hasLoaded = true;
  303. this.update();
  304. }, { passive: true });
  305. }
  306. /**
  307. * @hidden
  308. */
  309. ngOnDestroy() {
  310. this._unreg && this._unreg();
  311. this._content && this._content.removeImg(this);
  312. }
  313. }
  314. Img.decorators = [
  315. { type: Component, args: [{
  316. selector: 'ion-img',
  317. template: '<img>',
  318. changeDetection: ChangeDetectionStrategy.OnPush,
  319. encapsulation: ViewEncapsulation.None,
  320. },] },
  321. ];
  322. /** @nocollapse */
  323. Img.ctorParameters = () => [
  324. { type: ElementRef, },
  325. { type: Renderer, },
  326. { type: Platform, },
  327. { type: Content, decorators: [{ type: Optional },] },
  328. { type: DomController, },
  329. ];
  330. Img.propDecorators = {
  331. 'src': [{ type: Input },],
  332. 'bounds': [{ type: Input },],
  333. 'cache': [{ type: Input },],
  334. 'width': [{ type: Input },],
  335. 'height': [{ type: Input },],
  336. 'alt': [{ type: Input },],
  337. };
  338. function getUnitValue(val) {
  339. if (isPresent(val)) {
  340. if (typeof val === 'string') {
  341. if (val.indexOf('%') > -1 || val.indexOf('px') > -1) {
  342. return val;
  343. }
  344. if (val.length) {
  345. return val + 'px';
  346. }
  347. }
  348. else if (typeof val === 'number') {
  349. return val + 'px';
  350. }
  351. }
  352. return '';
  353. }
  354. //# sourceMappingURL=img.js.map