ion-infinite-scroll.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. import { proxyCustomElement, HTMLElement, createEvent, writeTask, readTask, h, Host } from '@stencil/core/internal/client';
  5. import { a as findClosestIonContent, p as printIonContentErrorMsg, g as getScrollElement } from './index8.js';
  6. import { b as getIonMode } from './ionic-global.js';
  7. const infiniteScrollCss = "ion-infinite-scroll{display:none;width:100%}.infinite-scroll-enabled{display:block}";
  8. const IonInfiniteScrollStyle0 = infiniteScrollCss;
  9. const InfiniteScroll = /*@__PURE__*/ proxyCustomElement(class InfiniteScroll extends HTMLElement {
  10. constructor() {
  11. super();
  12. this.__registerHost();
  13. this.ionInfinite = createEvent(this, "ionInfinite", 7);
  14. this.thrPx = 0;
  15. this.thrPc = 0;
  16. /**
  17. * didFire exists so that ionInfinite
  18. * does not fire multiple times if
  19. * users continue to scroll after
  20. * scrolling into the infinite
  21. * scroll threshold.
  22. */
  23. this.didFire = false;
  24. this.isBusy = false;
  25. this.onScroll = () => {
  26. const scrollEl = this.scrollEl;
  27. if (!scrollEl || !this.canStart()) {
  28. return 1;
  29. }
  30. const infiniteHeight = this.el.offsetHeight;
  31. if (infiniteHeight === 0) {
  32. // if there is no height of this element then do nothing
  33. return 2;
  34. }
  35. const scrollTop = scrollEl.scrollTop;
  36. const scrollHeight = scrollEl.scrollHeight;
  37. const height = scrollEl.offsetHeight;
  38. const threshold = this.thrPc !== 0 ? height * this.thrPc : this.thrPx;
  39. const distanceFromInfinite = this.position === 'bottom'
  40. ? scrollHeight - infiniteHeight - scrollTop - threshold - height
  41. : scrollTop - infiniteHeight - threshold;
  42. if (distanceFromInfinite < 0) {
  43. if (!this.didFire) {
  44. this.isLoading = true;
  45. this.didFire = true;
  46. this.ionInfinite.emit();
  47. return 3;
  48. }
  49. }
  50. return 4;
  51. };
  52. this.isLoading = false;
  53. this.threshold = '15%';
  54. this.disabled = false;
  55. this.position = 'bottom';
  56. }
  57. thresholdChanged() {
  58. const val = this.threshold;
  59. if (val.lastIndexOf('%') > -1) {
  60. this.thrPx = 0;
  61. this.thrPc = parseFloat(val) / 100;
  62. }
  63. else {
  64. this.thrPx = parseFloat(val);
  65. this.thrPc = 0;
  66. }
  67. }
  68. disabledChanged() {
  69. const disabled = this.disabled;
  70. if (disabled) {
  71. this.isLoading = false;
  72. this.isBusy = false;
  73. }
  74. this.enableScrollEvents(!disabled);
  75. }
  76. async connectedCallback() {
  77. const contentEl = findClosestIonContent(this.el);
  78. if (!contentEl) {
  79. printIonContentErrorMsg(this.el);
  80. return;
  81. }
  82. this.scrollEl = await getScrollElement(contentEl);
  83. this.thresholdChanged();
  84. this.disabledChanged();
  85. if (this.position === 'top') {
  86. writeTask(() => {
  87. if (this.scrollEl) {
  88. this.scrollEl.scrollTop = this.scrollEl.scrollHeight - this.scrollEl.clientHeight;
  89. }
  90. });
  91. }
  92. }
  93. disconnectedCallback() {
  94. this.enableScrollEvents(false);
  95. this.scrollEl = undefined;
  96. }
  97. /**
  98. * Call `complete()` within the `ionInfinite` output event handler when
  99. * your async operation has completed. For example, the `loading`
  100. * state is while the app is performing an asynchronous operation,
  101. * such as receiving more data from an AJAX request to add more items
  102. * to a data list. Once the data has been received and UI updated, you
  103. * then call this method to signify that the loading has completed.
  104. * This method will change the infinite scroll's state from `loading`
  105. * to `enabled`.
  106. */
  107. async complete() {
  108. const scrollEl = this.scrollEl;
  109. if (!this.isLoading || !scrollEl) {
  110. return;
  111. }
  112. this.isLoading = false;
  113. if (this.position === 'top') {
  114. /**
  115. * New content is being added at the top, but the scrollTop position stays the same,
  116. * which causes a scroll jump visually. This algorithm makes sure to prevent this.
  117. * (Frame 1)
  118. * - complete() is called, but the UI hasn't had time to update yet.
  119. * - Save the current content dimensions.
  120. * - Wait for the next frame using _dom.read, so the UI will be updated.
  121. * (Frame 2)
  122. * - Read the new content dimensions.
  123. * - Calculate the height difference and the new scroll position.
  124. * - Delay the scroll position change until other possible dom reads are done using _dom.write to be performant.
  125. * (Still frame 2, if I'm correct)
  126. * - Change the scroll position (= visually maintain the scroll position).
  127. * - Change the state to re-enable the InfiniteScroll.
  128. * - This should be after changing the scroll position, or it could
  129. * cause the InfiniteScroll to be triggered again immediately.
  130. * (Frame 3)
  131. * Done.
  132. */
  133. this.isBusy = true;
  134. // ******** DOM READ ****************
  135. // Save the current content dimensions before the UI updates
  136. const prev = scrollEl.scrollHeight - scrollEl.scrollTop;
  137. // ******** DOM READ ****************
  138. requestAnimationFrame(() => {
  139. readTask(() => {
  140. // UI has updated, save the new content dimensions
  141. const scrollHeight = scrollEl.scrollHeight;
  142. // New content was added on top, so the scroll position should be changed immediately to prevent it from jumping around
  143. const newScrollTop = scrollHeight - prev;
  144. // ******** DOM WRITE ****************
  145. requestAnimationFrame(() => {
  146. writeTask(() => {
  147. scrollEl.scrollTop = newScrollTop;
  148. this.isBusy = false;
  149. this.didFire = false;
  150. });
  151. });
  152. });
  153. });
  154. }
  155. else {
  156. this.didFire = false;
  157. }
  158. }
  159. canStart() {
  160. return !this.disabled && !this.isBusy && !!this.scrollEl && !this.isLoading;
  161. }
  162. enableScrollEvents(shouldListen) {
  163. if (this.scrollEl) {
  164. if (shouldListen) {
  165. this.scrollEl.addEventListener('scroll', this.onScroll);
  166. }
  167. else {
  168. this.scrollEl.removeEventListener('scroll', this.onScroll);
  169. }
  170. }
  171. }
  172. render() {
  173. const mode = getIonMode(this);
  174. const disabled = this.disabled;
  175. return (h(Host, { key: 'e844956795f69be33396ce4480aa7a54ad01b28c', class: {
  176. [mode]: true,
  177. 'infinite-scroll-loading': this.isLoading,
  178. 'infinite-scroll-enabled': !disabled,
  179. } }));
  180. }
  181. get el() { return this; }
  182. static get watchers() { return {
  183. "threshold": ["thresholdChanged"],
  184. "disabled": ["disabledChanged"]
  185. }; }
  186. static get style() { return IonInfiniteScrollStyle0; }
  187. }, [0, "ion-infinite-scroll", {
  188. "threshold": [1],
  189. "disabled": [4],
  190. "position": [1],
  191. "isLoading": [32],
  192. "complete": [64]
  193. }, undefined, {
  194. "threshold": ["thresholdChanged"],
  195. "disabled": ["disabledChanged"]
  196. }]);
  197. function defineCustomElement$1() {
  198. if (typeof customElements === "undefined") {
  199. return;
  200. }
  201. const components = ["ion-infinite-scroll"];
  202. components.forEach(tagName => { switch (tagName) {
  203. case "ion-infinite-scroll":
  204. if (!customElements.get(tagName)) {
  205. customElements.define(tagName, InfiniteScroll);
  206. }
  207. break;
  208. } });
  209. }
  210. const IonInfiniteScroll = InfiniteScroll;
  211. const defineCustomElement = defineCustomElement$1;
  212. export { IonInfiniteScroll, defineCustomElement };