refresher.utils.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. import { writeTask } from '@stencil/core/internal/client';
  5. import { c as createAnimation } from './animation.js';
  6. import { t as transitionEndAsync, c as componentOnReady, k as clamp } from './helpers.js';
  7. const getRefresherAnimationType = (contentEl) => {
  8. const previousSibling = contentEl.previousElementSibling;
  9. const hasHeader = previousSibling !== null && previousSibling.tagName === 'ION-HEADER';
  10. return hasHeader ? 'translate' : 'scale';
  11. };
  12. const createPullingAnimation = (type, pullingSpinner, refresherEl) => {
  13. return type === 'scale'
  14. ? createScaleAnimation(pullingSpinner, refresherEl)
  15. : createTranslateAnimation(pullingSpinner, refresherEl);
  16. };
  17. const createBaseAnimation = (pullingRefresherIcon) => {
  18. const spinner = pullingRefresherIcon.querySelector('ion-spinner');
  19. const circle = spinner.shadowRoot.querySelector('circle');
  20. const spinnerArrowContainer = pullingRefresherIcon.querySelector('.spinner-arrow-container');
  21. const arrowContainer = pullingRefresherIcon.querySelector('.arrow-container');
  22. const arrow = arrowContainer ? arrowContainer.querySelector('ion-icon') : null;
  23. const baseAnimation = createAnimation().duration(1000).easing('ease-out');
  24. const spinnerArrowContainerAnimation = createAnimation()
  25. .addElement(spinnerArrowContainer)
  26. .keyframes([
  27. { offset: 0, opacity: '0.3' },
  28. { offset: 0.45, opacity: '0.3' },
  29. { offset: 0.55, opacity: '1' },
  30. { offset: 1, opacity: '1' },
  31. ]);
  32. const circleInnerAnimation = createAnimation()
  33. .addElement(circle)
  34. .keyframes([
  35. { offset: 0, strokeDasharray: '1px, 200px' },
  36. { offset: 0.2, strokeDasharray: '1px, 200px' },
  37. { offset: 0.55, strokeDasharray: '100px, 200px' },
  38. { offset: 1, strokeDasharray: '100px, 200px' },
  39. ]);
  40. const circleOuterAnimation = createAnimation()
  41. .addElement(spinner)
  42. .keyframes([
  43. { offset: 0, transform: 'rotate(-90deg)' },
  44. { offset: 1, transform: 'rotate(210deg)' },
  45. ]);
  46. /**
  47. * Only add arrow animation if present
  48. * this allows users to customize the spinners
  49. * without errors being thrown
  50. */
  51. if (arrowContainer && arrow) {
  52. const arrowContainerAnimation = createAnimation()
  53. .addElement(arrowContainer)
  54. .keyframes([
  55. { offset: 0, transform: 'rotate(0deg)' },
  56. { offset: 0.3, transform: 'rotate(0deg)' },
  57. { offset: 0.55, transform: 'rotate(280deg)' },
  58. { offset: 1, transform: 'rotate(400deg)' },
  59. ]);
  60. const arrowAnimation = createAnimation()
  61. .addElement(arrow)
  62. .keyframes([
  63. { offset: 0, transform: 'translateX(2px) scale(0)' },
  64. { offset: 0.3, transform: 'translateX(2px) scale(0)' },
  65. { offset: 0.55, transform: 'translateX(-1.5px) scale(1)' },
  66. { offset: 1, transform: 'translateX(-1.5px) scale(1)' },
  67. ]);
  68. baseAnimation.addAnimation([arrowContainerAnimation, arrowAnimation]);
  69. }
  70. return baseAnimation.addAnimation([spinnerArrowContainerAnimation, circleInnerAnimation, circleOuterAnimation]);
  71. };
  72. const createScaleAnimation = (pullingRefresherIcon, refresherEl) => {
  73. /**
  74. * Do not take the height of the refresher icon
  75. * because at this point the DOM has not updated,
  76. * so the refresher icon is still hidden with
  77. * display: none.
  78. * The `ion-refresher` container height
  79. * is roughly the amount we need to offset
  80. * the icon by when pulling down.
  81. */
  82. const height = refresherEl.clientHeight;
  83. const spinnerAnimation = createAnimation()
  84. .addElement(pullingRefresherIcon)
  85. .keyframes([
  86. { offset: 0, transform: `scale(0) translateY(-${height}px)` },
  87. { offset: 1, transform: 'scale(1) translateY(100px)' },
  88. ]);
  89. return createBaseAnimation(pullingRefresherIcon).addAnimation([spinnerAnimation]);
  90. };
  91. const createTranslateAnimation = (pullingRefresherIcon, refresherEl) => {
  92. /**
  93. * Do not take the height of the refresher icon
  94. * because at this point the DOM has not updated,
  95. * so the refresher icon is still hidden with
  96. * display: none.
  97. * The `ion-refresher` container height
  98. * is roughly the amount we need to offset
  99. * the icon by when pulling down.
  100. */
  101. const height = refresherEl.clientHeight;
  102. const spinnerAnimation = createAnimation()
  103. .addElement(pullingRefresherIcon)
  104. .keyframes([
  105. { offset: 0, transform: `translateY(-${height}px)` },
  106. { offset: 1, transform: 'translateY(100px)' },
  107. ]);
  108. return createBaseAnimation(pullingRefresherIcon).addAnimation([spinnerAnimation]);
  109. };
  110. const createSnapBackAnimation = (pullingRefresherIcon) => {
  111. return createAnimation()
  112. .duration(125)
  113. .addElement(pullingRefresherIcon)
  114. .fromTo('transform', 'translateY(var(--ion-pulling-refresher-translate, 100px))', 'translateY(0px)');
  115. };
  116. // iOS Native Refresher
  117. // -----------------------------
  118. const setSpinnerOpacity = (spinner, opacity) => {
  119. spinner.style.setProperty('opacity', opacity.toString());
  120. };
  121. const handleScrollWhilePulling = (ticks, numTicks, pullAmount) => {
  122. const max = 1;
  123. writeTask(() => {
  124. ticks.forEach((el, i) => {
  125. /**
  126. * Compute the opacity of each tick
  127. * mark as a percentage of the pullAmount
  128. * offset by max / numTicks so
  129. * the tick marks are shown staggered.
  130. */
  131. const min = i * (max / numTicks);
  132. const range = max - min;
  133. const start = pullAmount - min;
  134. const progression = clamp(0, start / range, 1);
  135. el.style.setProperty('opacity', progression.toString());
  136. });
  137. });
  138. };
  139. const handleScrollWhileRefreshing = (spinner, lastVelocityY) => {
  140. writeTask(() => {
  141. // If user pulls down quickly, the spinner should spin faster
  142. spinner.style.setProperty('--refreshing-rotation-duration', lastVelocityY >= 1.0 ? '0.5s' : '2s');
  143. spinner.style.setProperty('opacity', '1');
  144. });
  145. };
  146. const translateElement = (el, value, duration = 200) => {
  147. if (!el) {
  148. return Promise.resolve();
  149. }
  150. const trans = transitionEndAsync(el, duration);
  151. writeTask(() => {
  152. el.style.setProperty('transition', `${duration}ms all ease-out`);
  153. if (value === undefined) {
  154. el.style.removeProperty('transform');
  155. }
  156. else {
  157. el.style.setProperty('transform', `translate3d(0px, ${value}, 0px)`);
  158. }
  159. });
  160. return trans;
  161. };
  162. // Utils
  163. // -----------------------------
  164. /**
  165. * In order to use the native iOS refresher the device must support rubber band scrolling.
  166. * As part of this, we need to exclude Desktop Safari because it has a slightly different rubber band effect that is not compatible with the native refresher in Ionic.
  167. *
  168. * We also need to be careful not to include devices that spoof their user agent.
  169. * For example, when using iOS emulation in Chrome the user agent will be spoofed such that
  170. * navigator.maxTouchPointer > 0. To work around this,
  171. * we check to see if the apple-pay-logo is supported as a named image which is only
  172. * true on Apple devices.
  173. *
  174. * We previously checked referencEl.style.webkitOverflowScrolling to explicitly check
  175. * for rubber band support. However, this property was removed on iPadOS and it's possible
  176. * that this will be removed on iOS in the future too.
  177. *
  178. */
  179. const supportsRubberBandScrolling = () => {
  180. return navigator.maxTouchPoints > 0 && CSS.supports('background: -webkit-named-image(apple-pay-logo-black)');
  181. };
  182. const shouldUseNativeRefresher = async (referenceEl, mode) => {
  183. const refresherContent = referenceEl.querySelector('ion-refresher-content');
  184. if (!refresherContent) {
  185. return Promise.resolve(false);
  186. }
  187. await new Promise((resolve) => componentOnReady(refresherContent, resolve));
  188. const pullingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-pulling ion-spinner');
  189. const refreshingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-refreshing ion-spinner');
  190. return (pullingSpinner !== null &&
  191. refreshingSpinner !== null &&
  192. ((mode === 'ios' && supportsRubberBandScrolling()) || mode === 'md'));
  193. };
  194. export { setSpinnerOpacity as a, handleScrollWhilePulling as b, createPullingAnimation as c, createSnapBackAnimation as d, supportsRubberBandScrolling as e, getRefresherAnimationType as g, handleScrollWhileRefreshing as h, shouldUseNativeRefresher as s, translateElement as t };