UNPKG

41.7 kBJavaScriptView Raw
1/*!
2 * (C) Ionic http://ionicframework.com - MIT License
3 */
4import { writeTask, proxyCustomElement, HTMLElement, createEvent, readTask, h, Host } from '@stencil/core/internal/client';
5import { a as isPlatform, b as getIonMode } from './ionic-global.js';
6import { g as getTimeGivenProgression } from './cubic-bezier.js';
7import { t as transitionEndAsync, c as componentOnReady, j as clamp, g as getElementRoot, r as raf } from './helpers.js';
8import { c as hapticImpact } from './haptic.js';
9import { c as createAnimation } from './animation.js';
10
11const getRefresherAnimationType = (contentEl) => {
12 const previousSibling = contentEl.previousElementSibling;
13 const hasHeader = previousSibling !== null && previousSibling.tagName === 'ION-HEADER';
14 return hasHeader ? 'translate' : 'scale';
15};
16const createPullingAnimation = (type, pullingSpinner, refresherEl) => {
17 return type === 'scale' ? createScaleAnimation(pullingSpinner, refresherEl) : createTranslateAnimation(pullingSpinner, refresherEl);
18};
19const createBaseAnimation = (pullingRefresherIcon) => {
20 const spinner = pullingRefresherIcon.querySelector('ion-spinner');
21 const circle = spinner.shadowRoot.querySelector('circle');
22 const spinnerArrowContainer = pullingRefresherIcon.querySelector('.spinner-arrow-container');
23 const arrowContainer = pullingRefresherIcon.querySelector('.arrow-container');
24 const arrow = (arrowContainer) ? arrowContainer.querySelector('ion-icon') : null;
25 const baseAnimation = createAnimation()
26 .duration(1000)
27 .easing('ease-out');
28 const spinnerArrowContainerAnimation = createAnimation()
29 .addElement(spinnerArrowContainer)
30 .keyframes([
31 { offset: 0, opacity: '0.3' },
32 { offset: 0.45, opacity: '0.3' },
33 { offset: 0.55, opacity: '1' },
34 { offset: 1, opacity: '1' }
35 ]);
36 const circleInnerAnimation = createAnimation()
37 .addElement(circle)
38 .keyframes([
39 { offset: 0, strokeDasharray: '1px, 200px' },
40 { offset: 0.20, strokeDasharray: '1px, 200px' },
41 { offset: 0.55, strokeDasharray: '100px, 200px' },
42 { offset: 1, strokeDasharray: '100px, 200px' }
43 ]);
44 const circleOuterAnimation = createAnimation()
45 .addElement(spinner)
46 .keyframes([
47 { offset: 0, transform: 'rotate(-90deg)' },
48 { offset: 1, transform: 'rotate(210deg)' }
49 ]);
50 /**
51 * Only add arrow animation if present
52 * this allows users to customize the spinners
53 * without errors being thrown
54 */
55 if (arrowContainer && arrow) {
56 const arrowContainerAnimation = createAnimation()
57 .addElement(arrowContainer)
58 .keyframes([
59 { offset: 0, transform: 'rotate(0deg)' },
60 { offset: 0.30, transform: 'rotate(0deg)' },
61 { offset: 0.55, transform: 'rotate(280deg)' },
62 { offset: 1, transform: 'rotate(400deg)' }
63 ]);
64 const arrowAnimation = createAnimation()
65 .addElement(arrow)
66 .keyframes([
67 { offset: 0, transform: 'translateX(2px) scale(0)' },
68 { offset: 0.30, transform: 'translateX(2px) scale(0)' },
69 { offset: 0.55, transform: 'translateX(-1.5px) scale(1)' },
70 { offset: 1, transform: 'translateX(-1.5px) scale(1)' }
71 ]);
72 baseAnimation.addAnimation([arrowContainerAnimation, arrowAnimation]);
73 }
74 return baseAnimation.addAnimation([spinnerArrowContainerAnimation, circleInnerAnimation, circleOuterAnimation]);
75};
76const createScaleAnimation = (pullingRefresherIcon, refresherEl) => {
77 /**
78 * Do not take the height of the refresher icon
79 * because at this point the DOM has not updated,
80 * so the refresher icon is still hidden with
81 * display: none.
82 * The `ion-refresher` container height
83 * is roughly the amount we need to offset
84 * the icon by when pulling down.
85 */
86 const height = refresherEl.clientHeight;
87 const spinnerAnimation = createAnimation()
88 .addElement(pullingRefresherIcon)
89 .keyframes([
90 { offset: 0, transform: `scale(0) translateY(-${height}px)` },
91 { offset: 1, transform: 'scale(1) translateY(100px)' }
92 ]);
93 return createBaseAnimation(pullingRefresherIcon).addAnimation([spinnerAnimation]);
94};
95const createTranslateAnimation = (pullingRefresherIcon, refresherEl) => {
96 /**
97 * Do not take the height of the refresher icon
98 * because at this point the DOM has not updated,
99 * so the refresher icon is still hidden with
100 * display: none.
101 * The `ion-refresher` container height
102 * is roughly the amount we need to offset
103 * the icon by when pulling down.
104 */
105 const height = refresherEl.clientHeight;
106 const spinnerAnimation = createAnimation()
107 .addElement(pullingRefresherIcon)
108 .keyframes([
109 { offset: 0, transform: `translateY(-${height}px)` },
110 { offset: 1, transform: 'translateY(100px)' }
111 ]);
112 return createBaseAnimation(pullingRefresherIcon).addAnimation([spinnerAnimation]);
113};
114const createSnapBackAnimation = (pullingRefresherIcon) => {
115 return createAnimation()
116 .duration(125)
117 .addElement(pullingRefresherIcon)
118 .fromTo('transform', 'translateY(var(--ion-pulling-refresher-translate, 100px))', 'translateY(0px)');
119};
120// iOS Native Refresher
121// -----------------------------
122const setSpinnerOpacity = (spinner, opacity) => {
123 spinner.style.setProperty('opacity', opacity.toString());
124};
125const handleScrollWhilePulling = (ticks, numTicks, pullAmount) => {
126 const max = 1;
127 writeTask(() => {
128 ticks.forEach((el, i) => {
129 /**
130 * Compute the opacity of each tick
131 * mark as a percentage of the pullAmount
132 * offset by max / numTicks so
133 * the tick marks are shown staggered.
134 */
135 const min = i * (max / numTicks);
136 const range = max - min;
137 const start = pullAmount - min;
138 const progression = clamp(0, start / range, 1);
139 el.style.setProperty('opacity', progression.toString());
140 });
141 });
142};
143const handleScrollWhileRefreshing = (spinner, lastVelocityY) => {
144 writeTask(() => {
145 // If user pulls down quickly, the spinner should spin faster
146 spinner.style.setProperty('--refreshing-rotation-duration', (lastVelocityY >= 1.0) ? '0.5s' : '2s');
147 spinner.style.setProperty('opacity', '1');
148 });
149};
150const translateElement = (el, value, duration = 200) => {
151 if (!el) {
152 return Promise.resolve();
153 }
154 const trans = transitionEndAsync(el, duration);
155 writeTask(() => {
156 el.style.setProperty('transition', `${duration}ms all ease-out`);
157 if (value === undefined) {
158 el.style.removeProperty('transform');
159 }
160 else {
161 el.style.setProperty('transform', `translate3d(0px, ${value}, 0px)`);
162 }
163 });
164 return trans;
165};
166// Utils
167// -----------------------------
168const shouldUseNativeRefresher = async (referenceEl, mode) => {
169 const refresherContent = referenceEl.querySelector('ion-refresher-content');
170 if (!refresherContent) {
171 return Promise.resolve(false);
172 }
173 await new Promise(resolve => componentOnReady(refresherContent, resolve));
174 const pullingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-pulling ion-spinner');
175 const refreshingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-refreshing ion-spinner');
176 return (pullingSpinner !== null &&
177 refreshingSpinner !== null &&
178 ((mode === 'ios' && isPlatform('mobile') && referenceEl.style.webkitOverflowScrolling !== undefined) ||
179 mode === 'md'));
180};
181
182const refresherIosCss = "ion-refresher{left:0;top:0;display:none;position:absolute;width:100%;height:60px;pointer-events:none;z-index:-1}[dir=rtl] ion-refresher,:host-context([dir=rtl]) ion-refresher{left:unset;right:unset;right:0}ion-refresher.refresher-active{display:block}ion-refresher-content{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.refresher-pulling,.refresher-refreshing{display:none;width:100%}.refresher-pulling-icon,.refresher-refreshing-icon{-webkit-transform-origin:center;transform-origin:center;-webkit-transition:200ms;transition:200ms;font-size:30px;text-align:center}[dir=rtl] .refresher-pulling-icon,:host-context([dir=rtl]) .refresher-pulling-icon,[dir=rtl] .refresher-refreshing-icon,:host-context([dir=rtl]) .refresher-refreshing-icon{-webkit-transform-origin:calc(100% - center);transform-origin:calc(100% - center)}.refresher-pulling-text,.refresher-refreshing-text{font-size:16px;text-align:center}ion-refresher-content .arrow-container{display:none}.refresher-pulling ion-refresher-content .refresher-pulling{display:block}.refresher-ready ion-refresher-content .refresher-pulling{display:block}.refresher-ready ion-refresher-content .refresher-pulling-icon{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.refresher-refreshing ion-refresher-content .refresher-refreshing{display:block}.refresher-cancelling ion-refresher-content .refresher-pulling{display:block}.refresher-cancelling ion-refresher-content .refresher-pulling-icon{-webkit-transform:scale(0);transform:scale(0)}.refresher-completing ion-refresher-content .refresher-refreshing{display:block}.refresher-completing ion-refresher-content .refresher-refreshing-icon{-webkit-transform:scale(0);transform:scale(0)}.refresher-native .refresher-pulling-text,.refresher-native .refresher-refreshing-text{display:none}.refresher-ios .refresher-pulling-icon,.refresher-ios .refresher-refreshing-icon{color:var(--ion-text-color, #000)}.refresher-ios .refresher-pulling-text,.refresher-ios .refresher-refreshing-text{color:var(--ion-text-color, #000)}.refresher-ios .refresher-refreshing .spinner-lines-ios line,.refresher-ios .refresher-refreshing .spinner-lines-small-ios line,.refresher-ios .refresher-refreshing .spinner-crescent circle{stroke:var(--ion-text-color, #000)}.refresher-ios .refresher-refreshing .spinner-bubbles circle,.refresher-ios .refresher-refreshing .spinner-circles circle,.refresher-ios .refresher-refreshing .spinner-dots circle{fill:var(--ion-text-color, #000)}ion-refresher.refresher-native{display:block;z-index:1}ion-refresher.refresher-native ion-spinner{margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0}@supports ((-webkit-margin-start: 0) or (margin-inline-start: 0)) or (-webkit-margin-start: 0){ion-refresher.refresher-native ion-spinner{margin-left:unset;margin-right:unset;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto}}.refresher-native .refresher-refreshing ion-spinner{--refreshing-rotation-duration:2s;display:none;-webkit-animation:var(--refreshing-rotation-duration) ease-out refresher-rotate forwards;animation:var(--refreshing-rotation-duration) ease-out refresher-rotate forwards}.refresher-native .refresher-refreshing{display:none;-webkit-animation:250ms linear refresher-pop forwards;animation:250ms linear refresher-pop forwards}.refresher-native ion-spinner{width:32px;height:32px;color:var(--ion-color-step-450, #747577)}.refresher-native.refresher-refreshing .refresher-pulling ion-spinner,.refresher-native.refresher-completing .refresher-pulling ion-spinner{display:none}.refresher-native.refresher-refreshing .refresher-refreshing ion-spinner,.refresher-native.refresher-completing .refresher-refreshing ion-spinner{display:block}.refresher-native.refresher-pulling .refresher-pulling ion-spinner{display:block}.refresher-native.refresher-pulling .refresher-refreshing ion-spinner{display:none}.refresher-native.refresher-completing ion-refresher-content .refresher-refreshing-icon{-webkit-transform:scale(0) rotate(180deg);transform:scale(0) rotate(180deg);-webkit-transition:300ms;transition:300ms}@-webkit-keyframes refresher-pop{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}50%{-webkit-transform:scale(1.2);transform:scale(1.2);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}100%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes refresher-pop{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}50%{-webkit-transform:scale(1.2);transform:scale(1.2);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}100%{-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes refresher-rotate{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(180deg);transform:rotate(180deg)}}@keyframes refresher-rotate{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(180deg);transform:rotate(180deg)}}";
183
184const refresherMdCss = "ion-refresher{left:0;top:0;display:none;position:absolute;width:100%;height:60px;pointer-events:none;z-index:-1}[dir=rtl] ion-refresher,:host-context([dir=rtl]) ion-refresher{left:unset;right:unset;right:0}ion-refresher.refresher-active{display:block}ion-refresher-content{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.refresher-pulling,.refresher-refreshing{display:none;width:100%}.refresher-pulling-icon,.refresher-refreshing-icon{-webkit-transform-origin:center;transform-origin:center;-webkit-transition:200ms;transition:200ms;font-size:30px;text-align:center}[dir=rtl] .refresher-pulling-icon,:host-context([dir=rtl]) .refresher-pulling-icon,[dir=rtl] .refresher-refreshing-icon,:host-context([dir=rtl]) .refresher-refreshing-icon{-webkit-transform-origin:calc(100% - center);transform-origin:calc(100% - center)}.refresher-pulling-text,.refresher-refreshing-text{font-size:16px;text-align:center}ion-refresher-content .arrow-container{display:none}.refresher-pulling ion-refresher-content .refresher-pulling{display:block}.refresher-ready ion-refresher-content .refresher-pulling{display:block}.refresher-ready ion-refresher-content .refresher-pulling-icon{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.refresher-refreshing ion-refresher-content .refresher-refreshing{display:block}.refresher-cancelling ion-refresher-content .refresher-pulling{display:block}.refresher-cancelling ion-refresher-content .refresher-pulling-icon{-webkit-transform:scale(0);transform:scale(0)}.refresher-completing ion-refresher-content .refresher-refreshing{display:block}.refresher-completing ion-refresher-content .refresher-refreshing-icon{-webkit-transform:scale(0);transform:scale(0)}.refresher-native .refresher-pulling-text,.refresher-native .refresher-refreshing-text{display:none}.refresher-md .refresher-pulling-icon,.refresher-md .refresher-refreshing-icon{color:var(--ion-text-color, #000)}.refresher-md .refresher-pulling-text,.refresher-md .refresher-refreshing-text{color:var(--ion-text-color, #000)}.refresher-md .refresher-refreshing .spinner-lines-md line,.refresher-md .refresher-refreshing .spinner-lines-small-md line,.refresher-md .refresher-refreshing .spinner-crescent circle{stroke:var(--ion-text-color, #000)}.refresher-md .refresher-refreshing .spinner-bubbles circle,.refresher-md .refresher-refreshing .spinner-circles circle,.refresher-md .refresher-refreshing .spinner-dots circle{fill:var(--ion-text-color, #000)}ion-refresher.refresher-native{display:block;z-index:1}ion-refresher.refresher-native ion-spinner{margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;width:24px;height:24px;color:var(--ion-color-primary, #3880ff)}@supports ((-webkit-margin-start: 0) or (margin-inline-start: 0)) or (-webkit-margin-start: 0){ion-refresher.refresher-native ion-spinner{margin-left:unset;margin-right:unset;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto}}ion-refresher.refresher-native .spinner-arrow-container{display:inherit}ion-refresher.refresher-native .arrow-container{display:block;position:absolute;width:24px;height:24px}ion-refresher.refresher-native .arrow-container ion-icon{margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;left:0;right:0;bottom:-4px;position:absolute;color:var(--ion-color-primary, #3880ff);font-size:12px}@supports ((-webkit-margin-start: 0) or (margin-inline-start: 0)) or (-webkit-margin-start: 0){ion-refresher.refresher-native .arrow-container ion-icon{margin-left:unset;margin-right:unset;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto}}ion-refresher.refresher-native.refresher-pulling ion-refresher-content .refresher-pulling,ion-refresher.refresher-native.refresher-ready ion-refresher-content .refresher-pulling{display:-ms-flexbox;display:flex}ion-refresher.refresher-native.refresher-refreshing ion-refresher-content .refresher-refreshing,ion-refresher.refresher-native.refresher-completing ion-refresher-content .refresher-refreshing,ion-refresher.refresher-native.refresher-cancelling ion-refresher-content .refresher-refreshing{display:-ms-flexbox;display:flex}ion-refresher.refresher-native .refresher-pulling-icon{-webkit-transform:translateY(calc(-100% - 10px));transform:translateY(calc(-100% - 10px))}ion-refresher.refresher-native .refresher-pulling-icon,ion-refresher.refresher-native .refresher-refreshing-icon{margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;border-radius:100%;padding-left:8px;padding-right:8px;padding-top:8px;padding-bottom:8px;display:-ms-flexbox;display:flex;border:1px solid var(--ion-color-step-200, #ececec);background:var(--ion-color-step-250, #ffffff);-webkit-box-shadow:0px 1px 6px rgba(0, 0, 0, 0.1);box-shadow:0px 1px 6px rgba(0, 0, 0, 0.1)}@supports ((-webkit-margin-start: 0) or (margin-inline-start: 0)) or (-webkit-margin-start: 0){ion-refresher.refresher-native .refresher-pulling-icon,ion-refresher.refresher-native .refresher-refreshing-icon{margin-left:unset;margin-right:unset;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto}}@supports ((-webkit-margin-start: 0) or (margin-inline-start: 0)) or (-webkit-margin-start: 0){ion-refresher.refresher-native .refresher-pulling-icon,ion-refresher.refresher-native .refresher-refreshing-icon{padding-left:unset;padding-right:unset;-webkit-padding-start:8px;padding-inline-start:8px;-webkit-padding-end:8px;padding-inline-end:8px}}";
185
186const Refresher = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
187 constructor() {
188 super();
189 this.__registerHost();
190 this.ionRefresh = createEvent(this, "ionRefresh", 7);
191 this.ionPull = createEvent(this, "ionPull", 7);
192 this.ionStart = createEvent(this, "ionStart", 7);
193 this.appliedStyles = false;
194 this.didStart = false;
195 this.progress = 0;
196 this.pointerDown = false;
197 this.needsCompletion = false;
198 this.didRefresh = false;
199 this.lastVelocityY = 0;
200 this.animations = [];
201 this.nativeRefresher = false;
202 /**
203 * The current state which the refresher is in. The refresher's states include:
204 *
205 * - `inactive` - The refresher is not being pulled down or refreshing and is currently hidden.
206 * - `pulling` - The user is actively pulling down the refresher, but has not reached the point yet that if the user lets go, it'll refresh.
207 * - `cancelling` - The user pulled down the refresher and let go, but did not pull down far enough to kick off the `refreshing` state. After letting go, the refresher is in the `cancelling` state while it is closing, and will go back to the `inactive` state once closed.
208 * - `ready` - The user has pulled down the refresher far enough that if they let go, it'll begin the `refreshing` state.
209 * - `refreshing` - The refresher is actively waiting on the async operation to end. Once the refresh handler calls `complete()` it will begin the `completing` state.
210 * - `completing` - The `refreshing` state has finished and the refresher is in the way of closing itself. Once closed, the refresher will go back to the `inactive` state.
211 */
212 this.state = 1 /* Inactive */;
213 /**
214 * The minimum distance the user must pull down until the
215 * refresher will go into the `refreshing` state.
216 * Does not apply when the refresher content uses a spinner,
217 * enabling the native refresher.
218 */
219 this.pullMin = 60;
220 /**
221 * The maximum distance of the pull until the refresher
222 * will automatically go into the `refreshing` state.
223 * Defaults to the result of `pullMin + 60`.
224 * Does not apply when the refresher content uses a spinner,
225 * enabling the native refresher.
226 */
227 this.pullMax = this.pullMin + 60;
228 /**
229 * Time it takes to close the refresher.
230 * Does not apply when the refresher content uses a spinner,
231 * enabling the native refresher.
232 */
233 this.closeDuration = '280ms';
234 /**
235 * Time it takes the refresher to snap back to the `refreshing` state.
236 * Does not apply when the refresher content uses a spinner,
237 * enabling the native refresher.
238 */
239 this.snapbackDuration = '280ms';
240 /**
241 * How much to multiply the pull speed by. To slow the pull animation down,
242 * pass a number less than `1`. To speed up the pull, pass a number greater
243 * than `1`. The default value is `1` which is equal to the speed of the cursor.
244 * If a negative value is passed in, the factor will be `1` instead.
245 *
246 * For example: If the value passed is `1.2` and the content is dragged by
247 * `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
248 * (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
249 * will be `8` pixels, less than the amount the cursor has moved.
250 *
251 * Does not apply when the refresher content uses a spinner,
252 * enabling the native refresher.
253 */
254 this.pullFactor = 1;
255 /**
256 * If `true`, the refresher will be hidden.
257 */
258 this.disabled = false;
259 }
260 disabledChanged() {
261 if (this.gesture) {
262 this.gesture.enable(!this.disabled);
263 }
264 }
265 async checkNativeRefresher() {
266 const useNativeRefresher = await shouldUseNativeRefresher(this.el, getIonMode(this));
267 if (useNativeRefresher && !this.nativeRefresher) {
268 const contentEl = this.el.closest('ion-content');
269 this.setupNativeRefresher(contentEl);
270 }
271 else if (!useNativeRefresher) {
272 this.destroyNativeRefresher();
273 }
274 }
275 destroyNativeRefresher() {
276 if (this.scrollEl && this.scrollListenerCallback) {
277 this.scrollEl.removeEventListener('scroll', this.scrollListenerCallback);
278 this.scrollListenerCallback = undefined;
279 }
280 this.nativeRefresher = false;
281 }
282 async resetNativeRefresher(el, state) {
283 this.state = state;
284 if (getIonMode(this) === 'ios') {
285 await translateElement(el, undefined, 300);
286 }
287 else {
288 await transitionEndAsync(this.el.querySelector('.refresher-refreshing-icon'), 200);
289 }
290 this.didRefresh = false;
291 this.needsCompletion = false;
292 this.pointerDown = false;
293 this.animations.forEach(ani => ani.destroy());
294 this.animations = [];
295 this.progress = 0;
296 this.state = 1 /* Inactive */;
297 }
298 async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
299 this.elementToTransform = this.scrollEl;
300 const ticks = pullingSpinner.shadowRoot.querySelectorAll('svg');
301 let MAX_PULL = this.scrollEl.clientHeight * 0.16;
302 const NUM_TICKS = ticks.length;
303 writeTask(() => ticks.forEach(el => el.style.setProperty('animation', 'none')));
304 this.scrollListenerCallback = () => {
305 // If pointer is not on screen or refresher is not active, ignore scroll
306 if (!this.pointerDown && this.state === 1 /* Inactive */) {
307 return;
308 }
309 readTask(() => {
310 // PTR should only be active when overflow scrolling at the top
311 const scrollTop = this.scrollEl.scrollTop;
312 const refresherHeight = this.el.clientHeight;
313 if (scrollTop > 0) {
314 /**
315 * If refresher is refreshing and user tries to scroll
316 * progressively fade refresher out/in
317 */
318 if (this.state === 8 /* Refreshing */) {
319 const ratio = clamp(0, scrollTop / (refresherHeight * 0.5), 1);
320 writeTask(() => setSpinnerOpacity(refreshingSpinner, 1 - ratio));
321 return;
322 }
323 return;
324 }
325 if (this.pointerDown) {
326 if (!this.didStart) {
327 this.didStart = true;
328 this.ionStart.emit();
329 }
330 // emit "pulling" on every move
331 if (this.pointerDown) {
332 this.ionPull.emit();
333 }
334 }
335 /**
336 * We want to delay the start of this gesture by ~30px
337 * when initially pulling down so the refresher does not
338 * overlap with the content. But when letting go of the
339 * gesture before the refresher completes, we want the
340 * refresher tick marks to quickly fade out.
341 */
342 const offset = (this.didStart) ? 30 : 0;
343 const pullAmount = this.progress = clamp(0, (Math.abs(scrollTop) - offset) / MAX_PULL, 1);
344 const shouldShowRefreshingSpinner = this.state === 8 /* Refreshing */ || pullAmount === 1;
345 if (shouldShowRefreshingSpinner) {
346 if (this.pointerDown) {
347 handleScrollWhileRefreshing(refreshingSpinner, this.lastVelocityY);
348 }
349 if (!this.didRefresh) {
350 this.beginRefresh();
351 this.didRefresh = true;
352 hapticImpact({ style: 'light' });
353 /**
354 * Translate the content element otherwise when pointer is removed
355 * from screen the scroll content will bounce back over the refresher
356 */
357 if (!this.pointerDown) {
358 translateElement(this.elementToTransform, `${refresherHeight}px`);
359 }
360 }
361 }
362 else {
363 this.state = 2 /* Pulling */;
364 handleScrollWhilePulling(ticks, NUM_TICKS, pullAmount);
365 }
366 });
367 };
368 this.scrollEl.addEventListener('scroll', this.scrollListenerCallback);
369 this.gesture = (await import('./index2.js')).createGesture({
370 el: this.scrollEl,
371 gestureName: 'refresher',
372 gesturePriority: 31,
373 direction: 'y',
374 threshold: 5,
375 onStart: () => {
376 this.pointerDown = true;
377 if (!this.didRefresh) {
378 translateElement(this.elementToTransform, '0px');
379 }
380 /**
381 * If the content had `display: none` when
382 * the refresher was initialized, its clientHeight
383 * will be 0. When the gesture starts, the content
384 * will be visible, so try to get the correct
385 * client height again. This is most common when
386 * using the refresher in an ion-menu.
387 */
388 if (MAX_PULL === 0) {
389 MAX_PULL = this.scrollEl.clientHeight * 0.16;
390 }
391 },
392 onMove: ev => {
393 this.lastVelocityY = ev.velocityY;
394 },
395 onEnd: () => {
396 this.pointerDown = false;
397 this.didStart = false;
398 if (this.needsCompletion) {
399 this.resetNativeRefresher(this.elementToTransform, 32 /* Completing */);
400 this.needsCompletion = false;
401 }
402 else if (this.didRefresh) {
403 readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
404 }
405 },
406 });
407 this.disabledChanged();
408 }
409 async setupMDNativeRefresher(contentEl, pullingSpinner, refreshingSpinner) {
410 const circle = getElementRoot(pullingSpinner).querySelector('circle');
411 const pullingRefresherIcon = this.el.querySelector('ion-refresher-content .refresher-pulling-icon');
412 const refreshingCircle = getElementRoot(refreshingSpinner).querySelector('circle');
413 if (circle !== null && refreshingCircle !== null) {
414 writeTask(() => {
415 circle.style.setProperty('animation', 'none');
416 // This lines up the animation on the refreshing spinner with the pulling spinner
417 refreshingSpinner.style.setProperty('animation-delay', '-655ms');
418 refreshingCircle.style.setProperty('animation-delay', '-655ms');
419 });
420 }
421 this.gesture = (await import('./index2.js')).createGesture({
422 el: this.scrollEl,
423 gestureName: 'refresher',
424 gesturePriority: 31,
425 direction: 'y',
426 threshold: 5,
427 canStart: () => this.state !== 8 /* Refreshing */ && this.state !== 32 /* Completing */ && this.scrollEl.scrollTop === 0,
428 onStart: (ev) => {
429 ev.data = { animation: undefined, didStart: false, cancelled: false };
430 },
431 onMove: (ev) => {
432 if ((ev.velocityY < 0 && this.progress === 0 && !ev.data.didStart) || ev.data.cancelled) {
433 ev.data.cancelled = true;
434 return;
435 }
436 if (!ev.data.didStart) {
437 ev.data.didStart = true;
438 this.state = 2 /* Pulling */;
439 writeTask(() => this.scrollEl.style.setProperty('--overflow', 'hidden'));
440 const animationType = getRefresherAnimationType(contentEl);
441 const animation = createPullingAnimation(animationType, pullingRefresherIcon, this.el);
442 ev.data.animation = animation;
443 animation.progressStart(false, 0);
444 this.ionStart.emit();
445 this.animations.push(animation);
446 return;
447 }
448 // Since we are using an easing curve, slow the gesture tracking down a bit
449 this.progress = clamp(0, (ev.deltaY / 180) * 0.5, 1);
450 ev.data.animation.progressStep(this.progress);
451 this.ionPull.emit();
452 },
453 onEnd: (ev) => {
454 if (!ev.data.didStart) {
455 return;
456 }
457 writeTask(() => this.scrollEl.style.removeProperty('--overflow'));
458 if (this.progress <= 0.4) {
459 this.gesture.enable(false);
460 ev.data.animation
461 .progressEnd(0, this.progress, 500)
462 .onFinish(() => {
463 this.animations.forEach(ani => ani.destroy());
464 this.animations = [];
465 this.gesture.enable(true);
466 this.state = 1 /* Inactive */;
467 });
468 return;
469 }
470 const progress = getTimeGivenProgression([0, 0], [0, 0], [1, 1], [1, 1], this.progress)[0];
471 const snapBackAnimation = createSnapBackAnimation(pullingRefresherIcon);
472 this.animations.push(snapBackAnimation);
473 writeTask(async () => {
474 pullingRefresherIcon.style.setProperty('--ion-pulling-refresher-translate', `${(progress * 100)}px`);
475 ev.data.animation.progressEnd();
476 await snapBackAnimation.play();
477 this.beginRefresh();
478 ev.data.animation.destroy();
479 });
480 }
481 });
482 this.disabledChanged();
483 }
484 async setupNativeRefresher(contentEl) {
485 if (this.scrollListenerCallback || !contentEl || this.nativeRefresher || !this.scrollEl) {
486 return;
487 }
488 /**
489 * If using non-native refresher before make sure
490 * we clean up any old CSS. This can happen when
491 * a user manually calls the refresh method in a
492 * component create callback before the native
493 * refresher is setup.
494 */
495 this.setCss(0, '', false, '');
496 this.nativeRefresher = true;
497 const pullingSpinner = this.el.querySelector('ion-refresher-content .refresher-pulling ion-spinner');
498 const refreshingSpinner = this.el.querySelector('ion-refresher-content .refresher-refreshing ion-spinner');
499 if (getIonMode(this) === 'ios') {
500 this.setupiOSNativeRefresher(pullingSpinner, refreshingSpinner);
501 }
502 else {
503 this.setupMDNativeRefresher(contentEl, pullingSpinner, refreshingSpinner);
504 }
505 }
506 componentDidUpdate() {
507 this.checkNativeRefresher();
508 }
509 async connectedCallback() {
510 if (this.el.getAttribute('slot') !== 'fixed') {
511 console.error('Make sure you use: <ion-refresher slot="fixed">');
512 return;
513 }
514 const contentEl = this.el.closest('ion-content');
515 if (!contentEl) {
516 console.error('<ion-refresher> must be used inside an <ion-content>');
517 return;
518 }
519 await new Promise(resolve => componentOnReady(contentEl, resolve));
520 this.scrollEl = await contentEl.getScrollElement();
521 this.backgroundContentEl = getElementRoot(contentEl).querySelector('#background-content');
522 if (await shouldUseNativeRefresher(this.el, getIonMode(this))) {
523 this.setupNativeRefresher(contentEl);
524 }
525 else {
526 this.gesture = (await import('./index2.js')).createGesture({
527 el: contentEl,
528 gestureName: 'refresher',
529 gesturePriority: 31,
530 direction: 'y',
531 threshold: 20,
532 passive: false,
533 canStart: () => this.canStart(),
534 onStart: () => this.onStart(),
535 onMove: ev => this.onMove(ev),
536 onEnd: () => this.onEnd(),
537 });
538 this.disabledChanged();
539 }
540 }
541 disconnectedCallback() {
542 this.destroyNativeRefresher();
543 this.scrollEl = undefined;
544 if (this.gesture) {
545 this.gesture.destroy();
546 this.gesture = undefined;
547 }
548 }
549 /**
550 * Call `complete()` when your async operation has completed.
551 * For example, the `refreshing` state is while the app is performing
552 * an asynchronous operation, such as receiving more data from an
553 * AJAX request. Once the data has been received, you then call this
554 * method to signify that the refreshing has completed and to close
555 * the refresher. This method also changes the refresher's state from
556 * `refreshing` to `completing`.
557 */
558 async complete() {
559 if (this.nativeRefresher) {
560 this.needsCompletion = true;
561 // Do not reset scroll el until user removes pointer from screen
562 if (!this.pointerDown) {
563 raf(() => raf(() => this.resetNativeRefresher(this.elementToTransform, 32 /* Completing */)));
564 }
565 }
566 else {
567 this.close(32 /* Completing */, '120ms');
568 }
569 }
570 /**
571 * Changes the refresher's state from `refreshing` to `cancelling`.
572 */
573 async cancel() {
574 if (this.nativeRefresher) {
575 // Do not reset scroll el until user removes pointer from screen
576 if (!this.pointerDown) {
577 raf(() => raf(() => this.resetNativeRefresher(this.elementToTransform, 16 /* Cancelling */)));
578 }
579 }
580 else {
581 this.close(16 /* Cancelling */, '');
582 }
583 }
584 /**
585 * A number representing how far down the user has pulled.
586 * The number `0` represents the user hasn't pulled down at all. The
587 * number `1`, and anything greater than `1`, represents that the user
588 * has pulled far enough down that when they let go then the refresh will
589 * happen. If they let go and the number is less than `1`, then the
590 * refresh will not happen, and the content will return to it's original
591 * position.
592 */
593 getProgress() {
594 return Promise.resolve(this.progress);
595 }
596 canStart() {
597 if (!this.scrollEl) {
598 return false;
599 }
600 if (this.state !== 1 /* Inactive */) {
601 return false;
602 }
603 // if the scrollTop is greater than zero then it's
604 // not possible to pull the content down yet
605 if (this.scrollEl.scrollTop > 0) {
606 return false;
607 }
608 return true;
609 }
610 onStart() {
611 this.progress = 0;
612 this.state = 1 /* Inactive */;
613 }
614 onMove(detail) {
615 if (!this.scrollEl) {
616 return;
617 }
618 // this method can get called like a bazillion times per second,
619 // so it's built to be as efficient as possible, and does its
620 // best to do any DOM read/writes only when absolutely necessary
621 // if multi-touch then get out immediately
622 const ev = detail.event;
623 if (ev.touches && ev.touches.length > 1) {
624 return;
625 }
626 // do nothing if it's actively refreshing
627 // or it's in the way of closing
628 // or this was never a startY
629 if ((this.state & 56 /* _BUSY_ */) !== 0) {
630 return;
631 }
632 const pullFactor = (Number.isNaN(this.pullFactor) || this.pullFactor < 0) ? 1 : this.pullFactor;
633 const deltaY = detail.deltaY * pullFactor;
634 // don't bother if they're scrolling up
635 // and have not already started dragging
636 if (deltaY <= 0) {
637 // the current Y is higher than the starting Y
638 // so they scrolled up enough to be ignored
639 this.progress = 0;
640 this.state = 1 /* Inactive */;
641 if (this.appliedStyles) {
642 // reset the styles only if they were applied
643 this.setCss(0, '', false, '');
644 return;
645 }
646 return;
647 }
648 if (this.state === 1 /* Inactive */) {
649 // this refresh is not already actively pulling down
650 // get the content's scrollTop
651 const scrollHostScrollTop = this.scrollEl.scrollTop;
652 // if the scrollTop is greater than zero then it's
653 // not possible to pull the content down yet
654 if (scrollHostScrollTop > 0) {
655 this.progress = 0;
656 return;
657 }
658 // content scrolled all the way to the top, and dragging down
659 this.state = 2 /* Pulling */;
660 }
661 // prevent native scroll events
662 if (ev.cancelable) {
663 ev.preventDefault();
664 }
665 // the refresher is actively pulling at this point
666 // move the scroll element within the content element
667 this.setCss(deltaY, '0ms', true, '');
668 if (deltaY === 0) {
669 // don't continue if there's no delta yet
670 this.progress = 0;
671 return;
672 }
673 const pullMin = this.pullMin;
674 // set pull progress
675 this.progress = deltaY / pullMin;
676 // emit "start" if it hasn't started yet
677 if (!this.didStart) {
678 this.didStart = true;
679 this.ionStart.emit();
680 }
681 // emit "pulling" on every move
682 this.ionPull.emit();
683 // do nothing if the delta is less than the pull threshold
684 if (deltaY < pullMin) {
685 // ensure it stays in the pulling state, cuz its not ready yet
686 this.state = 2 /* Pulling */;
687 return;
688 }
689 if (deltaY > this.pullMax) {
690 // they pulled farther than the max, so kick off the refresh
691 this.beginRefresh();
692 return;
693 }
694 // pulled farther than the pull min!!
695 // it is now in the `ready` state!!
696 // if they let go then it'll refresh, kerpow!!
697 this.state = 4 /* Ready */;
698 return;
699 }
700 onEnd() {
701 // only run in a zone when absolutely necessary
702 if (this.state === 4 /* Ready */) {
703 // they pulled down far enough, so it's ready to refresh
704 this.beginRefresh();
705 }
706 else if (this.state === 2 /* Pulling */) {
707 // they were pulling down, but didn't pull down far enough
708 // set the content back to it's original location
709 // and close the refresher
710 // set that the refresh is actively cancelling
711 this.cancel();
712 }
713 }
714 beginRefresh() {
715 // assumes we're already back in a zone
716 // they pulled down far enough, so it's ready to refresh
717 this.state = 8 /* Refreshing */;
718 // place the content in a hangout position while it thinks
719 this.setCss(this.pullMin, this.snapbackDuration, true, '');
720 // emit "refresh" because it was pulled down far enough
721 // and they let go to begin refreshing
722 this.ionRefresh.emit({
723 complete: this.complete.bind(this)
724 });
725 }
726 close(state, delay) {
727 // create fallback timer incase something goes wrong with transitionEnd event
728 setTimeout(() => {
729 this.state = 1 /* Inactive */;
730 this.progress = 0;
731 this.didStart = false;
732 this.setCss(0, '0ms', false, '');
733 }, 600);
734 // reset set the styles on the scroll element
735 // set that the refresh is actively cancelling/completing
736 this.state = state;
737 this.setCss(0, this.closeDuration, true, delay);
738 // TODO: stop gesture
739 }
740 setCss(y, duration, overflowVisible, delay) {
741 if (this.nativeRefresher) {
742 return;
743 }
744 this.appliedStyles = (y > 0);
745 writeTask(() => {
746 if (this.scrollEl && this.backgroundContentEl) {
747 const scrollStyle = this.scrollEl.style;
748 const backgroundStyle = this.backgroundContentEl.style;
749 scrollStyle.transform = backgroundStyle.transform = ((y > 0) ? `translateY(${y}px) translateZ(0px)` : '');
750 scrollStyle.transitionDuration = backgroundStyle.transitionDuration = duration;
751 scrollStyle.transitionDelay = backgroundStyle.transitionDelay = delay;
752 scrollStyle.overflow = (overflowVisible ? 'hidden' : '');
753 }
754 });
755 }
756 render() {
757 const mode = getIonMode(this);
758 return (h(Host, { slot: "fixed", class: {
759 [mode]: true,
760 // Used internally for styling
761 [`refresher-${mode}`]: true,
762 'refresher-native': this.nativeRefresher,
763 'refresher-active': this.state !== 1 /* Inactive */,
764 'refresher-pulling': this.state === 2 /* Pulling */,
765 'refresher-ready': this.state === 4 /* Ready */,
766 'refresher-refreshing': this.state === 8 /* Refreshing */,
767 'refresher-cancelling': this.state === 16 /* Cancelling */,
768 'refresher-completing': this.state === 32 /* Completing */,
769 } }));
770 }
771 get el() { return this; }
772 static get watchers() { return {
773 "disabled": ["disabledChanged"]
774 }; }
775 static get style() { return {
776 ios: refresherIosCss,
777 md: refresherMdCss
778 }; }
779}, [32, "ion-refresher", {
780 "pullMin": [2, "pull-min"],
781 "pullMax": [2, "pull-max"],
782 "closeDuration": [1, "close-duration"],
783 "snapbackDuration": [1, "snapback-duration"],
784 "pullFactor": [2, "pull-factor"],
785 "disabled": [4],
786 "nativeRefresher": [32],
787 "state": [32],
788 "complete": [64],
789 "cancel": [64],
790 "getProgress": [64]
791 }]);
792function defineCustomElement$1() {
793 if (typeof customElements === "undefined") {
794 return;
795 }
796 const components = ["ion-refresher"];
797 components.forEach(tagName => { switch (tagName) {
798 case "ion-refresher":
799 if (!customElements.get(tagName)) {
800 customElements.define(tagName, Refresher);
801 }
802 break;
803 } });
804}
805
806const IonRefresher = Refresher;
807const defineCustomElement = defineCustomElement$1;
808
809export { IonRefresher, defineCustomElement };