useArticleToggleMore.ts 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. import { onMounted, ref } from 'vue'
  3. import { waitForImagesToLoad } from '#shared/utils/dom.ts'
  4. import { waitForAnimationFrame } from '#shared/utils/helpers.ts'
  5. export const useArticleToggleMore = () => {
  6. const MIN_HEIGHT = 60
  7. const MAX_HEIGHT = 320
  8. let heightActual = 0
  9. let heightHidden = 0
  10. const bubbleElement = ref<HTMLDivElement>()
  11. const hasShowMore = ref(true)
  12. const shownMore = ref(false)
  13. const getSignatureMarker = (element: HTMLElement): HTMLElement | null => {
  14. const marker = element.querySelector('.js-signatureMarker') as HTMLElement
  15. if (marker) return marker
  16. return element.querySelector('div [data-signature=true]')
  17. }
  18. const setHeight = async () => {
  19. if (!bubbleElement.value) return
  20. const styles = bubbleElement.value.style
  21. styles.height = ''
  22. await waitForAnimationFrame()
  23. // it's possible it was remounted somehow
  24. if (!bubbleElement.value) return
  25. const height = bubbleElement.value.clientHeight
  26. heightActual = height
  27. const signatureMarker = getSignatureMarker(bubbleElement.value)
  28. const offsetTop = signatureMarker?.offsetTop || 0
  29. if (offsetTop > 0 && offsetTop < MAX_HEIGHT) {
  30. heightHidden = offsetTop < MIN_HEIGHT ? MIN_HEIGHT : offsetTop
  31. hasShowMore.value = true
  32. } else if (height > MAX_HEIGHT) {
  33. heightHidden = MAX_HEIGHT
  34. hasShowMore.value = true
  35. } else {
  36. hasShowMore.value = false
  37. heightHidden = 0
  38. }
  39. if (heightHidden) {
  40. styles.height = `${heightHidden}px`
  41. }
  42. }
  43. onMounted(async () => {
  44. if (!bubbleElement.value) return
  45. // Wait for inline images to load before calculating height
  46. // Resolved immediately if no images are present
  47. await waitForImagesToLoad(bubbleElement)
  48. await setHeight()
  49. })
  50. const toggleShowMore = () => {
  51. if (!bubbleElement.value) return
  52. shownMore.value = !shownMore.value
  53. const styles = bubbleElement.value.style
  54. styles.transition = 'height 0.3s ease-in-out'
  55. styles.height = shownMore.value
  56. ? `${heightActual + 10}px`
  57. : `${heightHidden}px`
  58. const ontransitionend = () => {
  59. styles.transition = ''
  60. bubbleElement.value?.removeEventListener('transitionend', ontransitionend)
  61. }
  62. bubbleElement.value?.addEventListener('transitionend', ontransitionend)
  63. }
  64. return {
  65. toggleShowMore,
  66. hasShowMore,
  67. shownMore,
  68. bubbleElement,
  69. }
  70. }