yelda-webchat 1.1.3 → 1.1.8

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.3",
3
+ "version": "1.1.8",
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
  }
@@ -48,6 +48,12 @@
48
48
  font-size: 35px !important;
49
49
  }
50
50
 
51
+ .yelda_container {
52
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
53
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
54
+ font-weight: normal;
55
+ }
56
+
51
57
  .yelda_container.inner {
52
58
  position: absolute !important;
53
59
  left: 45px;
@@ -138,7 +144,61 @@
138
144
  overflow: hidden;
139
145
  }
140
146
  }
147
+ /** CSS for assistant bubble text */
148
+ #yelda_assistant_bubble_text {
149
+ animation: 1s ease-out 0s 1 fadeAndSlideInFromRight;
150
+ position: absolute;
151
+ padding: 7px 10px;
152
+ border-radius: 8px;
153
+ box-shadow: #00000030 0px 2px 8px 0px;
154
+ background-color: white;
155
+ cursor: pointer;
156
+ right: 0;
157
+ bottom: 76px;
158
+ max-width: 300px;
159
+ width: calc(66vw);
160
+ text-align: left;
161
+ font-size: 14px;
162
+ font-weight: normal;
163
+ }
141
164
 
165
+ #yelda_assistant_bubble_text.hidden {
166
+ display: none;
167
+ }
168
+
169
+ /* Close button for bubble text */
170
+ .bubbleCloseButton {
171
+ position: relative;
172
+ top: -17px;
173
+ left: 95%;
174
+ color: white;
175
+ background: grey;
176
+ border-radius: 50%;
177
+ padding: 1px 4px;
178
+ font-size: 16px;
179
+ visibility: hidden; /* Don't show the close button by default */
180
+ }
142
181
 
182
+ /* Show the close button on mouse hover */
183
+ #yelda_assistant_bubble_text:hover .bubbleCloseButton {
184
+ visibility: visible;
185
+ }
143
186
 
187
+ /* Show the close button always on mobile screen */
188
+ @media only screen and (max-width: 768px) {
189
+ .bubbleCloseButton {
190
+ visibility: visible !important;
191
+ }
192
+ }
144
193
 
194
+ /** Animation for assistant bubble text */
195
+ @keyframes fadeAndSlideInFromRight {
196
+ 0% {
197
+ transform: translateY(10%);
198
+ opacity: 0;
199
+ }
200
+ 100% {
201
+ transform: translateY(0%);
202
+ opacity: 1;
203
+ }
204
+ }
@@ -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,24 +82,54 @@ 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
99
+ this.addCloseButtonForBubbleText()
95
100
 
96
101
  // Add click event to assistant text
97
- this.assistantBubbleText.addEventListener('click', this.openChat)
102
+ this.assistantBubbleText.addEventListener('click', this.openChat.bind(this))
98
103
 
99
104
  // add assistant text to webChatContainer using the webchatSettings
100
105
  this.webChatContainer.appendChild(this.assistantBubbleText)
101
106
  }
102
107
 
108
+ /**
109
+ * Add close button for bubble text
110
+ */
111
+ addCloseButtonForBubbleText() {
112
+ // Create close button and add it to the bubble text container
113
+ const closeButton = document.createElement('a')
114
+ closeButton.setAttribute('class', 'bubbleCloseButton')
115
+ closeButton.setAttribute('id', 'yelda_assistant_bubble_text_close')
116
+ closeButton.setAttribute('href', '#')
117
+ closeButton.innerHTML = '×'
118
+ closeButton.onclick = this.closeBubbleText.bind(this)
119
+ this.assistantBubbleText.prepend(closeButton)
120
+ }
121
+
122
+ /**
123
+ * Callback function for bubble text close button
124
+ * @param {Event} event
125
+ */
126
+ closeBubbleText(event) {
127
+ event.preventDefault()
128
+ event.stopPropagation()
129
+ this.removeElement('yelda_assistant_bubble_text')
130
+ this.assistantBubbleText = null
131
+ }
132
+
103
133
  /**
104
134
  * Create assistantImage element and add it to webChatContainer element
105
135
  */
