VInfiniteScroll.mjs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import { createVNode as _createVNode, createTextVNode as _createTextVNode } from "vue";
  2. // Styles
  3. import "./VInfiniteScroll.css";
  4. // Components
  5. import { VBtn } from "../VBtn/index.mjs";
  6. import { VProgressCircular } from "../VProgressCircular/index.mjs"; // Composables
  7. import { makeDimensionProps, useDimension } from "../../composables/dimensions.mjs";
  8. import { useIntersectionObserver } from "../../composables/intersectionObserver.mjs";
  9. import { useLocale } from "../../composables/locale.mjs";
  10. import { makeTagProps } from "../../composables/tag.mjs"; // Utilities
  11. import { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue';
  12. import { convertToUnit, defineComponent, genericComponent, propsFactory, useRender } from "../../util/index.mjs"; // Types
  13. export const makeVInfiniteScrollProps = propsFactory({
  14. color: String,
  15. direction: {
  16. type: String,
  17. default: 'vertical',
  18. validator: v => ['vertical', 'horizontal'].includes(v)
  19. },
  20. side: {
  21. type: String,
  22. default: 'end',
  23. validator: v => ['start', 'end', 'both'].includes(v)
  24. },
  25. mode: {
  26. type: String,
  27. default: 'intersect',
  28. validator: v => ['intersect', 'manual'].includes(v)
  29. },
  30. margin: [Number, String],
  31. loadMoreText: {
  32. type: String,
  33. default: '$vuetify.infiniteScroll.loadMore'
  34. },
  35. emptyText: {
  36. type: String,
  37. default: '$vuetify.infiniteScroll.empty'
  38. },
  39. ...makeDimensionProps(),
  40. ...makeTagProps()
  41. }, 'VInfiniteScroll');
  42. export const VInfiniteScrollIntersect = defineComponent({
  43. name: 'VInfiniteScrollIntersect',
  44. props: {
  45. side: {
  46. type: String,
  47. required: true
  48. },
  49. rootMargin: String
  50. },
  51. emits: {
  52. intersect: (side, isIntersecting) => true
  53. },
  54. setup(props, _ref) {
  55. let {
  56. emit
  57. } = _ref;
  58. const {
  59. intersectionRef,
  60. isIntersecting
  61. } = useIntersectionObserver();
  62. watch(isIntersecting, async val => {
  63. emit('intersect', props.side, val);
  64. });
  65. useRender(() => _createVNode("div", {
  66. "class": "v-infinite-scroll-intersect",
  67. "style": {
  68. '--v-infinite-margin-size': props.rootMargin
  69. },
  70. "ref": intersectionRef
  71. }, [_createTextVNode("\xA0")]));
  72. return {};
  73. }
  74. });
  75. export const VInfiniteScroll = genericComponent()({
  76. name: 'VInfiniteScroll',
  77. props: makeVInfiniteScrollProps(),
  78. emits: {
  79. load: options => true
  80. },
  81. setup(props, _ref2) {
  82. let {
  83. slots,
  84. emit
  85. } = _ref2;
  86. const rootEl = ref();
  87. const startStatus = shallowRef('ok');
  88. const endStatus = shallowRef('ok');
  89. const margin = computed(() => convertToUnit(props.margin));
  90. const isIntersecting = shallowRef(false);
  91. function setScrollAmount(amount) {
  92. if (!rootEl.value) return;
  93. const property = props.direction === 'vertical' ? 'scrollTop' : 'scrollLeft';
  94. rootEl.value[property] = amount;
  95. }
  96. function getScrollAmount() {
  97. if (!rootEl.value) return 0;
  98. const property = props.direction === 'vertical' ? 'scrollTop' : 'scrollLeft';
  99. return rootEl.value[property];
  100. }
  101. function getScrollSize() {
  102. if (!rootEl.value) return 0;
  103. const property = props.direction === 'vertical' ? 'scrollHeight' : 'scrollWidth';
  104. return rootEl.value[property];
  105. }
  106. function getContainerSize() {
  107. if (!rootEl.value) return 0;
  108. const property = props.direction === 'vertical' ? 'clientHeight' : 'clientWidth';
  109. return rootEl.value[property];
  110. }
  111. onMounted(() => {
  112. if (!rootEl.value) return;
  113. if (props.side === 'start') {
  114. setScrollAmount(getScrollSize());
  115. } else if (props.side === 'both') {
  116. setScrollAmount(getScrollSize() / 2 - getContainerSize() / 2);
  117. }
  118. });
  119. function setStatus(side, status) {
  120. if (side === 'start') {
  121. startStatus.value = status;
  122. } else if (side === 'end') {
  123. endStatus.value = status;
  124. }
  125. }
  126. function getStatus(side) {
  127. return side === 'start' ? startStatus.value : endStatus.value;
  128. }
  129. let previousScrollSize = 0;
  130. function handleIntersect(side, _isIntersecting) {
  131. isIntersecting.value = _isIntersecting;
  132. if (isIntersecting.value) {
  133. intersecting(side);
  134. }
  135. }
  136. function intersecting(side) {
  137. if (props.mode !== 'manual' && !isIntersecting.value) return;
  138. const status = getStatus(side);
  139. if (!rootEl.value || ['empty', 'loading'].includes(status)) return;
  140. previousScrollSize = getScrollSize();
  141. setStatus(side, 'loading');
  142. function done(status) {
  143. setStatus(side, status);
  144. nextTick(() => {
  145. if (status === 'empty' || status === 'error') return;
  146. if (status === 'ok' && side === 'start') {
  147. setScrollAmount(getScrollSize() - previousScrollSize + getScrollAmount());
  148. }
  149. if (props.mode !== 'manual') {
  150. nextTick(() => {
  151. window.requestAnimationFrame(() => {
  152. window.requestAnimationFrame(() => {
  153. window.requestAnimationFrame(() => {
  154. intersecting(side);
  155. });
  156. });
  157. });
  158. });
  159. }
  160. });
  161. }
  162. emit('load', {
  163. side,
  164. done
  165. });
  166. }
  167. const {
  168. t
  169. } = useLocale();
  170. function renderSide(side, status) {
  171. if (props.side !== side && props.side !== 'both') return;
  172. const onClick = () => intersecting(side);
  173. const slotProps = {
  174. side,
  175. props: {
  176. onClick,
  177. color: props.color
  178. }
  179. };
  180. if (status === 'error') return slots.error?.(slotProps);
  181. if (status === 'empty') return slots.empty?.(slotProps) ?? _createVNode("div", null, [t(props.emptyText)]);
  182. if (props.mode === 'manual') {
  183. if (status === 'loading') {
  184. return slots.loading?.(slotProps) ?? _createVNode(VProgressCircular, {
  185. "indeterminate": true,
  186. "color": props.color
  187. }, null);
  188. }
  189. return slots['load-more']?.(slotProps) ?? _createVNode(VBtn, {
  190. "variant": "outlined",
  191. "color": props.color,
  192. "onClick": onClick
  193. }, {
  194. default: () => [t(props.loadMoreText)]
  195. });
  196. }
  197. return slots.loading?.(slotProps) ?? _createVNode(VProgressCircular, {
  198. "indeterminate": true,
  199. "color": props.color
  200. }, null);
  201. }
  202. const {
  203. dimensionStyles
  204. } = useDimension(props);
  205. useRender(() => {
  206. const Tag = props.tag;
  207. const hasStartIntersect = props.side === 'start' || props.side === 'both';
  208. const hasEndIntersect = props.side === 'end' || props.side === 'both';
  209. const intersectMode = props.mode === 'intersect';
  210. return _createVNode(Tag, {
  211. "ref": rootEl,
  212. "class": ['v-infinite-scroll', `v-infinite-scroll--${props.direction}`, {
  213. 'v-infinite-scroll--start': hasStartIntersect,
  214. 'v-infinite-scroll--end': hasEndIntersect
  215. }],
  216. "style": dimensionStyles.value
  217. }, {
  218. default: () => [_createVNode("div", {
  219. "class": "v-infinite-scroll__side"
  220. }, [renderSide('start', startStatus.value)]), hasStartIntersect && intersectMode && _createVNode(VInfiniteScrollIntersect, {
  221. "key": "start",
  222. "side": "start",
  223. "onIntersect": handleIntersect,
  224. "rootMargin": margin.value
  225. }, null), slots.default?.(), hasEndIntersect && intersectMode && _createVNode(VInfiniteScrollIntersect, {
  226. "key": "end",
  227. "side": "end",
  228. "onIntersect": handleIntersect,
  229. "rootMargin": margin.value
  230. }, null), _createVNode("div", {
  231. "class": "v-infinite-scroll__side"
  232. }, [renderSide('end', endStatus.value)])]
  233. });
  234. });
  235. }
  236. });
  237. //# sourceMappingURL=VInfiniteScroll.mjs.map