@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,214 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="['webchat-carousel-template-frame', $style.slideItem]">
|
|
3
|
+
<!-- Top section with image and title overlay -->
|
|
4
|
+
<div :class="[$style.top, hasExtraInfo && $style.hasExtraInfo]">
|
|
5
|
+
<Typography
|
|
6
|
+
variant="body-semibold"
|
|
7
|
+
component="h4"
|
|
8
|
+
:class="'webchat-carousel-template-title'"
|
|
9
|
+
:id="titleId"
|
|
10
|
+
v-html="titleHtml"
|
|
11
|
+
/>
|
|
12
|
+
|
|
13
|
+
<!-- Image or broken image placeholder -->
|
|
14
|
+
<span v-if="isImageBroken" :class="$style.brokenImage" />
|
|
15
|
+
<img
|
|
16
|
+
v-else
|
|
17
|
+
:src="slide.image_url"
|
|
18
|
+
:alt="slide.image_alt_text || ''"
|
|
19
|
+
:class="$style.slideImage"
|
|
20
|
+
@error="handleImageError"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- Bottom section with subtitle and buttons -->
|
|
25
|
+
<div
|
|
26
|
+
v-if="hasExtraInfo"
|
|
27
|
+
:class="['webchat-carousel-template-content', $style.bottom]"
|
|
28
|
+
:role="defaultActionUrl ? 'link' : undefined"
|
|
29
|
+
:id="contentId"
|
|
30
|
+
:aria-describedby="defaultActionUrl && slide.subtitle ? subtitleId : undefined"
|
|
31
|
+
:aria-labelledby="defaultActionUrl && slide.title ? titleId : undefined"
|
|
32
|
+
:aria-label="defaultActionUrl ? `${titleHtml}. ${opensInNewTabLabel}` : undefined"
|
|
33
|
+
:tabindex="defaultActionUrl ? 0 : undefined"
|
|
34
|
+
@click="handleClick"
|
|
35
|
+
@keydown="handleKeyDown"
|
|
36
|
+
>
|
|
37
|
+
<!-- Subtitle -->
|
|
38
|
+
<Typography
|
|
39
|
+
v-if="slide.subtitle"
|
|
40
|
+
variant="body-regular"
|
|
41
|
+
:class="'webchat-carousel-template-subtitle'"
|
|
42
|
+
:id="subtitleId"
|
|
43
|
+
v-html="subtitleHtml"
|
|
44
|
+
/>
|
|
45
|
+
|
|
46
|
+
<!-- Action buttons -->
|
|
47
|
+
<ActionButtons
|
|
48
|
+
v-if="slide.buttons && slide.buttons.length > 0"
|
|
49
|
+
:payload="slide.buttons"
|
|
50
|
+
:action="shouldBeDisabled ? undefined : action"
|
|
51
|
+
:buttonClassName="buttonClassName"
|
|
52
|
+
:buttonListItemClassName="$style.buttonListItem"
|
|
53
|
+
:config="config"
|
|
54
|
+
:dataMessageId="dataMessageId"
|
|
55
|
+
:onEmitAnalytics="onEmitAnalytics"
|
|
56
|
+
:templateTextId="slide.title ? titleId : undefined"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
import { ref, computed, useCssModule } from 'vue'
|
|
64
|
+
import Typography from '../common/Typography.vue'
|
|
65
|
+
import ActionButtons from '../common/ActionButtons.vue'
|
|
66
|
+
import { useMessageContext } from '../../composables/useMessageContext'
|
|
67
|
+
import { useSanitize } from '../../composables/useSanitize'
|
|
68
|
+
import { getRandomId } from '../../utils/helpers'
|
|
69
|
+
import { sanitizeUrl } from '@braintree/sanitize-url'
|
|
70
|
+
import type { IWebchatAttachmentElement } from '../../types'
|
|
71
|
+
|
|
72
|
+
interface Props {
|
|
73
|
+
slide: IWebchatAttachmentElement
|
|
74
|
+
contentId: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const props = defineProps<Props>()
|
|
78
|
+
|
|
79
|
+
const $style = useCssModule()
|
|
80
|
+
|
|
81
|
+
// Context and config
|
|
82
|
+
const { action, config, onEmitAnalytics } = useMessageContext()
|
|
83
|
+
const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
|
|
84
|
+
|
|
85
|
+
// Sanitize HTML
|
|
86
|
+
const { processHTML } = useSanitize()
|
|
87
|
+
const titleHtml = computed(() => processHTML(props.slide.title || ''))
|
|
88
|
+
const subtitleHtml = computed(() => processHTML(props.slide.subtitle || ''))
|
|
89
|
+
|
|
90
|
+
// IDs for accessibility
|
|
91
|
+
const titleId = getRandomId('webchatCarouselTemplateTitle')
|
|
92
|
+
const subtitleId = getRandomId('webchatCarouselTemplateSubtitle')
|
|
93
|
+
|
|
94
|
+
// Image state
|
|
95
|
+
const isImageBroken = ref(false)
|
|
96
|
+
const handleImageError = () => {
|
|
97
|
+
isImageBroken.value = true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if card has extra info (subtitle or buttons)
|
|
101
|
+
const hasExtraInfo = computed(() => {
|
|
102
|
+
return !!(props.slide.subtitle || (props.slide.buttons && props.slide.buttons.length > 0))
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Default action URL (clickable card)
|
|
106
|
+
const defaultActionUrl = computed(() => {
|
|
107
|
+
return props.slide.default_action?.url
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Should buttons be disabled
|
|
111
|
+
const shouldBeDisabled = computed(() => {
|
|
112
|
+
// TODO: Add conversation ended check when messageParams available
|
|
113
|
+
return false
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Translations
|
|
117
|
+
const opensInNewTabLabel = computed(() => {
|
|
118
|
+
return config?.settings?.customTranslations?.ariaLabels?.opensInNewTab || 'Opens in new tab'
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Button class name
|
|
122
|
+
const buttonClassName = computed(() => {
|
|
123
|
+
return 'webchat-carousel-template-button'
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Handle card click (default action)
|
|
127
|
+
const handleClick = () => {
|
|
128
|
+
if (!defaultActionUrl.value) return
|
|
129
|
+
|
|
130
|
+
const url = config?.settings?.layout?.disableUrlButtonSanitization
|
|
131
|
+
? defaultActionUrl.value
|
|
132
|
+
: sanitizeUrl(defaultActionUrl.value)
|
|
133
|
+
|
|
134
|
+
// Prevent no-ops from sending you to a blank page
|
|
135
|
+
if (url === 'about:blank') return
|
|
136
|
+
|
|
137
|
+
window.open(url)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle keyboard navigation
|
|
141
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
142
|
+
if (defaultActionUrl.value && event.key === 'Enter') {
|
|
143
|
+
handleClick()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<style module>
|
|
149
|
+
.slideItem {
|
|
150
|
+
position: relative;
|
|
151
|
+
width: 206px;
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.slideItem .top {
|
|
156
|
+
position: relative;
|
|
157
|
+
border-radius: var(--cc-bubble-border-radius, 15px);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.slideItem .bottom {
|
|
161
|
+
border-bottom-left-radius: var(--cc-bubble-border-radius, 15px);
|
|
162
|
+
border-bottom-right-radius: var(--cc-bubble-border-radius, 15px);
|
|
163
|
+
border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
|
|
164
|
+
background-color: var(--cc-white, #ffffff);
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
gap: 8px;
|
|
168
|
+
padding: 8px;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.slideItem .top img {
|
|
173
|
+
aspect-ratio: 206/150;
|
|
174
|
+
object-fit: cover;
|
|
175
|
+
object-position: left;
|
|
176
|
+
border-radius: var(--cc-bubble-border-radius, 15px);
|
|
177
|
+
width: 100%;
|
|
178
|
+
display: block;
|
|
179
|
+
outline: none;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.slideItem .brokenImage {
|
|
183
|
+
aspect-ratio: 206/150;
|
|
184
|
+
width: 100%;
|
|
185
|
+
display: block;
|
|
186
|
+
outline: none;
|
|
187
|
+
border-radius: var(--cc-bubble-border-radius, 15px);
|
|
188
|
+
background-color: var(--cc-black-80, rgba(0, 0, 0, 0.8));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.slideItem .hasExtraInfo,
|
|
192
|
+
.slideItem .hasExtraInfo img,
|
|
193
|
+
.slideItem .brokenImage {
|
|
194
|
+
border-bottom-left-radius: 0px;
|
|
195
|
+
border-bottom-right-radius: 0px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.slideItem .top h4 {
|
|
199
|
+
position: absolute;
|
|
200
|
+
margin: 0px;
|
|
201
|
+
margin-inline-start: 8px;
|
|
202
|
+
bottom: 10px;
|
|
203
|
+
color: var(--cc-white, #ffffff);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.slideItem .bottom p {
|
|
207
|
+
padding: 0px;
|
|
208
|
+
margin: 0px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.slideItem .bottom .buttonListItem {
|
|
212
|
+
width: 100%;
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="imageData.url" :class="$style.wrapper">
|
|
3
|
+
<!-- Thumbnail -->
|
|
4
|
+
<div
|
|
5
|
+
ref="buttonRef"
|
|
6
|
+
:class="imageClasses"
|
|
7
|
+
:tabindex="isDownloadable ? 0 : -1"
|
|
8
|
+
:role="isDownloadable ? 'button' : undefined"
|
|
9
|
+
:aria-label="isDownloadable ? viewImageLabel : undefined"
|
|
10
|
+
@click="handleExpand"
|
|
11
|
+
@keydown="handleKeyDown"
|
|
12
|
+
>
|
|
13
|
+
<span v-if="isImageBroken" :class="$style.brokenImage" />
|
|
14
|
+
<img
|
|
15
|
+
v-else
|
|
16
|
+
:src="imageData.url"
|
|
17
|
+
:alt="imageData.altText || ''"
|
|
18
|
+
@error="handleImageError"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Download button -->
|
|
23
|
+
<ActionButton
|
|
24
|
+
v-if="imageData.button"
|
|
25
|
+
:button="imageData.button"
|
|
26
|
+
:action="action"
|
|
27
|
+
:config="config"
|
|
28
|
+
:on-emit-analytics="onEmitAnalytics"
|
|
29
|
+
:custom-icon="DownloadIcon"
|
|
30
|
+
:position="1"
|
|
31
|
+
:total="1"
|
|
32
|
+
:class-name="$style.downloadButtonWrapper"
|
|
33
|
+
class="webchat-buttons-template-button"
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
<!-- Lightbox modal -->
|
|
37
|
+
<Teleport to="body">
|
|
38
|
+
<div
|
|
39
|
+
v-if="showLightbox"
|
|
40
|
+
role="dialog"
|
|
41
|
+
:aria-label="lightboxLabel"
|
|
42
|
+
:class="$style.lightboxWrapper"
|
|
43
|
+
>
|
|
44
|
+
<div :class="$style.lightboxContent" @click="handleClose">
|
|
45
|
+
<img
|
|
46
|
+
:class="$style.fullImage"
|
|
47
|
+
:alt="imageData.altText"
|
|
48
|
+
:src="imageData.url"
|
|
49
|
+
data-testid="image-lightbox"
|
|
50
|
+
@click.stop
|
|
51
|
+
@touchmove.prevent="handleClose"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- Lightbox Header -->
|
|
56
|
+
<div :class="$style.lightboxHeader">
|
|
57
|
+
<div :class="$style.caption">{{ imageData.altText }}</div>
|
|
58
|
+
<div :class="$style.iconsGroup">
|
|
59
|
+
<button
|
|
60
|
+
ref="downloadButtonRef"
|
|
61
|
+
:class="$style.icon"
|
|
62
|
+
:aria-label="downloadLabel"
|
|
63
|
+
@click="handleDownload"
|
|
64
|
+
@keydown="handleKeyDownload"
|
|
65
|
+
>
|
|
66
|
+
<DownloadIcon />
|
|
67
|
+
</button>
|
|
68
|
+
<button
|
|
69
|
+
:class="$style.icon"
|
|
70
|
+
:aria-label="closeLabel"
|
|
71
|
+
@click="handleClose"
|
|
72
|
+
@keydown="handleKeyClose"
|
|
73
|
+
>
|
|
74
|
+
<CloseIcon />
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</Teleport>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
import { ref, computed, onMounted, onUnmounted, nextTick, useCssModule } from 'vue'
|
|
85
|
+
import { useMessageContext } from '../../composables/useMessageContext'
|
|
86
|
+
import { getChannelPayload } from '../../utils/matcher'
|
|
87
|
+
import ActionButton from '../common/ActionButton.vue'
|
|
88
|
+
import { DownloadIcon, CloseIcon } from '../../assets/svg'
|
|
89
|
+
import type { IWebchatImageAttachment, IWebchatButton } from '../../types'
|
|
90
|
+
|
|
91
|
+
const { message, config, action, onEmitAnalytics } = useMessageContext()
|
|
92
|
+
|
|
93
|
+
const $style = useCssModule()
|
|
94
|
+
|
|
95
|
+
// State
|
|
96
|
+
const showLightbox = ref(false)
|
|
97
|
+
const isImageBroken = ref(false)
|
|
98
|
+
const buttonRef = ref<HTMLDivElement>()
|
|
99
|
+
const downloadButtonRef = ref<HTMLButtonElement>()
|
|
100
|
+
|
|
101
|
+
// Get image data from message payload
|
|
102
|
+
const payload = computed(() => getChannelPayload(message, config))
|
|
103
|
+
const imageData = computed(() => {
|
|
104
|
+
const attachment = payload.value?.message?.attachment as IWebchatImageAttachment
|
|
105
|
+
return {
|
|
106
|
+
url: attachment?.payload?.url || '',
|
|
107
|
+
altText: attachment?.payload?.altText,
|
|
108
|
+
buttons: attachment?.payload?.buttons,
|
|
109
|
+
button: attachment?.payload?.buttons?.[0],
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Check if image is downloadable (has a web_url button)
|
|
114
|
+
const isDownloadable = computed(() => {
|
|
115
|
+
const buttons = imageData.value.buttons as IWebchatButton[] | undefined
|
|
116
|
+
return buttons?.some(button => 'type' in button && button.type === 'web_url') ?? false
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Dynamic aspect ratio setting
|
|
120
|
+
const isDynamicRatio = computed(() => !!config?.settings?.layout?.dynamicImageAspectRatio)
|
|
121
|
+
|
|
122
|
+
const imageClasses = computed(() => {
|
|
123
|
+
return {
|
|
124
|
+
[$style.fixedImage]: isDynamicRatio.value,
|
|
125
|
+
[$style.flexImage]: !isDynamicRatio.value,
|
|
126
|
+
[$style.webchatMediaTemplateImage]: true,
|
|
127
|
+
[$style.downloadable]: isDownloadable.value,
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Translations
|
|
132
|
+
const viewImageLabel = computed(() =>
|
|
133
|
+
config?.settings?.customTranslations?.ariaLabels?.viewImageInFullsize || 'View full-size image'
|
|
134
|
+
)
|
|
135
|
+
const lightboxLabel = computed(() =>
|
|
136
|
+
config?.settings?.customTranslations?.ariaLabels?.fullSizeImageViewerTitle || 'Full-size image viewer'
|
|
137
|
+
)
|
|
138
|
+
const downloadLabel = computed(() =>
|
|
139
|
+
config?.settings?.customTranslations?.ariaLabels?.downloadFullsizeImage || 'Download full-size image'
|
|
140
|
+
)
|
|
141
|
+
const closeLabel = computed(() =>
|
|
142
|
+
config?.settings?.customTranslations?.ariaLabels?.closeFullsizeImageModal || 'Close full-size image viewer'
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Handlers
|
|
146
|
+
const handleExpand = () => {
|
|
147
|
+
if (isDownloadable.value) {
|
|
148
|
+
showLightbox.value = true
|
|
149
|
+
// Focus the download button after lightbox opens
|
|
150
|
+
nextTick(() => {
|
|
151
|
+
downloadButtonRef.value?.focus()
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const handleClose = () => {
|
|
157
|
+
showLightbox.value = false
|
|
158
|
+
// Restore focus to thumbnail button
|
|
159
|
+
nextTick(() => {
|
|
160
|
+
buttonRef.value?.focus()
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const handleDownload = () => {
|
|
165
|
+
window.open(imageData.value.url, '_blank')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
169
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
170
|
+
event.preventDefault()
|
|
171
|
+
handleExpand()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const handleKeyDownload = (event: KeyboardEvent) => {
|
|
176
|
+
if (event.key === 'Enter') {
|
|
177
|
+
handleDownload()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const handleKeyClose = (event: KeyboardEvent) => {
|
|
182
|
+
if (event.key === 'Tab' || event.shiftKey) {
|
|
183
|
+
downloadButtonRef.value?.focus()
|
|
184
|
+
event.preventDefault()
|
|
185
|
+
}
|
|
186
|
+
if (event.key === 'Enter') {
|
|
187
|
+
handleClose()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const handleImageError = () => {
|
|
192
|
+
isImageBroken.value = true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Escape key listener for lightbox
|
|
196
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
197
|
+
if (event.code === 'Escape' && showLightbox.value) {
|
|
198
|
+
handleClose()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
onMounted(() => {
|
|
203
|
+
window.addEventListener('keydown', handleEscape)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
onUnmounted(() => {
|
|
207
|
+
window.removeEventListener('keydown', handleEscape)
|
|
208
|
+
})
|
|
209
|
+
</script>
|
|
210
|
+
|
|
211
|
+
<style module>
|
|
212
|
+
.wrapper {
|
|
213
|
+
position: relative;
|
|
214
|
+
border-radius: var(--cc-bubble-border-radius, 15px);
|
|
215
|
+
max-width: 295px;
|
|
216
|
+
width: 100%;
|
|
217
|
+
outline: none;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.wrapper .fixedImage,
|
|
221
|
+
.wrapper .flexImage {
|
|
222
|
+
border-top-left-radius: var(--cc-bubble-border-radius, 15px);
|
|
223
|
+
border-top-right-radius: var(--cc-bubble-border-radius, 15px);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.wrapper .fixedImage:focus-visible,
|
|
227
|
+
.wrapper .flexImage:focus-visible {
|
|
228
|
+
outline: 2px solid var(--cc-primary-color-focus, #1976d2);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.wrapper img {
|
|
232
|
+
border-radius: var(--cc-bubble-border-radius, 15px);
|
|
233
|
+
width: 100%;
|
|
234
|
+
display: block;
|
|
235
|
+
outline: none;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.downloadable {
|
|
239
|
+
background-color: var(--cc-white, #ffffff);
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.downloadable img {
|
|
245
|
+
border-bottom-left-radius: 0px;
|
|
246
|
+
border-bottom-right-radius: 0px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.downloadable img:hover,
|
|
250
|
+
.downloadable img:focus {
|
|
251
|
+
opacity: 0.8;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.flexImage img {
|
|
255
|
+
aspect-ratio: 16/9;
|
|
256
|
+
object-fit: cover;
|
|
257
|
+
object-position: left;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.fixedImage img {
|
|
261
|
+
height: auto;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.webchatMediaTemplateImage {
|
|
265
|
+
/* Base class for image containers */
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.downloadButtonWrapper {
|
|
269
|
+
padding: 16px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.brokenImage {
|
|
273
|
+
aspect-ratio: 16/9;
|
|
274
|
+
width: 100%;
|
|
275
|
+
display: block;
|
|
276
|
+
outline: none;
|
|
277
|
+
border-radius: var(--cc-bubble-border-radius, 15px);
|
|
278
|
+
background-color: var(--cc-black-80, rgba(0, 0, 0, 0.8));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Lightbox styles */
|
|
282
|
+
.lightboxWrapper {
|
|
283
|
+
position: fixed;
|
|
284
|
+
z-index: 5000;
|
|
285
|
+
left: 0;
|
|
286
|
+
top: 0;
|
|
287
|
+
width: 100%;
|
|
288
|
+
height: 100%;
|
|
289
|
+
background-color: rgba(0, 0, 0, 0.8);
|
|
290
|
+
touch-action: none;
|
|
291
|
+
overflow: hidden;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.lightboxContent {
|
|
295
|
+
position: relative;
|
|
296
|
+
height: 100%;
|
|
297
|
+
width: 100%;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.fullImage {
|
|
301
|
+
position: absolute;
|
|
302
|
+
top: 50%;
|
|
303
|
+
left: 50%;
|
|
304
|
+
max-width: 100%;
|
|
305
|
+
max-height: 100%;
|
|
306
|
+
height: auto;
|
|
307
|
+
transform: translate3d(-50%, -50%, 0);
|
|
308
|
+
overflow: hidden;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* Lightbox Header */
|
|
312
|
+
.lightboxHeader {
|
|
313
|
+
position: absolute;
|
|
314
|
+
top: 0;
|
|
315
|
+
height: 56px;
|
|
316
|
+
width: 100%;
|
|
317
|
+
background-color: var(--cc-white, #ffffff);
|
|
318
|
+
overflow: hidden;
|
|
319
|
+
display: flex;
|
|
320
|
+
justify-content: space-between;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.caption {
|
|
324
|
+
display: flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
margin-left: 15px;
|
|
327
|
+
color: var(--cc-black-10, #1a1a1a);
|
|
328
|
+
font-weight: 700;
|
|
329
|
+
font-size: 16px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.iconsGroup {
|
|
333
|
+
display: flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
margin-right: 10px;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.icon {
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
justify-content: center;
|
|
342
|
+
box-sizing: border-box;
|
|
343
|
+
background-color: transparent;
|
|
344
|
+
border: none;
|
|
345
|
+
outline: none;
|
|
346
|
+
margin: 0;
|
|
347
|
+
transition: background-color 0.1s ease-out, color 0.1s ease-out, fill 0.1s ease-out;
|
|
348
|
+
color: var(--cc-black-10, #1a1a1a);
|
|
349
|
+
border-radius: 50%;
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
width: 40px;
|
|
352
|
+
height: 40px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.icon:hover,
|
|
356
|
+
.icon:focus {
|
|
357
|
+
background-color: var(--cc-black-95, #f5f5f5);
|
|
358
|
+
opacity: 0.85;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.icon:focus-visible {
|
|
362
|
+
border: 2px solid var(--cc-primary-color-focus, #1976d2);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.icon svg {
|
|
366
|
+
fill: var(--cc-black-10, #1a1a1a);
|
|
367
|
+
}
|
|
368
|
+
</style>
|