useArticleContainerScroll.ts 3.9 KB

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