@cognigy/chat-components-vue 0.2.0 → 0.3.1
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/dist/chat-components-vue.css +1 -1
- package/dist/chat-components-vue.js +3744 -3642
- package/dist/components/Message.vue.d.ts +4 -0
- package/dist/components/common/ActionButton.vue.d.ts +2 -0
- package/dist/components/common/ActionButtons.vue.d.ts +2 -0
- package/dist/components/common/Typography.vue.d.ts +1 -1
- package/dist/components/messages/ListItem.vue.d.ts +1 -1
- package/dist/composables/useLiveRegion.d.ts +30 -0
- package/dist/index.d.ts +3 -2
- package/dist/types/index.d.ts +55 -0
- package/dist/utils/theme.d.ts +12 -1
- package/package.json +6 -2
- package/src/components/Message.vue +85 -53
- package/src/components/common/ActionButton.vue +53 -7
- package/src/components/common/ActionButtons.vue +4 -0
- package/src/components/common/ChatBubble.vue +8 -6
- package/src/components/common/ChatEvent.vue +5 -2
- package/src/components/common/TypingIndicator.vue +4 -1
- package/src/components/common/Typography.vue +56 -67
- package/src/components/messages/AudioMessage.vue +4 -1
- package/src/components/messages/DatePicker.vue +5 -27
- package/src/components/messages/FileMessage.vue +12 -3
- package/src/components/messages/Gallery.vue +96 -10
- package/src/components/messages/GalleryItem.vue +17 -5
- package/src/components/messages/ImageMessage.vue +20 -5
- package/src/components/messages/List.vue +54 -40
- package/src/components/messages/ListItem.vue +97 -63
- package/src/components/messages/TextMessage.vue +1 -1
- package/src/components/messages/TextWithButtons.vue +35 -11
- package/src/components/messages/VideoMessage.vue +35 -26
- package/src/composables/useLiveRegion.ts +101 -0
- package/src/index.ts +4 -1
- package/src/types/index.ts +71 -0
- package/src/utils/theme.ts +36 -1
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
:custom-icon="DownloadIcon"
|
|
30
30
|
:position="1"
|
|
31
31
|
:total="1"
|
|
32
|
-
:class-name="$style.
|
|
32
|
+
:class-name="$style.downloadButton"
|
|
33
33
|
class="webchat-buttons-template-button"
|
|
34
34
|
/>
|
|
35
35
|
|
|
@@ -88,7 +88,7 @@ import ActionButton from '../common/ActionButton.vue'
|
|
|
88
88
|
import { DownloadIcon, CloseIcon } from '../../assets/svg'
|
|
89
89
|
import type { IWebchatImageAttachment, IWebchatButton } from '../../types'
|
|
90
90
|
|
|
91
|
-
const { message, config, action, onEmitAnalytics } = useMessageContext()
|
|
91
|
+
const { message, config, action, onEmitAnalytics, onImageClick } = useMessageContext()
|
|
92
92
|
|
|
93
93
|
const $style = useCssModule()
|
|
94
94
|
|
|
@@ -144,6 +144,11 @@ const closeLabel = computed(() =>
|
|
|
144
144
|
|
|
145
145
|
// Handlers
|
|
146
146
|
const handleExpand = () => {
|
|
147
|
+
// Notify consumer via onImageClick callback
|
|
148
|
+
if (imageData.value.url) {
|
|
149
|
+
onImageClick?.(imageData.value.url)
|
|
150
|
+
}
|
|
151
|
+
|
|
147
152
|
if (isDownloadable.value) {
|
|
148
153
|
showLightbox.value = true
|
|
149
154
|
// Focus the download button after lightbox opens
|
|
@@ -215,6 +220,10 @@ onUnmounted(() => {
|
|
|
215
220
|
max-width: 295px;
|
|
216
221
|
width: 100%;
|
|
217
222
|
outline: none;
|
|
223
|
+
background-color: var(--cc-white, #ffffff);
|
|
224
|
+
box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
|
|
225
|
+
rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
|
|
226
|
+
rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
.wrapper .fixedImage,
|
|
@@ -238,7 +247,9 @@ onUnmounted(() => {
|
|
|
238
247
|
.downloadable {
|
|
239
248
|
background-color: var(--cc-white, #ffffff);
|
|
240
249
|
cursor: pointer;
|
|
241
|
-
border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
|
|
250
|
+
border: 1px solid var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8)));
|
|
251
|
+
border-bottom: none;
|
|
252
|
+
overflow: hidden;
|
|
242
253
|
}
|
|
243
254
|
|
|
244
255
|
.downloadable img {
|
|
@@ -265,8 +276,12 @@ onUnmounted(() => {
|
|
|
265
276
|
/* Base class for image containers */
|
|
266
277
|
}
|
|
267
278
|
|
|
268
|
-
.
|
|
269
|
-
|
|
279
|
+
.wrapper .downloadButton {
|
|
280
|
+
width: 100%;
|
|
281
|
+
box-sizing: border-box;
|
|
282
|
+
border: 1px solid var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8)));
|
|
283
|
+
border-top: none;
|
|
284
|
+
border-radius: 0 0 var(--cc-bubble-border-radius, 15px) var(--cc-bubble-border-radius, 15px);
|
|
270
285
|
}
|
|
271
286
|
|
|
272
287
|
.brokenImage {
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
<!-- Regular list items -->
|
|
18
18
|
<ul
|
|
19
|
-
:aria-labelledby="
|
|
19
|
+
:aria-labelledby="
|
|
20
|
+
headerElement ? `listHeader-header-${listTemplateId}` : undefined
|
|
21
|
+
"
|
|
20
22
|
:class="$style.list"
|
|
21
23
|
>
|
|
22
24
|
<ListItem
|
|
@@ -26,7 +28,9 @@
|
|
|
26
28
|
:headingLevel="headerElement ? 'h5' : 'h4'"
|
|
27
29
|
:id="`${listTemplateId}-${index}`"
|
|
28
30
|
:dividerBefore="index > 0"
|
|
29
|
-
:dividerAfter="
|
|
31
|
+
:dividerAfter="
|
|
32
|
+
Boolean(globalButton && index === regularElements.length - 1)
|
|
33
|
+
"
|
|
30
34
|
/>
|
|
31
35
|
</ul>
|
|
32
36
|
|
|
@@ -39,6 +43,7 @@
|
|
|
39
43
|
:containerClassName="$style.mainButtonWrapper"
|
|
40
44
|
:config="config"
|
|
41
45
|
:dataMessageId="dataMessageId"
|
|
46
|
+
:openXAppOverlay="openXAppOverlay"
|
|
42
47
|
:onEmitAnalytics="onEmitAnalytics"
|
|
43
48
|
size="large"
|
|
44
49
|
/>
|
|
@@ -46,86 +51,94 @@
|
|
|
46
51
|
</template>
|
|
47
52
|
|
|
48
53
|
<script setup lang="ts">
|
|
49
|
-
import { computed, onMounted, useCssModule } from
|
|
50
|
-
import ListItem from
|
|
51
|
-
import ActionButtons from
|
|
52
|
-
import { useMessageContext } from
|
|
53
|
-
import { getChannelPayload } from
|
|
54
|
-
import { getRandomId } from
|
|
55
|
-
import type { IWebchatTemplateAttachment } from
|
|
54
|
+
import { computed, onMounted, useCssModule } from "vue";
|
|
55
|
+
import ListItem from "./ListItem.vue";
|
|
56
|
+
import ActionButtons from "../common/ActionButtons.vue";
|
|
57
|
+
import { useMessageContext } from "../../composables/useMessageContext";
|
|
58
|
+
import { getChannelPayload } from "../../utils/matcher";
|
|
59
|
+
import { getRandomId } from "../../utils/helpers";
|
|
60
|
+
import type { IWebchatTemplateAttachment } from "../../types";
|
|
56
61
|
|
|
57
|
-
const $style = useCssModule()
|
|
62
|
+
const $style = useCssModule();
|
|
58
63
|
|
|
59
64
|
// Message context
|
|
60
|
-
const { message, config, action, onEmitAnalytics } = useMessageContext()
|
|
61
|
-
const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
|
|
65
|
+
const { message, config, action, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext();
|
|
66
|
+
const dataMessageId = window.__TEST_MESSAGE_ID__; // For testing
|
|
62
67
|
|
|
63
68
|
// Get list data from message payload
|
|
64
|
-
const payload = computed(() => getChannelPayload(message, config))
|
|
65
|
-
const attachment = computed(
|
|
69
|
+
const payload = computed(() => getChannelPayload(message, config));
|
|
70
|
+
const attachment = computed(
|
|
71
|
+
() =>
|
|
72
|
+
payload.value?.message?.attachment as IWebchatTemplateAttachment | undefined
|
|
73
|
+
);
|
|
66
74
|
|
|
67
75
|
// Extract list elements and configuration
|
|
68
76
|
const elements = computed(() => {
|
|
69
|
-
return attachment.value?.payload?.elements || []
|
|
70
|
-
})
|
|
77
|
+
return attachment.value?.payload?.elements || [];
|
|
78
|
+
});
|
|
71
79
|
|
|
72
80
|
const topElementStyle = computed(() => {
|
|
73
|
-
return attachment.value?.payload?.top_element_style
|
|
74
|
-
})
|
|
81
|
+
return attachment.value?.payload?.top_element_style;
|
|
82
|
+
});
|
|
75
83
|
|
|
76
84
|
const showTopElementLarge = computed(() => {
|
|
77
|
-
return topElementStyle.value ===
|
|
78
|
-
})
|
|
85
|
+
return topElementStyle.value === "large" || topElementStyle.value === true;
|
|
86
|
+
});
|
|
79
87
|
|
|
80
88
|
// Split elements into header and regular items
|
|
81
89
|
const headerElement = computed(() => {
|
|
82
|
-
return showTopElementLarge.value ? elements.value[0] : null
|
|
83
|
-
})
|
|
90
|
+
return showTopElementLarge.value ? elements.value[0] : null;
|
|
91
|
+
});
|
|
84
92
|
|
|
85
93
|
const regularElements = computed(() => {
|
|
86
|
-
return showTopElementLarge.value ? elements.value.slice(1) : elements.value
|
|
87
|
-
})
|
|
94
|
+
return showTopElementLarge.value ? elements.value.slice(1) : elements.value;
|
|
95
|
+
});
|
|
88
96
|
|
|
89
97
|
// Global button (first button in buttons array)
|
|
90
98
|
const globalButton = computed(() => {
|
|
91
|
-
const buttons = attachment.value?.payload?.buttons
|
|
92
|
-
return buttons?.[0]
|
|
93
|
-
})
|
|
99
|
+
const buttons = attachment.value?.payload?.buttons;
|
|
100
|
+
return buttons?.[0];
|
|
101
|
+
});
|
|
94
102
|
|
|
95
103
|
// Should buttons be disabled
|
|
96
104
|
const shouldBeDisabled = computed(() => {
|
|
97
|
-
|
|
98
|
-
return false
|
|
105
|
+
return messageParams?.isConversationEnded ?? false
|
|
99
106
|
})
|
|
100
107
|
|
|
101
108
|
// Generate unique ID for list
|
|
102
|
-
const listTemplateId = getRandomId(
|
|
109
|
+
const listTemplateId = getRandomId("webchatListTemplateRoot");
|
|
103
110
|
|
|
104
111
|
// Auto-focus first focusable element on mount
|
|
105
112
|
onMounted(() => {
|
|
106
|
-
if (!config?.settings?.widgetSettings?.enableAutoFocus) return
|
|
113
|
+
if (!config?.settings?.widgetSettings?.enableAutoFocus) return;
|
|
107
114
|
|
|
108
|
-
const chatHistory = document.getElementById(
|
|
109
|
-
|
|
115
|
+
const chatHistory = document.getElementById(
|
|
116
|
+
"webchatChatHistoryWrapperLiveLogPanel"
|
|
117
|
+
);
|
|
118
|
+
if (!chatHistory?.contains(document.activeElement)) return;
|
|
110
119
|
|
|
111
120
|
setTimeout(() => {
|
|
112
|
-
const listTemplateRoot = document.getElementById(listTemplateId)
|
|
121
|
+
const listTemplateRoot = document.getElementById(listTemplateId);
|
|
113
122
|
// Get the first focusable element within the list and add focus
|
|
114
123
|
const focusable = listTemplateRoot?.querySelectorAll(
|
|
115
124
|
'button, [href], [tabindex]:not([tabindex="-1"])'
|
|
116
|
-
)
|
|
117
|
-
const firstFocusable = focusable?.[0] as HTMLElement
|
|
118
|
-
firstFocusable?.focus()
|
|
119
|
-
}, 200)
|
|
120
|
-
})
|
|
125
|
+
);
|
|
126
|
+
const firstFocusable = focusable?.[0] as HTMLElement;
|
|
127
|
+
firstFocusable?.focus();
|
|
128
|
+
}, 200);
|
|
129
|
+
});
|
|
121
130
|
</script>
|
|
122
131
|
|
|
123
132
|
<style module>
|
|
124
133
|
.wrapper {
|
|
134
|
+
width: 295px;
|
|
125
135
|
max-width: 295px;
|
|
126
136
|
border-radius: var(--cc-bubble-border-radius, 15px);
|
|
127
|
-
border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
|
|
128
137
|
background-color: var(--cc-white, #ffffff);
|
|
138
|
+
color: rgba(0, 0, 0, 0.8);
|
|
139
|
+
box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
|
|
140
|
+
rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
|
|
141
|
+
rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
.wrapper .listItemRoot {
|
|
@@ -137,6 +150,7 @@ onMounted(() => {
|
|
|
137
150
|
list-style: none;
|
|
138
151
|
padding-inline-start: 0;
|
|
139
152
|
margin-block: 0;
|
|
153
|
+
margin-left: 0;
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
.list .listItemRoot {
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<component
|
|
3
3
|
:is="componentTag"
|
|
4
4
|
:class="[isHeaderElement && $style.headerRoot, $style.listItemRoot]"
|
|
5
|
-
:style="{
|
|
5
|
+
:style="{
|
|
6
|
+
backgroundImage:
|
|
7
|
+
isHeaderElement && element.image_url ? backgroundImage : undefined,
|
|
8
|
+
}"
|
|
6
9
|
:data-testid="isHeaderElement ? 'header-image' : 'list-item'"
|
|
7
10
|
:id="id"
|
|
8
11
|
>
|
|
@@ -13,17 +16,25 @@
|
|
|
13
16
|
<div
|
|
14
17
|
:class="contentClasses"
|
|
15
18
|
:role="defaultActionUrl ? 'link' : undefined"
|
|
16
|
-
:aria-label="
|
|
19
|
+
:aria-label="
|
|
20
|
+
defaultActionUrl ? `${titleHtml}. ${opensInNewTabLabel}` : undefined
|
|
21
|
+
"
|
|
17
22
|
:aria-describedby="element.subtitle ? subtitleId : undefined"
|
|
18
23
|
:tabindex="defaultActionUrl ? 0 : -1"
|
|
19
|
-
:style="
|
|
24
|
+
:style="
|
|
25
|
+
defaultActionUrl && !shouldBeDisabled ? { cursor: 'pointer' } : {}
|
|
26
|
+
"
|
|
20
27
|
@click="handleClick"
|
|
21
28
|
@keydown="handleKeyDown"
|
|
22
29
|
>
|
|
23
30
|
<!-- Header element content -->
|
|
24
31
|
<div
|
|
25
32
|
v-if="isHeaderElement"
|
|
26
|
-
:class="[
|
|
33
|
+
:class="[
|
|
34
|
+
'webchat-list-template-header-content',
|
|
35
|
+
$style.headerContent,
|
|
36
|
+
button && $style.headerContentWithButton,
|
|
37
|
+
]"
|
|
27
38
|
>
|
|
28
39
|
<!-- Title and subtitle -->
|
|
29
40
|
<Typography
|
|
@@ -31,8 +42,10 @@
|
|
|
31
42
|
:variant="isHeaderElement ? 'h2-semibold' : 'title1-semibold'"
|
|
32
43
|
:component="headingLevel"
|
|
33
44
|
:class="[
|
|
34
|
-
isHeaderElement
|
|
35
|
-
|
|
45
|
+
isHeaderElement
|
|
46
|
+
? 'webchat-list-template-header-title'
|
|
47
|
+
: 'webchat-list-template-element-title',
|
|
48
|
+
subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle,
|
|
36
49
|
]"
|
|
37
50
|
:id="isHeaderElement ? `listHeader-${id}` : `listItemHeader-${id}`"
|
|
38
51
|
v-html="titleHtml"
|
|
@@ -42,8 +55,10 @@
|
|
|
42
55
|
v-if="subtitleHtml"
|
|
43
56
|
variant="body-regular"
|
|
44
57
|
:class="[
|
|
45
|
-
isHeaderElement
|
|
46
|
-
|
|
58
|
+
isHeaderElement
|
|
59
|
+
? 'webchat-list-template-header-subtitle'
|
|
60
|
+
: 'webchat-list-template-element-subtitle',
|
|
61
|
+
$style.itemSubtitle,
|
|
47
62
|
]"
|
|
48
63
|
:id="subtitleId"
|
|
49
64
|
v-html="subtitleHtml"
|
|
@@ -53,7 +68,10 @@
|
|
|
53
68
|
<!-- Regular list item content -->
|
|
54
69
|
<div
|
|
55
70
|
v-else
|
|
56
|
-
:class="[
|
|
71
|
+
:class="[
|
|
72
|
+
'webchat-list-template-element-content',
|
|
73
|
+
$style.listItemContent,
|
|
74
|
+
]"
|
|
57
75
|
>
|
|
58
76
|
<div :class="$style.listItemText">
|
|
59
77
|
<!-- Title and subtitle -->
|
|
@@ -63,7 +81,7 @@
|
|
|
63
81
|
:component="headingLevel"
|
|
64
82
|
:class="[
|
|
65
83
|
'webchat-list-template-element-title',
|
|
66
|
-
subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle
|
|
84
|
+
subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle,
|
|
67
85
|
]"
|
|
68
86
|
:id="`listItemHeader-${id}`"
|
|
69
87
|
v-html="titleHtml"
|
|
@@ -72,7 +90,10 @@
|
|
|
72
90
|
<Typography
|
|
73
91
|
v-if="subtitleHtml"
|
|
74
92
|
variant="body-regular"
|
|
75
|
-
:class="[
|
|
93
|
+
:class="[
|
|
94
|
+
'webchat-list-template-element-subtitle',
|
|
95
|
+
$style.itemSubtitle,
|
|
96
|
+
]"
|
|
76
97
|
:id="subtitleId"
|
|
77
98
|
v-html="subtitleHtml"
|
|
78
99
|
/>
|
|
@@ -95,12 +116,22 @@
|
|
|
95
116
|
v-if="button"
|
|
96
117
|
:payload="[button]"
|
|
97
118
|
:action="shouldBeDisabled ? undefined : action"
|
|
98
|
-
:buttonClassName="
|
|
99
|
-
|
|
119
|
+
:buttonClassName="
|
|
120
|
+
isHeaderElement
|
|
121
|
+
? 'webchat-list-template-header-button'
|
|
122
|
+
: 'webchat-list-template-element-button'
|
|
123
|
+
"
|
|
124
|
+
:containerClassName="
|
|
125
|
+
isHeaderElement
|
|
126
|
+
? $style.listHeaderButtonWrapper
|
|
127
|
+
: $style.listItemButtonWrapper
|
|
128
|
+
"
|
|
100
129
|
:config="config"
|
|
101
130
|
:dataMessageId="dataMessageId"
|
|
131
|
+
:openXAppOverlay="openXAppOverlay"
|
|
102
132
|
:onEmitAnalytics="onEmitAnalytics"
|
|
103
133
|
size="large"
|
|
134
|
+
:variant="isHeaderElement ? 'primary' : 'secondary'"
|
|
104
135
|
/>
|
|
105
136
|
|
|
106
137
|
<!-- Divider after item -->
|
|
@@ -109,104 +140,106 @@
|
|
|
109
140
|
</template>
|
|
110
141
|
|
|
111
142
|
<script setup lang="ts">
|
|
112
|
-
import { computed, useCssModule } from
|
|
113
|
-
import Typography from
|
|
114
|
-
import ActionButtons from
|
|
115
|
-
import { useMessageContext } from
|
|
116
|
-
import { useSanitize } from
|
|
117
|
-
import { getRandomId, getBackgroundImage } from
|
|
118
|
-
import { sanitizeUrl } from
|
|
119
|
-
import type { IWebchatAttachmentElement } from
|
|
143
|
+
import { computed, useCssModule } from "vue";
|
|
144
|
+
import Typography from "../common/Typography.vue";
|
|
145
|
+
import ActionButtons from "../common/ActionButtons.vue";
|
|
146
|
+
import { useMessageContext } from "../../composables/useMessageContext";
|
|
147
|
+
import { useSanitize } from "../../composables/useSanitize";
|
|
148
|
+
import { getRandomId, getBackgroundImage } from "../../utils/helpers";
|
|
149
|
+
import { sanitizeUrl } from "@braintree/sanitize-url";
|
|
150
|
+
import type { IWebchatAttachmentElement } from "../../types";
|
|
120
151
|
|
|
121
152
|
interface Props {
|
|
122
|
-
element: IWebchatAttachmentElement
|
|
123
|
-
isHeaderElement?: boolean
|
|
124
|
-
headingLevel?:
|
|
125
|
-
id: string
|
|
126
|
-
dividerBefore?: boolean
|
|
127
|
-
dividerAfter?: boolean
|
|
153
|
+
element: IWebchatAttachmentElement;
|
|
154
|
+
isHeaderElement?: boolean;
|
|
155
|
+
headingLevel?: "h4" | "h5";
|
|
156
|
+
id: string;
|
|
157
|
+
dividerBefore?: boolean;
|
|
158
|
+
dividerAfter?: boolean;
|
|
128
159
|
}
|
|
129
160
|
|
|
130
161
|
const props = withDefaults(defineProps<Props>(), {
|
|
131
162
|
isHeaderElement: false,
|
|
132
|
-
headingLevel:
|
|
163
|
+
headingLevel: "h4",
|
|
133
164
|
dividerBefore: false,
|
|
134
165
|
dividerAfter: false,
|
|
135
|
-
})
|
|
166
|
+
});
|
|
136
167
|
|
|
137
|
-
const $style = useCssModule()
|
|
168
|
+
const $style = useCssModule();
|
|
138
169
|
|
|
139
170
|
// Context
|
|
140
|
-
const { action, config, onEmitAnalytics } = useMessageContext()
|
|
141
|
-
const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
|
|
171
|
+
const { action, config, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext();
|
|
172
|
+
const dataMessageId = window.__TEST_MESSAGE_ID__; // For testing
|
|
142
173
|
|
|
143
174
|
// Sanitize HTML
|
|
144
|
-
const { processHTML } = useSanitize()
|
|
145
|
-
const titleHtml = computed(() => processHTML(props.element.title ||
|
|
146
|
-
const subtitleHtml = computed(() => processHTML(props.element.subtitle ||
|
|
175
|
+
const { processHTML } = useSanitize();
|
|
176
|
+
const titleHtml = computed(() => processHTML(props.element.title || ""));
|
|
177
|
+
const subtitleHtml = computed(() => processHTML(props.element.subtitle || ""));
|
|
147
178
|
|
|
148
179
|
// IDs for accessibility
|
|
149
|
-
const subtitleId = getRandomId(
|
|
180
|
+
const subtitleId = getRandomId("webchatListTemplateHeaderSubtitle");
|
|
150
181
|
|
|
151
182
|
// Background image
|
|
152
183
|
const backgroundImage = computed(() => {
|
|
153
|
-
if (!props.element.image_url) return undefined
|
|
154
|
-
return getBackgroundImage(props.element.image_url)
|
|
155
|
-
})
|
|
184
|
+
if (!props.element.image_url) return undefined;
|
|
185
|
+
return getBackgroundImage(props.element.image_url);
|
|
186
|
+
});
|
|
156
187
|
|
|
157
188
|
// Button (only first button is used)
|
|
158
189
|
const button = computed(() => {
|
|
159
|
-
return props.element.buttons?.[0]
|
|
160
|
-
})
|
|
190
|
+
return props.element.buttons?.[0];
|
|
191
|
+
});
|
|
161
192
|
|
|
162
193
|
// Default action URL (clickable item)
|
|
163
194
|
const defaultActionUrl = computed(() => {
|
|
164
|
-
return props.element.default_action?.url
|
|
165
|
-
})
|
|
195
|
+
return props.element.default_action?.url;
|
|
196
|
+
});
|
|
166
197
|
|
|
167
198
|
// Should buttons be disabled
|
|
168
199
|
const shouldBeDisabled = computed(() => {
|
|
169
|
-
|
|
170
|
-
return false
|
|
200
|
+
return messageParams?.isConversationEnded ?? false
|
|
171
201
|
})
|
|
172
202
|
|
|
173
203
|
// Translations
|
|
174
204
|
const opensInNewTabLabel = computed(() => {
|
|
175
|
-
return
|
|
176
|
-
|
|
205
|
+
return (
|
|
206
|
+
config?.settings?.customTranslations?.ariaLabels?.opensInNewTab ||
|
|
207
|
+
"Opens in new tab"
|
|
208
|
+
);
|
|
209
|
+
});
|
|
177
210
|
|
|
178
211
|
// Component tag (div for header, li for regular items)
|
|
179
212
|
const componentTag = computed(() => {
|
|
180
|
-
return props.isHeaderElement ?
|
|
181
|
-
})
|
|
213
|
+
return props.isHeaderElement ? "div" : "li";
|
|
214
|
+
});
|
|
182
215
|
|
|
183
216
|
// Content classes
|
|
184
217
|
const contentClasses = computed(() => {
|
|
185
218
|
return props.isHeaderElement
|
|
186
|
-
? [
|
|
187
|
-
: [
|
|
188
|
-
})
|
|
219
|
+
? ["webchat-list-template-header", $style.headerContentWrapper]
|
|
220
|
+
: ["webchat-list-template-element", $style.listItemWrapper];
|
|
221
|
+
});
|
|
189
222
|
|
|
190
223
|
// Handle item click (default action)
|
|
191
224
|
const handleClick = () => {
|
|
192
|
-
if (shouldBeDisabled.value || !defaultActionUrl.value) return
|
|
225
|
+
if (shouldBeDisabled.value || !defaultActionUrl.value) return;
|
|
193
226
|
|
|
194
227
|
const url = config?.settings?.layout?.disableUrlButtonSanitization
|
|
195
228
|
? defaultActionUrl.value
|
|
196
|
-
: sanitizeUrl(defaultActionUrl.value)
|
|
229
|
+
: sanitizeUrl(defaultActionUrl.value);
|
|
197
230
|
|
|
198
231
|
// Prevent no-ops from sending you to a blank page
|
|
199
|
-
if (url ===
|
|
232
|
+
if (url === "about:blank") return;
|
|
200
233
|
|
|
201
|
-
window.open(url)
|
|
202
|
-
}
|
|
234
|
+
window.open(url);
|
|
235
|
+
};
|
|
203
236
|
|
|
204
237
|
// Handle keyboard navigation
|
|
205
238
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
206
|
-
if (defaultActionUrl.value && event.key ===
|
|
207
|
-
handleClick()
|
|
239
|
+
if (defaultActionUrl.value && event.key === "Enter") {
|
|
240
|
+
handleClick();
|
|
208
241
|
}
|
|
209
|
-
}
|
|
242
|
+
};
|
|
210
243
|
</script>
|
|
211
244
|
|
|
212
245
|
<style module>
|
|
@@ -215,7 +248,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
215
248
|
}
|
|
216
249
|
|
|
217
250
|
.divider {
|
|
218
|
-
border-
|
|
251
|
+
border-bottom: 1px solid var(--cc-list-divider-color, var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8))));
|
|
219
252
|
}
|
|
220
253
|
|
|
221
254
|
/* Header element styles */
|
|
@@ -284,6 +317,8 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
284
317
|
.itemSubtitle {
|
|
285
318
|
margin-top: 0px;
|
|
286
319
|
margin-bottom: 0px;
|
|
320
|
+
color: rgba(0, 0, 0, 0.54);
|
|
321
|
+
white-space: pre-line;
|
|
287
322
|
}
|
|
288
323
|
|
|
289
324
|
/* Regular list item styles */
|
|
@@ -310,7 +345,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
310
345
|
align-items: center;
|
|
311
346
|
gap: 16px;
|
|
312
347
|
width: 100%;
|
|
313
|
-
color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
|
|
314
348
|
}
|
|
315
349
|
|
|
316
350
|
.listItemText {
|
|
@@ -318,7 +352,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
318
352
|
}
|
|
319
353
|
|
|
320
354
|
.listItemText > * {
|
|
321
|
-
color:
|
|
355
|
+
color: inherit;
|
|
322
356
|
}
|
|
323
357
|
|
|
324
358
|
.listItemImage {
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
ignoreLiveRegion
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
|
-
<!-- Buttons -->
|
|
12
|
+
<!-- Buttons (hidden while progressive rendering is animating) -->
|
|
13
13
|
<ActionButtons
|
|
14
|
-
v-if="buttons.length > 0"
|
|
14
|
+
v-if="buttons.length > 0 && !isStillAnimating"
|
|
15
15
|
:payload="buttons"
|
|
16
16
|
:action="modifiedAction"
|
|
17
17
|
:buttonClassName="buttonClassName"
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
:config="config"
|
|
21
21
|
:onEmitAnalytics="onEmitAnalytics"
|
|
22
22
|
:templateTextId="webchatButtonTemplateTextId"
|
|
23
|
+
:openXAppOverlay="openXAppOverlay"
|
|
23
24
|
showUrlIcon
|
|
24
25
|
/>
|
|
25
26
|
</div>
|
|
@@ -32,10 +33,10 @@ import ActionButtons from '../common/ActionButtons.vue'
|
|
|
32
33
|
import { useMessageContext } from '../../composables/useMessageContext'
|
|
33
34
|
import { getChannelPayload } from '../../utils/matcher'
|
|
34
35
|
import { getRandomId } from '../../utils/helpers'
|
|
35
|
-
import type { IWebchatTemplateAttachment, IWebchatButton, IWebchatQuickReply } from '../../types'
|
|
36
|
+
import type { IWebchatTemplateAttachment, IWebchatButton, IWebchatQuickReply, IStreamingMessage } from '../../types'
|
|
36
37
|
|
|
37
38
|
// Message context
|
|
38
|
-
const { message, config, action, onEmitAnalytics } = useMessageContext()
|
|
39
|
+
const { message, config, action, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext()
|
|
39
40
|
|
|
40
41
|
const $style = useCssModule()
|
|
41
42
|
|
|
@@ -67,13 +68,12 @@ const classType = computed(() => {
|
|
|
67
68
|
return isQuickReplies.value ? 'quick-reply' : 'buttons'
|
|
68
69
|
})
|
|
69
70
|
|
|
70
|
-
// For quick replies, disable if there's already a reply
|
|
71
|
-
// Note: In the React version, this uses messageParams.hasReply
|
|
72
|
-
// For now, we'll just pass the action as-is since we don't have messageParams in Vue yet
|
|
71
|
+
// For quick replies, disable if there's already a reply or the conversation has ended
|
|
73
72
|
const modifiedAction = computed(() => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
const shouldBeDisabled =
|
|
74
|
+
(isQuickReplies.value && messageParams?.hasReply) ||
|
|
75
|
+
messageParams?.isConversationEnded
|
|
76
|
+
return shouldBeDisabled ? undefined : action
|
|
77
77
|
})
|
|
78
78
|
|
|
79
79
|
// Generate unique ID for accessibility
|
|
@@ -92,6 +92,15 @@ const containerStyle = computed(() => {
|
|
|
92
92
|
return {}
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
+
// Direction mapping for button alignment
|
|
96
|
+
const directionMapping = config?.settings?.widgetSettings?.sourceDirectionMapping
|
|
97
|
+
const messageDirection = computed(() => {
|
|
98
|
+
if (message.source === 'user') return directionMapping?.user || 'outgoing'
|
|
99
|
+
if (message.source === 'bot') return directionMapping?.bot || 'incoming'
|
|
100
|
+
if (message.source === 'agent') return directionMapping?.agent || 'incoming'
|
|
101
|
+
return 'incoming'
|
|
102
|
+
})
|
|
103
|
+
|
|
95
104
|
// Button class name
|
|
96
105
|
const buttonClassName = computed(() => {
|
|
97
106
|
return `${$style.button} webchat-${classType.value}-template-button`
|
|
@@ -99,7 +108,14 @@ const buttonClassName = computed(() => {
|
|
|
99
108
|
|
|
100
109
|
// Container class name
|
|
101
110
|
const containerClassName = computed(() => {
|
|
102
|
-
return `${$style.buttons} webchat-${classType.value}-template-replies-container`
|
|
111
|
+
return `${$style.buttons} ${$style[messageDirection.value]} webchat-${classType.value}-template-replies-container`
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Progressive message rendering — hide buttons while message text is still animating
|
|
115
|
+
const isStillAnimating = computed(() => {
|
|
116
|
+
if (!config?.settings?.behavior?.progressiveMessageRendering) return false
|
|
117
|
+
const animState = (message as IStreamingMessage).animationState
|
|
118
|
+
return animState === 'animating' || animState === 'start'
|
|
103
119
|
})
|
|
104
120
|
</script>
|
|
105
121
|
|
|
@@ -116,4 +132,12 @@ const containerClassName = computed(() => {
|
|
|
116
132
|
.button {
|
|
117
133
|
/* Button styles are inherited from ActionButton component */
|
|
118
134
|
}
|
|
135
|
+
|
|
136
|
+
.incoming {
|
|
137
|
+
justify-content: flex-start;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.outgoing {
|
|
141
|
+
justify-content: flex-end;
|
|
142
|
+
}
|
|
119
143
|
</style>
|