Source: imready/src/ImReadyManager.ts

  1. /*
  2. egjs-imready
  3. Copyright (c) 2020-present NAVER Corp.
  4. MIT license
  5. */
  6. import Component, { ComponentEvent } from "@egjs/component";
  7. import { ElementLoader } from "./loaders/ElementLoader";
  8. import { ArrayFormat, ElementInfo, ImReadyEvents, ImReadyLoaderOptions, ImReadyOptions } from "./types";
  9. import { toArray, getContentElements, hasLoadingAttribute } from "./utils";
  10. /**
  11. * @alias eg.ImReady
  12. * @extends eg.Component
  13. */
  14. class ImReadyManager extends Component<ImReadyEvents> {
  15. public options!: ImReadyOptions;
  16. private readyCount = 0;
  17. private preReadyCount = 0;
  18. private totalCount = 0;
  19. private totalErrorCount = 0;
  20. private isPreReadyOver = true;
  21. private elementInfos: ElementInfo[] = [];
  22. /**
  23. * @param - ImReady's options
  24. */
  25. constructor(options: Partial<ImReadyOptions> = {}) {
  26. super();
  27. this.options = {
  28. loaders: {},
  29. prefix: "data-",
  30. ...options,
  31. };
  32. }
  33. /**
  34. * Checks whether elements are in the ready state.
  35. * @ko 엘리먼트가 준비 상태인지 체크한다.
  36. * @elements - Elements to check ready status. <ko> 준비 상태를 체크할 엘리먼트들.</ko>
  37. * @example
  38. * ```html
  39. * <div>
  40. * <img src="./1.jpg" data-width="1280" data-height="853" style="width:100%"/>
  41. * <img src="./2.jpg" data-width="1280" data-height="853"/>
  42. * <img src="ERR" data-width="1280" data-height="853"/>
  43. * </div>
  44. * ```
  45. * ## Javascript
  46. * ```js
  47. * import ImReady from "@egjs/imready";
  48. *
  49. * const im = new ImReady(); // umd: eg.ImReady
  50. * im.check(document.querySelectorAll("img")).on({
  51. * preReadyElement: e => {
  52. * // 1, 3
  53. * // 2, 3
  54. * // 3, 3
  55. * console.log(e.preReadyCount, e.totalCount),
  56. * },
  57. * });
  58. * ```
  59. */
  60. public check(elements: ArrayFormat<HTMLElement>): this {
  61. const { prefix } = this.options;
  62. this.clear();
  63. this.elementInfos = toArray(elements).map((element, index) => {
  64. const loader = this.getLoader(element, { prefix });
  65. loader.check();
  66. loader.on("error", e => {
  67. this.onError(index, e.target);
  68. }).on("preReady", e => {
  69. const info = this.elementInfos[index];
  70. info.hasLoading = e.hasLoading;
  71. info.isSkip = e.isSkip;
  72. const isPreReady = this.checkPreReady(index);
  73. this.onPreReadyElement(index);
  74. isPreReady && this.onPreReady();
  75. }).on("ready", ({ withPreReady, hasLoading, isSkip }) => {
  76. const info = this.elementInfos[index];
  77. info.hasLoading = hasLoading;
  78. info.isSkip = isSkip;
  79. const isPreReady = withPreReady && this.checkPreReady(index);
  80. const isReady = this.checkReady(index);
  81. // Pre-ready and ready occur simultaneously
  82. withPreReady && this.onPreReadyElement(index);
  83. this.onReadyElement(index);
  84. isPreReady && this.onPreReady();
  85. isReady && this.onReady();
  86. });
  87. return {
  88. loader,
  89. element,
  90. hasLoading: false,
  91. hasError: false,
  92. isPreReady: false,
  93. isReady: false,
  94. isSkip: false,
  95. };
  96. });
  97. const length = this.elementInfos.length;
  98. this.totalCount = length;
  99. if (!length) {
  100. setTimeout(() => {
  101. this.onPreReady();
  102. this.onReady();
  103. });
  104. }
  105. return this;
  106. }
  107. /**
  108. * Gets the total count of elements to be checked.
  109. * @ko 체크하는 element의 총 개수를 가져온다.
  110. */
  111. public getTotalCount() {
  112. return this.totalCount;
  113. }
  114. /**
  115. * Whether the elements are all pre-ready. (all sizes are known)
  116. * @ko 엘리먼트들이 모두 사전 준비가 됐는지 (사이즈를 전부 알 수 있는지) 여부.
  117. */
  118. public isPreReady() {
  119. return this.elementInfos.every(info => info.isPreReady);
  120. }
  121. /**
  122. * Whether the elements are all ready.
  123. * @ko 엘리먼트들이 모두 준비가 됐는지 여부.
  124. */
  125. public isReady() {
  126. return this.elementInfos.every(info => info.isReady);
  127. }
  128. /**
  129. * Whether an error has occurred in the elements in the current state.
  130. * @ko 현재 상태에서 엘리먼트들이 에러가 발생했는지 여부.
  131. */
  132. public hasError() {
  133. return this.totalErrorCount > 0;
  134. }
  135. /**
  136. * Clears events of elements being checked.
  137. * @ko 체크 중인 엘리먼트들의 이벤트를 해제 한다.
  138. */
  139. public clear() {
  140. this.isPreReadyOver = false;
  141. this.totalCount = 0;
  142. this.preReadyCount = 0;
  143. this.readyCount = 0;
  144. this.totalErrorCount = 0;
  145. this.elementInfos.forEach(info => {
  146. if (info.loader) {
  147. info.loader.destroy();
  148. }
  149. });
  150. this.elementInfos = [];
  151. }
  152. /**
  153. * Destory all events.
  154. * @ko 모든 이벤트를 해제 한다.
  155. */
  156. public destroy() {
  157. this.clear();
  158. this.off();
  159. }
  160. private getLoader(element: HTMLElement, options: ImReadyLoaderOptions) {
  161. const tagName = element.tagName.toLowerCase();
  162. const loaders = this.options.loaders;
  163. const prefix = options.prefix;
  164. const tags = Object.keys(loaders);
  165. if (loaders[tagName]) {
  166. return new loaders[tagName](element, options);
  167. }
  168. const loader = new ElementLoader(element, options);
  169. const children = toArray(element.querySelectorAll<HTMLElement>(tags.join(", ")));
  170. loader.setHasLoading(children.some(el => hasLoadingAttribute(el, prefix)));
  171. let withPreReady = false;
  172. const childrenImReady = this.clone().on("error", e => {
  173. loader.onError(e.target);
  174. }).on("ready", () => {
  175. loader.onReady(withPreReady);
  176. });
  177. loader.on("requestChildren", () => {
  178. // has not data size
  179. const contentElements = getContentElements(element, tags, this.options.prefix);
  180. childrenImReady.check(contentElements).on("preReady", e => {
  181. withPreReady = e.isReady;
  182. if (!withPreReady) {
  183. loader.onPreReady();
  184. }
  185. });
  186. }).on("reqeustReadyChildren", () => {
  187. // has data size
  188. // loader call preReady
  189. // check only video, image elements
  190. childrenImReady.check(children);
  191. }).on("requestDestroy", () => {
  192. childrenImReady.destroy();
  193. });
  194. return loader;
  195. }
  196. private clone() {
  197. return new ImReadyManager({ ...this.options });
  198. }
  199. private checkPreReady(index: number) {
  200. this.elementInfos[index].isPreReady = true;
  201. ++this.preReadyCount;
  202. if (this.preReadyCount < this.totalCount) {
  203. return false;
  204. }
  205. return true;
  206. }
  207. private checkReady(index: number) {
  208. this.elementInfos[index].isReady = true;
  209. ++this.readyCount;
  210. if (this.readyCount < this.totalCount) {
  211. return false;
  212. }
  213. return true;
  214. }
  215. private onError(index: number, target: HTMLElement) {
  216. const info = this.elementInfos[index];
  217. info.hasError = true;
  218. /**
  219. * An event occurs if the image, video fails to load.
  220. * @ko 이미지, 비디오가 로딩에 실패하면 이벤트가 발생한다.
  221. * @event eg.ImReady#error
  222. * @param {eg.ImReady.OnError} e - The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
  223. * @example
  224. * ```html
  225. * <div>
  226. * <img src="./1.jpg" data-width="1280" data-height="853" style="width:100%"/>
  227. * <img src="./2.jpg"/>
  228. * <img src="ERR"/>
  229. * </div>
  230. * ```
  231. * ## Javascript
  232. * ```js
  233. * import ImReady from "@egjs/imready";
  234. *
  235. * const im = new ImReady(); // umd: eg.ImReady
  236. * im.check([document.querySelector("div")]).on({
  237. * error: e => {
  238. * // <div>...</div>, 0, <img src="ERR"/>
  239. * console.log(e.element, e.index, e.target),
  240. * },
  241. * });
  242. * ```
  243. */
  244. this.trigger(new ComponentEvent("error", {
  245. element: info.element,
  246. index,
  247. target,
  248. errorCount: this.getErrorCount(),
  249. totalErrorCount: ++this.totalErrorCount,
  250. }));
  251. }
  252. private onPreReadyElement(index: number) {
  253. const info = this.elementInfos[index];
  254. /**
  255. * An event occurs when the element is pre-ready (when the loading attribute is applied or the size is known)
  256. * @ko 해당 엘리먼트가 사전 준비되었을 때(loading 속성이 적용되었거나 사이즈를 알 수 있을 때) 이벤트가 발생한다.
  257. * @event eg.ImReady#preReadyElement
  258. * @param {eg.ImReady.OnPreReadyElement} e - The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
  259. * @example
  260. * ```html
  261. * <div>
  262. * <img src="./1.jpg" data-width="1280" data-height="853" style="width:100%"/>
  263. * <img src="./2.jpg" data-width="1280" data-height="853"/>
  264. * <img src="ERR" data-width="1280" data-height="853"/>
  265. * </div>
  266. * ```
  267. * ## Javascript
  268. * ```js
  269. * import ImReady from "@egjs/imready";
  270. *
  271. * const im = new ImReady(); // umd: eg.ImReady
  272. * im.check(document.querySelectorAll("img")).on({
  273. * preReadyElement: e => {
  274. * // 1, 3
  275. * // 2, 3
  276. * // 3, 3
  277. * console.log(e.preReadyCount, e.totalCount),
  278. * },
  279. * });
  280. * ```
  281. */
  282. this.trigger(new ComponentEvent("preReadyElement", {
  283. element: info.element,
  284. index,
  285. preReadyCount: this.preReadyCount,
  286. readyCount: this.readyCount,
  287. totalCount: this.totalCount,
  288. isPreReady: this.isPreReady(),
  289. isReady: this.isReady(),
  290. hasLoading: info.hasLoading,
  291. isSkip: info.isSkip,
  292. }));
  293. }
  294. private onPreReady() {
  295. this.isPreReadyOver = true;
  296. /**
  297. * An event occurs when all element are pre-ready (When all elements have the loading attribute applied or the size is known)
  298. * @ko 모든 엘리먼트들이 사전 준비된 경우 (모든 엘리먼트들이 loading 속성이 적용되었거나 사이즈를 알 수 있는 경우) 이벤트가 발생한다.
  299. * @event eg.ImReady#preReady
  300. * @param {eg.ImReady.OnPreReady} e - The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
  301. * @example
  302. * ```html
  303. * <div>
  304. * <img src="./1.jpg" data-width="1280" data-height="853" style="width:100%"/>
  305. * <img src="./2.jpg" data-width="1280" data-height="853"/>
  306. * <img src="ERR" data-width="1280" data-height="853"/>
  307. * </div>
  308. * ```
  309. * ## Javascript
  310. * ```js
  311. * import ImReady from "@egjs/imready";
  312. *
  313. * const im = new ImReady(); // umd: eg.ImReady
  314. * im.check(document.querySelectorAll("img")).on({
  315. * preReady: e => {
  316. * // 0, 3
  317. * console.log(e.readyCount, e.totalCount),
  318. * },
  319. * });
  320. * ```
  321. */
  322. this.trigger(new ComponentEvent("preReady", {
  323. readyCount: this.readyCount,
  324. totalCount: this.totalCount,
  325. isReady: this.isReady(),
  326. hasLoading: this.hasLoading(),
  327. }));
  328. }
  329. private onReadyElement(index: number) {
  330. const info = this.elementInfos[index];
  331. /**
  332. * An event occurs when the element is ready
  333. * @ko 해당 엘리먼트가 준비가 되었을 때 이벤트가 발생한다.
  334. * @event eg.ImReady#readyElement
  335. * @param {eg.ImReady.OnReadyElement} e - The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
  336. * @example
  337. * ```html
  338. * <div>
  339. * <img src="./1.jpg" data-width="1280" data-height="853" style="width:100%"/>
  340. * <img src="./2.jpg" data-width="1280" data-height="853"/>
  341. * <img src="ERR" data-width="1280" data-height="853"/>
  342. * </div>
  343. * ```
  344. * ## Javascript
  345. * ```js
  346. * import ImReady from "@egjs/imready";
  347. *
  348. * const im = new ImReady(); // umd: eg.ImReady
  349. * im.check(document.querySelectorAll("img")).on({
  350. * readyElement: e => {
  351. * // 1, 0, false, 3
  352. * // 2, 1, false, 3
  353. * // 3, 2, true, 3
  354. * console.log(e.readyCount, e.index, e.hasError, e.totalCount),
  355. * },
  356. * });
  357. * ```
  358. */
  359. this.trigger(new ComponentEvent("readyElement", {
  360. index,
  361. element: info.element,
  362. hasError: info.hasError,
  363. errorCount: this.getErrorCount(),
  364. totalErrorCount: this.totalErrorCount,
  365. preReadyCount: this.preReadyCount,
  366. readyCount: this.readyCount,
  367. totalCount: this.totalCount,
  368. isPreReady: this.isPreReady(),
  369. isReady: this.isReady(),
  370. hasLoading: info.hasLoading,
  371. isPreReadyOver: this.isPreReadyOver,
  372. isSkip: info.isSkip,
  373. }));
  374. }
  375. private onReady() {
  376. /**
  377. * An event occurs when all element are ready
  378. * @ko 모든 엘리먼트들이 준비된 경우 이벤트가 발생한다.
  379. * @event eg.ImReady#ready
  380. * @param {eg.ImReady.OnReady} e - The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
  381. * @example
  382. * ```html
  383. * <div>
  384. * <img src="./1.jpg" data-width="1280" data-height="853" style="width:100%"/>
  385. * <img src="./2.jpg" data-width="1280" data-height="853"/>
  386. * <img src="ERR" data-width="1280" data-height="853"/>
  387. * </div>
  388. * ```
  389. * ## Javascript
  390. * ```js
  391. * import ImReady from "@egjs/imready";
  392. *
  393. * const im = new ImReady(); // umd: eg.ImReady
  394. * im.check(document.querySelectorAll("img")).on({
  395. * preReady: e => {
  396. * // 0, 3
  397. * console.log(e.readyCount, e.totalCount),
  398. * },
  399. * ready: e => {
  400. * // 1, 3
  401. * console.log(e.errorCount, e.totalCount),
  402. * },
  403. * });
  404. * ```
  405. */
  406. this.trigger(new ComponentEvent("ready", {
  407. errorCount: this.getErrorCount(),
  408. totalErrorCount: this.totalErrorCount,
  409. totalCount: this.totalCount,
  410. }));
  411. }
  412. private getErrorCount() {
  413. return this.elementInfos.filter(info => info.hasError).length;
  414. }
  415. private hasLoading() {
  416. return this.elementInfos.some(info => info.hasLoading);
  417. }
  418. }
  419. export default ImReadyManager;
comments powered by Disqus