ion-refresher.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  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 { g as getTimeGivenProgression } from './cubic-bezier.js';
  6. import { I as ION_CONTENT_CLASS_SELECTOR, b as ION_CONTENT_ELEMENT_SELECTOR, p as printIonContentErrorMsg, g as getScrollElement } from './index8.js';
  7. import { t as transitionEndAsync, k as clamp, g as getElementRoot, c as componentOnReady, r as raf } from './helpers.js';
  8. import { p as printIonError } from './index4.js';
  9. import { c as hapticImpact, I as ImpactStyle } from './haptic.js';
  10. import { b as getIonMode } from './ionic-global.js';
  11. import { s as shouldUseNativeRefresher, t as translateElement, a as setSpinnerOpacity, h as handleScrollWhileRefreshing, b as handleScrollWhilePulling, c as createPullingAnimation, d as createSnapBackAnimation, g as getRefresherAnimationType } from './refresher.utils.js';
  12. const refresherIosCss = "ion-refresher{top:0;display:none;position:absolute;width:100%;height:60px;pointer-events:none;z-index:-1}ion-refresher{inset-inline-start: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}:host-context([dir=rtl]) .refresher-pulling-icon,:host-context([dir=rtl]) .refresher-refreshing-icon{-webkit-transform-origin:calc(100% - center);transform-origin:calc(100% - center)}[dir=rtl] .refresher-pulling-icon,[dir=rtl] .refresher-refreshing-icon{-webkit-transform-origin:calc(100% - center);transform-origin:calc(100% - center)}@supports selector(:dir(rtl)){.refresher-pulling-icon:dir(rtl),.refresher-refreshing-icon:dir(rtl){-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{-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;margin-top:0;margin-bottom:0}.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, var(--ion-background-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)}}";
  13. const IonRefresherIosStyle0 = refresherIosCss;
  14. const refresherMdCss = "ion-refresher{top:0;display:none;position:absolute;width:100%;height:60px;pointer-events:none;z-index:-1}ion-refresher{inset-inline-start: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}:host-context([dir=rtl]) .refresher-pulling-icon,:host-context([dir=rtl]) .refresher-refreshing-icon{-webkit-transform-origin:calc(100% - center);transform-origin:calc(100% - center)}[dir=rtl] .refresher-pulling-icon,[dir=rtl] .refresher-refreshing-icon{-webkit-transform-origin:calc(100% - center);transform-origin:calc(100% - center)}@supports selector(:dir(rtl)){.refresher-pulling-icon:dir(rtl),.refresher-refreshing-icon:dir(rtl){-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{-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;margin-top:0;margin-bottom:0;width:24px;height:24px;color:var(--ion-color-primary, #0054e9)}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{-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;margin-top:0;margin-bottom:0;left:0;right:0;bottom:-4px;position:absolute;color:var(--ion-color-primary, #0054e9);font-size:12px}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{-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;margin-top:0;margin-bottom:0;border-radius:100%;-webkit-padding-start:8px;padding-inline-start:8px;-webkit-padding-end:8px;padding-inline-end:8px;padding-top:8px;padding-bottom:8px;display:-ms-flexbox;display:flex;border:1px solid var(--ion-color-step-200, var(--ion-background-color-step-200, #ececec));background:var(--ion-color-step-250, var(--ion-background-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)}";
  15. const IonRefresherMdStyle0 = refresherMdCss;
  16. const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLElement {
  17. constructor() {
  18. super();
  19. this.__registerHost();
  20. this.ionRefresh = createEvent(this, "ionRefresh", 7);
  21. this.ionPull = createEvent(this, "ionPull", 7);
  22. this.ionStart = createEvent(this, "ionStart", 7);
  23. this.appliedStyles = false;
  24. this.didStart = false;
  25. this.progress = 0;
  26. this.pointerDown = false;
  27. this.needsCompletion = false;
  28. this.didRefresh = false;
  29. this.contentFullscreen = false;
  30. this.lastVelocityY = 0;
  31. this.animations = [];
  32. this.nativeRefresher = false;
  33. this.state = 1 /* RefresherState.Inactive */;
  34. this.pullMin = 60;
  35. this.pullMax = this.pullMin + 60;
  36. this.closeDuration = '280ms';
  37. this.snapbackDuration = '280ms';
  38. this.pullFactor = 1;
  39. this.disabled = false;
  40. }
  41. disabledChanged() {
  42. if (this.gesture) {
  43. this.gesture.enable(!this.disabled);
  44. }
  45. }
  46. async checkNativeRefresher() {
  47. const useNativeRefresher = await shouldUseNativeRefresher(this.el, getIonMode(this));
  48. if (useNativeRefresher && !this.nativeRefresher) {
  49. const contentEl = this.el.closest('ion-content');
  50. this.setupNativeRefresher(contentEl);
  51. }
  52. else if (!useNativeRefresher) {
  53. this.destroyNativeRefresher();
  54. }
  55. }
  56. destroyNativeRefresher() {
  57. if (this.scrollEl && this.scrollListenerCallback) {
  58. this.scrollEl.removeEventListener('scroll', this.scrollListenerCallback);
  59. this.scrollListenerCallback = undefined;
  60. }
  61. this.nativeRefresher = false;
  62. }
  63. async resetNativeRefresher(el, state) {
  64. this.state = state;
  65. if (getIonMode(this) === 'ios') {
  66. await translateElement(el, undefined, 300);
  67. }
  68. else {
  69. await transitionEndAsync(this.el.querySelector('.refresher-refreshing-icon'), 200);
  70. }
  71. this.didRefresh = false;
  72. this.needsCompletion = false;
  73. this.pointerDown = false;
  74. this.animations.forEach((ani) => ani.destroy());
  75. this.animations = [];
  76. this.progress = 0;
  77. this.state = 1 /* RefresherState.Inactive */;
  78. }
  79. async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
  80. this.elementToTransform = this.scrollEl;
  81. const ticks = pullingSpinner.shadowRoot.querySelectorAll('svg');
  82. let MAX_PULL = this.scrollEl.clientHeight * 0.16;
  83. const NUM_TICKS = ticks.length;
  84. writeTask(() => ticks.forEach((el) => el.style.setProperty('animation', 'none')));
  85. this.scrollListenerCallback = () => {
  86. // If pointer is not on screen or refresher is not active, ignore scroll
  87. if (!this.pointerDown && this.state === 1 /* RefresherState.Inactive */) {
  88. return;
  89. }
  90. readTask(() => {
  91. // PTR should only be active when overflow scrolling at the top
  92. const scrollTop = this.scrollEl.scrollTop;
  93. const refresherHeight = this.el.clientHeight;
  94. if (scrollTop > 0) {
  95. /**
  96. * If refresher is refreshing and user tries to scroll
  97. * progressively fade refresher out/in
  98. */
  99. if (this.state === 8 /* RefresherState.Refreshing */) {
  100. const ratio = clamp(0, scrollTop / (refresherHeight * 0.5), 1);
  101. writeTask(() => setSpinnerOpacity(refreshingSpinner, 1 - ratio));
  102. return;
  103. }
  104. return;
  105. }
  106. if (this.pointerDown) {
  107. if (!this.didStart) {
  108. this.didStart = true;
  109. this.ionStart.emit();
  110. }
  111. // emit "pulling" on every move
  112. if (this.pointerDown) {
  113. this.ionPull.emit();
  114. }
  115. }
  116. /**
  117. * We want to delay the start of this gesture by ~30px
  118. * when initially pulling down so the refresher does not
  119. * overlap with the content. But when letting go of the
  120. * gesture before the refresher completes, we want the
  121. * refresher tick marks to quickly fade out.
  122. */
  123. const offset = this.didStart ? 30 : 0;
  124. const pullAmount = (this.progress = clamp(0, (Math.abs(scrollTop) - offset) / MAX_PULL, 1));
  125. const shouldShowRefreshingSpinner = this.state === 8 /* RefresherState.Refreshing */ || pullAmount === 1;
  126. if (shouldShowRefreshingSpinner) {
  127. if (this.pointerDown) {
  128. handleScrollWhileRefreshing(refreshingSpinner, this.lastVelocityY);
  129. }
  130. if (!this.didRefresh) {
  131. this.beginRefresh();
  132. this.didRefresh = true;
  133. hapticImpact({ style: ImpactStyle.Light });
  134. /**
  135. * Translate the content element otherwise when pointer is removed
  136. * from screen the scroll content will bounce back over the refresher
  137. */
  138. if (!this.pointerDown) {
  139. translateElement(this.elementToTransform, `${refresherHeight}px`);
  140. }
  141. }
  142. }
  143. else {
  144. this.state = 2 /* RefresherState.Pulling */;
  145. handleScrollWhilePulling(ticks, NUM_TICKS, pullAmount);
  146. }
  147. });
  148. };
  149. this.scrollEl.addEventListener('scroll', this.scrollListenerCallback);
  150. this.gesture = (await import('./index3.js')).createGesture({
  151. el: this.scrollEl,
  152. gestureName: 'refresher',
  153. gesturePriority: 31,
  154. direction: 'y',
  155. threshold: 5,
  156. onStart: () => {
  157. this.pointerDown = true;
  158. if (!this.didRefresh) {
  159. translateElement(this.elementToTransform, '0px');
  160. }
  161. /**
  162. * If the content had `display: none` when
  163. * the refresher was initialized, its clientHeight
  164. * will be 0. When the gesture starts, the content
  165. * will be visible, so try to get the correct
  166. * client height again. This is most common when
  167. * using the refresher in an ion-menu.
  168. */
  169. if (MAX_PULL === 0) {
  170. MAX_PULL = this.scrollEl.clientHeight * 0.16;
  171. }
  172. },
  173. onMove: (ev) => {
  174. this.lastVelocityY = ev.velocityY;
  175. },
  176. onEnd: () => {
  177. this.pointerDown = false;
  178. this.didStart = false;
  179. if (this.needsCompletion) {
  180. this.resetNativeRefresher(this.elementToTransform, 32 /* RefresherState.Completing */);
  181. this.needsCompletion = false;
  182. }
  183. else if (this.didRefresh) {
  184. readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
  185. }
  186. },
  187. });
  188. this.disabledChanged();
  189. }
  190. async setupMDNativeRefresher(contentEl, pullingSpinner, refreshingSpinner) {
  191. const circle = getElementRoot(pullingSpinner).querySelector('circle');
  192. const pullingRefresherIcon = this.el.querySelector('ion-refresher-content .refresher-pulling-icon');
  193. const refreshingCircle = getElementRoot(refreshingSpinner).querySelector('circle');
  194. if (circle !== null && refreshingCircle !== null) {
  195. writeTask(() => {
  196. circle.style.setProperty('animation', 'none');
  197. // This lines up the animation on the refreshing spinner with the pulling spinner
  198. refreshingSpinner.style.setProperty('animation-delay', '-655ms');
  199. refreshingCircle.style.setProperty('animation-delay', '-655ms');
  200. });
  201. }
  202. this.gesture = (await import('./index3.js')).createGesture({
  203. el: this.scrollEl,
  204. gestureName: 'refresher',
  205. gesturePriority: 31,
  206. direction: 'y',
  207. threshold: 5,
  208. canStart: () => this.state !== 8 /* RefresherState.Refreshing */ &&
  209. this.state !== 32 /* RefresherState.Completing */ &&
  210. this.scrollEl.scrollTop === 0,
  211. onStart: (ev) => {
  212. this.progress = 0;
  213. ev.data = { animation: undefined, didStart: false, cancelled: false };
  214. },
  215. onMove: (ev) => {
  216. if ((ev.velocityY < 0 && this.progress === 0 && !ev.data.didStart) || ev.data.cancelled) {
  217. ev.data.cancelled = true;
  218. return;
  219. }
  220. if (!ev.data.didStart) {
  221. ev.data.didStart = true;
  222. this.state = 2 /* RefresherState.Pulling */;
  223. // When ion-refresher is being used with a custom scroll target, the overflow styles need to be applied directly instead of via a css variable
  224. const { scrollEl } = this;
  225. const overflowProperty = scrollEl.matches(ION_CONTENT_CLASS_SELECTOR) ? 'overflow' : '--overflow';
  226. writeTask(() => scrollEl.style.setProperty(overflowProperty, 'hidden'));
  227. const animationType = getRefresherAnimationType(contentEl);
  228. const animation = createPullingAnimation(animationType, pullingRefresherIcon, this.el);
  229. ev.data.animation = animation;
  230. animation.progressStart(false, 0);
  231. this.ionStart.emit();
  232. this.animations.push(animation);
  233. return;
  234. }
  235. // Since we are using an easing curve, slow the gesture tracking down a bit
  236. this.progress = clamp(0, (ev.deltaY / 180) * 0.5, 1);
  237. ev.data.animation.progressStep(this.progress);
  238. this.ionPull.emit();
  239. },
  240. onEnd: (ev) => {
  241. if (!ev.data.didStart) {
  242. return;
  243. }
  244. this.gesture.enable(false);
  245. const { scrollEl } = this;
  246. const overflowProperty = scrollEl.matches(ION_CONTENT_CLASS_SELECTOR) ? 'overflow' : '--overflow';
  247. writeTask(() => scrollEl.style.removeProperty(overflowProperty));
  248. if (this.progress <= 0.4) {
  249. ev.data.animation.progressEnd(0, this.progress, 500).onFinish(() => {
  250. this.animations.forEach((ani) => ani.destroy());
  251. this.animations = [];
  252. this.gesture.enable(true);
  253. this.state = 1 /* RefresherState.Inactive */;
  254. });
  255. return;
  256. }
  257. const progress = getTimeGivenProgression([0, 0], [0, 0], [1, 1], [1, 1], this.progress)[0];
  258. const snapBackAnimation = createSnapBackAnimation(pullingRefresherIcon);
  259. this.animations.push(snapBackAnimation);
  260. writeTask(async () => {
  261. pullingRefresherIcon.style.setProperty('--ion-pulling-refresher-translate', `${progress * 100}px`);
  262. ev.data.animation.progressEnd();
  263. await snapBackAnimation.play();
  264. this.beginRefresh();
  265. ev.data.animation.destroy();
  266. this.gesture.enable(true);
  267. });
  268. },
  269. });
  270. this.disabledChanged();
  271. }
  272. async setupNativeRefresher(contentEl) {
  273. if (this.scrollListenerCallback || !contentEl || this.nativeRefresher || !this.scrollEl) {
  274. return;
  275. }
  276. /**
  277. * If using non-native refresher before make sure
  278. * we clean up any old CSS. This can happen when
  279. * a user manually calls the refresh method in a
  280. * component create callback before the native
  281. * refresher is setup.
  282. */
  283. this.setCss(0, '', false, '');
  284. this.nativeRefresher = true;
  285. const pullingSpinner = this.el.querySelector('ion-refresher-content .refresher-pulling ion-spinner');
  286. const refreshingSpinner = this.el.querySelector('ion-refresher-content .refresher-refreshing ion-spinner');
  287. if (getIonMode(this) === 'ios') {
  288. this.setupiOSNativeRefresher(pullingSpinner, refreshingSpinner);
  289. }
  290. else {
  291. this.setupMDNativeRefresher(contentEl, pullingSpinner, refreshingSpinner);
  292. }
  293. }
  294. componentDidUpdate() {
  295. this.checkNativeRefresher();
  296. }
  297. async connectedCallback() {
  298. if (this.el.getAttribute('slot') !== 'fixed') {
  299. printIonError('[ion-refresher] - Make sure you use: <ion-refresher slot="fixed">');
  300. return;
  301. }
  302. const contentEl = this.el.closest(ION_CONTENT_ELEMENT_SELECTOR);
  303. if (!contentEl) {
  304. printIonContentErrorMsg(this.el);
  305. return;
  306. }
  307. /**
  308. * Waits for the content to be ready before querying the scroll
  309. * or the background content element.
  310. */
  311. componentOnReady(contentEl, async () => {
  312. const customScrollTarget = contentEl.querySelector(ION_CONTENT_CLASS_SELECTOR);
  313. /**
  314. * Query the custom scroll target (if available), first. In refresher implementations,
  315. * the ion-refresher element will always be a direct child of ion-content (slot="fixed"). By
  316. * querying the custom scroll target first and falling back to the ion-content element,
  317. * the correct scroll element will be returned by the implementation.
  318. */
  319. this.scrollEl = await getScrollElement(customScrollTarget !== null && customScrollTarget !== void 0 ? customScrollTarget : contentEl);
  320. /**
  321. * Query the background content element from the host ion-content element directly.
  322. */
  323. this.backgroundContentEl = await contentEl.getBackgroundElement();
  324. /**
  325. * Check if the content element is fullscreen to apply the correct styles
  326. * when the refresher is refreshing. Otherwise, the refresher will be
  327. * hidden because it is positioned behind the background content element.
  328. */
  329. this.contentFullscreen = contentEl.fullscreen;
  330. if (await shouldUseNativeRefresher(this.el, getIonMode(this))) {
  331. this.setupNativeRefresher(contentEl);
  332. }
  333. else {
  334. this.gesture = (await import('./index3.js')).createGesture({
  335. el: contentEl,
  336. gestureName: 'refresher',
  337. gesturePriority: 31,
  338. direction: 'y',
  339. threshold: 20,
  340. passive: false,
  341. canStart: () => this.canStart(),
  342. onStart: () => this.onStart(),
  343. onMove: (ev) => this.onMove(ev),
  344. onEnd: () => this.onEnd(),
  345. });
  346. this.disabledChanged();
  347. }
  348. });
  349. }
  350. disconnectedCallback() {
  351. this.destroyNativeRefresher();
  352. this.scrollEl = undefined;
  353. if (this.gesture) {
  354. this.gesture.destroy();
  355. this.gesture = undefined;
  356. }
  357. }
  358. /**
  359. * Call `complete()` when your async operation has completed.
  360. * For example, the `refreshing` state is while the app is performing
  361. * an asynchronous operation, such as receiving more data from an
  362. * AJAX request. Once the data has been received, you then call this
  363. * method to signify that the refreshing has completed and to close
  364. * the refresher. This method also changes the refresher's state from
  365. * `refreshing` to `completing`.
  366. */
  367. async complete() {
  368. if (this.nativeRefresher) {
  369. this.needsCompletion = true;
  370. // Do not reset scroll el until user removes pointer from screen
  371. if (!this.pointerDown) {
  372. raf(() => raf(() => this.resetNativeRefresher(this.elementToTransform, 32 /* RefresherState.Completing */)));
  373. }
  374. }
  375. else {
  376. this.close(32 /* RefresherState.Completing */, '120ms');
  377. }
  378. }
  379. /**
  380. * Changes the refresher's state from `refreshing` to `cancelling`.
  381. */
  382. async cancel() {
  383. if (this.nativeRefresher) {
  384. // Do not reset scroll el until user removes pointer from screen
  385. if (!this.pointerDown) {
  386. raf(() => raf(() => this.resetNativeRefresher(this.elementToTransform, 16 /* RefresherState.Cancelling */)));
  387. }
  388. }
  389. else {
  390. this.close(16 /* RefresherState.Cancelling */, '');
  391. }
  392. }
  393. /**
  394. * A number representing how far down the user has pulled.
  395. * The number `0` represents the user hasn't pulled down at all. The
  396. * number `1`, and anything greater than `1`, represents that the user
  397. * has pulled far enough down that when they let go then the refresh will
  398. * happen. If they let go and the number is less than `1`, then the
  399. * refresh will not happen, and the content will return to it's original
  400. * position.
  401. */
  402. getProgress() {
  403. return Promise.resolve(this.progress);
  404. }
  405. canStart() {
  406. if (!this.scrollEl) {
  407. return false;
  408. }
  409. if (this.state !== 1 /* RefresherState.Inactive */) {
  410. return false;
  411. }
  412. // if the scrollTop is greater than zero then it's
  413. // not possible to pull the content down yet
  414. if (this.scrollEl.scrollTop > 0) {
  415. return false;
  416. }
  417. return true;
  418. }
  419. onStart() {
  420. this.progress = 0;
  421. this.state = 1 /* RefresherState.Inactive */;
  422. this.memoizeOverflowStyle();
  423. /**
  424. * If the content is fullscreen, then we need to
  425. * set the offset-top style on the background content
  426. * element to ensure that the refresher is shown.
  427. */
  428. if (this.contentFullscreen && this.backgroundContentEl) {
  429. this.backgroundContentEl.style.setProperty('--offset-top', '0px');
  430. }
  431. }
  432. onMove(detail) {
  433. if (!this.scrollEl) {
  434. return;
  435. }
  436. // this method can get called like a bazillion times per second,
  437. // so it's built to be as efficient as possible, and does its
  438. // best to do any DOM read/writes only when absolutely necessary
  439. // if multi-touch then get out immediately
  440. const ev = detail.event;
  441. if (ev.touches !== undefined && ev.touches.length > 1) {
  442. return;
  443. }
  444. // do nothing if it's actively refreshing
  445. // or it's in the way of closing
  446. // or this was never a startY
  447. if ((this.state & 56 /* RefresherState._BUSY_ */) !== 0) {
  448. return;
  449. }
  450. const pullFactor = Number.isNaN(this.pullFactor) || this.pullFactor < 0 ? 1 : this.pullFactor;
  451. const deltaY = detail.deltaY * pullFactor;
  452. // don't bother if they're scrolling up
  453. // and have not already started dragging
  454. if (deltaY <= 0) {
  455. // the current Y is higher than the starting Y
  456. // so they scrolled up enough to be ignored
  457. this.progress = 0;
  458. this.state = 1 /* RefresherState.Inactive */;
  459. if (this.appliedStyles) {
  460. // reset the styles only if they were applied
  461. this.setCss(0, '', false, '');
  462. return;
  463. }
  464. return;
  465. }
  466. if (this.state === 1 /* RefresherState.Inactive */) {
  467. // this refresh is not already actively pulling down
  468. // get the content's scrollTop
  469. const scrollHostScrollTop = this.scrollEl.scrollTop;
  470. // if the scrollTop is greater than zero then it's
  471. // not possible to pull the content down yet
  472. if (scrollHostScrollTop > 0) {
  473. this.progress = 0;
  474. return;
  475. }
  476. // content scrolled all the way to the top, and dragging down
  477. this.state = 2 /* RefresherState.Pulling */;
  478. }
  479. // prevent native scroll events
  480. if (ev.cancelable) {
  481. ev.preventDefault();
  482. }
  483. // the refresher is actively pulling at this point
  484. // move the scroll element within the content element
  485. this.setCss(deltaY, '0ms', true, '');
  486. if (deltaY === 0) {
  487. // don't continue if there's no delta yet
  488. this.progress = 0;
  489. return;
  490. }
  491. const pullMin = this.pullMin;
  492. // set pull progress
  493. this.progress = deltaY / pullMin;
  494. // emit "start" if it hasn't started yet
  495. if (!this.didStart) {
  496. this.didStart = true;
  497. this.ionStart.emit();
  498. }
  499. // emit "pulling" on every move
  500. this.ionPull.emit();
  501. // do nothing if the delta is less than the pull threshold
  502. if (deltaY < pullMin) {
  503. // ensure it stays in the pulling state, cuz its not ready yet
  504. this.state = 2 /* RefresherState.Pulling */;
  505. return;
  506. }
  507. if (deltaY > this.pullMax) {
  508. // they pulled farther than the max, so kick off the refresh
  509. this.beginRefresh();
  510. return;
  511. }
  512. // pulled farther than the pull min!!
  513. // it is now in the `ready` state!!
  514. // if they let go then it'll refresh, kerpow!!
  515. this.state = 4 /* RefresherState.Ready */;
  516. return;
  517. }
  518. onEnd() {
  519. // only run in a zone when absolutely necessary
  520. if (this.state === 4 /* RefresherState.Ready */) {
  521. // they pulled down far enough, so it's ready to refresh
  522. this.beginRefresh();
  523. }
  524. else if (this.state === 2 /* RefresherState.Pulling */) {
  525. // they were pulling down, but didn't pull down far enough
  526. // set the content back to it's original location
  527. // and close the refresher
  528. // set that the refresh is actively cancelling
  529. this.cancel();
  530. }
  531. else if (this.state === 1 /* RefresherState.Inactive */) {
  532. /**
  533. * The pull to refresh gesture was aborted
  534. * so we should immediately restore any overflow styles
  535. * that have been modified. Do not call this.cancel
  536. * because the styles will only be reset after a timeout.
  537. * If the gesture is aborted then scrolling should be
  538. * available right away.
  539. */
  540. this.restoreOverflowStyle();
  541. }
  542. }
  543. beginRefresh() {
  544. // assumes we're already back in a zone
  545. // they pulled down far enough, so it's ready to refresh
  546. this.state = 8 /* RefresherState.Refreshing */;
  547. // place the content in a hangout position while it thinks
  548. this.setCss(this.pullMin, this.snapbackDuration, true, '');
  549. // emit "refresh" because it was pulled down far enough
  550. // and they let go to begin refreshing
  551. this.ionRefresh.emit({
  552. complete: this.complete.bind(this),
  553. });
  554. }
  555. close(state, delay) {
  556. // create fallback timer incase something goes wrong with transitionEnd event
  557. setTimeout(() => {
  558. var _a;
  559. this.state = 1 /* RefresherState.Inactive */;
  560. this.progress = 0;
  561. this.didStart = false;
  562. /**
  563. * Reset any overflow styles so the
  564. * user can scroll again.
  565. */
  566. this.setCss(0, '0ms', false, '', true);
  567. /**
  568. * Reset the offset-top style on the background content
  569. * when the refresher is no longer refreshing and the
  570. * content is fullscreen.
  571. *
  572. * This ensures that the behavior of background content
  573. * does not change when refreshing is complete.
  574. */
  575. if (this.contentFullscreen && this.backgroundContentEl) {
  576. (_a = this.backgroundContentEl) === null || _a === void 0 ? void 0 : _a.style.removeProperty('--offset-top');
  577. }
  578. }, 600);
  579. // reset the styles on the scroll element
  580. // set that the refresh is actively cancelling/completing
  581. this.state = state;
  582. this.setCss(0, this.closeDuration, true, delay);
  583. }
  584. setCss(y, duration, overflowVisible, delay, shouldRestoreOverflowStyle = false) {
  585. if (this.nativeRefresher) {
  586. return;
  587. }
  588. this.appliedStyles = y > 0;
  589. writeTask(() => {
  590. if (this.scrollEl && this.backgroundContentEl) {
  591. const scrollStyle = this.scrollEl.style;
  592. const backgroundStyle = this.backgroundContentEl.style;
  593. scrollStyle.transform = backgroundStyle.transform = y > 0 ? `translateY(${y}px) translateZ(0px)` : '';
  594. scrollStyle.transitionDuration = backgroundStyle.transitionDuration = duration;
  595. scrollStyle.transitionDelay = backgroundStyle.transitionDelay = delay;
  596. scrollStyle.overflow = overflowVisible ? 'hidden' : '';
  597. }
  598. /**
  599. * Reset the overflow styles only once
  600. * the pull to refresh effect has been closed.
  601. * This ensures that the gesture is done
  602. * and the refresh operation has either
  603. * been aborted or has completed.
  604. */
  605. if (shouldRestoreOverflowStyle) {
  606. this.restoreOverflowStyle();
  607. }
  608. });
  609. }
  610. memoizeOverflowStyle() {
  611. if (this.scrollEl) {
  612. const { overflow, overflowX, overflowY } = this.scrollEl.style;
  613. this.overflowStyles = {
  614. overflow: overflow !== null && overflow !== void 0 ? overflow : '',
  615. overflowX: overflowX !== null && overflowX !== void 0 ? overflowX : '',
  616. overflowY: overflowY !== null && overflowY !== void 0 ? overflowY : '',
  617. };
  618. }
  619. }
  620. restoreOverflowStyle() {
  621. if (this.overflowStyles !== undefined && this.scrollEl !== undefined) {
  622. const { overflow, overflowX, overflowY } = this.overflowStyles;
  623. this.scrollEl.style.overflow = overflow;
  624. this.scrollEl.style.overflowX = overflowX;
  625. this.scrollEl.style.overflowY = overflowY;
  626. this.overflowStyles = undefined;
  627. }
  628. }
  629. render() {
  630. const mode = getIonMode(this);
  631. return (h(Host, { key: 'c717c16f2ca3e42351848cc8ad37918dec28961d', slot: "fixed", class: {
  632. [mode]: true,
  633. // Used internally for styling
  634. [`refresher-${mode}`]: true,
  635. 'refresher-native': this.nativeRefresher,
  636. 'refresher-active': this.state !== 1 /* RefresherState.Inactive */,
  637. 'refresher-pulling': this.state === 2 /* RefresherState.Pulling */,
  638. 'refresher-ready': this.state === 4 /* RefresherState.Ready */,
  639. 'refresher-refreshing': this.state === 8 /* RefresherState.Refreshing */,
  640. 'refresher-cancelling': this.state === 16 /* RefresherState.Cancelling */,
  641. 'refresher-completing': this.state === 32 /* RefresherState.Completing */,
  642. } }));
  643. }
  644. get el() { return this; }
  645. static get watchers() { return {
  646. "disabled": ["disabledChanged"]
  647. }; }
  648. static get style() { return {
  649. ios: IonRefresherIosStyle0,
  650. md: IonRefresherMdStyle0
  651. }; }
  652. }, [32, "ion-refresher", {
  653. "pullMin": [2, "pull-min"],
  654. "pullMax": [2, "pull-max"],
  655. "closeDuration": [1, "close-duration"],
  656. "snapbackDuration": [1, "snapback-duration"],
  657. "pullFactor": [2, "pull-factor"],
  658. "disabled": [4],
  659. "nativeRefresher": [32],
  660. "state": [32],
  661. "complete": [64],
  662. "cancel": [64],
  663. "getProgress": [64]
  664. }, undefined, {
  665. "disabled": ["disabledChanged"]
  666. }]);
  667. function defineCustomElement$1() {
  668. if (typeof customElements === "undefined") {
  669. return;
  670. }
  671. const components = ["ion-refresher"];
  672. components.forEach(tagName => { switch (tagName) {
  673. case "ion-refresher":
  674. if (!customElements.get(tagName)) {
  675. customElements.define(tagName, Refresher);
  676. }
  677. break;
  678. } });
  679. }
  680. const IonRefresher = Refresher;
  681. const defineCustomElement = defineCustomElement$1;
  682. export { IonRefresher, defineCustomElement };