useElementScroll.ts 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { useScroll, useThrottleFn } from '@vueuse/core'
  3. import { whenever } from '@vueuse/shared'
  4. import { computed, type ComputedRef, isRef, ref, watch } from 'vue'
  5. import type { MaybeRef } from '@vueuse/shared'
  6. interface Options {
  7. scrollStartThreshold?: ComputedRef<number | undefined>
  8. }
  9. export const useElementScroll = (
  10. scrollContainerElement: MaybeRef<HTMLElement>,
  11. options?: Options,
  12. ) => {
  13. const { y, directions } = useScroll(scrollContainerElement, {
  14. eventListenerOptions: { passive: true },
  15. })
  16. const isScrollingDown = ref(false)
  17. const isScrollingUp = ref(false)
  18. const resetScrolls = () => {
  19. isScrollingDown.value = false
  20. isScrollingUp.value = false
  21. }
  22. const reachedTop = computed(() => y.value === 0)
  23. const scrollNode = computed(() =>
  24. isRef(scrollContainerElement)
  25. ? scrollContainerElement.value
  26. : scrollContainerElement,
  27. )
  28. const reachedBottom = computed(
  29. () =>
  30. // NB: Check if this is the most optimal calculation.
  31. // In Webkit based browsers it sometimes results in -0.5 right on the bottom edge,
  32. // hence the need for the lower bound.
  33. y.value -
  34. (scrollNode.value?.scrollHeight ?? 0) +
  35. (scrollNode.value?.offsetHeight ?? 0) >
  36. -1,
  37. )
  38. const isScrollable = computed(
  39. () => scrollNode.value?.scrollHeight > scrollNode.value?.clientHeight,
  40. )
  41. const hasReachedThreshold = computed(
  42. () => y.value > (options?.scrollStartThreshold?.value || 0),
  43. )
  44. const omitValueChanges = computed(() => {
  45. return !hasReachedThreshold.value || !isScrollable.value || reachedTop.value
  46. })
  47. whenever(reachedTop, resetScrolls, { flush: 'post' })
  48. const throttledFn = useThrottleFn((newY, oldY) => {
  49. if (omitValueChanges.value) return
  50. if (hasReachedThreshold.value) {
  51. resetScrolls()
  52. }
  53. if (newY > oldY) {
  54. isScrollingDown.value = true
  55. isScrollingUp.value = false
  56. }
  57. if (newY < oldY) {
  58. isScrollingDown.value = false
  59. isScrollingUp.value = true
  60. }
  61. }, 500) // avoid scrolling glitch
  62. watch(y, throttledFn, { flush: 'post' })
  63. return {
  64. y,
  65. directions,
  66. reachedTop,
  67. reachedBottom,
  68. isScrollingDown,
  69. isScrollingUp,
  70. isScrollable,
  71. }
  72. }