@cognigy/chat-components-vue 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +178 -0
- package/dist/assets/svg/ArrowBackIcon.vue.d.ts +9 -0
- package/dist/assets/svg/AudioPauseIcon.vue.d.ts +2 -0
- package/dist/assets/svg/AudioPlayIcon.vue.d.ts +2 -0
- package/dist/assets/svg/CloseIcon.vue.d.ts +2 -0
- package/dist/assets/svg/DownloadIcon.vue.d.ts +2 -0
- package/dist/assets/svg/VideoPlayIcon.vue.d.ts +9 -0
- package/dist/assets/svg/index.d.ts +7 -0
- package/dist/chat-components-vue.css +1 -0
- package/dist/chat-components-vue.js +9858 -0
- package/dist/components/Message.vue.d.ts +11 -0
- package/dist/components/common/ActionButton.vue.d.ts +59 -0
- package/dist/components/common/ActionButtons.vue.d.ts +36 -0
- package/dist/components/common/ChatBubble.vue.d.ts +22 -0
- package/dist/components/common/ChatEvent.vue.d.ts +20 -0
- package/dist/components/common/LinkIcon.vue.d.ts +2 -0
- package/dist/components/common/TypingIndicator.vue.d.ts +21 -0
- package/dist/components/common/Typography.vue.d.ts +38 -0
- package/dist/components/messages/AdaptiveCard.vue.d.ts +2 -0
- package/dist/components/messages/AudioMessage.vue.d.ts +5 -0
- package/dist/components/messages/DatePicker.vue.d.ts +2 -0
- package/dist/components/messages/FileMessage.vue.d.ts +2 -0
- package/dist/components/messages/Gallery.vue.d.ts +2 -0
- package/dist/components/messages/GalleryItem.vue.d.ts +7 -0
- package/dist/components/messages/ImageMessage.vue.d.ts +5 -0
- package/dist/components/messages/List.vue.d.ts +2 -0
- package/dist/components/messages/ListItem.vue.d.ts +16 -0
- package/dist/components/messages/TextMessage.vue.d.ts +15 -0
- package/dist/components/messages/TextWithButtons.vue.d.ts +2 -0
- package/dist/components/messages/VideoMessage.vue.d.ts +5 -0
- package/dist/composables/useChannelPayload.d.ts +47 -0
- package/dist/composables/useCollation.d.ts +47 -0
- package/dist/composables/useImageContext.d.ts +13 -0
- package/dist/composables/useMessageContext.d.ts +18 -0
- package/dist/composables/useSanitize.d.ts +8 -0
- package/dist/index.d.ts +33 -0
- package/dist/types/index.d.ts +275 -0
- package/dist/utils/helpers.d.ts +56 -0
- package/dist/utils/matcher.d.ts +20 -0
- package/dist/utils/sanitize.d.ts +28 -0
- package/dist/utils/theme.d.ts +18 -0
- package/package.json +94 -0
- package/src/assets/svg/ArrowBackIcon.vue +30 -0
- package/src/assets/svg/AudioPauseIcon.vue +20 -0
- package/src/assets/svg/AudioPlayIcon.vue +19 -0
- package/src/assets/svg/CloseIcon.vue +10 -0
- package/src/assets/svg/DownloadIcon.vue +10 -0
- package/src/assets/svg/VideoPlayIcon.vue +25 -0
- package/src/assets/svg/index.ts +7 -0
- package/src/components/Message.vue +152 -0
- package/src/components/common/ActionButton.vue +354 -0
- package/src/components/common/ActionButtons.vue +170 -0
- package/src/components/common/ChatBubble.vue +109 -0
- package/src/components/common/ChatEvent.vue +84 -0
- package/src/components/common/LinkIcon.vue +34 -0
- package/src/components/common/TypingIndicator.vue +202 -0
- package/src/components/common/Typography.vue +196 -0
- package/src/components/messages/AdaptiveCard.vue +292 -0
- package/src/components/messages/AudioMessage.vue +391 -0
- package/src/components/messages/DatePicker.vue +135 -0
- package/src/components/messages/FileMessage.vue +195 -0
- package/src/components/messages/Gallery.vue +296 -0
- package/src/components/messages/GalleryItem.vue +214 -0
- package/src/components/messages/ImageMessage.vue +368 -0
- package/src/components/messages/List.vue +149 -0
- package/src/components/messages/ListItem.vue +344 -0
- package/src/components/messages/TextMessage.vue +203 -0
- package/src/components/messages/TextWithButtons.vue +119 -0
- package/src/components/messages/VideoMessage.vue +343 -0
- package/src/composables/useChannelPayload.ts +101 -0
- package/src/composables/useCollation.ts +163 -0
- package/src/composables/useImageContext.ts +27 -0
- package/src/composables/useMessageContext.ts +41 -0
- package/src/composables/useSanitize.ts +25 -0
- package/src/index.ts +71 -0
- package/src/types/index.ts +373 -0
- package/src/utils/helpers.ts +164 -0
- package/src/utils/matcher.ts +283 -0
- package/src/utils/sanitize.ts +133 -0
- package/src/utils/theme.ts +58 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="elements.length > 0"
|
|
4
|
+
:class="['webchat-list-template-root', $style.wrapper]"
|
|
5
|
+
:id="listTemplateId"
|
|
6
|
+
data-testid="list-message"
|
|
7
|
+
>
|
|
8
|
+
<!-- Header element (first element if top_element_style is large) -->
|
|
9
|
+
<ListItem
|
|
10
|
+
v-if="headerElement"
|
|
11
|
+
:element="headerElement"
|
|
12
|
+
isHeaderElement
|
|
13
|
+
headingLevel="h4"
|
|
14
|
+
:id="`header-${listTemplateId}`"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<!-- Regular list items -->
|
|
18
|
+
<ul
|
|
19
|
+
:aria-labelledby="headerElement ? `listHeader-header-${listTemplateId}` : undefined"
|
|
20
|
+
:class="$style.list"
|
|
21
|
+
>
|
|
22
|
+
<ListItem
|
|
23
|
+
v-for="(element, index) in regularElements"
|
|
24
|
+
:key="index"
|
|
25
|
+
:element="element"
|
|
26
|
+
:headingLevel="headerElement ? 'h5' : 'h4'"
|
|
27
|
+
:id="`${listTemplateId}-${index}`"
|
|
28
|
+
:dividerBefore="index > 0"
|
|
29
|
+
:dividerAfter="Boolean(globalButton && index === regularElements.length - 1)"
|
|
30
|
+
/>
|
|
31
|
+
</ul>
|
|
32
|
+
|
|
33
|
+
<!-- Global button at bottom -->
|
|
34
|
+
<ActionButtons
|
|
35
|
+
v-if="globalButton"
|
|
36
|
+
:payload="[globalButton]"
|
|
37
|
+
:action="shouldBeDisabled ? undefined : action"
|
|
38
|
+
buttonClassName="webchat-list-template-global-button"
|
|
39
|
+
:containerClassName="$style.mainButtonWrapper"
|
|
40
|
+
:config="config"
|
|
41
|
+
:dataMessageId="dataMessageId"
|
|
42
|
+
:onEmitAnalytics="onEmitAnalytics"
|
|
43
|
+
size="large"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup lang="ts">
|
|
49
|
+
import { computed, onMounted, useCssModule } from 'vue'
|
|
50
|
+
import ListItem from './ListItem.vue'
|
|
51
|
+
import ActionButtons from '../common/ActionButtons.vue'
|
|
52
|
+
import { useMessageContext } from '../../composables/useMessageContext'
|
|
53
|
+
import { getChannelPayload } from '../../utils/matcher'
|
|
54
|
+
import { getRandomId } from '../../utils/helpers'
|
|
55
|
+
import type { IWebchatTemplateAttachment } from '../../types'
|
|
56
|
+
|
|
57
|
+
const $style = useCssModule()
|
|
58
|
+
|
|
59
|
+
// Message context
|
|
60
|
+
const { message, config, action, onEmitAnalytics } = useMessageContext()
|
|
61
|
+
const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
|
|
62
|
+
|
|
63
|
+
// Get list data from message payload
|
|
64
|
+
const payload = computed(() => getChannelPayload(message, config))
|
|
65
|
+
const attachment = computed(() => payload.value?.message?.attachment as IWebchatTemplateAttachment | undefined)
|
|
66
|
+
|
|
67
|
+
// Extract list elements and configuration
|
|
68
|
+
const elements = computed(() => {
|
|
69
|
+
return attachment.value?.payload?.elements || []
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const topElementStyle = computed(() => {
|
|
73
|
+
return attachment.value?.payload?.top_element_style
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const showTopElementLarge = computed(() => {
|
|
77
|
+
return topElementStyle.value === 'large' || topElementStyle.value === true
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Split elements into header and regular items
|
|
81
|
+
const headerElement = computed(() => {
|
|
82
|
+
return showTopElementLarge.value ? elements.value[0] : null
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const regularElements = computed(() => {
|
|
86
|
+
return showTopElementLarge.value ? elements.value.slice(1) : elements.value
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Global button (first button in buttons array)
|
|
90
|
+
const globalButton = computed(() => {
|
|
91
|
+
const buttons = attachment.value?.payload?.buttons
|
|
92
|
+
return buttons?.[0]
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Should buttons be disabled
|
|
96
|
+
const shouldBeDisabled = computed(() => {
|
|
97
|
+
// TODO: Add conversation ended check when messageParams available
|
|
98
|
+
return false
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Generate unique ID for list
|
|
102
|
+
const listTemplateId = getRandomId('webchatListTemplateRoot')
|
|
103
|
+
|
|
104
|
+
// Auto-focus first focusable element on mount
|
|
105
|
+
onMounted(() => {
|
|
106
|
+
if (!config?.settings?.widgetSettings?.enableAutoFocus) return
|
|
107
|
+
|
|
108
|
+
const chatHistory = document.getElementById('webchatChatHistoryWrapperLiveLogPanel')
|
|
109
|
+
if (!chatHistory?.contains(document.activeElement)) return
|
|
110
|
+
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
const listTemplateRoot = document.getElementById(listTemplateId)
|
|
113
|
+
// Get the first focusable element within the list and add focus
|
|
114
|
+
const focusable = listTemplateRoot?.querySelectorAll(
|
|
115
|
+
'button, [href], [tabindex]:not([tabindex="-1"])'
|
|
116
|
+
)
|
|
117
|
+
const firstFocusable = focusable?.[0] as HTMLElement
|
|
118
|
+
firstFocusable?.focus()
|
|
119
|
+
}, 200)
|
|
120
|
+
})
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<style module>
|
|
124
|
+
.wrapper {
|
|
125
|
+
max-width: 295px;
|
|
126
|
+
border-radius: var(--cc-bubble-border-radius, 15px);
|
|
127
|
+
border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
|
|
128
|
+
background-color: var(--cc-white, #ffffff);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.wrapper .listItemRoot {
|
|
132
|
+
border-top-right-radius: var(--cc-bubble-border-radius, 15px);
|
|
133
|
+
border-top-left-radius: var(--cc-bubble-border-radius, 15px);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.list {
|
|
137
|
+
list-style: none;
|
|
138
|
+
padding-inline-start: 0;
|
|
139
|
+
margin-block: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.list .listItemRoot {
|
|
143
|
+
list-style: none;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.mainButtonWrapper {
|
|
147
|
+
padding: 16px;
|
|
148
|
+
}
|
|
149
|
+
</style>
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="componentTag"
|
|
4
|
+
:class="[isHeaderElement && $style.headerRoot, $style.listItemRoot]"
|
|
5
|
+
:style="{ backgroundImage: isHeaderElement && element.image_url ? backgroundImage : undefined }"
|
|
6
|
+
:data-testid="isHeaderElement ? 'header-image' : 'list-item'"
|
|
7
|
+
:id="id"
|
|
8
|
+
>
|
|
9
|
+
<!-- Divider before item -->
|
|
10
|
+
<div v-if="!isHeaderElement && dividerBefore" :class="$style.divider" />
|
|
11
|
+
|
|
12
|
+
<!-- Item content wrapper -->
|
|
13
|
+
<div
|
|
14
|
+
:class="contentClasses"
|
|
15
|
+
:role="defaultActionUrl ? 'link' : undefined"
|
|
16
|
+
:aria-label="defaultActionUrl ? `${titleHtml}. ${opensInNewTabLabel}` : undefined"
|
|
17
|
+
:aria-describedby="element.subtitle ? subtitleId : undefined"
|
|
18
|
+
:tabindex="defaultActionUrl ? 0 : -1"
|
|
19
|
+
:style="defaultActionUrl && !shouldBeDisabled ? { cursor: 'pointer' } : {}"
|
|
20
|
+
@click="handleClick"
|
|
21
|
+
@keydown="handleKeyDown"
|
|
22
|
+
>
|
|
23
|
+
<!-- Header element content -->
|
|
24
|
+
<div
|
|
25
|
+
v-if="isHeaderElement"
|
|
26
|
+
:class="['webchat-list-template-header-content', $style.headerContent, button && $style.headerContentWithButton]"
|
|
27
|
+
>
|
|
28
|
+
<!-- Title and subtitle -->
|
|
29
|
+
<Typography
|
|
30
|
+
v-if="titleHtml"
|
|
31
|
+
:variant="isHeaderElement ? 'h2-semibold' : 'title1-semibold'"
|
|
32
|
+
:component="headingLevel"
|
|
33
|
+
:class="[
|
|
34
|
+
isHeaderElement ? 'webchat-list-template-header-title' : 'webchat-list-template-element-title',
|
|
35
|
+
subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle
|
|
36
|
+
]"
|
|
37
|
+
:id="isHeaderElement ? `listHeader-${id}` : `listItemHeader-${id}`"
|
|
38
|
+
v-html="titleHtml"
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<Typography
|
|
42
|
+
v-if="subtitleHtml"
|
|
43
|
+
variant="body-regular"
|
|
44
|
+
:class="[
|
|
45
|
+
isHeaderElement ? 'webchat-list-template-header-subtitle' : 'webchat-list-template-element-subtitle',
|
|
46
|
+
$style.itemSubtitle
|
|
47
|
+
]"
|
|
48
|
+
:id="subtitleId"
|
|
49
|
+
v-html="subtitleHtml"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Regular list item content -->
|
|
54
|
+
<div
|
|
55
|
+
v-else
|
|
56
|
+
:class="['webchat-list-template-element-content', $style.listItemContent]"
|
|
57
|
+
>
|
|
58
|
+
<div :class="$style.listItemText">
|
|
59
|
+
<!-- Title and subtitle -->
|
|
60
|
+
<Typography
|
|
61
|
+
v-if="titleHtml"
|
|
62
|
+
variant="title1-semibold"
|
|
63
|
+
:component="headingLevel"
|
|
64
|
+
:class="[
|
|
65
|
+
'webchat-list-template-element-title',
|
|
66
|
+
subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle
|
|
67
|
+
]"
|
|
68
|
+
:id="`listItemHeader-${id}`"
|
|
69
|
+
v-html="titleHtml"
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<Typography
|
|
73
|
+
v-if="subtitleHtml"
|
|
74
|
+
variant="body-regular"
|
|
75
|
+
:class="['webchat-list-template-element-subtitle', $style.itemSubtitle]"
|
|
76
|
+
:id="subtitleId"
|
|
77
|
+
v-html="subtitleHtml"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Image thumbnail for regular items -->
|
|
82
|
+
<div
|
|
83
|
+
v-if="element.image_url"
|
|
84
|
+
:class="$style.listItemImage"
|
|
85
|
+
:style="{ backgroundImage }"
|
|
86
|
+
data-testid="regular-image"
|
|
87
|
+
>
|
|
88
|
+
<span role="img" :aria-label="element.image_alt_text || ''" />
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- Button for item -->
|
|
94
|
+
<ActionButtons
|
|
95
|
+
v-if="button"
|
|
96
|
+
:payload="[button]"
|
|
97
|
+
:action="shouldBeDisabled ? undefined : action"
|
|
98
|
+
:buttonClassName="isHeaderElement ? 'webchat-list-template-header-button' : 'webchat-list-template-element-button'"
|
|
99
|
+
:containerClassName="isHeaderElement ? $style.listHeaderButtonWrapper : $style.listItemButtonWrapper"
|
|
100
|
+
:config="config"
|
|
101
|
+
:dataMessageId="dataMessageId"
|
|
102
|
+
:onEmitAnalytics="onEmitAnalytics"
|
|
103
|
+
size="large"
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<!-- Divider after item -->
|
|
107
|
+
<div v-if="!isHeaderElement && dividerAfter" :class="$style.divider" />
|
|
108
|
+
</component>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<script setup lang="ts">
|
|
112
|
+
import { computed, useCssModule } from 'vue'
|
|
113
|
+
import Typography from '../common/Typography.vue'
|
|
114
|
+
import ActionButtons from '../common/ActionButtons.vue'
|
|
115
|
+
import { useMessageContext } from '../../composables/useMessageContext'
|
|
116
|
+
import { useSanitize } from '../../composables/useSanitize'
|
|
117
|
+
import { getRandomId, getBackgroundImage } from '../../utils/helpers'
|
|
118
|
+
import { sanitizeUrl } from '@braintree/sanitize-url'
|
|
119
|
+
import type { IWebchatAttachmentElement } from '../../types'
|
|
120
|
+
|
|
121
|
+
interface Props {
|
|
122
|
+
element: IWebchatAttachmentElement
|
|
123
|
+
isHeaderElement?: boolean
|
|
124
|
+
headingLevel?: 'h4' | 'h5'
|
|
125
|
+
id: string
|
|
126
|
+
dividerBefore?: boolean
|
|
127
|
+
dividerAfter?: boolean
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
131
|
+
isHeaderElement: false,
|
|
132
|
+
headingLevel: 'h4',
|
|
133
|
+
dividerBefore: false,
|
|
134
|
+
dividerAfter: false,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const $style = useCssModule()
|
|
138
|
+
|
|
139
|
+
// Context
|
|
140
|
+
const { action, config, onEmitAnalytics } = useMessageContext()
|
|
141
|
+
const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
|
|
142
|
+
|
|
143
|
+
// Sanitize HTML
|
|
144
|
+
const { processHTML } = useSanitize()
|
|
145
|
+
const titleHtml = computed(() => processHTML(props.element.title || ''))
|
|
146
|
+
const subtitleHtml = computed(() => processHTML(props.element.subtitle || ''))
|
|
147
|
+
|
|
148
|
+
// IDs for accessibility
|
|
149
|
+
const subtitleId = getRandomId('webchatListTemplateHeaderSubtitle')
|
|
150
|
+
|
|
151
|
+
// Background image
|
|
152
|
+
const backgroundImage = computed(() => {
|
|
153
|
+
if (!props.element.image_url) return undefined
|
|
154
|
+
return getBackgroundImage(props.element.image_url)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Button (only first button is used)
|
|
158
|
+
const button = computed(() => {
|
|
159
|
+
return props.element.buttons?.[0]
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Default action URL (clickable item)
|
|
163
|
+
const defaultActionUrl = computed(() => {
|
|
164
|
+
return props.element.default_action?.url
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Should buttons be disabled
|
|
168
|
+
const shouldBeDisabled = computed(() => {
|
|
169
|
+
// TODO: Add conversation ended check when messageParams available
|
|
170
|
+
return false
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Translations
|
|
174
|
+
const opensInNewTabLabel = computed(() => {
|
|
175
|
+
return config?.settings?.customTranslations?.ariaLabels?.opensInNewTab || 'Opens in new tab'
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// Component tag (div for header, li for regular items)
|
|
179
|
+
const componentTag = computed(() => {
|
|
180
|
+
return props.isHeaderElement ? 'div' : 'li'
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Content classes
|
|
184
|
+
const contentClasses = computed(() => {
|
|
185
|
+
return props.isHeaderElement
|
|
186
|
+
? ['webchat-list-template-header', $style.headerContentWrapper]
|
|
187
|
+
: ['webchat-list-template-element', $style.listItemWrapper]
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// Handle item click (default action)
|
|
191
|
+
const handleClick = () => {
|
|
192
|
+
if (shouldBeDisabled.value || !defaultActionUrl.value) return
|
|
193
|
+
|
|
194
|
+
const url = config?.settings?.layout?.disableUrlButtonSanitization
|
|
195
|
+
? defaultActionUrl.value
|
|
196
|
+
: sanitizeUrl(defaultActionUrl.value)
|
|
197
|
+
|
|
198
|
+
// Prevent no-ops from sending you to a blank page
|
|
199
|
+
if (url === 'about:blank') return
|
|
200
|
+
|
|
201
|
+
window.open(url)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Handle keyboard navigation
|
|
205
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
206
|
+
if (defaultActionUrl.value && event.key === 'Enter') {
|
|
207
|
+
handleClick()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<style module>
|
|
213
|
+
.listItemRoot {
|
|
214
|
+
list-style: none;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.divider {
|
|
218
|
+
border-top: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Header element styles */
|
|
222
|
+
.headerRoot {
|
|
223
|
+
aspect-ratio: 16/9;
|
|
224
|
+
background-size: cover;
|
|
225
|
+
background-position: center center;
|
|
226
|
+
position: relative;
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-direction: column;
|
|
229
|
+
border-top-right-radius: var(--cc-bubble-border-radius, 15px);
|
|
230
|
+
border-top-left-radius: var(--cc-bubble-border-radius, 15px);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.headerRoot::before {
|
|
234
|
+
content: "";
|
|
235
|
+
position: absolute;
|
|
236
|
+
top: 0;
|
|
237
|
+
left: 0;
|
|
238
|
+
width: 100%;
|
|
239
|
+
height: 100%;
|
|
240
|
+
background-color: hsla(0, 0%, 0%, 0.4); /* image overlay */
|
|
241
|
+
border-radius: inherit;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.headerRoot:focus {
|
|
245
|
+
opacity: 0.6;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.headerContentWrapper {
|
|
249
|
+
border-radius: inherit;
|
|
250
|
+
position: relative;
|
|
251
|
+
flex-grow: 1;
|
|
252
|
+
align-content: flex-end;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.headerContentWrapper:focus-visible {
|
|
256
|
+
outline: 2px solid var(--cc-primary-color-focus, #1976d2);
|
|
257
|
+
box-shadow: inset 0 0 0 2px var(--cc-white, #ffffff);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.headerContent {
|
|
261
|
+
padding: 16px 16px 12px 16px;
|
|
262
|
+
color: var(--cc-white, #ffffff);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.headerContent > * {
|
|
266
|
+
color: var(--cc-white, #ffffff);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.headerContentWithButton {
|
|
270
|
+
padding-bottom: 72px; /* 12px headerContent bottom padding + 44px button height + 16px button bottom padding */
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Title and subtitle styles */
|
|
274
|
+
.itemTitle {
|
|
275
|
+
margin-top: 0px;
|
|
276
|
+
margin-bottom: 0px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.itemTitleWithSubtitle {
|
|
280
|
+
margin-top: 0px;
|
|
281
|
+
margin-bottom: 8px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.itemSubtitle {
|
|
285
|
+
margin-top: 0px;
|
|
286
|
+
margin-bottom: 0px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Regular list item styles */
|
|
290
|
+
.listItemWrapper {
|
|
291
|
+
position: relative;
|
|
292
|
+
display: flex;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.listItemWrapper:focus {
|
|
296
|
+
outline: none;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.listItemWrapper:focus-visible {
|
|
300
|
+
outline: 2px solid var(--cc-primary-color-focus, #1976d2);
|
|
301
|
+
outline-offset: -10px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.listItemContent {
|
|
305
|
+
padding: 16px 16px 12px 16px;
|
|
306
|
+
overflow-wrap: break-word;
|
|
307
|
+
display: flex;
|
|
308
|
+
-webkit-box-pack: justify;
|
|
309
|
+
justify-content: space-between;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 16px;
|
|
312
|
+
width: 100%;
|
|
313
|
+
color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.listItemText {
|
|
317
|
+
width: 100%;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.listItemText > * {
|
|
321
|
+
color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.listItemImage {
|
|
325
|
+
background-size: cover;
|
|
326
|
+
background-position: center center;
|
|
327
|
+
border-radius: 10px;
|
|
328
|
+
width: 86px;
|
|
329
|
+
height: 102px;
|
|
330
|
+
flex-shrink: 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* Button container styles */
|
|
334
|
+
.listHeaderButtonWrapper {
|
|
335
|
+
position: absolute;
|
|
336
|
+
bottom: 16px;
|
|
337
|
+
right: 16px;
|
|
338
|
+
left: 16px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.listItemButtonWrapper {
|
|
342
|
+
padding: 0 16px 16px 16px;
|
|
343
|
+
}
|
|
344
|
+
</style>
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, useCssModule } from 'vue'
|
|
3
|
+
import { useMessageContext } from '../../composables/useMessageContext'
|
|
4
|
+
import { useSanitize } from '../../composables/useSanitize'
|
|
5
|
+
import ChatBubble from '../common/ChatBubble.vue'
|
|
6
|
+
import { replaceUrlsWithHTMLanchorElem } from '../../utils/helpers'
|
|
7
|
+
import MarkdownIt from 'markdown-it'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
content?: string | string[]
|
|
11
|
+
className?: string
|
|
12
|
+
markdownClassName?: string
|
|
13
|
+
id?: string
|
|
14
|
+
ignoreLiveRegion?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
18
|
+
content: undefined,
|
|
19
|
+
className: '',
|
|
20
|
+
markdownClassName: '',
|
|
21
|
+
id: undefined,
|
|
22
|
+
ignoreLiveRegion: false,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const styles = useCssModule()
|
|
26
|
+
const { message, config } = useMessageContext()
|
|
27
|
+
const { processHTML } = useSanitize()
|
|
28
|
+
|
|
29
|
+
// Initialize markdown-it with HTML support and GFM tables
|
|
30
|
+
const md = new MarkdownIt({
|
|
31
|
+
html: true,
|
|
32
|
+
linkify: true,
|
|
33
|
+
breaks: true,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Get text content
|
|
37
|
+
const text = computed(() => message?.text)
|
|
38
|
+
const source = computed(() => message?.source)
|
|
39
|
+
|
|
40
|
+
// Use prop content or message text
|
|
41
|
+
const content = computed(() => {
|
|
42
|
+
const contentValue = props.content || text.value || ''
|
|
43
|
+
return Array.isArray(contentValue) ? contentValue.join('') : contentValue
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Determine if markdown should be rendered
|
|
47
|
+
const renderMarkdown = computed(() => {
|
|
48
|
+
return config?.settings?.behavior?.renderMarkdown &&
|
|
49
|
+
(source.value === 'bot' || source.value === 'engagement')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Optionally transform URL strings into clickable links
|
|
53
|
+
const enhancedURLsText = computed(() => {
|
|
54
|
+
if (config?.settings?.widgetSettings?.disableRenderURLsAsLinks) {
|
|
55
|
+
return content.value
|
|
56
|
+
}
|
|
57
|
+
return replaceUrlsWithHTMLanchorElem(content.value)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Determine if sanitization should be ignored
|
|
61
|
+
const ignoreSanitization = computed(() => {
|
|
62
|
+
return source.value === 'user' && config?.settings?.widgetSettings?.disableTextInputSanitization
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Process content: sanitize HTML if needed
|
|
66
|
+
const processedContent = computed(() => {
|
|
67
|
+
if (ignoreSanitization.value) {
|
|
68
|
+
return enhancedURLsText.value
|
|
69
|
+
}
|
|
70
|
+
return processHTML(enhancedURLsText.value)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Render markdown if enabled
|
|
74
|
+
const markdownContent = computed(() => {
|
|
75
|
+
if (!renderMarkdown.value) return ''
|
|
76
|
+
|
|
77
|
+
// Render markdown to HTML
|
|
78
|
+
let html = md.render(processedContent.value || content.value)
|
|
79
|
+
|
|
80
|
+
// Make all links open in new tab
|
|
81
|
+
html = html.replace(/<a /g, '<a target="_blank" rel="noreferrer" ')
|
|
82
|
+
|
|
83
|
+
return html
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Compute CSS classes
|
|
87
|
+
const textClasses = computed(() => {
|
|
88
|
+
const classes = [props.className]
|
|
89
|
+
if (!renderMarkdown.value) {
|
|
90
|
+
classes.push(styles.text)
|
|
91
|
+
}
|
|
92
|
+
return classes.filter(Boolean)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const markdownClasses = computed(() => {
|
|
96
|
+
const classes = [styles.markdown, props.markdownClassName]
|
|
97
|
+
return classes.filter(Boolean)
|
|
98
|
+
})
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
<ChatBubble>
|
|
103
|
+
<div
|
|
104
|
+
v-if="renderMarkdown"
|
|
105
|
+
:class="markdownClasses"
|
|
106
|
+
v-html="markdownContent"
|
|
107
|
+
/>
|
|
108
|
+
<p
|
|
109
|
+
v-else
|
|
110
|
+
:id="id"
|
|
111
|
+
:class="textClasses"
|
|
112
|
+
v-html="processedContent"
|
|
113
|
+
/>
|
|
114
|
+
</ChatBubble>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<style module>
|
|
118
|
+
.text {
|
|
119
|
+
white-space: pre-wrap;
|
|
120
|
+
overflow-wrap: break-word;
|
|
121
|
+
display: inline;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.text a:focus-visible {
|
|
125
|
+
outline: 2px solid var(--cc-primary-color-focus);
|
|
126
|
+
outline-offset: 2px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.text img {
|
|
130
|
+
max-width: 100%;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.markdown {
|
|
134
|
+
display: inline;
|
|
135
|
+
white-space: normal;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.markdown > p:only-child {
|
|
139
|
+
margin: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.markdown *:first-child {
|
|
143
|
+
margin-top: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.markdown *:last-child {
|
|
147
|
+
margin-bottom: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.markdown p:last-child {
|
|
151
|
+
display: inline;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.markdown table {
|
|
155
|
+
border-collapse: separate;
|
|
156
|
+
border-spacing: 0;
|
|
157
|
+
margin-bottom: 4px;
|
|
158
|
+
margin-top: 4px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.markdown th {
|
|
162
|
+
text-align: left;
|
|
163
|
+
border-top-width: 1px;
|
|
164
|
+
border-bottom-width: 1px;
|
|
165
|
+
border-left-width: 1px;
|
|
166
|
+
border-right-width: 0px;
|
|
167
|
+
background-color: var(--cc-black-90);
|
|
168
|
+
border-style: solid;
|
|
169
|
+
border-color: var(--cc-black-80);
|
|
170
|
+
padding: 4px 12px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.markdown th:first-child {
|
|
174
|
+
border-top-left-radius: 6px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.markdown th:last-child {
|
|
178
|
+
border-top-right-radius: 6px;
|
|
179
|
+
border-right-width: 1px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.markdown td {
|
|
183
|
+
border-style: solid;
|
|
184
|
+
border-bottom-width: 1px;
|
|
185
|
+
border-left-width: 1px;
|
|
186
|
+
border-top-width: 0px;
|
|
187
|
+
border-right-width: 0px;
|
|
188
|
+
border-color: var(--cc-black-80);
|
|
189
|
+
padding: 4px 12px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.markdown td:last-child {
|
|
193
|
+
border-right-width: 1px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.markdown tbody tr:last-child td:first-child {
|
|
197
|
+
border-bottom-left-radius: 6px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.markdown tbody tr:last-child td:last-child {
|
|
201
|
+
border-bottom-right-radius: 6px;
|
|
202
|
+
}
|
|
203
|
+
</style>
|