UNPKG

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