@hellotext/hellotext 2.3.9 → 2.4.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/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 +165 -22
- package/lib/controllers/webchat_controller.js +173 -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 +187 -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,9 @@ export default class extends Controller {
|
|
|
51
56
|
'unreadCounter',
|
|
52
57
|
'typingIndicator',
|
|
53
58
|
'typingIndicatorTemplate',
|
|
59
|
+
'teaser',
|
|
60
|
+
'openingSequence',
|
|
61
|
+
'openingSequenceMessage',
|
|
54
62
|
]
|
|
55
63
|
|
|
56
64
|
initialize() {
|
|
@@ -63,6 +71,7 @@ export default class extends Controller {
|
|
|
63
71
|
)
|
|
64
72
|
|
|
65
73
|
this.files = []
|
|
74
|
+
this.messageIds = new Set()
|
|
66
75
|
|
|
67
76
|
this.onMessageReceived = this.onMessageReceived.bind(this)
|
|
68
77
|
this.onMessageReaction = this.onMessageReaction.bind(this)
|
|
@@ -77,13 +86,24 @@ export default class extends Controller {
|
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
connect() {
|
|
89
|
+
useBehaviour(this)
|
|
80
90
|
usePopover(this)
|
|
81
|
-
|
|
82
|
-
this
|
|
83
|
-
this.triggerTarget.classList.add(...WebchatConfiguration.triggerClasses)
|
|
91
|
+
useOpeningSequence(this)
|
|
92
|
+
useTeaser(this)
|
|
84
93
|
|
|
85
94
|
this.setupFloatingUI({ trigger: this.triggerTarget, popover: this.popoverTarget })
|
|
86
95
|
|
|
96
|
+
if (this.hasTeaserTarget) {
|
|
97
|
+
this.setupFloatingUI({
|
|
98
|
+
trigger: this.triggerTarget,
|
|
99
|
+
popover: this.teaserTarget,
|
|
100
|
+
strategy: 'absolute',
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.setupTeaser()
|
|
105
|
+
this.setupOpeningSequence()
|
|
106
|
+
|
|
87
107
|
this.webChatChannel.onMessage(this.onMessageReceived)
|
|
88
108
|
this.webChatChannel.onTypingStart(this.onTypingStart)
|
|
89
109
|
|
|
@@ -91,21 +111,22 @@ export default class extends Controller {
|
|
|
91
111
|
|
|
92
112
|
this.messagesContainerTarget.addEventListener('scroll', this.onScroll)
|
|
93
113
|
|
|
94
|
-
if (!Hellotext.business.features.white_label) {
|
|
95
|
-
this.toolbarTarget.appendChild(LogoBuilder.build())
|
|
96
|
-
}
|
|
97
|
-
|
|
98
114
|
if (this.shouldOpenOnMount) {
|
|
99
115
|
this.openValue = true
|
|
100
116
|
}
|
|
101
117
|
|
|
102
118
|
Hellotext.eventEmitter.dispatch('webchat:mounted')
|
|
103
119
|
this.broadcastChannel.addEventListener('message', this.onOutboundMessageSent)
|
|
120
|
+
this.scheduleBehaviourOpen()
|
|
104
121
|
|
|
105
122
|
super.connect()
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
disconnect() {
|
|
126
|
+
this.cancelBehaviourOpen()
|
|
127
|
+
this.teardownTeaser()
|
|
128
|
+
this.teardownOpeningSequence()
|
|
129
|
+
|
|
109
130
|
this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent)
|
|
110
131
|
this.messagesContainerTarget.removeEventListener('scroll', this.onScroll)
|
|
111
132
|
|
|
@@ -273,7 +294,7 @@ export default class extends Controller {
|
|
|
273
294
|
image.src = attachmentUrl
|
|
274
295
|
image.style.display = 'block'
|
|
275
296
|
|
|
276
|
-
|
|
297
|
+
this.messageAttachmentsContainer(element)?.appendChild(image)
|
|
277
298
|
})
|
|
278
299
|
}
|
|
279
300
|
|
|
@@ -291,7 +312,7 @@ export default class extends Controller {
|
|
|
291
312
|
|
|
292
313
|
onClickOutside(event) {
|
|
293
314
|
if (
|
|
294
|
-
WebchatConfiguration.
|
|
315
|
+
WebchatConfiguration.mode === modes.POPOVER &&
|
|
295
316
|
this.openValue &&
|
|
296
317
|
event.target.nodeType &&
|
|
297
318
|
this.element.contains(event.target) === false
|
|
@@ -310,6 +331,7 @@ export default class extends Controller {
|
|
|
310
331
|
|
|
311
332
|
onPopoverOpened() {
|
|
312
333
|
this.popoverTarget.classList.remove(...this.fadeOutClasses)
|
|
334
|
+
this.dismissTeaserForSession?.()
|
|
313
335
|
|
|
314
336
|
if (!this.onMobile) {
|
|
315
337
|
this.inputTarget.focus()
|
|
@@ -329,6 +351,12 @@ export default class extends Controller {
|
|
|
329
351
|
Hellotext.eventEmitter.dispatch('webchat:opened')
|
|
330
352
|
localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'opened')
|
|
331
353
|
|
|
354
|
+
if (this.messageTeaserValue) {
|
|
355
|
+
this.messageTeaserValue = null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.startOpeningSequence()
|
|
359
|
+
|
|
332
360
|
if (this.unreadCounterTarget.style.display === 'none') return
|
|
333
361
|
|
|
334
362
|
this.unreadCounterTarget.style.display = 'none'
|
|
@@ -367,7 +395,11 @@ export default class extends Controller {
|
|
|
367
395
|
}
|
|
368
396
|
|
|
369
397
|
onMessageReceived(message) {
|
|
370
|
-
const { id, body, attachments } = message
|
|
398
|
+
const { id, body, attachments, teaser } = message
|
|
399
|
+
|
|
400
|
+
if (!this.claimMessageId(id)) return
|
|
401
|
+
|
|
402
|
+
this.hideTeaser?.()
|
|
371
403
|
|
|
372
404
|
if (message.carousel) {
|
|
373
405
|
return this.insertCarouselMessage(message)
|
|
@@ -380,6 +412,8 @@ export default class extends Controller {
|
|
|
380
412
|
element.style.display = 'flex'
|
|
381
413
|
|
|
382
414
|
element.querySelector('[data-body]').innerHTML = div.innerHTML
|
|
415
|
+
|
|
416
|
+
element.setAttribute('data-id', id)
|
|
383
417
|
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
384
418
|
|
|
385
419
|
if (attachments) {
|
|
@@ -388,7 +422,7 @@ export default class extends Controller {
|
|
|
388
422
|
image.src = attachmentUrl
|
|
389
423
|
image.style.display = 'block'
|
|
390
424
|
|
|
391
|
-
|
|
425
|
+
this.messageAttachmentsContainer(element)?.appendChild(image)
|
|
392
426
|
})
|
|
393
427
|
}
|
|
394
428
|
|
|
@@ -402,21 +436,45 @@ export default class extends Controller {
|
|
|
402
436
|
|
|
403
437
|
element.scrollIntoView({ behavior: 'smooth' })
|
|
404
438
|
|
|
439
|
+
this.updateMessageTeaser(teaser)
|
|
440
|
+
|
|
405
441
|
if (this.openValue) {
|
|
406
442
|
this.messagesAPI.markAsSeen(id)
|
|
407
443
|
return
|
|
408
444
|
}
|
|
409
445
|
|
|
410
|
-
this.
|
|
446
|
+
this.incrementUnreadCounter()
|
|
447
|
+
}
|
|
411
448
|
|
|
412
|
-
|
|
413
|
-
|
|
449
|
+
// TCP broadcasts may deliver the same incoming message twice before Stimulus
|
|
450
|
+
// has connected the appended message target. Claiming the id in memory closes
|
|
451
|
+
// that window, while the target check still drops messages rendered earlier.
|
|
452
|
+
claimMessageId(id) {
|
|
453
|
+
const messageTargets = this.messageTargets || []
|
|
454
|
+
|
|
455
|
+
if (this.messageIds.has(id)) return false
|
|
456
|
+
|
|
457
|
+
this.messageIds.add(id)
|
|
458
|
+
|
|
459
|
+
return !messageTargets.some(element => element.dataset.id === id)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
updateMessageTeaser(teaser) {
|
|
463
|
+
this.messageTeaserValue = teaser
|
|
464
|
+
|
|
465
|
+
if (this.messageTeaserValue && this.hasTeaserTarget) {
|
|
466
|
+
this.teaserTarget.innerHTML = this.messageTeaserValue
|
|
467
|
+
this.teaserTarget.classList.toggle('invisible', this.openValue)
|
|
468
|
+
}
|
|
414
469
|
}
|
|
415
470
|
|
|
416
471
|
insertCarouselMessage(message) {
|
|
417
472
|
const html = message.html
|
|
418
473
|
const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild
|
|
419
474
|
|
|
475
|
+
element.setAttribute('data-id', message.id)
|
|
476
|
+
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
477
|
+
|
|
420
478
|
this.clearTypingIndicator()
|
|
421
479
|
this.messagesContainerTarget.appendChild(element)
|
|
422
480
|
|
|
@@ -427,14 +485,14 @@ export default class extends Controller {
|
|
|
427
485
|
body: element.querySelector('[data-body]')?.innerText || '',
|
|
428
486
|
})
|
|
429
487
|
|
|
488
|
+
this.updateMessageTeaser(message.teaser)
|
|
489
|
+
|
|
430
490
|
if (this.openValue) {
|
|
431
491
|
this.messagesAPI.markAsSeen(message.id)
|
|
432
492
|
return
|
|
433
493
|
}
|
|
434
494
|
|
|
435
|
-
this.
|
|
436
|
-
const unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1
|
|
437
|
-
this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
|
|
495
|
+
this.incrementUnreadCounter()
|
|
438
496
|
}
|
|
439
497
|
|
|
440
498
|
resizeInput() {
|
|
@@ -451,6 +509,8 @@ export default class extends Controller {
|
|
|
451
509
|
}
|
|
452
510
|
|
|
453
511
|
async sendQuickReplyMessage({ detail: { id, product, buttonId, body, cardElement } }) {
|
|
512
|
+
this.dismissTeaserForSession?.()
|
|
513
|
+
|
|
454
514
|
const formData = new FormData()
|
|
455
515
|
|
|
456
516
|
formData.append('message[body]', body)
|
|
@@ -460,6 +520,7 @@ export default class extends Controller {
|
|
|
460
520
|
|
|
461
521
|
formData.append('session', Hellotext.session)
|
|
462
522
|
formData.append('locale', Locale.toString())
|
|
523
|
+
this.appendOpeningSequenceMessageIds(formData)
|
|
463
524
|
|
|
464
525
|
const element = this.buildMessageElement()
|
|
465
526
|
const attachment = cardElement.querySelector('img')?.cloneNode(true)
|
|
@@ -470,7 +531,7 @@ export default class extends Controller {
|
|
|
470
531
|
attachment.removeAttribute('width')
|
|
471
532
|
attachment.removeAttribute('height')
|
|
472
533
|
|
|
473
|
-
|
|
534
|
+
this.messageAttachmentsContainer(element)?.appendChild(attachment)
|
|
474
535
|
}
|
|
475
536
|
|
|
476
537
|
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
@@ -503,6 +564,7 @@ export default class extends Controller {
|
|
|
503
564
|
const data = await response.json()
|
|
504
565
|
|
|
505
566
|
this.dispatch('set:id', { target: element, detail: data.id })
|
|
567
|
+
this.clearRevealedOpeningSequenceMessageIds()
|
|
506
568
|
|
|
507
569
|
const message = {
|
|
508
570
|
id: data.id,
|
|
@@ -517,9 +579,94 @@ export default class extends Controller {
|
|
|
517
579
|
Hellotext.eventEmitter.dispatch('webchat:message:sent', message)
|
|
518
580
|
}
|
|
519
581
|
|
|
520
|
-
async
|
|
582
|
+
async sendTeaserQuickReply(event) {
|
|
583
|
+
event.preventDefault()
|
|
584
|
+
event.stopPropagation()
|
|
585
|
+
|
|
586
|
+
const button = event.currentTarget
|
|
587
|
+
const value = (button.dataset.value || '').trim()
|
|
588
|
+
const label = [button.dataset.text, button.textContent]
|
|
589
|
+
.map(text => (text || '').trim())
|
|
590
|
+
.find(text => text.length > 0)
|
|
591
|
+
const text = value || label
|
|
592
|
+
|
|
593
|
+
if (!text) return
|
|
594
|
+
|
|
595
|
+
this.dismissTeaserForSession?.()
|
|
596
|
+
this.show()
|
|
597
|
+
|
|
598
|
+
const buttonType = (button.dataset.type || '').trim() || 'quick_reply'
|
|
521
599
|
const formData = new FormData()
|
|
522
600
|
|
|
601
|
+
formData.append('message[body]', text)
|
|
602
|
+
formData.append('session', Hellotext.session)
|
|
603
|
+
formData.append('locale', Locale.toString())
|
|
604
|
+
this.appendOpeningSequenceMessageIds(formData)
|
|
605
|
+
|
|
606
|
+
const element = this.buildMessageElement()
|
|
607
|
+
|
|
608
|
+
element.querySelector('[data-body]').innerText = text
|
|
609
|
+
|
|
610
|
+
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
611
|
+
this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget)
|
|
612
|
+
} else {
|
|
613
|
+
this.messagesContainerTarget.appendChild(element)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
element.scrollIntoView({ behavior: 'smooth' })
|
|
617
|
+
|
|
618
|
+
this.broadcastChannel.postMessage({
|
|
619
|
+
type: 'message:sent',
|
|
620
|
+
element: element.outerHTML,
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
if (!this.typingIndicatorVisible) {
|
|
624
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
625
|
+
this.optimisticTypingTimeout = setTimeout(() => {
|
|
626
|
+
this.showOptimisticTypingIndicator()
|
|
627
|
+
}, this.optimisticTypingIndicatorWaitValue)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const response = await this.messagesAPI.create(formData)
|
|
631
|
+
|
|
632
|
+
if (response.failed) {
|
|
633
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
634
|
+
|
|
635
|
+
this.broadcastChannel.postMessage({
|
|
636
|
+
type: 'message:failed',
|
|
637
|
+
id: element.id,
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
return element.classList.add('failed')
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const data = await response.json()
|
|
644
|
+
element.setAttribute('data-id', data.id)
|
|
645
|
+
this.clearRevealedOpeningSequenceMessageIds()
|
|
646
|
+
|
|
647
|
+
Hellotext.eventEmitter.dispatch('webchat:message:sent', {
|
|
648
|
+
id: data.id,
|
|
649
|
+
body: text,
|
|
650
|
+
attachments: [],
|
|
651
|
+
type: 'quick_reply',
|
|
652
|
+
teaser: {
|
|
653
|
+
text: label || text,
|
|
654
|
+
value: value || text,
|
|
655
|
+
type: buttonType,
|
|
656
|
+
},
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
if (data.conversation && data.conversation !== this.conversationIdValue) {
|
|
660
|
+
this.conversationIdValue = data.conversation
|
|
661
|
+
this.webChatChannel.updateSubscriptionWith(this.conversationIdValue)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (this.typingIndicatorVisible) {
|
|
665
|
+
this.resetTypingIndicatorTimer()
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async sendMessage(e) {
|
|
523
670
|
const message = {
|
|
524
671
|
body: this.inputTarget.value,
|
|
525
672
|
attachments: this.files,
|
|
@@ -533,6 +680,10 @@ export default class extends Controller {
|
|
|
533
680
|
return
|
|
534
681
|
}
|
|
535
682
|
|
|
683
|
+
this.dismissTeaserForSession?.()
|
|
684
|
+
|
|
685
|
+
const formData = new FormData()
|
|
686
|
+
|
|
536
687
|
if (this.inputTarget.value.trim().length > 0) {
|
|
537
688
|
formData.append('message[body]', this.inputTarget.value)
|
|
538
689
|
} else {
|
|
@@ -545,6 +696,7 @@ export default class extends Controller {
|
|
|
545
696
|
|
|
546
697
|
formData.append('session', Hellotext.session)
|
|
547
698
|
formData.append('locale', Locale.toString())
|
|
699
|
+
this.appendOpeningSequenceMessageIds(formData)
|
|
548
700
|
|
|
549
701
|
const element = this.buildMessageElement()
|
|
550
702
|
|
|
@@ -558,7 +710,7 @@ export default class extends Controller {
|
|
|
558
710
|
|
|
559
711
|
if (attachments.length > 0) {
|
|
560
712
|
attachments.forEach(attachment => {
|
|
561
|
-
|
|
713
|
+
this.messageAttachmentsContainer(element)?.appendChild(attachment.cloneNode(true))
|
|
562
714
|
})
|
|
563
715
|
}
|
|
564
716
|
|
|
@@ -614,6 +766,7 @@ export default class extends Controller {
|
|
|
614
766
|
const data = await response.json()
|
|
615
767
|
element.setAttribute('data-id', data.id)
|
|
616
768
|
message.id = data.id
|
|
769
|
+
this.clearRevealedOpeningSequenceMessageIds()
|
|
617
770
|
|
|
618
771
|
Hellotext.eventEmitter.dispatch('webchat:message:sent', message)
|
|
619
772
|
|
|
@@ -644,6 +797,17 @@ export default class extends Controller {
|
|
|
644
797
|
return element
|
|
645
798
|
}
|
|
646
799
|
|
|
800
|
+
messageAttachmentsContainer(element) {
|
|
801
|
+
return element.querySelector('[data-attachments-container], [data-attachment-container]')
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
incrementUnreadCounter() {
|
|
805
|
+
this.unreadCounterTarget.style.display = 'flex'
|
|
806
|
+
|
|
807
|
+
const unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1
|
|
808
|
+
this.unreadCounterTarget.innerText = Math.min(unreadCount, 9)
|
|
809
|
+
}
|
|
810
|
+
|
|
647
811
|
openAttachment() {
|
|
648
812
|
this.attachmentInputTarget.click()
|
|
649
813
|
}
|
|
@@ -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
|
*
|
package/src/models/business.js
CHANGED
|
@@ -10,6 +10,8 @@ import BusinessesAPI from '../api/businesses'
|
|
|
10
10
|
/**
|
|
11
11
|
* @typedef {Object} BusinessWebchat
|
|
12
12
|
* @property {String} [id] - Dashboard webchat id configured for the business.
|
|
13
|
+
* @property {Object} [appearance] - Dashboard appearance defaults for the webchat.
|
|
14
|
+
* @property {Object} [whatsapp] - Dashboard WhatsApp handoff defaults for the webchat.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
/**
|