useArticleContainerScroll.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { useScroll, useThrottleFn, whenever } from '@vueuse/core'
  3. import { computed, type Ref, ref, type ShallowRef, watch } from 'vue'
  4. import type { TicketById } from '#shared/entities/ticket/types.ts'
  5. import type ArticleList from '#desktop/pages/ticket/components/TicketDetailView/ArticleList.vue'
  6. import TicketDetailTopBar from '#desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/TicketDetailTopBar.vue'
  7. export const useArticleContainerScroll = (
  8. ticket: Ref<TicketById>,
  9. contentContainerElement: Readonly<ShallowRef<HTMLDivElement | null>>,
  10. articleListInstance: Readonly<
  11. ShallowRef<InstanceType<typeof ArticleList> | null>
  12. >,
  13. topBarInstance: Readonly<
  14. ShallowRef<InstanceType<typeof TicketDetailTopBar> | null>
  15. >,
  16. ) => {
  17. const THROTTLE_TIME = 250
  18. const { arrivedState } = useScroll(contentContainerElement, {
  19. eventListenerOptions: { passive: true },
  20. })
  21. const isHidingTicketDetails = ref(false)
  22. const isReachingBottom = ref(false)
  23. const previousPosition = ref(0)
  24. const reset = () => {
  25. isReachingBottom.value = false
  26. isHidingTicketDetails.value = false
  27. previousPosition.value = 0
  28. }
  29. const _isHoveringOnTopBar = ref(false)
  30. const isHoveringOnTopBar = computed({
  31. get: () => _isHoveringOnTopBar.value,
  32. set: (value) => {
  33. _isHoveringOnTopBar.value = value
  34. if (value) {
  35. isHidingTicketDetails.value = false
  36. }
  37. },
  38. })
  39. const handleScroll = useThrottleFn((event: Event) => {
  40. const container = event.target! as HTMLDivElement
  41. const { scrollHeight, clientHeight } = container
  42. const isScrollable = scrollHeight > clientHeight
  43. if (!isScrollable) return reset()
  44. const scrollTop = container.scrollTop ?? 0
  45. isReachingBottom.value = scrollTop + clientHeight < scrollHeight
  46. // If we keep the pointer on the top bar we do not want to hide the details if the user starts to scroll on the same time.
  47. if (!isHoveringOnTopBar.value) {
  48. isHidingTicketDetails.value =
  49. scrollTop > (topBarInstance.value?.$el.clientHeight ?? 0)
  50. }
  51. previousPosition.value = scrollTop
  52. }, THROTTLE_TIME)
  53. watch(
  54. () => arrivedState.bottom,
  55. (value) => {
  56. isReachingBottom.value = !value
  57. if (isHoveringOnTopBar.value) return
  58. isHidingTicketDetails.value = true
  59. },
  60. )
  61. whenever(
  62. () => arrivedState.top,
  63. () => {
  64. if (isHoveringOnTopBar.value) return
  65. isHidingTicketDetails.value = false
  66. },
  67. )
  68. watch(
  69. () => ticket.value?.id,
  70. () => {
  71. articleListInstance.value?.setDidInitialScroll(false)
  72. },
  73. { immediate: true },
  74. )
  75. watch(
  76. () => articleListInstance.value?.rows,
  77. async () => {
  78. if (articleListInstance.value?.didScrollInitially) return
  79. await articleListInstance.value?.scrollToArticle()
  80. articleListInstance.value?.setDidInitialScroll(true)
  81. // Normally handleScroll runs after we this, in some edge cases if it is not triggered we reset the states.
  82. reset()
  83. },
  84. { flush: 'post' },
  85. )
  86. // Handling scrolling to bottom if new article is added
  87. watch(
  88. () => articleListInstance.value?.rows,
  89. (newRows, oldRows) => {
  90. if (!newRows || !oldRows) return
  91. if (newRows.at(-1)?.key === oldRows.at(-1)?.key) return
  92. // article got removed
  93. if (newRows.at(-1)?.key === oldRows.at(-2)?.key) return
  94. // article got added
  95. articleListInstance.value?.scrollToArticle()
  96. },
  97. )
  98. return {
  99. handleScroll,
  100. isHoveringOnTopBar,
  101. isHidingTicketDetails,
  102. isReachingBottom,
  103. }
  104. }