yelda-webchat 1.1.2 → 1.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yelda-webchat",
3
- "version": "1.1.2",
3
+ "version": "1.1.7",
4
4
  "description": "Website Chat Plugin",
5
5
  "main": "./dist/js/yeldaWebchatInjector.min.js",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "lint:check": "eslint --ext .js src",
22
22
  "lint:fix": "eslint --fix --ext .js src",
23
23
  "test": "mocha --require esm ./test/*.js",
24
- "uploadBuildToS3": "npm run prepack && node uploadBuildToS3 build -s $npm_config_semver -d $npm_config_destinationPath"
24
+ "uploadBuildToS3": "npm run prepack && node uploadBuildToS3 build -s $npm_config_semver -d $npm_config_destinationPath -o $otp"
25
25
  },
26
26
  "devDependencies": {
27
27
  "autoprefixer-loader": "^3.2.0",
@@ -11,4 +11,24 @@ module.exports = {
11
11
  PRODUCTION: 'app.yelda.ai',
12
12
  STAGING: 'staging.yelda.ai',
13
13
  },
14
+ FRAME_EVENT_TYPES: {
15
+ SENT: {
16
+ OPEN_CHAT: 'openChat',
17
+ LEAVE_VIEWPORT: 'leaveViewPort',
18
+ SEND_USER_MESSAGE: 'sendUserMessage',
19
+ URL_UPDATE: 'urlUpdate'
20
+ },
21
+ RECEIVED: {
22
+ OPEN_CHAT: 'openChat',
23
+ CLOSE_CHAT: 'closeChat',
24
+ ABSTAIN_LEAVE_VIEWPORT: 'abstainLeaveViewport',
25
+ OPEN_LIGHT_GALLERY: 'openLightGallery',
26
+ IS_SENDING_MESSAGE: 'isSendingMessage',
27
+ LISTEN_LEAVE_VIEWPORT: 'listenLeaveViewport',
28
+ LISTEN_URL_UPDATE: 'listenUrlUpdate',
29
+ ABSTAIN_URL_UPDATE: 'abstainUrlUpdate',
30
+ ADD_BUBBLE_TEXT: 'addBubbleText',
31
+ ADD_MINIMAL_NOTIFICATION_TEXT: 'addMinimalNotificationText'
32
+ }
33
+ }
14
34
  }
@@ -138,7 +138,58 @@
138
138
  overflow: hidden;
139
139
  }
140
140
  }
141
+ /** CSS for assistant bubble text */
142
+ #yelda_assistant_bubble_text {
143
+ position: absolute;
144
+ white-space: nowrap;
145
+ text-align: center;
146
+ transform: translate(-104%, -140%);
147
+ padding: 5px;
148
+ border: 1px solid #000000;
149
+ border-radius: 3px;
150
+ box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
151
+ background-color: white;
152
+ cursor: pointer;
153
+ animation: 1s ease-out 0s 1 fadeAndSlideInFromRight;
154
+ }
141
155
 
156
+ #yelda_assistant_bubble_text.hidden {
157
+ display: none;
158
+ }
142
159
 
160
+ /** css for adding tooltip like triangle to the assistant bubble text */
161
+ #yelda_assistant_bubble_text::before,
162
+ #yelda_assistant_bubble_text::after {
163
+ content: "";
164
+ position: absolute;
165
+ border-top: 5px solid transparent;
166
+ border-bottom: 5px solid transparent;
167
+ top: 50%;
168
+ left: 100%;
169
+ }
143
170
 
171
+ /* The border of the triangle */
172
+ #yelda_assistant_bubble_text::before {
173
+ border-left: 10px solid #000;
174
+ margin-top: -5px;
175
+ margin-left: 1px;
176
+ }
144
177
 
