@hellotext/hellotext 2.4.2 → 2.4.3
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 +144 -25
- package/lib/controllers/message_controller.js +145 -25
- package/lib/controllers/mixins/usePopover.cjs +2 -0
- package/lib/controllers/mixins/usePopover.js +2 -0
- package/lib/controllers/webchat/useTeaser.cjs +3 -1
- package/lib/controllers/webchat/useTeaser.js +3 -1
- package/lib/controllers/webchat_controller.cjs +195 -25
- package/lib/controllers/webchat_controller.js +209 -25
- package/lib/core/configuration.cjs +19 -0
- package/lib/core/configuration.js +19 -0
- package/lib/models/business.cjs +86 -5
- package/lib/models/business.js +82 -5
- package/lib/models/webchat.cjs +18 -3
- package/lib/models/webchat.js +27 -6
- package/package.json +1 -1
- package/src/controllers/message_controller.js +145 -22
- package/src/controllers/mixins/usePopover.js +1 -0
- package/src/controllers/webchat/useTeaser.js +4 -1
- package/src/controllers/webchat_controller.js +219 -27
- package/src/core/configuration.js +25 -0
- package/src/models/business.js +86 -5
- package/src/models/webchat.js +22 -3
|
@@ -13,7 +13,15 @@ import { useBehaviour } from './webchat/useBehaviour'
|
|
|
13
13
|
import { useOpeningSequence } from './webchat/useOpeningSequence'
|
|
14
14
|
import { useTeaser } from './webchat/useTeaser'
|
|
15
15
|
|
|
16
|
+
const POPOVER_ANIMATION_DURATION = 120
|
|
17
|
+
const MESSAGE_TIMESTAMP_FORMAT_OPTIONS = {
|
|
18
|
+
hour: 'numeric',
|
|
19
|
+
minute: '2-digit',
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
export default class extends Controller {
|
|
23
|
+
static messageTimestampFormatters = {}
|
|
24
|
+
|
|
17
25
|
static values = {
|
|
18
26
|
id: String,
|
|
19
27
|
conversationId: String,
|
|
@@ -83,6 +91,7 @@ export default class extends Controller {
|
|
|
83
91
|
this.onScroll = this.onScroll.bind(this)
|
|
84
92
|
|
|
85
93
|
this.onOutboundMessageSent = this.onOutboundMessageSent.bind(this)
|
|
94
|
+
this.closePopoverOnEscape = this.closePopoverOnEscape.bind(this)
|
|
86
95
|
this.broadcastChannel = new BroadcastChannel(`hellotext--webchat--${this.idValue}`)
|
|
87
96
|
|
|
88
97
|
super.initialize()
|
|
@@ -106,6 +115,7 @@ export default class extends Controller {
|
|
|
106
115
|
|
|
107
116
|
this.setupTeaser()
|
|
108
117
|
this.setupOpeningSequence()
|
|
118
|
+
this.localizeMessageTimestamps()
|
|
109
119
|
|
|
110
120
|
this.webChatChannel.onMessage(this.onMessageReceived)
|
|
111
121
|
this.webChatChannel.onTypingStart(this.onTypingStart)
|
|
@@ -120,6 +130,7 @@ export default class extends Controller {
|
|
|
120
130
|
|
|
121
131
|
Hellotext.eventEmitter.dispatch('webchat:mounted')
|
|
122
132
|
this.broadcastChannel.addEventListener('message', this.onOutboundMessageSent)
|
|
133
|
+
window.addEventListener('keydown', this.closePopoverOnEscape, true)
|
|
123
134
|
this.scheduleBehaviourOpen()
|
|
124
135
|
|
|
125
136
|
super.connect()
|
|
@@ -127,11 +138,13 @@ export default class extends Controller {
|
|
|
127
138
|
|
|
128
139
|
disconnect() {
|
|
129
140
|
this.cancelBehaviourOpen()
|
|
141
|
+
this.clearPopoverOpenAnimation()
|
|
130
142
|
this.teardownTeaser()
|
|
131
143
|
this.teardownOpeningSequence()
|
|
132
144
|
|
|
133
145
|
this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent)
|
|
134
146
|
this.messagesContainerTarget.removeEventListener('scroll', this.onScroll)
|
|
147
|
+
window.removeEventListener('keydown', this.closePopoverOnEscape, true)
|
|
135
148
|
|
|
136
149
|
// Clean up typing indicator timeouts
|
|
137
150
|
this.clearTypingIndicator()
|
|
@@ -228,6 +241,7 @@ export default class extends Controller {
|
|
|
228
241
|
'message:sent': data => {
|
|
229
242
|
const element = new DOMParser().parseFromString(data.element, 'text/html').body
|
|
230
243
|
.firstElementChild
|
|
244
|
+
this.localizeMessageTimestamps(element)
|
|
231
245
|
|
|
232
246
|
// Insert message before typing indicator if one exists
|
|
233
247
|
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
@@ -239,7 +253,9 @@ export default class extends Controller {
|
|
|
239
253
|
element.scrollIntoView({ behavior: 'instant' })
|
|
240
254
|
},
|
|
241
255
|
'message:failed': data => {
|
|
242
|
-
this.messagesContainerTarget.querySelector(`#${data.id}`)
|
|
256
|
+
const element = this.messagesContainerTarget.querySelector(`#${data.id}`)
|
|
257
|
+
|
|
258
|
+
this.markMessageFailed(element, data.reason)
|
|
243
259
|
},
|
|
244
260
|
}
|
|
245
261
|
|
|
@@ -271,6 +287,7 @@ export default class extends Controller {
|
|
|
271
287
|
|
|
272
288
|
messages.forEach(message => {
|
|
273
289
|
const { body, attachments } = message
|
|
290
|
+
const createdAt = message.created_at || message.createdAt
|
|
274
291
|
|
|
275
292
|
const div = document.createElement('div')
|
|
276
293
|
div.innerHTML = body
|
|
@@ -302,6 +319,7 @@ export default class extends Controller {
|
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
element.setAttribute('data-body', body)
|
|
322
|
+
this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt)
|
|
305
323
|
this.messagesContainerTarget.prepend(element)
|
|
306
324
|
})
|
|
307
325
|
|
|
@@ -325,11 +343,29 @@ export default class extends Controller {
|
|
|
325
343
|
}
|
|
326
344
|
|
|
327
345
|
closePopover() {
|
|
328
|
-
this.
|
|
346
|
+
this.clearPopoverOpenAnimation()
|
|
347
|
+
this.popoverTarget.classList.remove(...this.fadeOutClasses)
|
|
348
|
+
this.openValue = false
|
|
349
|
+
}
|
|
329
350
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
351
|
+
preparePopoverOpenAnimation() {
|
|
352
|
+
this.clearPopoverOpenAnimation()
|
|
353
|
+
this.popoverTarget.classList.remove(...this.fadeOutClasses)
|
|
354
|
+
this.popoverTarget.classList.add('hellotext--webchat-popover-opening')
|
|
355
|
+
|
|
356
|
+
this.popoverOpenAnimationTimeout = setTimeout(() => {
|
|
357
|
+
this.popoverTarget.classList.remove('hellotext--webchat-popover-opening')
|
|
358
|
+
this.popoverOpenAnimationTimeout = null
|
|
359
|
+
}, POPOVER_ANIMATION_DURATION)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
clearPopoverOpenAnimation() {
|
|
363
|
+
if (this.popoverOpenAnimationTimeout) {
|
|
364
|
+
clearTimeout(this.popoverOpenAnimationTimeout)
|
|
365
|
+
this.popoverOpenAnimationTimeout = null
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.popoverTarget?.classList.remove('hellotext--webchat-popover-opening')
|
|
333
369
|
}
|
|
334
370
|
|
|
335
371
|
onPopoverOpened() {
|
|
@@ -369,6 +405,7 @@ export default class extends Controller {
|
|
|
369
405
|
}
|
|
370
406
|
|
|
371
407
|
onPopoverClosed() {
|
|
408
|
+
this.clearPopoverOpenAnimation()
|
|
372
409
|
Hellotext.eventEmitter.dispatch('webchat:closed')
|
|
373
410
|
localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'closed')
|
|
374
411
|
}
|
|
@@ -399,6 +436,7 @@ export default class extends Controller {
|
|
|
399
436
|
|
|
400
437
|
onMessageReceived(message) {
|
|
401
438
|
const { id, body, attachments, teaser } = message
|
|
439
|
+
const createdAt = message.created_at || message.createdAt
|
|
402
440
|
|
|
403
441
|
if (!this.claimMessageId(id)) return
|
|
404
442
|
|
|
@@ -418,6 +456,7 @@ export default class extends Controller {
|
|
|
418
456
|
|
|
419
457
|
element.setAttribute('data-id', id)
|
|
420
458
|
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
459
|
+
this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), createdAt)
|
|
421
460
|
|
|
422
461
|
if (attachments) {
|
|
423
462
|
attachments.forEach(attachmentUrl => {
|
|
@@ -487,6 +526,7 @@ export default class extends Controller {
|
|
|
487
526
|
|
|
488
527
|
element.setAttribute('data-id', message.id)
|
|
489
528
|
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
529
|
+
this.localizeMessageTimestamps(element)
|
|
490
530
|
|
|
491
531
|
this.clearTypingIndicator()
|
|
492
532
|
this.messagesContainerTarget.appendChild(element)
|
|
@@ -527,16 +567,16 @@ export default class extends Controller {
|
|
|
527
567
|
const formData = new FormData()
|
|
528
568
|
|
|
529
569
|
formData.append('message[body]', body)
|
|
530
|
-
formData.append('message[replied_to]', id)
|
|
531
|
-
formData.append('message[product]', product)
|
|
532
|
-
formData.append('message[button]', buttonId)
|
|
570
|
+
if (id) formData.append('message[replied_to]', id)
|
|
571
|
+
if (product) formData.append('message[product]', product)
|
|
572
|
+
if (buttonId) formData.append('message[button]', buttonId)
|
|
533
573
|
|
|
534
574
|
formData.append('session', Hellotext.session)
|
|
535
575
|
formData.append('locale', Locale.toString())
|
|
536
576
|
this.appendOpeningSequenceMessageIds(formData)
|
|
537
577
|
|
|
538
578
|
const element = this.buildMessageElement()
|
|
539
|
-
const attachment = cardElement
|
|
579
|
+
const attachment = cardElement?.querySelector('img')?.cloneNode(true)
|
|
540
580
|
|
|
541
581
|
element.querySelector('[data-body]').innerText = body
|
|
542
582
|
|
|
@@ -566,17 +606,13 @@ export default class extends Controller {
|
|
|
566
606
|
// Clear the optimistic typing indicator on failure
|
|
567
607
|
clearTimeout(this.optimisticTypingTimeout)
|
|
568
608
|
|
|
569
|
-
this.
|
|
570
|
-
type: 'message:failed',
|
|
571
|
-
id: element.id,
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
return element.classList.add('failed')
|
|
609
|
+
return this.markMessageFailedFromResponse(response, element)
|
|
575
610
|
}
|
|
576
611
|
|
|
577
612
|
const data = await response.json()
|
|
578
613
|
|
|
579
614
|
this.dispatch('set:id', { target: element, detail: data.id })
|
|
615
|
+
this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt)
|
|
580
616
|
this.clearRevealedOpeningSequenceMessageIds()
|
|
581
617
|
|
|
582
618
|
const message = {
|
|
@@ -645,16 +681,12 @@ export default class extends Controller {
|
|
|
645
681
|
if (response.failed) {
|
|
646
682
|
clearTimeout(this.optimisticTypingTimeout)
|
|
647
683
|
|
|
648
|
-
this.
|
|
649
|
-
type: 'message:failed',
|
|
650
|
-
id: element.id,
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
return element.classList.add('failed')
|
|
684
|
+
return this.markMessageFailedFromResponse(response, element)
|
|
654
685
|
}
|
|
655
686
|
|
|
656
687
|
const data = await response.json()
|
|
657
688
|
element.setAttribute('data-id', data.id)
|
|
689
|
+
this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt)
|
|
658
690
|
this.clearRevealedOpeningSequenceMessageIds()
|
|
659
691
|
|
|
660
692
|
Hellotext.eventEmitter.dispatch('webchat:message:sent', {
|
|
@@ -768,17 +800,13 @@ export default class extends Controller {
|
|
|
768
800
|
// Clear the optimistic typing indicator on failure
|
|
769
801
|
clearTimeout(this.optimisticTypingTimeout)
|
|
770
802
|
|
|
771
|
-
this.
|
|
772
|
-
type: 'message:failed',
|
|
773
|
-
id: element.id,
|
|
774
|
-
})
|
|
775
|
-
|
|
776
|
-
return element.classList.add('failed')
|
|
803
|
+
return this.markMessageFailedFromResponse(response, element)
|
|
777
804
|
}
|
|
778
805
|
|
|
779
806
|
const data = await response.json()
|
|
780
807
|
element.setAttribute('data-id', data.id)
|
|
781
808
|
message.id = data.id
|
|
809
|
+
this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), data.created_at || data.createdAt)
|
|
782
810
|
this.clearRevealedOpeningSequenceMessageIds()
|
|
783
811
|
|
|
784
812
|
Hellotext.eventEmitter.dispatch('webchat:message:sent', message)
|
|
@@ -807,9 +835,173 @@ export default class extends Controller {
|
|
|
807
835
|
element.setAttribute('data-controller', 'hellotext--message')
|
|
808
836
|
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
809
837
|
|
|
838
|
+
this.localizeMessageTimestamp(element.querySelector('[data-message-timestamp]'), new Date())
|
|
810
839
|
return element
|
|
811
840
|
}
|
|
812
841
|
|
|
842
|
+
focusCompose(event) {
|
|
843
|
+
const { target } = event
|
|
844
|
+
const ignoredSelector = [
|
|
845
|
+
'button',
|
|
846
|
+
'a',
|
|
847
|
+
'input',
|
|
848
|
+
'textarea',
|
|
849
|
+
'select',
|
|
850
|
+
'label',
|
|
851
|
+
'[role="button"]',
|
|
852
|
+
'em-emoji-picker',
|
|
853
|
+
'[data-hellotext--webchat--emoji-target~="popover"]',
|
|
854
|
+
'[data-controller~="hellotext--webchat--emoji"]',
|
|
855
|
+
].join(', ')
|
|
856
|
+
|
|
857
|
+
if (!this.hasInputTarget || target.closest(ignoredSelector)) return
|
|
858
|
+
|
|
859
|
+
event.preventDefault()
|
|
860
|
+
this.inputTarget.focus()
|
|
861
|
+
|
|
862
|
+
if (typeof this.inputTarget.selectionStart === 'number') {
|
|
863
|
+
const position = this.inputTarget.value.length
|
|
864
|
+
this.inputTarget.setSelectionRange(position, position)
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
closePopoverFromHeader(event) {
|
|
869
|
+
const { target } = event
|
|
870
|
+
|
|
871
|
+
if (target.closest('.hellotext--webchat-header-channel-button, .hellotext--webchat-close-button')) return
|
|
872
|
+
|
|
873
|
+
event.preventDefault()
|
|
874
|
+
this.closePopover()
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
closePopoverOnEscape(event) {
|
|
878
|
+
if (event.key !== 'Escape' || !this.openValue) return
|
|
879
|
+
|
|
880
|
+
event.preventDefault()
|
|
881
|
+
event.stopPropagation()
|
|
882
|
+
this.closePopover()
|
|
883
|
+
|
|
884
|
+
this.triggerTarget?.focus?.()
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async markMessageFailedFromResponse(response, element) {
|
|
888
|
+
const reason = await this.messageFailureReason(response)
|
|
889
|
+
|
|
890
|
+
this.markMessageFailed(element, reason)
|
|
891
|
+
this.broadcastChannel.postMessage({
|
|
892
|
+
type: 'message:failed',
|
|
893
|
+
id: element.id,
|
|
894
|
+
reason,
|
|
895
|
+
})
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
markMessageFailed(element, reason) {
|
|
899
|
+
if (!element) return
|
|
900
|
+
|
|
901
|
+
element.classList.add('failed')
|
|
902
|
+
|
|
903
|
+
if (!reason) return
|
|
904
|
+
|
|
905
|
+
const timestamp = element.querySelector('[data-message-timestamp]')
|
|
906
|
+
|
|
907
|
+
if (timestamp) {
|
|
908
|
+
timestamp.textContent = reason
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
localizeMessageTimestamps(root = this.element) {
|
|
913
|
+
if (!root) return
|
|
914
|
+
|
|
915
|
+
const timestamps = root.matches?.('time[datetime][data-message-timestamp]')
|
|
916
|
+
? [root]
|
|
917
|
+
: Array.from(root.querySelectorAll?.('time[datetime][data-message-timestamp]') || [])
|
|
918
|
+
|
|
919
|
+
timestamps.forEach(timestamp => this.localizeMessageTimestamp(timestamp))
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
localizeMessageTimestamp(timestamp, value = timestamp?.getAttribute('datetime')) {
|
|
923
|
+
if (!timestamp || !value) return
|
|
924
|
+
|
|
925
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
926
|
+
|
|
927
|
+
if (Number.isNaN(date.getTime())) return
|
|
928
|
+
|
|
929
|
+
timestamp.setAttribute('datetime', date.toISOString())
|
|
930
|
+
timestamp.textContent = this.formatMessageTimestamp(date)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
formatMessageTimestamp(date) {
|
|
934
|
+
return this.constructor.messageTimestampFormatterFor(Locale.toString()).format(date)
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
static messageTimestampFormatterFor(locale) {
|
|
938
|
+
const key = locale || 'default'
|
|
939
|
+
|
|
940
|
+
if (!this.messageTimestampFormatters[key]) {
|
|
941
|
+
this.messageTimestampFormatters[key] = this.buildMessageTimestampFormatter(locale)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return this.messageTimestampFormatters[key]
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
static buildMessageTimestampFormatter(locale) {
|
|
948
|
+
try {
|
|
949
|
+
return new Intl.DateTimeFormat(locale || undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS)
|
|
950
|
+
} catch (_) {
|
|
951
|
+
return new Intl.DateTimeFormat(undefined, MESSAGE_TIMESTAMP_FORMAT_OPTIONS)
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async messageFailureReason(response) {
|
|
956
|
+
const nativeResponse = response?.data || response?.response
|
|
957
|
+
const fallback = nativeResponse?.statusText || 'Message failed'
|
|
958
|
+
|
|
959
|
+
try {
|
|
960
|
+
const jsonResponse = nativeResponse?.clone ? nativeResponse.clone() : nativeResponse
|
|
961
|
+
const payload = await jsonResponse?.json?.()
|
|
962
|
+
const reason = this.messageFailureReasonFromPayload(payload)
|
|
963
|
+
|
|
964
|
+
if (reason) return reason
|
|
965
|
+
} catch (_) {
|
|
966
|
+
// Fall through to text parsing/fallback. Some stores strip content-type or
|
|
967
|
+
// return non-JSON failures, so the timestamp still needs a useful reason.
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
try {
|
|
971
|
+
const textResponse = nativeResponse?.clone ? nativeResponse.clone() : nativeResponse
|
|
972
|
+
const text = await textResponse?.text?.()
|
|
973
|
+
|
|
974
|
+
return this.messageFailureReasonFromText(text) || fallback
|
|
975
|
+
} catch (_) {
|
|
976
|
+
return fallback
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
messageFailureReasonFromText(text) {
|
|
981
|
+
if (typeof text !== 'string') return null
|
|
982
|
+
|
|
983
|
+
const value = text.trim()
|
|
984
|
+
if (!value || value.startsWith('<')) return null
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
return this.messageFailureReasonFromPayload(JSON.parse(value)) || value
|
|
988
|
+
} catch (_) {
|
|
989
|
+
return value
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
messageFailureReasonFromPayload(payload) {
|
|
994
|
+
if (!payload) return null
|
|
995
|
+
|
|
996
|
+
return [
|
|
997
|
+
payload.error?.message,
|
|
998
|
+
payload.message,
|
|
999
|
+
payload.errors?.message,
|
|
1000
|
+
payload.errors?.[0]?.message,
|
|
1001
|
+
payload.errors?.[0]?.description,
|
|
1002
|
+
].find(reason => typeof reason === 'string' && reason.trim().length > 0)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
813
1005
|
messageAttachmentsContainer(element) {
|
|
814
1006
|
return element.querySelector('[data-attachments-container], [data-attachment-container]')
|
|
815
1007
|
}
|
|
@@ -32,6 +32,11 @@ class Configuration {
|
|
|
32
32
|
*/
|
|
33
33
|
static assign(props) {
|
|
34
34
|
if (props) {
|
|
35
|
+
const shouldInferActionCableUrl = (
|
|
36
|
+
Object.prototype.hasOwnProperty.call(props, 'apiRoot') &&
|
|
37
|
+
!Object.prototype.hasOwnProperty.call(props, 'actionCableUrl')
|
|
38
|
+
)
|
|
39
|
+
|
|
35
40
|
Object.entries(props).forEach(([key, value]) => {
|
|
36
41
|
if (key === 'forms') {
|
|
37
42
|
this.forms = Forms.assign(value)
|
|
@@ -41,6 +46,10 @@ class Configuration {
|
|
|
41
46
|
this[key] = value
|
|
42
47
|
}
|
|
43
48
|
})
|
|
49
|
+
|
|
50
|
+
if (shouldInferActionCableUrl) {
|
|
51
|
+
this.actionCableUrl = this.actionCableUrlForApiRoot(this.apiRoot)
|
|
52
|
+
}
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
return this
|
|
@@ -57,6 +66,22 @@ class Configuration {
|
|
|
57
66
|
static endpoint(path) {
|
|
58
67
|
return `${this.apiRoot}/${path}`
|
|
59
68
|
}
|
|
69
|
+
|
|
70
|
+
static actionCableUrlForApiRoot(apiRoot) {
|
|
71
|
+
try {
|
|
72
|
+
const url = new URL(apiRoot)
|
|
73
|
+
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
74
|
+
|
|
75
|
+
url.protocol = protocol
|
|
76
|
+
url.pathname = '/cable'
|
|
77
|
+
url.search = ''
|
|
78
|
+
url.hash = ''
|
|
79
|
+
|
|
80
|
+
return url.toString()
|
|
81
|
+
} catch (_) {
|
|
82
|
+
return this.actionCableUrl
|
|
83
|
+
}
|
|
84
|
+
}
|
|
60
85
|
}
|
|
61
86
|
|
|
62
87
|
export { Configuration }
|
package/src/models/business.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import locales from '../locales'
|
|
2
2
|
import BusinessesAPI from '../api/businesses'
|
|
3
3
|
|
|
4
|
+
const stylesheetAttribute = 'data-hellotext-stylesheet'
|
|
5
|
+
const stylesheetLoadTimeout = 10000
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* @typedef {Object} BusinessCountry
|
|
6
9
|
* @property {String} [code] - ISO country code configured for the business.
|
|
@@ -38,6 +41,8 @@ class Business {
|
|
|
38
41
|
constructor(id) {
|
|
39
42
|
this.id = id
|
|
40
43
|
this.data = null
|
|
44
|
+
this.stylesheet = null
|
|
45
|
+
this.stylesheetLoaded = Promise.resolve(false)
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
/**
|
|
@@ -81,15 +86,91 @@ class Business {
|
|
|
81
86
|
setData(data) {
|
|
82
87
|
this.data = data
|
|
83
88
|
|
|
84
|
-
if (typeof document !== 'undefined') {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
if (typeof document !== 'undefined' && data.style_url) {
|
|
90
|
+
this.stylesheet = this.constructor.ensureStylesheet(data.style_url)
|
|
91
|
+
this.stylesheetLoaded = this.constructor.waitForStylesheet(this.stylesheet)
|
|
92
|
+
} else {
|
|
93
|
+
this.stylesheet = null
|
|
94
|
+
this.stylesheetLoaded = Promise.resolve(false)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static get stylesheetSelector() {
|
|
99
|
+
return `link[rel="stylesheet"][${stylesheetAttribute}]`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static ensureStylesheet(styleUrl) {
|
|
103
|
+
const href = this.normalizedStylesheetUrl(styleUrl)
|
|
104
|
+
const existingLink = this.stylesheetLinks.find(link => link.href === href)
|
|
105
|
+
|
|
106
|
+
if (existingLink) {
|
|
107
|
+
existingLink.setAttribute(stylesheetAttribute, 'true')
|
|
108
|
+
return existingLink
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const linkTag = document.createElement('link')
|
|
112
|
+
linkTag.rel = 'stylesheet'
|
|
113
|
+
linkTag.href = styleUrl
|
|
114
|
+
linkTag.setAttribute(stylesheetAttribute, 'true')
|
|
115
|
+
this.waitForStylesheet(linkTag)
|
|
116
|
+
|
|
117
|
+
document.head.append(linkTag)
|
|
118
|
+
|
|
119
|
+
return linkTag
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static get stylesheetLinks() {
|
|
123
|
+
if (typeof document === 'undefined') return []
|
|
124
|
+
|
|
125
|
+
return Array.from(document.querySelectorAll(this.stylesheetSelector))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static get latestStylesheet() {
|
|
129
|
+
return this.stylesheetLinks[this.stylesheetLinks.length - 1]
|
|
130
|
+
}
|
|
88
131
|
|
|
89
|
-
|
|
132
|
+
static normalizedStylesheetUrl(styleUrl) {
|
|
133
|
+
try {
|
|
134
|
+
return new URL(styleUrl, document.baseURI).href
|
|
135
|
+
} catch (_error) {
|
|
136
|
+
return styleUrl
|
|
90
137
|
}
|
|
91
138
|
}
|
|
92
139
|
|
|
140
|
+
static waitForStylesheet(linkTag) {
|
|
141
|
+
if (!linkTag) return Promise.resolve(false)
|
|
142
|
+
if (this.stylesheetIsLoaded(linkTag)) return Promise.resolve(true)
|
|
143
|
+
if (linkTag.dataset.hellotextStylesheetLoaded === 'false') return Promise.resolve(false)
|
|
144
|
+
if (linkTag._hellotextStylesheetLoaded) return linkTag._hellotextStylesheetLoaded
|
|
145
|
+
|
|
146
|
+
linkTag._hellotextStylesheetLoaded = new Promise(resolve => {
|
|
147
|
+
let timeout
|
|
148
|
+
|
|
149
|
+
const finish = loaded => {
|
|
150
|
+
clearTimeout(timeout)
|
|
151
|
+
linkTag.removeEventListener('load', handleLoad)
|
|
152
|
+
linkTag.removeEventListener('error', handleError)
|
|
153
|
+
linkTag.dataset.hellotextStylesheetLoaded = loaded ? 'true' : 'false'
|
|
154
|
+
resolve(loaded)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const handleLoad = () => finish(this.stylesheetIsLoaded(linkTag))
|
|
158
|
+
const handleError = () => finish(false)
|
|
159
|
+
|
|
160
|
+
linkTag.addEventListener('load', handleLoad)
|
|
161
|
+
linkTag.addEventListener('error', handleError)
|
|
162
|
+
|
|
163
|
+
timeout = setTimeout(() => finish(this.stylesheetIsLoaded(linkTag)), stylesheetLoadTimeout)
|
|
164
|
+
if (timeout.unref) timeout.unref()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
return linkTag._hellotextStylesheetLoaded
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static stylesheetIsLoaded(linkTag) {
|
|
171
|
+
return linkTag.dataset.hellotextStylesheetLoaded === 'true' || !!linkTag.sheet
|
|
172
|
+
}
|
|
173
|
+
|
|
93
174
|
get subscription() {
|
|
94
175
|
return this.data.subscription
|
|
95
176
|
}
|
package/src/models/webchat.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
import { Configuration } from '../core'
|
|
2
2
|
|
|
3
3
|
import API from '../api'
|
|
4
|
+
import { Business } from './business'
|
|
4
5
|
|
|
5
6
|
class Webchat {
|
|
6
7
|
static async load(id) {
|
|
7
|
-
|
|
8
|
+
const webchat = new Webchat({
|
|
8
9
|
id,
|
|
9
10
|
html: await API.webchats.get(id),
|
|
10
11
|
})
|
|
12
|
+
|
|
13
|
+
webchat.rendered = webchat.render()
|
|
14
|
+
|
|
15
|
+
return webchat
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
constructor(data) {
|
|
14
19
|
this.data = data
|
|
15
|
-
this.
|
|
20
|
+
this.mounted = false
|
|
21
|
+
this.rendered = Promise.resolve(false)
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
render() {
|
|
24
|
+
async render() {
|
|
19
25
|
this.applyBehaviourOverride()
|
|
26
|
+
|
|
27
|
+
if (!await this.stylesheetLoaded) {
|
|
28
|
+
console.warn('Hellotext webchat was not mounted because its stylesheet failed to load.')
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
this.containerToAppendTo.appendChild(this.data.html)
|
|
33
|
+
this.mounted = true
|
|
34
|
+
|
|
35
|
+
return true
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
applyBehaviourOverride() {
|
|
@@ -50,6 +65,10 @@ class Webchat {
|
|
|
50
65
|
get containerToAppendTo() {
|
|
51
66
|
return document.querySelector(Configuration.webchat.container)
|
|
52
67
|
}
|
|
68
|
+
|
|
69
|
+
get stylesheetLoaded() {
|
|
70
|
+
return Business.waitForStylesheet(Business.latestStylesheet)
|
|
71
|
+
}
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
export { Webchat }
|