@hellotext/hellotext 2.3.9 → 2.4.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/hellotext.js +1 -1
- package/dist/webchat-emoji-en.js +1 -0
- package/dist/webchat-emoji-es.js +1 -0
- package/dist/webchat-emoji.js +1 -0
- package/index.d.ts +30 -4
- package/lib/api/webchats.cjs +20 -0
- package/lib/api/webchats.js +20 -0
- package/lib/controllers/mixins/usePopover.cjs +7 -2
- package/lib/controllers/mixins/usePopover.js +7 -2
- package/lib/controllers/webchat/emoji_picker_controller.cjs +49 -10
- package/lib/controllers/webchat/emoji_picker_controller.js +66 -8
- package/lib/controllers/webchat/useBehaviour.cjs +74 -0
- package/lib/controllers/webchat/useBehaviour.js +67 -0
- package/lib/controllers/webchat/useOpeningSequence.cjs +103 -0
- package/lib/controllers/webchat/useOpeningSequence.js +96 -0
- package/lib/controllers/webchat/useTeaser.cjs +163 -0
- package/lib/controllers/webchat/useTeaser.js +156 -0
- package/lib/controllers/webchat_controller.cjs +168 -22
- package/lib/controllers/webchat_controller.js +176 -24
- package/lib/core/configuration/webchat.cjs +117 -42
- package/lib/core/configuration/webchat.js +121 -43
- package/lib/hellotext.cjs +28 -4
- package/lib/hellotext.js +28 -1
- package/lib/models/business.cjs +2 -0
- package/lib/models/business.js +2 -0
- package/lib/models/webchat.cjs +25 -0
- package/lib/models/webchat.js +25 -0
- package/package.json +1 -1
- package/src/api/webchats.js +17 -0
- package/src/controllers/mixins/usePopover.js +4 -2
- package/src/controllers/webchat/emoji_picker_controller.js +51 -9
- package/src/controllers/webchat/useBehaviour.js +81 -0
- package/src/controllers/webchat/useOpeningSequence.js +127 -0
- package/src/controllers/webchat/useTeaser.js +192 -0
- package/src/controllers/webchat_controller.js +200 -23
- package/src/core/configuration/webchat.js +127 -43
- package/src/hellotext.js +32 -4
- package/src/models/business.js +2 -0
- package/src/models/webchat.js +28 -0
|
@@ -6,10 +6,12 @@ import WebchatChannel from '../channels/webchat_channel'
|
|
|
6
6
|
import Hellotext from '../hellotext'
|
|
7
7
|
|
|
8
8
|
import { Locale } from '../core'
|
|
9
|
-
import { Webchat as WebchatConfiguration,
|
|
9
|
+
import { Webchat as WebchatConfiguration, modes } from '../core/configuration/webchat'
|
|
10
10
|
|
|
11
|
-
import { LogoBuilder } from '../builders/logo_builder'
|
|
12
11
|
import { usePopover } from './mixins/usePopover'
|
|
12
|
+
import { useBehaviour } from './webchat/useBehaviour'
|
|
13
|
+
import { useOpeningSequence } from './webchat/useOpeningSequence'
|
|
14
|
+
import { useTeaser } from './webchat/useTeaser'
|
|
13
15
|
|
|
14
16
|
export default class extends Controller {
|
|
15
17
|
static values = {
|
|
@@ -26,7 +28,10 @@ export default class extends Controller {
|
|
|
26
28
|
typingIndicatorKeepAlive: { type: Number, default: 30000 }, // 30 seconds
|
|
27
29
|
offset: { type: Number, default: 24 },
|
|
28
30
|
padding: { type: Number, default: 24 },
|
|
29
|
-
optimisticTypingIndicatorWait: { type: Number, default: 1000 }, // 1 second
|
|
31
|
+
optimisticTypingIndicatorWait: { type: Number, default: 1000 }, // 1 second,
|
|
32
|
+
teaser: Object,
|
|
33
|
+
messageTeaser: String,
|
|
34
|
+
behaviour: Object,
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
static classes = ['fadeOut']
|
|
@@ -51,6 +56,12 @@ export default class extends Controller {
|
|
|
51
56
|
'unreadCounter',
|
|
52
57
|
'typingIndicator',
|
|
53
58
|
'typingIndicatorTemplate',
|
|
59
|
+
'teaser',
|
|
60
|
+
'teaserMessage',
|
|
61
|
+
'inboundMessageTeaser',
|
|
62
|
+
'inboundMessageTeaserBody',
|
|
63
|
+
'openingSequence',
|
|
64
|
+
'openingSequenceMessage',
|
|
54
65
|
]
|
|
55
66
|
|
|
56
67
|
initialize() {
|
|
@@ -63,6 +74,7 @@ export default class extends Controller {
|
|
|
63
74
|
)
|
|
64
75
|
|
|
65
76
|
this.files = []
|
|
77
|
+
this.messageIds = new Set()
|
|
66
78
|
|
|
67
79
|
this.onMessageReceived = this.onMessageReceived.bind(this)
|
|
68
80
|
this.onMessageReaction = this.onMessageReaction.bind(this)
|
|
@@ -77,13 +89,24 @@ export default class extends Controller {
|
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
connect() {
|
|
92
|
+
useBehaviour(this)
|
|
80
93
|
usePopover(this)
|
|
81
|
-
|
|
82
|
-
this
|
|
83
|
-
this.triggerTarget.classList.add(...WebchatConfiguration.triggerClasses)
|
|
94
|
+
useOpeningSequence(this)
|
|
95
|
+
useTeaser(this)
|
|
84
96
|
|
|
85
97
|
this.setupFloatingUI({ trigger: this.triggerTarget, popover: this.popoverTarget })
|
|
86
98
|
|
|
99
|
+
if (this.hasTeaserTarget) {
|
|
100
|
+
this.setupFloatingUI({
|
|
101
|
+
trigger: this.triggerTarget,
|
|
102
|
+
popover: this.teaserTarget,
|
|
103
|
+
strategy: 'absolute',
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.setupTeaser()
|
|
108
|
+
this.setupOpeningSequence()
|
|
109
|
+
|
|
87
110
|
this.webChatChannel.onMessage(this.onMessageReceived)
|
|
88
111
|
this.webChatChannel.onTypingStart(this.onTypingStart)
|
|
89
112
|
|
|
@@ -91,21 +114,22 @@ export default class extends Controller {
|
|
|
91
114
|
|
|
92
115
|
this.messagesContainerTarget.addEventListener('scroll', this.onScroll)
|
|
93
116
|
|
|
94
|
-
if (!Hellotext.business.features.white_label) {
|
|
95
|
-
this.toolbarTarget.appendChild(LogoBuilder.build())
|
|
96
|
-
}
|
|
97
|
-
|
|
98
117
|
if (this.shouldOpenOnMount) {
|
|
99
118
|
this.openValue = true
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
Hellotext.eventEmitter.dispatch('webchat:mounted')
|
|
103
122
|
this.broadcastChannel.addEventListener('message', this.onOutboundMessageSent)
|
|
123
|
+
this.scheduleBehaviourOpen()
|
|
104
124
|
|
|
105
125
|
super.connect()
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
disconnect() {
|
|
129
|
+
this.cancelBehaviourOpen()
|
|
130
|
+
this.teardownTeaser()
|
|
131
|
+
this.teardownOpeningSequence()
|
|
132
|
+
|
|
109
133
|
this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent)
|
|
110
134
|
this.messagesContainerTarget.removeEventListener('scroll', this.onScroll)
|
|
111
135
|
|
|
@@ -273,7 +297,7 @@ export default class extends Controller {
|
|
|
273
297
|
image.src = attachmentUrl
|
|
274
298
|
image.style.display = 'block'
|
|
275
299
|
|
|
276
|
-
|
|
300
|
+
this.messageAttachmentsContainer(element)?.appendChild(image)
|
|
277
301
|
})
|
|
278
302
|
}
|
|
279
303
|
|
|
@@ -291,7 +315,7 @@ export default class extends Controller {
|
|
|
291
315
|
|
|
292
316
|
onClickOutside(event) {
|
|
293
317
|
if (
|
|
294
|
-
WebchatConfiguration.
|
|
318
|
+
WebchatConfiguration.mode === modes.POPOVER &&
|
|
295
319
|
this.openValue &&
|
|
296
320
|
event.target.nodeType &&
|
|
297
321
|
this.element.contains(event.target) === false
|
|
@@ -310,6 +334,7 @@ export default class extends Controller {
|
|
|
310
334
|
|
|
311
335
|
onPopoverOpened() {
|
|
312
336
|
this.popoverTarget.classList.remove(...this.fadeOutClasses)
|
|
337
|
+
this.dismissTeaserForSession?.()
|
|
313
338
|
|
|
314
339
|
if (!this.onMobile) {
|
|
315
340
|
this.inputTarget.focus()
|
|
@@ -329,6 +354,12 @@ export default class extends Controller {
|
|
|
329
354
|
Hellotext.eventEmitter.dispatch('webchat:opened')
|
|
330
355
|
localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'opened')
|
|
331
356
|
|
|
357
|
+
if (this.messageTeaserValue) {
|
|
358
|
+
this.messageTeaserValue = null
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.startOpeningSequence()
|
|
362
|
+
|
|
332
363
|
if (this.unreadCounterTarget.style.display === 'none') return
|
|
333
364
|
|
|
334
365
|
this.unreadCounterTarget.style.display = 'none'
|
|
@@ -367,7 +398,11 @@ export default class extends Controller {
|
|
|
367
398
|
}
|
|
368
399
|
|
|
369
400
|
onMessageReceived(message) {
|
|
370
|
-
const { id, body, attachments } = message
|
|
401
|
+
const { id, body, attachments, teaser } = message
|
|
402
|
+
|
|
403
|
+
if (!this.claimMessageId(id)) return
|
|
404
|
+
|
|
405
|
+
this.hideTeaser?.()
|
|
371
406
|
|
|
372
407
|
if (message.carousel) {
|
|
373
408
|
return this.insertCarouselMessage(message)
|
|
@@ -380,6 +415,8 @@ export default class extends Controller {
|
|
|
380
415
|
element.style.display = 'flex'
|
|
381
416
|
|
|
382
417
|
element.querySelector('[data-body]').innerHTML = div.innerHTML
|
|
418
|
+
|
|
419
|
+
element.setAttribute('data-id', id)
|
|
383
420
|
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
384
421
|
|
|
385
422
|
if (attachments) {
|
|
@@ -388,7 +425,7 @@ export default class extends Controller {
|
|
|
388
425
|
image.src = attachmentUrl
|
|
389
426
|
image.style.display = 'block'
|
|
390
427
|
|
|
391
|
-
|
|
428
|
+
this.messageAttachmentsContainer(element)?.appendChild(image)
|
|
392
429
|
})
|
|
393
430
|
}
|
|
394
431
|
|
|
@@ -402,21 +439,55 @@ export default class extends Controller {
|
|
|
402
439
|
|
|
403
440
|
element.scrollIntoView({ behavior: 'smooth' })
|
|
404
441
|
|
|
442
|
+
this.updateMessageTeaser(teaser)
|
|
443
|
+
|
|
405
444
|
if (this.openValue) {
|
|
406
445
|
this.messagesAPI.markAsSeen(id)
|
|
407
446
|
return
|
|
408
447
|
}
|
|
409
448
|
|
|
410
|
-
this.
|
|
449
|
+
this.incrementUnreadCounter()
|
|
450
|
+
}
|
|
411
451
|
|
|
412
|
-
|
|
413
|
-
|
|
452
|
+
// TCP broadcasts may deliver the same incoming message twice before Stimulus
|
|
453
|
+
// has connected the appended message target. Claiming the id in memory closes
|
|
454
|
+
// that window, while the target check still drops messages rendered earlier.
|
|
455
|
+
claimMessageId(id) {
|
|
456
|
+
const messageTargets = this.messageTargets || []
|
|
457
|
+
|
|
458
|
+
if (this.messageIds.has(id)) return false
|
|
459
|
+
|
|
460
|
+
this.messageIds.add(id)
|
|
461
|
+
|
|
462
|
+
return !messageTargets.some(element => element.dataset.id === id)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
updateMessageTeaser(teaser) {
|
|
466
|
+
this.messageTeaserValue = teaser
|
|
467
|
+
|
|
468
|
+
if (
|
|
469
|
+
!this.messageTeaserValue ||
|
|
470
|
+
!this.hasTeaserTarget ||
|
|
471
|
+
!this.hasInboundMessageTeaserTarget ||
|
|
472
|
+
!this.hasInboundMessageTeaserBodyTarget
|
|
473
|
+
) {
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this.teaserMessageTargets.forEach(teaserMessage => teaserMessage.classList.add('hidden'))
|
|
478
|
+
this.inboundMessageTeaserBodyTarget.innerHTML = this.messageTeaserValue
|
|
479
|
+
this.inboundMessageTeaserTarget.classList.remove('hidden')
|
|
480
|
+
|
|
481
|
+
this.teaserTarget.classList.toggle('invisible', this.openValue)
|
|
414
482
|
}
|
|
415
483
|
|
|
416
484
|
insertCarouselMessage(message) {
|
|
417
485
|
const html = message.html
|
|
418
486
|
const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild
|
|
419
487
|
|
|
488
|
+
element.setAttribute('data-id', message.id)
|
|
489
|
+
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
490
|
+
|
|
420
491
|
this.clearTypingIndicator()
|
|
421
492
|
this.messagesContainerTarget.appendChild(element)
|
|
422
493
|
|
|
@@ -427,14 +498,14 @@ export default class extends Controller {
|
|
|
427
498
|
body: element.querySelector('[data-body]')?.innerText || '',
|
|
428
499
|
})
|
|
429
500
|
|
|
501
|
+
this.updateMessageTeaser(message.teaser)
|
|
502
|
+
|
|
430
503
|
if (this.openValue) {
|
|
431
504
|
this.messagesAPI.markAsSeen(message.id)
|
|
432
505
|
return
|
|
433
506
|
}
|
|
434
507
|
|
|
435
|
-
this.
|
|
436
|
-
const unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1
|
|
437
|
-
this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
|
|
508
|
+
this.incrementUnreadCounter()
|
|
438
509
|
}
|
|
439
510
|
|
|
440
511
|
resizeInput() {
|
|
@@ -451,6 +522,8 @@ export default class extends Controller {
|
|
|
451
522
|
}
|
|
452
523
|
|
|
453
524
|
async sendQuickReplyMessage({ detail: { id, product, buttonId, body, cardElement } }) {
|
|
525
|
+
this.dismissTeaserForSession?.()
|
|
526
|
+
|
|
454
527
|
const formData = new FormData()
|
|
455
528
|
|
|
456
529
|
formData.append('message[body]', body)
|
|
@@ -460,6 +533,7 @@ export default class extends Controller {
|
|
|
460
533
|
|
|
461
534
|
formData.append('session', Hellotext.session)
|
|
462
535
|
formData.append('locale', Locale.toString())
|
|
536
|
+
this.appendOpeningSequenceMessageIds(formData)
|
|
463
537
|
|
|
464
538
|
const element = this.buildMessageElement()
|
|
465
539
|
const attachment = cardElement.querySelector('img')?.cloneNode(true)
|
|
@@ -470,7 +544,7 @@ export default class extends Controller {
|
|
|
470
544
|
attachment.removeAttribute('width')
|
|
471
545
|
attachment.removeAttribute('height')
|
|
472
546
|
|
|
473
|
-
|
|
547
|
+
this.messageAttachmentsContainer(element)?.appendChild(attachment)
|
|
474
548
|
}
|
|
475
549
|
|
|
476
550
|
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
@@ -503,6 +577,7 @@ export default class extends Controller {
|
|
|
503
577
|
const data = await response.json()
|
|
504
578
|
|
|
505
579
|
this.dispatch('set:id', { target: element, detail: data.id })
|
|
580
|
+
this.clearRevealedOpeningSequenceMessageIds()
|
|
506
581
|
|
|
507
582
|
const message = {
|
|
508
583
|
id: data.id,
|
|
@@ -517,9 +592,94 @@ export default class extends Controller {
|
|
|
517
592
|
Hellotext.eventEmitter.dispatch('webchat:message:sent', message)
|
|
518
593
|
}
|
|
519
594
|
|
|
520
|
-
async
|
|
595
|
+
async sendTeaserQuickReply(event) {
|
|
596
|
+
event.preventDefault()
|
|
597
|
+
event.stopPropagation()
|
|
598
|
+
|
|
599
|
+
const button = event.currentTarget
|
|
600
|
+
const value = (button.dataset.value || '').trim()
|
|
601
|
+
const label = [button.dataset.text, button.textContent]
|
|
602
|
+
.map(text => (text || '').trim())
|
|
603
|
+
.find(text => text.length > 0)
|
|
604
|
+
const text = value || label
|
|
605
|
+
|
|
606
|
+
if (!text) return
|
|
607
|
+
|
|
608
|
+
this.dismissTeaserForSession?.()
|
|
609
|
+
this.show()
|
|
610
|
+
|
|
611
|
+
const buttonType = (button.dataset.type || '').trim() || 'quick_reply'
|
|
521
612
|
const formData = new FormData()
|
|
522
613
|
|
|
614
|
+
formData.append('message[body]', text)
|
|
615
|
+
formData.append('session', Hellotext.session)
|
|
616
|
+
formData.append('locale', Locale.toString())
|
|
617
|
+
this.appendOpeningSequenceMessageIds(formData)
|
|
618
|
+
|
|
619
|
+
const element = this.buildMessageElement()
|
|
620
|
+
|
|
621
|
+
element.querySelector('[data-body]').innerText = text
|
|
622
|
+
|
|
623
|
+
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
624
|
+
this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget)
|
|
625
|
+
} else {
|
|
626
|
+
this.messagesContainerTarget.appendChild(element)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
element.scrollIntoView({ behavior: 'smooth' })
|
|
630
|
+
|
|
631
|
+
this.broadcastChannel.postMessage({
|
|
632
|
+
type: 'message:sent',
|
|
633
|
+
element: element.outerHTML,
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
if (!this.typingIndicatorVisible) {
|
|
637
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
638
|
+
this.optimisticTypingTimeout = setTimeout(() => {
|
|
639
|
+
this.showOptimisticTypingIndicator()
|
|
640
|
+
}, this.optimisticTypingIndicatorWaitValue)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const response = await this.messagesAPI.create(formData)
|
|
644
|
+
|
|
645
|
+
if (response.failed) {
|
|
646
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
647
|
+
|
|
648
|
+
this.broadcastChannel.postMessage({
|
|
649
|
+
type: 'message:failed',
|
|
650
|
+
id: element.id,
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
return element.classList.add('failed')
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const data = await response.json()
|
|
657
|
+
element.setAttribute('data-id', data.id)
|
|
658
|
+
this.clearRevealedOpeningSequenceMessageIds()
|
|
659
|
+
|
|
660
|
+
Hellotext.eventEmitter.dispatch('webchat:message:sent', {
|
|
661
|
+
id: data.id,
|
|
662
|
+
body: text,
|
|
663
|
+
attachments: [],
|
|
664
|
+
type: 'quick_reply',
|
|
665
|
+
teaser: {
|
|
666
|
+
text: label || text,
|
|
667
|
+
value: value || text,
|
|
668
|
+
type: buttonType,
|
|
669
|
+
},
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
if (data.conversation && data.conversation !== this.conversationIdValue) {
|
|
673
|
+
this.conversationIdValue = data.conversation
|
|
674
|
+
this.webChatChannel.updateSubscriptionWith(this.conversationIdValue)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (this.typingIndicatorVisible) {
|
|
678
|
+
this.resetTypingIndicatorTimer()
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async sendMessage(e) {
|
|
523
683
|
const message = {
|
|
524
684
|
body: this.inputTarget.value,
|
|
525
685
|
attachments: this.files,
|
|
@@ -533,6 +693,10 @@ export default class extends Controller {
|
|
|
533
693
|
return
|
|
534
694
|
}
|
|
535
695
|
|
|
696
|
+
this.dismissTeaserForSession?.()
|
|
697
|
+
|
|
698
|
+
const formData = new FormData()
|
|
699
|
+
|
|
536
700
|
if (this.inputTarget.value.trim().length > 0) {
|
|
537
701
|
formData.append('message[body]', this.inputTarget.value)
|
|
538
702
|
} else {
|
|
@@ -545,6 +709,7 @@ export default class extends Controller {
|
|
|
545
709
|
|
|
546
710
|
formData.append('session', Hellotext.session)
|
|
547
711
|
formData.append('locale', Locale.toString())
|
|
712
|
+
this.appendOpeningSequenceMessageIds(formData)
|
|
548
713
|
|
|
549
714
|
const element = this.buildMessageElement()
|
|
550
715
|
|
|
@@ -558,7 +723,7 @@ export default class extends Controller {
|
|
|
558
723
|
|
|
559
724
|
if (attachments.length > 0) {
|
|
560
725
|
attachments.forEach(attachment => {
|
|
561
|
-
|
|
726
|
+
this.messageAttachmentsContainer(element)?.appendChild(attachment.cloneNode(true))
|
|
562
727
|
})
|
|
563
728
|
}
|
|
564
729
|
|
|
@@ -614,6 +779,7 @@ export default class extends Controller {
|
|
|
614
779
|
const data = await response.json()
|
|
615
780
|
element.setAttribute('data-id', data.id)
|
|
616
781
|
message.id = data.id
|
|
782
|
+
this.clearRevealedOpeningSequenceMessageIds()
|
|
617
783
|
|
|
618
784
|
Hellotext.eventEmitter.dispatch('webchat:message:sent', message)
|
|
619
785
|
|
|
@@ -644,6 +810,17 @@ export default class extends Controller {
|
|
|
644
810
|
return element
|
|
645
811
|
}
|
|
646
812
|
|
|
813
|
+
messageAttachmentsContainer(element) {
|
|
814
|
+
return element.querySelector('[data-attachments-container], [data-attachment-container]')
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
incrementUnreadCounter() {
|
|
818
|
+
this.unreadCounterTarget.style.display = 'flex'
|
|
819
|
+
|
|
820
|
+
const unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1
|
|
821
|
+
this.unreadCounterTarget.innerText = Math.min(unreadCount, 9)
|
|
822
|
+
}
|
|
823
|
+
|
|
647
824
|
openAttachment() {
|
|
648
825
|
this.attachmentInputTarget.click()
|
|
649
826
|
}
|
|
@@ -28,13 +28,13 @@ const strategies = {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* @typedef {'modal' | 'popover'}
|
|
31
|
+
* @typedef {'modal' | 'popover'} Mode
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* @enum {
|
|
35
|
+
* @enum {Mode}
|
|
36
36
|
*/
|
|
37
|
-
const
|
|
37
|
+
const modes = {
|
|
38
38
|
MODAL: 'modal',
|
|
39
39
|
POPOVER: 'popover',
|
|
40
40
|
}
|
|
@@ -46,6 +46,20 @@ const behaviors = {
|
|
|
46
46
|
* @property {string} typography - The typography style (e.g., font family).
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} Appearance
|
|
51
|
+
* @property {Object} [header] - Header appearance overrides.
|
|
52
|
+
* @property {string} [header.name] - Business name shown in the webchat header.
|
|
53
|
+
* @property {Object} [launcher] - Launcher appearance overrides.
|
|
54
|
+
* @property {string} [launcher.iconUrl] - Image URL used for the launcher icon.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {Object} WhatsApp
|
|
59
|
+
* @property {string} [number] - WhatsApp number used by the handoff configuration.
|
|
60
|
+
* @property {boolean} [restrictToChannel] - Whether handoff should be restricted to the configured channel.
|
|
61
|
+
*/
|
|
62
|
+
|
|
49
63
|
/**
|
|
50
64
|
* @class Webchat
|
|
51
65
|
* @classdesc
|
|
@@ -53,10 +67,11 @@ const behaviors = {
|
|
|
53
67
|
* @property {String} id - the id of the webchat.
|
|
54
68
|
* @property {String} container - the container to append the webchat to, defaults to 'body'
|
|
55
69
|
* @property {Placement} placement - the placement of the webchat within the container, defaults to "bottom-right".
|
|
56
|
-
* @property {
|
|
57
|
-
* @property {
|
|
58
|
-
* @property {Behaviour} behaviour - the behaviour of the webchat, defaults to 'popover'.
|
|
70
|
+
* @property {Mode} mode - how the webchat behaves while open, defaults to 'popover'.
|
|
71
|
+
* @property {Object} behaviour - runtime opening behaviour for the webchat.
|
|
59
72
|
* @property {Style} style - the style of the webchat.
|
|
73
|
+
* @property {Appearance} appearance - appearance overrides.
|
|
74
|
+
* @property {WhatsApp} whatsapp - WhatsApp handoff overrides.
|
|
60
75
|
* @property {Strategy} strategy - the strategy used to position the webchat. Defaults to 'absolute'
|
|
61
76
|
*/
|
|
62
77
|
|
|
@@ -64,10 +79,12 @@ class Webchat {
|
|
|
64
79
|
static _id
|
|
65
80
|
static _container = 'body'
|
|
66
81
|
static _placement = 'bottom-right'
|
|
67
|
-
static _classes = []
|
|
68
|
-
static _triggerClasses = []
|
|
69
82
|
static _style = {}
|
|
70
|
-
static
|
|
83
|
+
static _appearance = {}
|
|
84
|
+
static _whatsapp = {}
|
|
85
|
+
static _mode = modes.POPOVER
|
|
86
|
+
static _behaviour = null
|
|
87
|
+
static _hasBehaviourOverride = false
|
|
71
88
|
static _strategy = null
|
|
72
89
|
|
|
73
90
|
static set container(value) {
|
|
@@ -90,38 +107,6 @@ class Webchat {
|
|
|
90
107
|
return this._placement
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
static set classes(value) {
|
|
94
|
-
if (!Array.isArray(value) && typeof value !== 'string') {
|
|
95
|
-
throw new Error('classes must be an array or a string')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
this._classes = value
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
static get classes() {
|
|
102
|
-
if (typeof this._classes === 'string') {
|
|
103
|
-
return this._classes.split(',').map(c => c.trim())
|
|
104
|
-
} else {
|
|
105
|
-
return this._classes
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
static set triggerClasses(value) {
|
|
110
|
-
if (!Array.isArray(value) && typeof value !== 'string') {
|
|
111
|
-
throw new Error('triggerClasses must be an array or a string')
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
this._triggerClasses = value
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
static get triggerClasses() {
|
|
118
|
-
if (typeof this._triggerClasses === 'string') {
|
|
119
|
-
return this._triggerClasses.split(',').map(c => c.trim())
|
|
120
|
-
} else {
|
|
121
|
-
return this._triggerClasses
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
110
|
static set id(value) {
|
|
126
111
|
this._id = value
|
|
127
112
|
}
|
|
@@ -160,18 +145,113 @@ class Webchat {
|
|
|
160
145
|
this._style = value
|
|
161
146
|
}
|
|
162
147
|
|
|
148
|
+
static get appearance() {
|
|
149
|
+
return this._appearance
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static set appearance(value) {
|
|
153
|
+
if (!this.isPlainObject(value)) {
|
|
154
|
+
throw new Error('Appearance must be an object')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
Object.entries(value).forEach(([key, nestedValue]) => {
|
|
158
|
+
if (!['header', 'launcher'].includes(key)) {
|
|
159
|
+
throw new Error(`Invalid appearance property: ${key}`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!this.isPlainObject(nestedValue)) {
|
|
163
|
+
throw new Error(`Appearance ${key} must be an object`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Object.entries(nestedValue).forEach(([nestedKey, propertyValue]) => {
|
|
167
|
+
if (key === 'header' && nestedKey !== 'name') {
|
|
168
|
+
throw new Error(`Invalid appearance header property: ${nestedKey}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (key === 'launcher' && nestedKey !== 'iconUrl') {
|
|
172
|
+
throw new Error(`Invalid appearance launcher property: ${nestedKey}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (propertyValue == null) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (typeof propertyValue !== 'string') {
|
|
180
|
+
throw new Error(`Invalid appearance ${key}.${nestedKey} value: ${propertyValue}`)
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
this._appearance = value
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static get whatsapp() {
|
|
189
|
+
return this._whatsapp
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
static set whatsapp(value) {
|
|
193
|
+
if (!this.isPlainObject(value)) {
|
|
194
|
+
throw new Error('WhatsApp must be an object')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Object.entries(value).forEach(([key, nestedValue]) => {
|
|
198
|
+
if (!['number', 'restrictToChannel'].includes(key)) {
|
|
199
|
+
throw new Error(`Invalid WhatsApp property: ${key}`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (nestedValue == null) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (key === 'number' && typeof nestedValue !== 'string') {
|
|
207
|
+
throw new Error(`Invalid WhatsApp number value: ${nestedValue}`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (key === 'restrictToChannel' && typeof nestedValue !== 'boolean') {
|
|
211
|
+
throw new Error(`Invalid WhatsApp restrictToChannel value: ${nestedValue}`)
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
this._whatsapp = value
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static get mode() {
|
|
219
|
+
return this._mode
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
static set mode(value) {
|
|
223
|
+
if (!Object.values(modes).includes(value)) {
|
|
224
|
+
throw new Error(`Invalid mode value: ${value}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this._mode = value
|
|
228
|
+
}
|
|
229
|
+
|
|
163
230
|
static get behaviour() {
|
|
164
231
|
return this._behaviour
|
|
165
232
|
}
|
|
166
233
|
|
|
167
234
|
static set behaviour(value) {
|
|
168
|
-
if (
|
|
235
|
+
if (value == null) {
|
|
236
|
+
this._behaviour = value
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
169
241
|
throw new Error(`Invalid behaviour value: ${value}`)
|
|
170
242
|
}
|
|
171
243
|
|
|
172
244
|
this._behaviour = value
|
|
173
245
|
}
|
|
174
246
|
|
|
247
|
+
static get hasBehaviourOverride() {
|
|
248
|
+
return this._hasBehaviourOverride
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static set behaviourOverride(value) {
|
|
252
|
+
this._hasBehaviourOverride = !!value
|
|
253
|
+
}
|
|
254
|
+
|
|
175
255
|
static get strategy() {
|
|
176
256
|
if (this._strategy) {
|
|
177
257
|
return this._strategy
|
|
@@ -204,6 +284,10 @@ class Webchat {
|
|
|
204
284
|
/^rgba?\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3},?\s*(0|1|0?\.\d+)?\s*\)$/.test(value)
|
|
205
285
|
)
|
|
206
286
|
}
|
|
287
|
+
|
|
288
|
+
static isPlainObject(value) {
|
|
289
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
290
|
+
}
|
|
207
291
|
}
|
|
208
292
|
|
|
209
|
-
export {
|
|
293
|
+
export { modes, Webchat }
|
package/src/hellotext.js
CHANGED
|
@@ -28,10 +28,16 @@ class Hellotext {
|
|
|
28
28
|
this.query = new Query()
|
|
29
29
|
|
|
30
30
|
const businessData = await this.business.hydrate()
|
|
31
|
-
const webchatConfig = config.webchat === false ?
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const webchatConfig = config.webchat === false ?
|
|
32
|
+
false :
|
|
33
|
+
this.mergeWebchatConfig((businessData && businessData.webchat) || {}, config.webchat || {})
|
|
34
|
+
|
|
35
|
+
const hasExplicitBehaviourOverride = (
|
|
36
|
+
config.webchat &&
|
|
37
|
+
config.webchat !== false &&
|
|
38
|
+
Object.prototype.hasOwnProperty.call(config.webchat, 'behaviour')
|
|
39
|
+
)
|
|
40
|
+
Configuration.webchat.behaviourOverride = hasExplicitBehaviourOverride
|
|
35
41
|
|
|
36
42
|
if (webchatConfig && webchatConfig.id) {
|
|
37
43
|
Configuration.webchat.assign(webchatConfig)
|
|
@@ -43,6 +49,28 @@ class Hellotext {
|
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
static mergeWebchatConfig(dashboardConfig, localConfig) {
|
|
53
|
+
return this.deepMergePlainObjects(dashboardConfig, localConfig)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static deepMergePlainObjects(base, override) {
|
|
57
|
+
const result = { ...base }
|
|
58
|
+
|
|
59
|
+
Object.entries(override).forEach(([key, value]) => {
|
|
60
|
+
if (this.isPlainObject(value) && this.isPlainObject(result[key])) {
|
|
61
|
+
result[key] = this.deepMergePlainObjects(result[key], value)
|
|
62
|
+
} else {
|
|
63
|
+
result[key] = value
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static isPlainObject(value) {
|
|
71
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
72
|
+
}
|
|
73
|
+
|
|
46
74
|
/**
|
|
47
75
|
* Tracks an action that has happened on the page
|
|
48
76
|
*
|