@cognigy/chat-components-vue 0.1.0 → 0.3.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/dist/chat-components-vue.css +1 -1
- package/dist/chat-components-vue.js +12386 -5741
- package/dist/components/Message.vue.d.ts +4 -0
- package/dist/components/common/Typography.vue.d.ts +1 -1
- package/dist/components/messages/AdaptiveCardRenderer.vue.d.ts +35 -0
- 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 +105 -1
- package/dist/utils/helpers.d.ts +3 -2
- package/dist/utils/matcher.d.ts +3 -3
- package/dist/utils/theme.d.ts +12 -1
- package/package.json +8 -3
- package/src/components/Message.vue +98 -55
- package/src/components/common/ActionButton.vue +16 -7
- 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/AdaptiveCard.vue +322 -225
- package/src/components/messages/AdaptiveCardRenderer.vue +260 -0
- 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 +56 -42
- package/src/components/messages/ListItem.vue +105 -68
- 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/useCollation.ts +28 -45
- package/src/composables/useLiveRegion.ts +101 -0
- package/src/index.ts +4 -1
- package/src/types/index.ts +127 -2
- package/src/utils/helpers.ts +46 -24
- package/src/utils/matcher.ts +20 -6
- package/src/utils/sanitize.ts +1 -2
- package/src/utils/theme.ts +42 -1
|
@@ -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 {
|
|
125
|
-
max-width:
|
|
126
|
-
border-radius: var(--cc-bubble-border-radius,
|
|
127
|
-
|
|
134
|
+
max-width: 250px;
|
|
135
|
+
border-radius: var(--cc-bubble-border-radius, 16px);
|
|
136
|
+
overflow: hidden;
|
|
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,10 +116,19 @@
|
|
|
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"
|
|
104
134
|
/>
|
|
@@ -109,104 +139,106 @@
|
|
|
109
139
|
</template>
|
|
110
140
|
|
|
111
141
|
<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
|
|
142
|
+
import { computed, useCssModule } from "vue";
|
|
143
|
+
import Typography from "../common/Typography.vue";
|
|
144
|
+
import ActionButtons from "../common/ActionButtons.vue";
|
|
145
|
+
import { useMessageContext } from "../../composables/useMessageContext";
|
|
146
|
+
import { useSanitize } from "../../composables/useSanitize";
|
|
147
|
+
import { getRandomId, getBackgroundImage } from "../../utils/helpers";
|
|
148
|
+
import { sanitizeUrl } from "@braintree/sanitize-url";
|
|
149
|
+
import type { IWebchatAttachmentElement } from "../../types";
|
|
120
150
|
|
|
121
151
|
interface Props {
|
|
122
|
-
element: IWebchatAttachmentElement
|
|
123
|
-
isHeaderElement?: boolean
|
|
124
|
-
headingLevel?:
|
|
125
|
-
id: string
|
|
126
|
-
dividerBefore?: boolean
|
|
127
|
-
dividerAfter?: boolean
|
|
152
|
+
element: IWebchatAttachmentElement;
|
|
153
|
+
isHeaderElement?: boolean;
|
|
154
|
+
headingLevel?: "h4" | "h5";
|
|
155
|
+
id: string;
|
|
156
|
+
dividerBefore?: boolean;
|
|
157
|
+
dividerAfter?: boolean;
|
|
128
158
|
}
|
|
129
159
|
|
|
130
160
|
const props = withDefaults(defineProps<Props>(), {
|
|
131
161
|
isHeaderElement: false,
|
|
132
|
-
headingLevel:
|
|
162
|
+
headingLevel: "h4",
|
|
133
163
|
dividerBefore: false,
|
|
134
164
|
dividerAfter: false,
|
|
135
|
-
})
|
|
165
|
+
});
|
|
136
166
|
|
|
137
|
-
const $style = useCssModule()
|
|
167
|
+
const $style = useCssModule();
|
|
138
168
|
|
|
139
169
|
// Context
|
|
140
|
-
const { action, config, onEmitAnalytics } = useMessageContext()
|
|
141
|
-
const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
|
|
170
|
+
const { action, config, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext();
|
|
171
|
+
const dataMessageId = window.__TEST_MESSAGE_ID__; // For testing
|
|
142
172
|
|
|
143
173
|
// Sanitize HTML
|
|
144
|
-
const { processHTML } = useSanitize()
|
|
145
|
-
const titleHtml = computed(() => processHTML(props.element.title ||
|
|
146
|
-
const subtitleHtml = computed(() => processHTML(props.element.subtitle ||
|
|
174
|
+
const { processHTML } = useSanitize();
|
|
175
|
+
const titleHtml = computed(() => processHTML(props.element.title || ""));
|
|
176
|
+
const subtitleHtml = computed(() => processHTML(props.element.subtitle || ""));
|
|
147
177
|
|
|
148
178
|
// IDs for accessibility
|
|
149
|
-
const subtitleId = getRandomId(
|
|
179
|
+
const subtitleId = getRandomId("webchatListTemplateHeaderSubtitle");
|
|
150
180
|
|
|
151
181
|
// Background image
|
|
152
182
|
const backgroundImage = computed(() => {
|
|
153
|
-
if (!props.element.image_url) return undefined
|
|
154
|
-
return getBackgroundImage(props.element.image_url)
|
|
155
|
-
})
|
|
183
|
+
if (!props.element.image_url) return undefined;
|
|
184
|
+
return getBackgroundImage(props.element.image_url);
|
|
185
|
+
});
|
|
156
186
|
|
|
157
187
|
// Button (only first button is used)
|
|
158
188
|
const button = computed(() => {
|
|
159
|
-
return props.element.buttons?.[0]
|
|
160
|
-
})
|
|
189
|
+
return props.element.buttons?.[0];
|
|
190
|
+
});
|
|
161
191
|
|
|
162
192
|
// Default action URL (clickable item)
|
|
163
193
|
const defaultActionUrl = computed(() => {
|
|
164
|
-
return props.element.default_action?.url
|
|
165
|
-
})
|
|
194
|
+
return props.element.default_action?.url;
|
|
195
|
+
});
|
|
166
196
|
|
|
167
197
|
// Should buttons be disabled
|
|
168
198
|
const shouldBeDisabled = computed(() => {
|
|
169
|
-
|
|
170
|
-
return false
|
|
199
|
+
return messageParams?.isConversationEnded ?? false
|
|
171
200
|
})
|
|
172
201
|
|
|
173
202
|
// Translations
|
|
174
203
|
const opensInNewTabLabel = computed(() => {
|
|
175
|
-
return
|
|
176
|
-
|
|
204
|
+
return (
|
|
205
|
+
config?.settings?.customTranslations?.ariaLabels?.opensInNewTab ||
|
|
206
|
+
"Opens in new tab"
|
|
207
|
+
);
|
|
208
|
+
});
|
|
177
209
|
|
|
178
210
|
// Component tag (div for header, li for regular items)
|
|
179
211
|
const componentTag = computed(() => {
|
|
180
|
-
return props.isHeaderElement ?
|
|
181
|
-
})
|
|
212
|
+
return props.isHeaderElement ? "div" : "li";
|
|
213
|
+
});
|
|
182
214
|
|
|
183
215
|
// Content classes
|
|
184
216
|
const contentClasses = computed(() => {
|
|
185
217
|
return props.isHeaderElement
|
|
186
|
-
? [
|
|
187
|
-
: [
|
|
188
|
-
})
|
|
218
|
+
? ["webchat-list-template-header", $style.headerContentWrapper]
|
|
219
|
+
: ["webchat-list-template-element", $style.listItemWrapper];
|
|
220
|
+
});
|
|
189
221
|
|
|
190
222
|
// Handle item click (default action)
|
|
191
223
|
const handleClick = () => {
|
|
192
|
-
if (shouldBeDisabled.value || !defaultActionUrl.value) return
|
|
224
|
+
if (shouldBeDisabled.value || !defaultActionUrl.value) return;
|
|
193
225
|
|
|
194
226
|
const url = config?.settings?.layout?.disableUrlButtonSanitization
|
|
195
227
|
? defaultActionUrl.value
|
|
196
|
-
: sanitizeUrl(defaultActionUrl.value)
|
|
228
|
+
: sanitizeUrl(defaultActionUrl.value);
|
|
197
229
|
|
|
198
230
|
// Prevent no-ops from sending you to a blank page
|
|
199
|
-
if (url ===
|
|
231
|
+
if (url === "about:blank") return;
|
|
200
232
|
|
|
201
|
-
window.open(url)
|
|
202
|
-
}
|
|
233
|
+
window.open(url);
|
|
234
|
+
};
|
|
203
235
|
|
|
204
236
|
// Handle keyboard navigation
|
|
205
237
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
206
|
-
if (defaultActionUrl.value && event.key ===
|
|
207
|
-
handleClick()
|
|
238
|
+
if (defaultActionUrl.value && event.key === "Enter") {
|
|
239
|
+
handleClick();
|
|
208
240
|
}
|
|
209
|
-
}
|
|
241
|
+
};
|
|
210
242
|
</script>
|
|
211
243
|
|
|
212
244
|
<style module>
|
|
@@ -215,7 +247,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
215
247
|
}
|
|
216
248
|
|
|
217
249
|
.divider {
|
|
218
|
-
border-
|
|
250
|
+
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
251
|
}
|
|
220
252
|
|
|
221
253
|
/* Header element styles */
|
|
@@ -277,13 +309,19 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
277
309
|
}
|
|
278
310
|
|
|
279
311
|
.itemTitleWithSubtitle {
|
|
280
|
-
margin-top:
|
|
281
|
-
margin-bottom:
|
|
312
|
+
margin-top: 2px;
|
|
313
|
+
margin-bottom: 5px;
|
|
314
|
+
font-size: 0.9375rem; /* 15px */
|
|
315
|
+
font-weight: bold;
|
|
316
|
+
line-height: 1.45em; /* 23.2px */
|
|
282
317
|
}
|
|
283
318
|
|
|
284
319
|
.itemSubtitle {
|
|
285
|
-
margin
|
|
286
|
-
|
|
320
|
+
margin: 0;
|
|
321
|
+
color: rgba(0, 0, 0, 0.54);
|
|
322
|
+
font-size: 0.8125rem; /* 13px */
|
|
323
|
+
white-space: pre-line;
|
|
324
|
+
line-height: 1.45em; /* 14.5px */
|
|
287
325
|
}
|
|
288
326
|
|
|
289
327
|
/* Regular list item styles */
|
|
@@ -302,7 +340,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
302
340
|
}
|
|
303
341
|
|
|
304
342
|
.listItemContent {
|
|
305
|
-
padding:
|
|
343
|
+
padding: 10px;
|
|
306
344
|
overflow-wrap: break-word;
|
|
307
345
|
display: flex;
|
|
308
346
|
-webkit-box-pack: justify;
|
|
@@ -310,7 +348,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
310
348
|
align-items: center;
|
|
311
349
|
gap: 16px;
|
|
312
350
|
width: 100%;
|
|
313
|
-
color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
|
|
314
351
|
}
|
|
315
352
|
|
|
316
353
|
.listItemText {
|
|
@@ -318,7 +355,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
318
355
|
}
|
|
319
356
|
|
|
320
357
|
.listItemText > * {
|
|
321
|
-
color:
|
|
358
|
+
color: inherit;
|
|
322
359
|
}
|
|
323
360
|
|
|
324
361
|
.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>
|