UNPKG

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