@hellotext/hellotext 2.1.6 → 2.2.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/lib/controllers/message_controller.cjs +124 -0
- package/lib/controllers/message_controller.js +120 -0
- package/lib/controllers/webchat_controller.cjs +105 -6
- package/lib/controllers/webchat_controller.js +116 -11
- package/lib/index.cjs +2 -0
- package/lib/index.js +2 -0
- package/package.json +1 -1
- package/src/controllers/message_controller.js +95 -0
- package/src/controllers/webchat_controller.js +113 -9
- package/src/index.js +2 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = {
|
|
5
|
+
id: String,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
static targets = ['carouselContainer', 'leftFade', 'rightFade', 'carouselCard']
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.updateFades()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setId({ detail: id }) {
|
|
15
|
+
this.idValue = id
|
|
16
|
+
this.element.id = id
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
onScroll() {
|
|
20
|
+
this.updateFades()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
quickReply({ currentTarget }) {
|
|
24
|
+
const card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]')
|
|
25
|
+
|
|
26
|
+
this.dispatch('quickReply', {
|
|
27
|
+
detail: {
|
|
28
|
+
id: this.idValue,
|
|
29
|
+
product: card.dataset.id,
|
|
30
|
+
buttonId: currentTarget.dataset.id,
|
|
31
|
+
body: currentTarget.dataset.text,
|
|
32
|
+
cardElement: card,
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
moveToLeft() {
|
|
38
|
+
if (!this.hasCarouselContainerTarget) return
|
|
39
|
+
|
|
40
|
+
const scrollAmount = this.getScrollAmount()
|
|
41
|
+
|
|
42
|
+
this.carouselContainerTarget.scrollBy({
|
|
43
|
+
left: -scrollAmount,
|
|
44
|
+
behavior: 'smooth',
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
moveToRight() {
|
|
49
|
+
if (!this.hasCarouselContainerTarget) return
|
|
50
|
+
|
|
51
|
+
const scrollAmount = this.getScrollAmount()
|
|
52
|
+
|
|
53
|
+
this.carouselContainerTarget.scrollBy({
|
|
54
|
+
left: scrollAmount,
|
|
55
|
+
behavior: 'smooth',
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getScrollAmount() {
|
|
60
|
+
// Get the actual card width from DOM
|
|
61
|
+
const firstCard = this.carouselContainerTarget.querySelector('.message__carousel_card')
|
|
62
|
+
|
|
63
|
+
if (!firstCard) {
|
|
64
|
+
return 280 // Fallback to default desktop card width
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cardWidth = firstCard.offsetWidth
|
|
68
|
+
const gap = 16 // gap-x-4 = 1rem = 16px
|
|
69
|
+
|
|
70
|
+
return cardWidth + gap
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
updateFades() {
|
|
74
|
+
if (!this.hasCarouselContainerTarget) return
|
|
75
|
+
|
|
76
|
+
const scrollLeft = this.carouselContainerTarget.scrollLeft
|
|
77
|
+
const maxScroll =
|
|
78
|
+
this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth
|
|
79
|
+
|
|
80
|
+
// Show left fade if scrolled past start
|
|
81
|
+
if (scrollLeft > 0) {
|
|
82
|
+
this.leftFadeTarget.classList.remove('hidden')
|
|
83
|
+
} else {
|
|
84
|
+
this.leftFadeTarget.classList.add('hidden')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Show right fade if not at end
|
|
88
|
+
if (scrollLeft < maxScroll - 1) {
|
|
89
|
+
// -1 for rounding errors
|
|
90
|
+
this.rightFadeTarget.classList.remove('hidden')
|
|
91
|
+
} else {
|
|
92
|
+
this.rightFadeTarget.classList.add('hidden')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -309,6 +309,8 @@ export default class extends Controller {
|
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
onPopoverOpened() {
|
|
312
|
+
this.popoverTarget.classList.remove(...this.fadeOutClasses)
|
|
313
|
+
|
|
312
314
|
if (!this.onMobile) {
|
|
313
315
|
this.inputTarget.focus()
|
|
314
316
|
}
|
|
@@ -367,6 +369,10 @@ export default class extends Controller {
|
|
|
367
369
|
onMessageReceived(message) {
|
|
368
370
|
const { id, body, attachments } = message
|
|
369
371
|
|
|
372
|
+
if (message.carousel) {
|
|
373
|
+
return this.insertCarouselMessage(message)
|
|
374
|
+
}
|
|
375
|
+
|
|
370
376
|
const div = document.createElement('div')
|
|
371
377
|
div.innerHTML = body
|
|
372
378
|
|
|
@@ -407,6 +413,30 @@ export default class extends Controller {
|
|
|
407
413
|
this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
|
|
408
414
|
}
|
|
409
415
|
|
|
416
|
+
insertCarouselMessage(message) {
|
|
417
|
+
const html = message.html
|
|
418
|
+
const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild
|
|
419
|
+
|
|
420
|
+
this.clearTypingIndicator()
|
|
421
|
+
this.messagesContainerTarget.appendChild(element)
|
|
422
|
+
|
|
423
|
+
element.scrollIntoView({ behavior: 'smooth' })
|
|
424
|
+
|
|
425
|
+
Hellotext.eventEmitter.dispatch('webchat:message:received', {
|
|
426
|
+
...message,
|
|
427
|
+
body: element.querySelector('[data-body]')?.innerText || '',
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
if (this.openValue) {
|
|
431
|
+
this.messagesAPI.markAsSeen(message.id)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
this.unreadCounterTarget.style.display = 'flex'
|
|
436
|
+
const unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1
|
|
437
|
+
this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
|
|
438
|
+
}
|
|
439
|
+
|
|
410
440
|
resizeInput() {
|
|
411
441
|
const maxHeight = 96
|
|
412
442
|
|
|
@@ -420,6 +450,73 @@ export default class extends Controller {
|
|
|
420
450
|
this.inputTarget.style.height = `${Math.min(scrollHeight, maxHeight)}px`
|
|
421
451
|
}
|
|
422
452
|
|
|
453
|
+
async sendQuickReplyMessage({ detail: { id, product, buttonId, body, cardElement } }) {
|
|
454
|
+
const formData = new FormData()
|
|
455
|
+
|
|
456
|
+
formData.append('message[body]', body)
|
|
457
|
+
formData.append('message[replied_to]', id)
|
|
458
|
+
formData.append('message[product]', product)
|
|
459
|
+
formData.append('message[button]', buttonId)
|
|
460
|
+
|
|
461
|
+
formData.append('session', Hellotext.session)
|
|
462
|
+
formData.append('locale', Locale.toString())
|
|
463
|
+
|
|
464
|
+
const element = this.buildMessageElement()
|
|
465
|
+
const attachment = cardElement.querySelector('img')?.cloneNode(true)
|
|
466
|
+
|
|
467
|
+
element.querySelector('[data-body]').innerText = body
|
|
468
|
+
|
|
469
|
+
if (attachment) {
|
|
470
|
+
attachment.removeAttribute('width')
|
|
471
|
+
attachment.removeAttribute('height')
|
|
472
|
+
|
|
473
|
+
element.querySelector('[data-attachment-container]').appendChild(attachment)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
477
|
+
this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget)
|
|
478
|
+
} else {
|
|
479
|
+
this.messagesContainerTarget.appendChild(element)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
element.scrollIntoView({ behavior: 'smooth' })
|
|
483
|
+
|
|
484
|
+
this.broadcastChannel.postMessage({
|
|
485
|
+
type: 'message:sent',
|
|
486
|
+
element: element.outerHTML,
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const response = await this.messagesAPI.create(formData)
|
|
490
|
+
|
|
491
|
+
if (response.failed) {
|
|
492
|
+
// Clear the optimistic typing indicator on failure
|
|
493
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
494
|
+
|
|
495
|
+
this.broadcastChannel.postMessage({
|
|
496
|
+
type: 'message:failed',
|
|
497
|
+
id: element.id,
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
return element.classList.add('failed')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const data = await response.json()
|
|
504
|
+
|
|
505
|
+
this.dispatch('set:id', { target: element, detail: data.id })
|
|
506
|
+
|
|
507
|
+
const message = {
|
|
508
|
+
id: data.id,
|
|
509
|
+
body: body,
|
|
510
|
+
attachments: attachment ? [attachment.src] : [],
|
|
511
|
+
replied_to: id,
|
|
512
|
+
product: product,
|
|
513
|
+
button: buttonId,
|
|
514
|
+
type: 'quick_reply',
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
Hellotext.eventEmitter.dispatch('webchat:message:sent', message)
|
|
518
|
+
}
|
|
519
|
+
|
|
423
520
|
async sendMessage(e) {
|
|
424
521
|
const formData = new FormData()
|
|
425
522
|
|
|
@@ -449,14 +546,7 @@ export default class extends Controller {
|
|
|
449
546
|
formData.append('session', Hellotext.session)
|
|
450
547
|
formData.append('locale', Locale.toString())
|
|
451
548
|
|
|
452
|
-
const element = this.
|
|
453
|
-
|
|
454
|
-
element.id = `hellotext--webchat--${this.idValue}--message--${Date.now()}`
|
|
455
|
-
|
|
456
|
-
element.classList.add('received')
|
|
457
|
-
element.style.removeProperty('display')
|
|
458
|
-
|
|
459
|
-
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
549
|
+
const element = this.buildMessageElement()
|
|
460
550
|
|
|
461
551
|
if (this.inputTarget.value.trim().length > 0) {
|
|
462
552
|
element.querySelector('[data-body]').innerText = this.inputTarget.value
|
|
@@ -468,7 +558,7 @@ export default class extends Controller {
|
|
|
468
558
|
|
|
469
559
|
if (attachments.length > 0) {
|
|
470
560
|
attachments.forEach(attachment => {
|
|
471
|
-
element.querySelector('[data-attachment-container]').appendChild(attachment)
|
|
561
|
+
element.querySelector('[data-attachment-container]').appendChild(attachment.cloneNode(true))
|
|
472
562
|
})
|
|
473
563
|
}
|
|
474
564
|
|
|
@@ -540,6 +630,20 @@ export default class extends Controller {
|
|
|
540
630
|
this.attachmentContainerTarget.style.display = ''
|
|
541
631
|
}
|
|
542
632
|
|
|
633
|
+
buildMessageElement() {
|
|
634
|
+
const element = this.messageTemplateTarget.cloneNode(true)
|
|
635
|
+
|
|
636
|
+
element.id = `hellotext--webchat--${this.idValue}--message--${Date.now()}`
|
|
637
|
+
|
|
638
|
+
element.classList.add('received')
|
|
639
|
+
element.style.removeProperty('display')
|
|
640
|
+
|
|
641
|
+
element.setAttribute('data-controller', 'hellotext--message')
|
|
642
|
+
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
643
|
+
|
|
644
|
+
return element
|
|
645
|
+
}
|
|
646
|
+
|
|
543
647
|
openAttachment() {
|
|
544
648
|
this.attachmentInputTarget.click()
|
|
545
649
|
}
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Application } from '@hotwired/stimulus'
|
|
|
2
2
|
import Hellotext from './hellotext'
|
|
3
3
|
|
|
4
4
|
import FormController from './controllers/form_controller'
|
|
5
|
+
import MessageController from './controllers/message_controller'
|
|
5
6
|
import WebChatEmojiController from './controllers/webchat/emoji_picker_controller'
|
|
6
7
|
import WebchatController from './controllers/webchat_controller'
|
|
7
8
|
|
|
@@ -10,6 +11,7 @@ const application = Application.start()
|
|
|
10
11
|
application.register('hellotext--form', FormController)
|
|
11
12
|
application.register('hellotext--webchat', WebchatController)
|
|
12
13
|
application.register('hellotext--webchat--emoji', WebChatEmojiController)
|
|
14
|
+
application.register('hellotext--message', MessageController)
|
|
13
15
|
|
|
14
16
|
window.Hellotext = Hellotext
|
|
15
17
|
|