@@ -131,7 +161,7 @@ class YeldaChat {
131
161
  this.assistantImage.innerHTML = '<i class="fas fa-comment"></i>'
132
162
 
133
163
  // Add click event to assistant image
134
- this.assistantImage.addEventListener('click', this.openChat)
164
+ this.assistantImage.addEventListener('click', this.openChat.bind(this))
135
165
 
136
166
  // add assistantImage to webChatContainer using the webchatSettings
137
167
  this.updateAssistantImageWithAssistantSettings()
@@ -276,7 +306,7 @@ class YeldaChat {
276
306
  /**
277
307
  * add CSS class which controls the opacity of the frame container if the iframe:
278
308
  * - should be opened on load OR has a start button
279
- * - cannot be closed (canBeClosed explicitely set to false)
309
+ * - cannot be closed (canBeClosed explicitly set to false)
280
310
  *
281
311
  * 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
312
  * Otherwise,
@@ -365,41 +395,83 @@ class YeldaChat {
365
395
  }
366
396
 
367
397
  /**
368
- * Handles communcation between parent window and iframe, mainly for open and closing the chat
398
+ * Handles communication between parent window and iframe, mainly for open and closing the chat
369
399
  * handles also the chat bubble style
370
400
  * @param {event} event
371
401
  */
372
402
  messageListener(event) {
373
- if (event.data === 'closeChat' || event.message === 'closeChat') {
374
- this.closeChat()
375
- return
376
- }
377
-
378
- if (event.data === 'openChat' || event.message === 'openChat') {
379
- this.openChat()
403
+ // event.message is only there to support IE11.
404
+ const eventData = event.data || event.message
405
+ if (!eventData) {
380
406
  return
381
407
  }
382
408
 
383
- if (event.data && event.data.event && event.data.event === 'openLightGallery') {
384
- if (!event.data.mediaSources || !event.data.mediaSources.length) {
385
- return
409
+ /*
410
+ * Simple event management parent.postMessage('string message')
411
+ */
412
+ if (!eventData.event) {
413
+ switch(eventData) {
414
+ case config.FRAME_EVENT_TYPES.RECEIVED.OPEN_CHAT:
415
+ // 'openChat' event triggered from webchat
416
+ this.openChat()
417
+ break
418
+ case config.FRAME_EVENT_TYPES.RECEIVED.CLOSE_CHAT:
419
+ // 'closeChat' event triggered from webchat
420
+ this.closeChat()
421
+ break
422
+ case config.FRAME_EVENT_TYPES.RECEIVED.ABSTAIN_LEAVE_VIEWPORT:
423
+ // 'abstainLeaveViewport' event triggered from webchat to stop listening to the leave viewport event
424
+ this.listenLeaveViewport(true)
425
+ break
426
+ case config.FRAME_EVENT_TYPES.RECEIVED.LISTEN_LEAVE_VIEWPORT:
427
+ // Triggered from webchat to listen leave viewport event
428
+ this.listenLeaveViewport()
429
+ break
430
+ case config.FRAME_EVENT_TYPES.RECEIVED.LISTEN_URL_UPDATE:
431
+ // Triggered from webchat to listen for url update
432
+ this.listenUrlUpdate()
433
+ break
434
+ case config.FRAME_EVENT_TYPES.RECEIVED.ABSTAIN_URL_UPDATE:
435
+ // Triggered form webchat to stop listening for url update
436
+ this.listenUrlUpdate(true)
437
+ break
438
+ case config.FRAME_EVENT_TYPES.RECEIVED.ADD_BUBBLE_TEXT:
439
+ // Triggered from webchat to add assistant bubble text if possible
440
+ if (!this.configurationData.hasOwnProperty('canBeClosed') || this.configurationData.canBeClosed) {
441
+ this.addAssistantBubbleText()
442
+ }
443
+ break;
386
444
  }
387
-
388
- this.handleLightGallery(event.data)
389
445
  return
390
446
  }
391
447
 
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
448
+ /*
449
+ * Complex event management parent.postMessage({ event: 'xxxx', data: yyyy })
397
450
  */
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
451
+ switch(eventData.event) {
452
+ case config.FRAME_EVENT_TYPES.RECEIVED.OPEN_LIGHT_GALLERY:
453
+ if (!eventData.mediaSources || !eventData.mediaSources.length) {
454
+ return
455
+ }
456
+
457
+ this.handleLightGallery(eventData)
458
+ break
459
+ case config.FRAME_EVENT_TYPES.RECEIVED.IS_SENDING_MESSAGE:
460
+ /**
461
+ * Custom event to send to Yelda to keep track of the webchat ability to receive a new message
462
+ * If isSendingMessage data is true, it means that a user message has already been sent to the webchat and
463
+ * is still waiting for an answer
464
+ * If isSendingMessage data is false, the webchat is ready to receive new user messages
465
+ */
466
+ if (eventData.hasOwnProperty('data')) {
467
+ window.dispatchEvent(new CustomEvent('isSendingMessage', { detail: eventData.data }))
468
+ }
469
+ break
470
+ case config.FRAME_EVENT_TYPES.RECEIVED.ADD_MINIMAL_NOTIFICATION_TEXT:
471
+ if (!this.configurationData.hasOwnProperty('canBeClosed') || this.configurationData.canBeClosed) {
472
+ this.addAssistantBubbleText(eventData.text)
473
+ }
474
+ break;
403
475
  }
404
476
  }
405
477
 
@@ -483,8 +555,16 @@ class YeldaChat {
483
555
  yeldaIframeContainerElement.classList.add('y_active')
484
556
  }
485
557
 
558
+ // hide the assistant bubble text while opening the webchat window
559
+ if (this.assistantBubbleText) {
560
+ this.assistantBubbleText = null
561
+ }
562
+
563
+ // Remove assistant bubble text while opening the webchat window
564
+ this.removeElement('yelda_assistant_bubble_text')
565
+
486
566
  // Propagate the event to the webchat
487
- document.getElementById('web_chat_frame').contentWindow.postMessage('openChat', '*')
567
+ document.getElementById('web_chat_frame').contentWindow.postMessage(config.FRAME_EVENT_TYPES.SENT.OPEN_CHAT, '*')
488
568
  }
489
569
 
490
570
  /**
@@ -509,7 +589,7 @@ class YeldaChat {
509
589
  }
510
590
 
511
591
  /**
512
- * Load CSS asynchroneously
592
+ * Load CSS asynchronously
513
593
  */
514
594
  loadCssAsync() {
515
595
  const head = document.getElementsByTagName('head')[0]
@@ -523,7 +603,7 @@ class YeldaChat {
523
603
  }
524
604
 
525
605
  /**
526
- * Gererate webchatURL and create webchatIframe
606
+ * Generate webchatURL and create webchatIframe
527
607
  * @param {Object} data { chatUrl, assistantId, assistantSlug, shouldBeOpened, isStartBtn, canBeClosed }
528
608
  */
529
609
  setUpChatIFrame(data) {
@@ -674,7 +754,7 @@ class YeldaChat {
674
754
  return isFound
675
755
  }
676
756
 
677
- /**
757
+ /**
678
758
  * return getAssistantSettings endpoint URL from data
679
759
  * @param {Object} data { assistantUrl, assistantId, settingId }
680
760
  * @return {String} url
@@ -754,7 +834,13 @@ class YeldaChat {
754
834
  this.webchatSettings = null
755
835
  }
756
836
 
837
+ // Remove event listeners when switching bots
838
+ this.listenLeaveViewport(true)
839
+ this.listenUrlUpdate(true)
840
+
757
841
  this.loadChat(data)
842
+
843
+ this.configurationData = data
758
844
  return resolve()
759
845
  })
760
846
  }
@@ -795,17 +881,17 @@ class YeldaChat {
795
881
 
796
882
  /**
797
883
  * Return if the webchat loadChat settings are different from the yeldaChat global assistant settings
798
- *
799
- * - yeldaChat can be init (and reset) either with the settingId parameter or the pair (assistantId, locale) that we store globally
800
- * - Each new webchat loading request implies a "getAssistantSettings" async API call,
801
- * => If multiple new webchat loading requests are done in a short amount of time,
802
- * "getAssistantSettings" response can be subject to a race condition issue:
884
+ *
885
+ * - yeldaChat can be init (and reset) either with the settingId parameter or the pair (assistantId, locale) that we store globally
886
+ * - Each new webchat loading request implies a "getAssistantSettings" async API call,
887
+ * => If multiple new webchat loading requests are done in a short amount of time,
888
+ * "getAssistantSettings" response can be subject to a race condition issue:
803
889
  * The latest requested answer received might not be the latest API call answer.
804
- *
890
+ *
805
891
  * isDataOutdated checks if one of the global parameter is different the ones from the webchat settings we got from the "getAssistantSettings" API call
806
892
  * so the "loadChat" function can be informed that the latest request is not the one currently handled
807
893
  * And can stop the current process to let the chat load with the global latest requested settings
808
- *
894
+ *
809
895
  * @param {Object} webchatSettings - current settings from the reset or init yeldaChat request
810
896
  * @param {String} webchatSettings.settingId
811
897
  * @param {String} webchatSettings.assistantId
@@ -861,7 +947,6 @@ class YeldaChat {
861
947
  // because the assistant image containing the openChat event wouldn't have been created
862
948
  if (!data.hasOwnProperty('canBeClosed') || data.canBeClosed) {
863
949
  this.addAssistantImage()
864
- this.addAssistantBubbleText()
865
950
  }
866
951
 
867
952
  // add the frame lister to receive message from iframe to the parent
@@ -879,7 +964,7 @@ class YeldaChat {
879
964
  if (this.bubbleContainer) {
880
965
  this.assistantImage.removeAttribute('id', 'yelda_assistant_img')
881
966
  this.assistantImage.removeAttribute('class', 'yelda_assistant_img default')
882
- this.assistantImage.removeEventListener('click', this.openChat)
967
+ this.assistantImage.removeEventListener('click', this.openChat.bind(this))
883
968
  this.assistantImage.replaceWith(this.bubbleContainerClone)
884
969
  } else {
885
970
  this.removeElement('yelda_assistant_img')
@@ -946,8 +1031,91 @@ class YeldaChat {
946
1031
  const webchatFrame = document.getElementById('web_chat_frame')
947
1032
  if (webchatFrame) {
948
1033
  this.openChat()
949
- webchatFrame.contentWindow.postMessage({ event: 'sendUserMessage', data: message })
1034
+ webchatFrame.contentWindow.postMessage({ event: config.FRAME_EVENT_TYPES.SENT.SEND_USER_MESSAGE, data: message })
1035
+ }
1036
+ }
1037
+
1038
+ /**
1039
+ * Listen to url update on the page
1040
+ * @param {Boolean} shouldRemoveEvent
1041
+ */
1042
+ listenUrlUpdate(shouldRemoveEvent = false) {
1043
+ if (shouldRemoveEvent && this.urlObserver) {
1044
+ this.urlObserver.disconnect()
1045
+ this.urlObserver = null
1046
+ return
1047
+ }
1048
+
1049
+ try {
1050
+ this.previousUrl = ''
1051
+ this.urlObserver = new MutationObserver(() => {
1052
+ if (location.href === this.previousUrl) {
1053
+ return
1054
+ }
1055
+ this.previousUrl = location.href
1056
+ this.urlUpdateListener()
1057
+ })
1058
+
1059
+ this.urlObserver.observe(document, {childList: true, subtree: true})
1060
+ } catch(err) {
1061
+ console.warn('listenUrlUpdate error', err)
1062
+ }
1063
+ }
1064
+
1065
+ /**
1066
+ * Callback for url update listener
1067
+ */
1068
+ urlUpdateListener() {
1069
+ const webchatFrame = document.getElementById('web_chat_frame')
1070
+
1071
+ if (!webchatFrame) {
1072
+ return
950
1073
  }
1074
+ webchatFrame.contentWindow.postMessage({ event: config.FRAME_EVENT_TYPES.SENT.URL_UPDATE, data: window.location.href }, '*')
1075
+ }
1076
+
1077
+ /**
1078
+ * Listen leave viewport event handling
1079
+ * @param {Boolean} shouldRemoveEvent - default false
1080
+ */
1081
+ listenLeaveViewport(shouldRemoveEvent = false) {
1082
+ const md = new MobileDetect(navigator.userAgent)
1083
+ const isMobile = md.mobile() !== null
1084
+
1085
+ /**
1086
+ * Not needed to listen on mobile since webchat covers the full page of the site
1087
+ * and we can't able to detect mouseout events
1088
+ */
1089
+ if (isMobile) {
1090
+ return
1091
+ }
1092
+
1093
+ const eventHandler = window[shouldRemoveEvent ? 'removeEventListener' : 'addEventListener']
1094
+
1095
+ if (!this.viewportListenerBind) {
1096
+ this.viewportListenerBind = this.viewportListener.bind(this)
1097
+ }
1098
+ // mouseout event is used to listen leave viewport
1099
+ eventHandler('mouseout', this.viewportListenerBind)
1100
+ }
1101
+
1102
+ /**
1103
+ * Callback for leave viewport listener
1104
+ * @param {Event} event
1105
+ */
1106
+ viewportListener(event) {
1107
+ /**
1108
+ * when mouse moves between dom elements mouseout event is trigged but we need to trigger
1109
+ * the event when mouse moves out of the viewport so toElement and relatedTarget is checked
1110
+ */
1111
+ if (event.toElement || event.relatedTarget) {
1112
+ return
1113
+ }
1114
+
1115
+ // When mouse leaves the viewport send 'leaveViewPort' to webchat
1116
+ document.getElementById('web_chat_frame').contentWindow.postMessage({
1117
+ event: config.FRAME_EVENT_TYPES.SENT.LEAVE_VIEWPORT
1118
+ }, '*')
951
1119
  }
952
1120
  }
953
1121
 
@@ -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
  /**