178
+ /* The white fill of the triangle */
179
+ #yelda_assistant_bubble_text::after {
180
+ border-left: 10px solid white;
181
+ margin-top: -5px;
182
+ z-index: 1;
183
+ }
184
+
185
+ /** Animation for assistant bubble text */
186
+ @keyframes fadeAndSlideInFromRight {
187
+ 0% {
188
+ transform: translateX(0%) translateY(-140%);
189
+ opacity: 0;
190
+ }
191
+ 100% {
192
+ transform: translateX(-104%) translateY(-140%);
193
+ opacity: 1;
194
+ }
195
+ }
@@ -44,7 +44,7 @@ class YeldaChat {
44
44
  * get Specific parameter from the url
45
45
  * @param {*} parameter
46
46
  * @param {*} defaultValue, default empty string
47
- * @return {String} urlParamter
47
+ * @return {String} urlParameter
48
48
  */
49
49
  getUrlParam(parameter, defaultValue = '', url = window.location.href) {
50
50
  let urlParameter = defaultValue
@@ -82,19 +82,23 @@ class YeldaChat {
82
82
 
83
83
  /**
84
84
  * Create an assistantBubbleText component and add it to webChatContainer element
85
+ * @param {String} text - default null
85
86
  */
86
- addAssistantBubbleText() {
87
- if (!this.webChatContainer || !this.webchatSettings || !this.webchatSettings.bubbleText) {
87
+ addAssistantBubbleText(text = null) {
88
+ text = text || this.webchatSettings && this.webchatSettings.bubbleText
89
+ if (!this.webChatContainer || !text) {
88
90
  return
89
91
  }
90
92
 
93
+ // Remove the assistant bubble text element before adding a new one
94
+ this.removeElement('yelda_assistant_bubble_text')
91
95
  // create assistantBubbleText element and set the text
92
96
  this.assistantBubbleText = document.createElement('span')
93
97
  this.assistantBubbleText.setAttribute('id', 'yelda_assistant_bubble_text')
94
- this.assistantBubbleText.innerText = this.webchatSettings.bubbleText
98
+ this.assistantBubbleText.innerText = text || this.webchatSettings.bubbleText
95
99
 
96
100
  // Add click event to assistant text
97
- this.assistantBubbleText.addEventListener('click', this.openChat)
101
+ this.assistantBubbleText.addEventListener('click', this.openChat.bind(this))
98
102
 
99
103
  // add assistant text to webChatContainer using the webchatSettings
100
104
  this.webChatContainer.appendChild(this.assistantBubbleText)
@@ -131,7 +135,7 @@ class YeldaChat {
131
135
  this.assistantImage.innerHTML = '<i class="fas fa-comment"></i>'
132
136
 
133
137
  // Add click event to assistant image
134
- this.assistantImage.addEventListener('click', this.openChat)
138
+ this.assistantImage.addEventListener('click', this.openChat.bind(this))
135
139
 
136
140
  // add assistantImage to webChatContainer using the webchatSettings
137
141
  this.updateAssistantImageWithAssistantSettings()
@@ -276,7 +280,7 @@ class YeldaChat {
276
280
  /**
277
281
  * add CSS class which controls the opacity of the frame container if the iframe:
278
282
  * - should be opened on load OR has a start button
279
- * - cannot be closed (canBeClosed explicitely set to false)
283
+ * - cannot be closed (canBeClosed explicitly set to false)
280
284
  *
281
285
  * If we are sure that the webchat should be opened and displayed all the time, we can add the y_active class right away
282
286
  * Otherwise,
@@ -365,41 +369,81 @@ class YeldaChat {
365
369
  }
366
370
 
367
371
  /**
368
- * Handles communcation between parent window and iframe, mainly for open and closing the chat
372
+ * Handles communication between parent window and iframe, mainly for open and closing the chat
369
373
  * handles also the chat bubble style
370
374
  * @param {event} event
371
375
  */
372
376
  messageListener(event) {
373
- if (event.data === 'closeChat' || event.message === 'closeChat') {
374
- this.closeChat()
377
+ // event.message is only there to support IE11.
378
+ const eventData = event.data || event.message
379
+ if (!eventData) {
375
380
  return
376
381
  }
377
382
 
378
- if (event.data === 'openChat' || event.message === 'openChat') {
379
- this.openChat()
380
- return
381
- }
382
-
383
- if (event.data && event.data.event && event.data.event === 'openLightGallery') {
384
- if (!event.data.mediaSources || !event.data.mediaSources.length) {
385
- return
383
+ /*
384
+ * Simple event management parent.postMessage('string message')
385
+ */
386
+ if (!eventData.event) {
387
+ switch(eventData) {
388
+ case config.FRAME_EVENT_TYPES.RECEIVED.OPEN_CHAT:
389
+ // 'openChat' event triggered from webchat
390
+ this.openChat()
391
+ break
392
+ case config.FRAME_EVENT_TYPES.RECEIVED.CLOSE_CHAT:
393
+ // 'closeChat' event triggered from webchat
394
+ this.closeChat()
395
+ break
396
+ case config.FRAME_EVENT_TYPES.RECEIVED.ABSTAIN_LEAVE_VIEWPORT:
397
+ // 'abstainLeaveViewport' event triggered from webchat to stop listening to the leave viewport event
398
+ this.listenLeaveViewport(true)
399
+ break
400
+ case config.FRAME_EVENT_TYPES.RECEIVED.LISTEN_LEAVE_VIEWPORT:
401
+ // Triggered from webchat to listen leave viewport event
402
+ this.listenLeaveViewport()
403
+ break
404
+ case config.FRAME_EVENT_TYPES.RECEIVED.LISTEN_URL_UPDATE:
405
+ // Triggered from webchat to listen for url update
406
+ this.listenUrlUpdate()
407
+ break
408
+ case config.FRAME_EVENT_TYPES.RECEIVED.ABSTAIN_URL_UPDATE:
409
+ // Triggered form webchat to stop listening for url update
410
+ this.listenUrlUpdate(true)
411
+ break
412
+ case config.FRAME_EVENT_TYPES.RECEIVED.ADD_BUBBLE_TEXT:
413
+ // Triggered from webchat to add assistant bubble text if possible
414
+ if (!this.configurationData.hasOwnProperty('canBeClosed') || this.configurationData.canBeClosed) {
415
+ this.addAssistantBubbleText()
416
+ }
417
+ break;
386
418
  }
387
-
388
- this.handleLightGallery(event.data)
389
419
  return
390
420
  }
391
421
 
392
- /**
393
- * Custom event to send to Yelda to keep track of the webchat ability to receive a new message
394
- * If isSendingMessage data is true, it means that a user message has already been sent to the webchat and
395
- * is still waiting for an answer
396
- * If isSendingMessage data is false, the webchat is ready to receive new user messages
422
+ /*
423
+ * Complex event management parent.postMessage({ event: 'xxxx', data: yyyy })
397
424
  */
398
- if (event.data && event.data.event && event.data.event === 'isSendingMessage') {
399
- if (event.data.hasOwnProperty('data')) {
400
- window.dispatchEvent(new CustomEvent('isSendingMessage', { detail: event.data.data }))
401
- }
402
- return
425
+ switch(eventData.event) {
426
+ case config.FRAME_EVENT_TYPES.RECEIVED.OPEN_LIGHT_GALLERY:
427
+ if (!eventData.mediaSources || !eventData.mediaSources.length) {
428
+ return
429
+ }
430
+
431
+ this.handleLightGallery(eventData)
432
+ break
433
+ case config.FRAME_EVENT_TYPES.RECEIVED.IS_SENDING_MESSAGE:
434
+ /**
435
+ * Custom event to send to Yelda to keep track of the webchat ability to receive a new message
436
+ * If isSendingMessage data is true, it means that a user message has already been sent to the webchat and
437
+ * is still waiting for an answer
438
+ * If isSendingMessage data is false, the webchat is ready to receive new user messages
439
+ */
440
+ if (eventData.hasOwnProperty('data')) {
441
+ window.dispatchEvent(new CustomEvent('isSendingMessage', { detail: eventData.data }))
442
+ }
443
+ break
444
+ case config.FRAME_EVENT_TYPES.RECEIVED.ADD_MINIMAL_NOTIFICATION_TEXT:
445
+ this.addAssistantBubbleText(eventData.text)
446
+ break;
403
447
  }
404
448
  }
405
449
 
@@ -483,8 +527,16 @@ class YeldaChat {
483
527
  yeldaIframeContainerElement.classList.add('y_active')
484
528
  }
485
529
 
530
+ // hide the assistant bubble text while opening the webchat window
531
+ if (this.assistantBubbleText) {
532
+ this.assistantBubbleText = null
533
+ }
534
+
535
+ // Remove assistant bubble text while opening the webchat window
536
+ this.removeElement('yelda_assistant_bubble_text')
537
+
486
538
  // Propagate the event to the webchat
487
- document.getElementById('web_chat_frame').contentWindow.postMessage('openChat', '*')
539
+ document.getElementById('web_chat_frame').contentWindow.postMessage(config.FRAME_EVENT_TYPES.SENT.OPEN_CHAT, '*')
488
540
  }
489
541
 
490
542
  /**
@@ -509,7 +561,7 @@ class YeldaChat {
509
561
  }
510
562
 
511
563
  /**
512
- * Load CSS asynchroneously
564
+ * Load CSS asynchronously
513
565
  */
514
566
  loadCssAsync() {
515
567
  const head = document.getElementsByTagName('head')[0]
@@ -523,7 +575,7 @@ class YeldaChat {
523
575
  }
524
576
 
525
577
  /**
526
- * Gererate webchatURL and create webchatIframe
578
+ * Generate webchatURL and create webchatIframe
527
579
  * @param {Object} data { chatUrl, assistantId, assistantSlug, shouldBeOpened, isStartBtn, canBeClosed }
528
580
  */
529
581
  setUpChatIFrame(data) {
@@ -674,7 +726,7 @@ class YeldaChat {
674
726
  return isFound
675
727
  }
676
728
 
677
- /**
729
+ /**
678
730
  * return getAssistantSettings endpoint URL from data
679
731
  * @param {Object} data { assistantUrl, assistantId, settingId }
680
732
  * @return {String} url
@@ -737,10 +789,15 @@ class YeldaChat {
737
789
  return resolve()
738
790
  }
739
791
 
792
+
793
+ // These data will be needed to avoid loading outdated data in the chat (in case of multiple chat load requests)
794
+ this.assistantId = data.assistantId
795
+ this.locale = data.locale
796
+ this.settingId = data.settingId
797
+
740
798
  // Get assistant settings from backend
741
799
  try {
742
800
  this.webchatSettings = await this.getAssistantSettings(data)
743
-
744
801
  // Webchat can be loaded thanks to a single settingId or group of data : {assistantId, assistantSlug, locale}
745
802
  // If only settingId is provided, then we will fill {assistantId, assistantSlug, locale} thanks to the assistant settings
746
803
  data = this.updateAssistantData(data, this.webchatSettings)
@@ -749,7 +806,13 @@ class YeldaChat {
749
806
  this.webchatSettings = null
750
807
  }
751
808
 
809
+ // Remove event listeners when switching bots
810
+ this.listenLeaveViewport(true)
811
+ this.listenUrlUpdate(true)
812
+
752
813
  this.loadChat(data)
814
+
815
+ this.configurationData = data
753
816
  return resolve()
754
817
  })
755
818
  }
@@ -767,7 +830,9 @@ class YeldaChat {
767
830
  return false
768
831
  }
769
832
 
770
- // if webchatSettings is null load the chat
833
+ // if webchatSettings is null, it means that the admins didn't created custom webchat settings or
834
+ // or that the API call to get the settings failed
835
+ // In this case we can still load the webchat with the default settings
771
836
  if (!webchatSettings) {
772
837
  return true
773
838
  }
@@ -786,11 +851,47 @@ class YeldaChat {
786
851
  return true
787
852
  }
788
853
 
854
+ /**
855
+ * Return if the webchat loadChat settings are different from the yeldaChat global assistant settings
856
+ *
857
+ * - yeldaChat can be init (and reset) either with the settingId parameter or the pair (assistantId, locale) that we store globally
858
+ * - Each new webchat loading request implies a "getAssistantSettings" async API call,
859
+ * => If multiple new webchat loading requests are done in a short amount of time,
860
+ * "getAssistantSettings" response can be subject to a race condition issue:
861
+ * The latest requested answer received might not be the latest API call answer.
862
+ *
863
+ * isDataOutdated checks if one of the global parameter is different the ones from the webchat settings we got from the "getAssistantSettings" API call
864
+ * so the "loadChat" function can be informed that the latest request is not the one currently handled
865
+ * And can stop the current process to let the chat load with the global latest requested settings
866
+ *
867
+ * @param {Object} webchatSettings - current settings from the reset or init yeldaChat request
868
+ * @param {String} webchatSettings.settingId
869
+ * @param {String} webchatSettings.assistantId
870
+ * @param {String} webchatSettings.locale
871
+ * @param {Object} globalData - latest globally stored settings from the reset or init yeldaChat request
872
+ * @param {String} globalData.settingId
873
+ * @param {String} globalData.assistantId
874
+ * @param {String} globalData.locale
875
+ * @returns {Boolean}
876
+ */
877
+ isDataOutdated(data, { settingId, assistantId, locale }) {
878
+ if (data.settingId && data.settingId !== settingId) {
879
+ return true
880
+ }
881
+ if (!data.settingId && (data.assistantId !== assistantId || data.locale !== locale)) {
882
+ return true
883
+ }
884
+ return false
885
+ }
886
+
789
887
  /**
790
888
  * Load the chat after getting the webchat settings if publication is enabled
791
- * @param {Object} data { data.assistantUrl, data.assistantId }
889
+ * @param {Object} data { data.assistantUrl, data.assistantId, data.locale }
792
890
  */
793
891
  loadChat(data) {
892
+ if (this.isDataOutdated(data, this)) {
893
+ return
894
+ }
794
895
  if (!this.shouldChatBeLoaded(this.webchatSettings, data)) {
795
896
  this.unLoadChat()
796
897
  return
@@ -818,7 +919,6 @@ class YeldaChat {
818
919
  // because the assistant image containing the openChat event wouldn't have been created
819
920
  if (!data.hasOwnProperty('canBeClosed') || data.canBeClosed) {
820
921
  this.addAssistantImage()
821
- this.addAssistantBubbleText()
822
922
  }
823
923
 
824
924
  // add the frame lister to receive message from iframe to the parent
@@ -836,7 +936,7 @@ class YeldaChat {
836
936
  if (this.bubbleContainer) {
837
937
  this.assistantImage.removeAttribute('id', 'yelda_assistant_img')
838
938
  this.assistantImage.removeAttribute('class', 'yelda_assistant_img default')
839
- this.assistantImage.removeEventListener('click', this.openChat)
939
+ this.assistantImage.removeEventListener('click', this.openChat.bind(this))
840
940
  this.assistantImage.replaceWith(this.bubbleContainerClone)
841
941
  } else {
842
942
  this.removeElement('yelda_assistant_img')
@@ -903,9 +1003,92 @@ class YeldaChat {
903
1003
  const webchatFrame = document.getElementById('web_chat_frame')
904
1004
  if (webchatFrame) {
905
1005
  this.openChat()
906
- webchatFrame.contentWindow.postMessage({ event: 'sendUserMessage', data: message })
1006
+ webchatFrame.contentWindow.postMessage({ event: config.FRAME_EVENT_TYPES.SENT.SEND_USER_MESSAGE, data: message })
1007
+ }
1008
+ }
1009
+
1010
+ /**
1011
+ * Listen to url update on the page
1012
+ * @param {Boolean} shouldRemoveEvent
1013
+ */
1014
+ listenUrlUpdate(shouldRemoveEvent = false) {
1015
+ if (shouldRemoveEvent && this.urlObserver) {
1016
+ this.urlObserver.disconnect()
1017
+ this.urlObserver = null
1018
+ return
1019
+ }
1020
+
1021
+ try {
1022
+ this.previousUrl = ''
1023
+ this.urlObserver = new MutationObserver(() => {
1024
+ if (location.href === this.previousUrl) {
1025
+ return
1026
+ }
1027
+ this.previousUrl = location.href
1028
+ this.urlUpdateListener()
1029
+ })
1030
+
1031
+ this.urlObserver.observe(document, {childList: true, subtree: true})
1032
+ } catch(err) {
1033
+ console.warn('listenUrlUpdate error', err)
907
1034
  }
908
1035
  }
1036
+
1037
+ /**
1038
+ * Callback for url update listener
1039
+ */
1040
+ urlUpdateListener() {
1041
+ const webchatFrame = document.getElementById('web_chat_frame')
1042
+
1043
+ if (!webchatFrame) {
1044
+ return
1045
+ }
1046
+ webchatFrame.contentWindow.postMessage({ event: config.FRAME_EVENT_TYPES.SENT.URL_UPDATE, data: window.location.href }, '*')
1047
+ }
1048
+
1049
+ /**
1050
+ * Listen leave viewport event handling
1051
+ * @param {Boolean} shouldRemoveEvent - default false
1052
+ */
1053
+ listenLeaveViewport(shouldRemoveEvent = false) {
1054
+ const md = new MobileDetect(navigator.userAgent)
1055
+ const isMobile = md.mobile() !== null
1056
+
1057
+ /**
1058
+ * Not needed to listen on mobile since webchat covers the full page of the site
1059
+ * and we can't able to detect mouseout events
1060
+ */
1061
+ if (isMobile) {
1062
+ return
1063
+ }
1064
+
1065
+ const eventHandler = window[shouldRemoveEvent ? 'removeEventListener' : 'addEventListener']
1066
+
1067
+ if (!this.viewportListenerBind) {
1068
+ this.viewportListenerBind = this.viewportListener.bind(this)
1069
+ }
1070
+ // mouseout event is used to listen leave viewport
1071
+ eventHandler('mouseout', this.viewportListenerBind)
1072
+ }
1073
+
1074
+ /**
1075
+ * Callback for leave viewport listener
1076
+ * @param {Event} event
1077
+ */
1078
+ viewportListener(event) {
1079
+ /**
1080
+ * when mouse moves between dom elements mouseout event is trigged but we need to trigger
1081
+ * the event when mouse moves out of the viewport so toElement and relatedTarget is checked
1082
+ */
1083
+ if (event.toElement || event.relatedTarget) {
1084
+ return
1085
+ }
1086
+
1087
+ // When mouse leaves the viewport send 'leaveViewPort' to webchat
1088
+ document.getElementById('web_chat_frame').contentWindow.postMessage({
1089
+ event: config.FRAME_EVENT_TYPES.SENT.LEAVE_VIEWPORT
1090
+ }, '*')
1091
+ }
909
1092
  }
910
1093
 
911
1094
  let yeldaChat = new YeldaChat()
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * To copy dist folder to Yelda repo include the yelda repo static folder path in the command
8
8
  * Like this
9
- * npm run uploadBuildToS3 --destinationPath=/home/Documents/www/yelda/git/yelda/frontend/static --semver=patch
9
+ * npm run uploadBuildToS3 --destinationPath=/home/Documents/www/yelda/git/yelda/frontend/static --semver=patch -o <code>
10
10
  * This command uploadBuildToS3 runs build, test, upload to s3, npm version update, push to git and move dist folder to yelda repo
11
11
  */
12
12
 
@@ -32,7 +32,12 @@ const argv = yargs
32
32
  destinationPath: {
33
33
  description: 'Destination path to copy',
34
34
  alias: 'd',
35
- type: 'string',
35
+ type: 'string'
36
+ },
37
+ otp: {
38
+ description: 'Npm one-time password from your authenticator',
39
+ alias: 'o',
40
+ type: 'string'
36
41
  }
37
42
  })
38
43
  .argv
@@ -47,7 +52,7 @@ const main = async () => {
47
52
  await uploadToS3()
48
53
 
49
54
  await copyDistToYelda()
50
-
55
+
51
56
  const branchName = await getCurrentBranch()
52
57
 
53
58
  const version = await updateNPMVersion()
@@ -117,7 +122,9 @@ const updateNPMVersion = async () => {
117
122
  */
118
123
  const publishPackage = async () => {
119
124
  console.info('Publishing the package ...')
120
- return await executeCommand('npm publish')
125
+ const otp = (argv._.includes('build') && argv.otp) ? `--otp=${argv.otp}` : ''
126
+
127
+ return await executeCommand(`npm publish ${otp}`)
121
128
  }
122
129
 
123
130
  /**