LazyLoad.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { ROLE } from '../../constants/attributes';
  2. import { CLASS_LOADING } from '../../constants/classes';
  3. import {
  4. EVENT_LAZYLOAD_LOADED,
  5. EVENT_MOUNTED,
  6. EVENT_MOVED,
  7. EVENT_REFRESH,
  8. EVENT_RESIZE,
  9. EVENT_SCROLLED,
  10. } from '../../constants/events';
  11. import { EventInterface } from '../../constructors';
  12. import { Splide } from '../../core/Splide/Splide';
  13. import { BaseComponent, Components, Options } from '../../types';
  14. import {
  15. addClass,
  16. child,
  17. create,
  18. display,
  19. getAttribute,
  20. queryAll,
  21. remove,
  22. removeAttribute,
  23. removeClass,
  24. setAttribute,
  25. } from '../../utils';
  26. import { SlideComponent } from '../Slides/Slide';
  27. import { IMAGE_SELECTOR, SRC_DATA_ATTRIBUTE, SRCSET_DATA_ATTRIBUTE } from './constants';
  28. /**
  29. * The interface for the LazyLoad component.
  30. *
  31. * @since 3.0.0
  32. */
  33. export interface LazyLoadComponent extends BaseComponent {
  34. }
  35. /**
  36. * The interface for all components.
  37. *
  38. * @since 3.0.0
  39. */
  40. export interface LazyLoadImagesData {
  41. _img: HTMLImageElement;
  42. _spinner: HTMLSpanElement;
  43. _Slide: SlideComponent;
  44. src: string | null;
  45. srcset: string | null;
  46. }
  47. /**
  48. * The component for lazily loading images.
  49. *
  50. * @since 3.0.0
  51. *
  52. * @param Splide - A Splide instance.
  53. * @param Components - A collection of components.
  54. * @param options - Options.
  55. *
  56. * @return An LazyLoad component object.
  57. */
  58. export function LazyLoad( Splide: Splide, Components: Components, options: Options ): LazyLoadComponent {
  59. const { on, off, bind, emit } = EventInterface( Splide );
  60. const isSequential = options.lazyLoad === 'sequential';
  61. /**
  62. * Stores data of images.
  63. */
  64. let images: LazyLoadImagesData[] = [];
  65. /**
  66. * The current index of images.
  67. */
  68. let index = 0;
  69. /**
  70. * Called when the component is mounted.
  71. */
  72. function mount(): void {
  73. if ( options.lazyLoad ) {
  74. init();
  75. on( EVENT_REFRESH, destroy );
  76. on( EVENT_REFRESH, init );
  77. if ( ! isSequential ) {
  78. on( [ EVENT_MOUNTED, EVENT_REFRESH, EVENT_MOVED, EVENT_SCROLLED ], observe );
  79. }
  80. }
  81. }
  82. /**
  83. * Finds images that contain specific data attributes.
  84. */
  85. function init() {
  86. Components.Slides.forEach( _Slide => {
  87. queryAll<HTMLImageElement>( _Slide.slide, IMAGE_SELECTOR ).forEach( _img => {
  88. const src = getAttribute( _img, SRC_DATA_ATTRIBUTE );
  89. const srcset = getAttribute( _img, SRCSET_DATA_ATTRIBUTE );
  90. if ( src !== _img.src || srcset !== _img.srcset ) {
  91. const className = options.classes.spinner;
  92. const parent = _img.parentElement;
  93. const _spinner = child( parent, `.${ className }` ) || create( 'span', className, parent );
  94. setAttribute( _spinner, ROLE, 'presentation' );
  95. images.push( { _img, _Slide, src, srcset, _spinner } );
  96. ! _img.src && display( _img, 'none' );
  97. }
  98. } );
  99. } );
  100. if ( isSequential ) {
  101. loadNext();
  102. }
  103. }
  104. /**
  105. * Destroys the component.
  106. */
  107. function destroy() {
  108. index = 0;
  109. images = [];
  110. }
  111. /**
  112. * Checks how close each image is from the active slide, and determines whether to start loading or not.
  113. * The last `+1` is for the current page.
  114. */
  115. function observe(): void {
  116. images = images.filter( data => {
  117. const distance = options.perPage * ( ( options.preloadPages || 1 ) + 1 ) - 1;
  118. if ( data._Slide.isWithin( Splide.index, distance ) ) {
  119. return load( data );
  120. }
  121. return true;
  122. } );
  123. if ( ! images.length ) {
  124. off( EVENT_MOVED );
  125. }
  126. }
  127. /**
  128. * Starts loading the image in the data.
  129. *
  130. * @param data - A LazyLoadImagesData object.
  131. */
  132. function load( data: LazyLoadImagesData ): void {
  133. const { _img } = data;
  134. addClass( data._Slide.slide, CLASS_LOADING );
  135. bind( _img, 'load error', e => { onLoad( data, e.type === 'error' ) } );
  136. [ 'srcset', 'src' ].forEach( name => {
  137. if ( data[ name ] ) {
  138. setAttribute( _img, name, data[ name ] );
  139. removeAttribute( _img, name === 'src' ? SRC_DATA_ATTRIBUTE : SRCSET_DATA_ATTRIBUTE );
  140. }
  141. } );
  142. }
  143. /**
  144. * Called when the image is loaded or any error occurs.
  145. *
  146. * @param data - A LazyLoadImagesData object.
  147. * @param error - `true` if this method is called on error.
  148. */
  149. function onLoad( data: LazyLoadImagesData, error: boolean ): void {
  150. const { _Slide } = data;
  151. removeClass( _Slide.slide, CLASS_LOADING );
  152. if ( ! error ) {
  153. remove( data._spinner );
  154. display( data._img, '' );
  155. emit( EVENT_LAZYLOAD_LOADED, data._img, _Slide );
  156. emit( EVENT_RESIZE );
  157. }
  158. if ( isSequential ) {
  159. loadNext();
  160. }
  161. }
  162. /**
  163. * Starts loading a next image.
  164. */
  165. function loadNext(): void {
  166. if ( index < images.length ) {
  167. load( images[ index++ ] );
  168. }
  169. }
  170. return {
  171. mount,
  172. destroy,
  173. };
  174. }