pull-to-refresh.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /**
  2. * 下拉刷新通用工具 (BEXPullRefresh)
  3. *
  4. * 用法:
  5. * var ptr = BEXPullRefresh.init(document.getElementById('myContainer'), {
  6. * onRefresh: async function () { await fetchData(); },
  7. * texts: { pull: '下拉刷新', release: '释放刷新', refreshing: '刷新中...' },
  8. * threshold: 56
  9. * });
  10. *
  11. * // 手动销毁:ptr.destroy();
  12. * // 销毁全部:BEXPullRefresh.destroyAll();
  13. */
  14. (function () {
  15. 'use strict';
  16. var instances = [];
  17. function PTRInstance(container, options) {
  18. var opts = options || {};
  19. var threshold = opts.threshold || 56;
  20. var onRefresh = opts.onRefresh || function () { return Promise.resolve(); };
  21. var texts = opts.texts || {
  22. pull: 'Pull to refresh',
  23. release: 'Release to refresh',
  24. refreshing: 'Refreshing...'
  25. };
  26. var state = { pulling: false, refreshing: false, startY: 0, currentY: 0 };
  27. var indicatorEl = null;
  28. var spinnerEl = null;
  29. var textEl = null;
  30. function buildDOM() {
  31. indicatorEl = document.createElement('div');
  32. indicatorEl.className = 'ptr-indicator';
  33. indicatorEl.innerHTML =
  34. '<div class="ptr-spinner"></div>' +
  35. '<span class="ptr-text">' + texts.pull + '</span>';
  36. var parent = container.parentNode;
  37. parent.insertBefore(indicatorEl, container);
  38. spinnerEl = indicatorEl.querySelector('.ptr-spinner');
  39. textEl = indicatorEl.querySelector('.ptr-text');
  40. container.classList.add('ptr-container');
  41. // 确保父级支持 absolute 定位
  42. if (parent !== document.body) {
  43. var parentPos = window.getComputedStyle(parent).position;
  44. if (parentPos === 'static') parent.style.position = 'relative';
  45. }
  46. }
  47. function getScrollTop() {
  48. return document.documentElement.scrollTop || document.body.scrollTop || 0;
  49. }
  50. function applyTransform(y, noTransition) {
  51. if (noTransition) container.classList.add('ptr-pulling');
  52. else container.classList.remove('ptr-pulling');
  53. container.style.transform = 'translateY(' + y + 'px)';
  54. // 指示器从 translateY(-100%) → 0%:y 0→threshold 时,百分比 -100→0
  55. var pullPercent = ((y / threshold) * 100 - 100).toFixed(1);
  56. indicatorEl.style.transform = 'translateY(' + pullPercent + '%)';
  57. }
  58. function setSpinnerOpacity(opacity) {
  59. spinnerEl.style.opacity = String(opacity);
  60. }
  61. function startRefresh() {
  62. state.refreshing = true;
  63. indicatorEl.classList.add('ptr-refreshing');
  64. applyTransform(threshold, false);
  65. setSpinnerOpacity(1);
  66. textEl.textContent = texts.refreshing;
  67. onRefresh().then(function () {
  68. finishRefresh();
  69. }).catch(function () {
  70. finishRefresh();
  71. });
  72. }
  73. function finishRefresh() {
  74. state.refreshing = false;
  75. indicatorEl.classList.remove('ptr-refreshing');
  76. container.classList.remove('ptr-pulling');
  77. container.style.transform = 'translateY(0)';
  78. indicatorEl.style.transform = 'translateY(-100%)';
  79. setSpinnerOpacity(0);
  80. textEl.textContent = texts.pull;
  81. }
  82. function onTouchStart(e) {
  83. if (state.refreshing) return;
  84. state.startY = e.touches[0].clientY;
  85. state.pulling = false;
  86. }
  87. function isDocumentScrollLocked() {
  88. var htmlStyle = window.getComputedStyle(document.documentElement);
  89. return htmlStyle.overflow === 'hidden' || htmlStyle.overflowY === 'hidden';
  90. }
  91. function onTouchMove(e) {
  92. if (state.refreshing || isDocumentScrollLocked()) return;
  93. var scrollTop = getScrollTop();
  94. if (scrollTop > 0) return;
  95. var deltaY = e.touches[0].clientY - state.startY;
  96. if (deltaY <= 0) return;
  97. state.pulling = true;
  98. state.currentY = Math.min(deltaY * 0.5, threshold * 1.5);
  99. applyTransform(state.currentY, true);
  100. setSpinnerOpacity(Math.min(state.currentY / threshold, 1));
  101. textEl.textContent = state.currentY >= threshold ? texts.release : texts.pull;
  102. }
  103. function onTouchEnd() {
  104. if (!state.pulling || state.refreshing) return;
  105. state.pulling = false;
  106. if (state.currentY >= threshold) {
  107. startRefresh();
  108. } else {
  109. applyTransform(0, false);
  110. setSpinnerOpacity(0);
  111. textEl.textContent = texts.pull;
  112. }
  113. }
  114. this.destroy = function () {
  115. document.removeEventListener('touchstart', onTouchStart);
  116. document.removeEventListener('touchmove', onTouchMove);
  117. document.removeEventListener('touchend', onTouchEnd);
  118. if (indicatorEl && indicatorEl.parentNode) {
  119. indicatorEl.parentNode.removeChild(indicatorEl);
  120. }
  121. container.classList.remove('ptr-container', 'ptr-pulling');
  122. container.style.transform = '';
  123. var idx = instances.indexOf(this);
  124. if (idx > -1) instances.splice(idx, 1);
  125. };
  126. buildDOM();
  127. document.addEventListener('touchstart', onTouchStart, { passive: true });
  128. document.addEventListener('touchmove', onTouchMove, { passive: true });
  129. document.addEventListener('touchend', onTouchEnd);
  130. instances.push(this);
  131. }
  132. window.BEXPullRefresh = {
  133. init: function (container, options) {
  134. if (!container) throw new Error('BEXPullRefresh.init: container is required');
  135. return new PTRInstance(container, options);
  136. },
  137. destroyAll: function () {
  138. while (instances.length) {
  139. instances[instances.length - 1].destroy();
  140. }
  141. }
  142. };
  143. })();