useElementScroll.ts 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  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. () =>
  40. scrollNode.value?.scrollHeight > scrollNode.value?.clientHeight ||
  41. y.value > 0,
  42. )
  43. const hasReachedThreshold = computed(
  44. () => y.value > (options?.scrollStartThreshold?.value || 0),
  45. )
  46. const omitValueChanges = computed(() => {
  47. return !hasReachedThreshold.value || !isScrollable.value || reachedTop.value
  48. })
  49. whenever(reachedTop, resetScrolls, { flush: 'post' })
  50. const throttledFn = useThrottleFn((newY, oldY) => {
  51. if (omitValueChanges.value) return
  52. if (hasReachedThreshold.value) {
  53. resetScrolls()
  54. }
  55. if (newY > oldY) {
  56. isScrollingDown.value = true
  57. isScrollingUp.value = false
  58. }
  59. if (newY < oldY) {
  60. isScrollingDown.value = false
  61. isScrollingUp.value = true
  62. }
  63. }, 500) // avoid scrolling glitch
  64. watch(y, throttledFn, { flush: 'post' })
  65. return {
  66. y,
  67. directions,
  68. reachedTop,
  69. reachedBottom,
  70. isScrollingDown,
  71. isScrollingUp,
  72. isScrollable,
  73. }
  74. }