img.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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. var Img = (function () {
  90. function Img(_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. Object.defineProperty(Img.prototype, "src", {
  120. /**
  121. * @input {string} The source of the image.
  122. */
  123. get: function () {
  124. return this._src;
  125. },
  126. set: function (newSrc) {
  127. // if the source hasn't changed, then um, let's not change it
  128. if (newSrc !== this._src) {
  129. // we're changing the source
  130. // so abort any active http requests
  131. // and render the image empty
  132. this.reset();
  133. // update to the new src
  134. this._src = newSrc;
  135. // Are they using an actual datauri already,
  136. // or reset any existing datauri we might be holding onto
  137. this._hasLoaded = newSrc.indexOf('data:') === 0;
  138. // run update to kick off requests or render if everything is good
  139. this.update();
  140. }
  141. },
  142. enumerable: true,
  143. configurable: true
  144. });
  145. /**
  146. * @hidden
  147. */
  148. Img.prototype.reset = function () {
  149. if (this._requestingSrc) {
  150. // abort any active requests
  151. (void 0) /* console.debug */;
  152. this._srcAttr('');
  153. this._requestingSrc = null;
  154. }
  155. if (this._renderedSrc) {
  156. // clear out the currently rendered img
  157. (void 0) /* console.debug */;
  158. this._renderedSrc = null;
  159. this._isLoaded(false);
  160. }
  161. };
  162. /**
  163. * @hidden
  164. */
  165. Img.prototype.update = function () {
  166. var _this = this;
  167. // only attempt an update if there is an active src
  168. // and the content containing the image considers it updatable
  169. if (this._src && this._content.isImgsUpdatable()) {
  170. if (this.canRequest && (this._src !== this._renderedSrc && this._src !== this._requestingSrc) && !this._hasLoaded) {
  171. // only begin the request if we "can" request
  172. // begin the image request if the src is different from the rendered src
  173. // and if we don't already has a tmpDataUri
  174. (void 0) /* console.debug */;
  175. this._requestingSrc = this._src;
  176. this._isLoaded(false);
  177. this._srcAttr(this._src);
  178. // set the dimensions of the image if we do have different data
  179. this._setDims();
  180. }
  181. if (this.canRender && this._hasLoaded && this._src !== this._renderedSrc) {
  182. // we can render and we have a datauri to render
  183. this._renderedSrc = this._src;
  184. this._setDims();
  185. this._dom.write(function () {
  186. if (_this._hasLoaded) {
  187. (void 0) /* console.debug */;
  188. _this._isLoaded(true);
  189. _this._srcAttr(_this._src);
  190. }
  191. });
  192. }
  193. }
  194. };
  195. /**
  196. * @internal
  197. */
  198. Img.prototype._isLoaded = function (isLoaded) {
  199. var renderer = this._renderer;
  200. var ele = this._elementRef.nativeElement;
  201. renderer.setElementClass(ele, 'img-loaded', isLoaded);
  202. renderer.setElementClass(ele, 'img-unloaded', !isLoaded);
  203. };
  204. /**
  205. * @internal
  206. */
  207. Img.prototype._srcAttr = function (srcAttr) {
  208. var imgEle = this._img;
  209. var renderer = this._renderer;
  210. if (imgEle && imgEle.src !== srcAttr) {
  211. renderer.setElementAttribute(this._img, 'src', srcAttr);
  212. renderer.setElementAttribute(this._img, 'alt', this.alt);
  213. }
  214. };
  215. Object.defineProperty(Img.prototype, "top", {
  216. /**
  217. * @hidden
  218. */
  219. get: function () {
  220. var bounds = this._getBounds();
  221. return bounds && bounds.top || 0;
  222. },
  223. enumerable: true,
  224. configurable: true
  225. });
  226. Object.defineProperty(Img.prototype, "bottom", {
  227. /**
  228. * @hidden
  229. */
  230. get: function () {
  231. var bounds = this._getBounds();
  232. return bounds && bounds.bottom || 0;
  233. },
  234. enumerable: true,
  235. configurable: true
  236. });
  237. Img.prototype._getBounds = function () {
  238. if (this._bounds) {
  239. // we've been manually passed bounds data
  240. // this is probably from Virtual Scroll items
  241. return this._bounds;
  242. }
  243. if (!this._rect) {
  244. // we don't have bounds from virtual scroll
  245. // so let's do the raw DOM lookup w/ getBoundingClientRect
  246. this._rect = this._elementRef.nativeElement.getBoundingClientRect();
  247. (void 0) /* console.debug */;
  248. }
  249. return this._rect;
  250. };
  251. Object.defineProperty(Img.prototype, "bounds", {
  252. /**
  253. * @input {any} Sets the bounding rectangle of the element relative to the viewport.
  254. * When using `VirtualScroll`, each virtual item should pass its bounds to each
  255. * `ion-img`. The passed in data object should include `top` and `bottom` properties.
  256. */
  257. set: function (b) {
  258. if (isPresent(b)) {
  259. this._bounds = b;
  260. }
  261. },
  262. enumerable: true,
  263. configurable: true
  264. });
  265. Object.defineProperty(Img.prototype, "cache", {
  266. /**
  267. * @input {boolean} After an image has been successfully downloaded, it can be cached
  268. * in-memory. This is useful for `VirtualScroll` by allowing image responses to be
  269. * cached, and not rendered, until after scrolling has completed, which allows for
  270. * smoother scrolling.
  271. */
  272. get: function () {
  273. return this._cache;
  274. },
  275. set: function (val) {
  276. this._cache = isTrueProperty(val);
  277. },
  278. enumerable: true,
  279. configurable: true
  280. });
  281. Object.defineProperty(Img.prototype, "width", {
  282. /**
  283. * @input {string} Image width. If this property is not set it's important that
  284. * the dimensions are still set using CSS. If the dimension is just a number it
  285. * will assume the `px` unit.
  286. */
  287. set: function (val) {
  288. this._wQ = getUnitValue(val);
  289. this._setDims();
  290. },
  291. enumerable: true,
  292. configurable: true
  293. });
  294. Object.defineProperty(Img.prototype, "height", {
  295. /**
  296. * @input {string} Image height. If this property is not set it's important that
  297. * the dimensions are still set using CSS. If the dimension is just a number it
  298. * will assume the `px` unit.
  299. */
  300. set: function (val) {
  301. this._hQ = getUnitValue(val);
  302. this._setDims();
  303. },
  304. enumerable: true,
  305. configurable: true
  306. });
  307. Img.prototype._setDims = function () {
  308. var _this = this;
  309. // only set the dimensions if we can render
  310. // and only if the dimensions have changed from when we last set it
  311. if (this.canRender && (this._w !== this._wQ || this._h !== this._hQ)) {
  312. var wrapperEle = this._elementRef.nativeElement;
  313. var renderer = this._renderer;
  314. this._dom.write(function () {
  315. if (_this._w !== _this._wQ) {
  316. _this._w = _this._wQ;
  317. renderer.setElementStyle(wrapperEle, 'width', _this._w);
  318. }
  319. if (_this._h !== _this._hQ) {
  320. _this._h = _this._hQ;
  321. renderer.setElementStyle(wrapperEle, 'height', _this._h);
  322. }
  323. });
  324. }
  325. };
  326. /**
  327. * @hidden
  328. */
  329. Img.prototype.ngAfterContentInit = function () {
  330. var _this = this;
  331. this._img = this._elementRef.nativeElement.firstChild;
  332. this._unreg = this._plt.registerListener(this._img, 'load', function () {
  333. _this._hasLoaded = true;
  334. _this.update();
  335. }, { passive: true });
  336. };
  337. /**
  338. * @hidden
  339. */
  340. Img.prototype.ngOnDestroy = function () {
  341. this._unreg && this._unreg();
  342. this._content && this._content.removeImg(this);
  343. };
  344. Img.decorators = [
  345. { type: Component, args: [{
  346. selector: 'ion-img',
  347. template: '<img>',
  348. changeDetection: ChangeDetectionStrategy.OnPush,
  349. encapsulation: ViewEncapsulation.None,
  350. },] },
  351. ];
  352. /** @nocollapse */
  353. Img.ctorParameters = function () { return [
  354. { type: ElementRef, },
  355. { type: Renderer, },
  356. { type: Platform, },
  357. { type: Content, decorators: [{ type: Optional },] },
  358. { type: DomController, },
  359. ]; };
  360. Img.propDecorators = {
  361. 'src': [{ type: Input },],
  362. 'bounds': [{ type: Input },],
  363. 'cache': [{ type: Input },],
  364. 'width': [{ type: Input },],
  365. 'height': [{ type: Input },],
  366. 'alt': [{ type: Input },],
  367. };
  368. return Img;
  369. }());
  370. export { Img };
  371. function getUnitValue(val) {
  372. if (isPresent(val)) {
  373. if (typeof val === 'string') {
  374. if (val.indexOf('%') > -1 || val.indexOf('px') > -1) {
  375. return val;
  376. }
  377. if (val.length) {
  378. return val + 'px';
  379. }
  380. }
  381. else if (typeof val === 'number') {
  382. return val + 'px';
  383. }
  384. }
  385. return '';
  386. }
  387. //# sourceMappingURL=img.js.map