ArticleBubbleBody.vue 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, toRef, watch, nextTick, onMounted } from 'vue'
  4. import { useArticleToggleMore } from '#shared/composables/useArticleToggleMore.ts'
  5. import { useHtmlInlineImages } from '#shared/composables/useHtmlInlineImages.ts'
  6. import { useHtmlLinks } from '#shared/composables/useHtmlLinks.ts'
  7. import { type ImageViewerFile } from '#shared/composables/useImageViewer.ts'
  8. import type { TicketArticle } from '#shared/entities/ticket/types.ts'
  9. import { textToHtml } from '#shared/utils/helpers.ts'
  10. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  11. interface Props {
  12. article: TicketArticle
  13. showMetaInformation: boolean
  14. position: 'left' | 'right'
  15. inlineImages: ImageViewerFile[]
  16. }
  17. const props = defineProps<Props>()
  18. const emit = defineEmits<{
  19. preview: [image: ImageViewerFile]
  20. }>()
  21. const { shownMore, bubbleElement, hasShowMore, toggleShowMore } =
  22. useArticleToggleMore()
  23. const bodyClasses = computed(() =>
  24. props.position === 'right'
  25. ? ['dark:bg-stone-500', 'bg-blue-100', 'Content--customer']
  26. : ['dark:bg-gray-400', 'bg-white', 'Content--agent'],
  27. )
  28. const body = computed(() => {
  29. if (props.article.contentType !== 'text/html') {
  30. return textToHtml(props.article.bodyWithUrls)
  31. }
  32. return props.article.bodyWithUrls
  33. })
  34. const { setupLinksHandlers } = useHtmlLinks('/desktop')
  35. const { populateInlineImages } = useHtmlInlineImages(
  36. toRef(props, 'inlineImages'),
  37. (index) => emit('preview', props.inlineImages[index]),
  38. )
  39. watch(
  40. () => body,
  41. async () => {
  42. await nextTick()
  43. if (bubbleElement.value) {
  44. setupLinksHandlers(bubbleElement.value)
  45. populateInlineImages(bubbleElement.value)
  46. }
  47. },
  48. )
  49. onMounted(() => {
  50. if (bubbleElement.value) {
  51. setupLinksHandlers(bubbleElement.value)
  52. populateInlineImages(bubbleElement.value)
  53. }
  54. })
  55. </script>
  56. <template>
  57. <div
  58. class="Content -:pt-9 -:p-3 relative transition-[padding]"
  59. :class="[
  60. bodyClasses,
  61. {
  62. 'pt-3': showMetaInformation,
  63. },
  64. ]"
  65. >
  66. <div
  67. v-if="!showMetaInformation"
  68. class="absolute top-3 flex w-full px-3 ltr:left-0 rtl:right-0"
  69. >
  70. <CommonLabel class="font-bold" size="small" variant="neutral">
  71. {{
  72. article.author.fullname ||
  73. `${article.author.firstname} ${article.author.lastname}`
  74. }}
  75. </CommonLabel>
  76. <CommonDateTime
  77. class="text-xs ltr:ml-auto rtl:mr-auto"
  78. :date-time="article.createdAt"
  79. />
  80. </div>
  81. <div
  82. ref="bubbleElement"
  83. data-test-id="article-content"
  84. class="overflow-hidden text-sm"
  85. >
  86. <!-- eslint-disable vue/no-v-html-->
  87. <div v-html="body" />
  88. </div>
  89. <div
  90. v-if="hasShowMore"
  91. class="relative"
  92. :class="{
  93. BubbleGradient: hasShowMore && !shownMore,
  94. }"
  95. ></div>
  96. <CommonButton
  97. v-if="hasShowMore"
  98. class="!p-0 !outline-transparent"
  99. size="medium"
  100. @click.prevent="toggleShowMore"
  101. @keydown.enter.prevent="toggleShowMore"
  102. >
  103. {{ shownMore ? $t('See less') : $t('See more') }}
  104. </CommonButton>
  105. </div>
  106. </template>
  107. <style scoped>
  108. .BubbleGradient::before {
  109. content: '';
  110. position: absolute;
  111. left: 0;
  112. right: 0;
  113. bottom: 0;
  114. height: 46px;
  115. pointer-events: none;
  116. }
  117. .Content--agent .BubbleGradient::before {
  118. background: linear-gradient(rgba(255, 255, 255, 0), theme('colors.white'));
  119. }
  120. [data-theme='dark'] .Content--agent .BubbleGradient::before {
  121. background: linear-gradient(rgba(255, 255, 255, 0), theme('colors.gray.400'));
  122. }
  123. .Content--customer .BubbleGradient::before {
  124. background: linear-gradient(rgba(255, 255, 255, 0), theme('colors.blue.100'));
  125. }
  126. [data-theme='dark'] .Content--customer .BubbleGradient::before {
  127. background: linear-gradient(
  128. rgba(255, 255, 255, 0),
  129. theme('colors.stone.500')
  130. );
  131. }
  132. </style>