mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	feat: Add a popout option on webwidget (#1174)
* feat: Add a popout option on webwidget
This commit is contained in:
		| @@ -14,6 +14,7 @@ const runSDK = ({ baseUrl, websiteToken }) => { | |||||||
|     locale: chatwootSettings.locale, |     locale: chatwootSettings.locale, | ||||||
|     type: getBubbleView(chatwootSettings.type), |     type: getBubbleView(chatwootSettings.type), | ||||||
|     launcherTitle: chatwootSettings.launcherTitle || '', |     launcherTitle: chatwootSettings.launcherTitle || '', | ||||||
|  |     showPopoutButton: chatwootSettings.showPopoutButton || false, | ||||||
|  |  | ||||||
|     toggle() { |     toggle() { | ||||||
|       IFrameHelper.events.toggleBubble(); |       IFrameHelper.events.toggleBubble(); | ||||||
|   | |||||||
| @@ -99,6 +99,7 @@ export const IFrameHelper = { | |||||||
|         locale: window.$chatwoot.locale, |         locale: window.$chatwoot.locale, | ||||||
|         position: window.$chatwoot.position, |         position: window.$chatwoot.position, | ||||||
|         hideMessageBubble: window.$chatwoot.hideMessageBubble, |         hideMessageBubble: window.$chatwoot.hideMessageBubble, | ||||||
|  |         showPopoutButton: window.$chatwoot.showPopoutButton, | ||||||
|       }); |       }); | ||||||
|       IFrameHelper.onLoad({ |       IFrameHelper.onLoad({ | ||||||
|         widgetColor: message.config.channelConfig.widgetColor, |         widgetColor: message.config.channelConfig.widgetColor, | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ | |||||||
|     :unread-message-count="unreadMessageCount" |     :unread-message-count="unreadMessageCount" | ||||||
|     :is-left-aligned="isLeftAligned" |     :is-left-aligned="isLeftAligned" | ||||||
|     :hide-message-bubble="hideMessageBubble" |     :hide-message-bubble="hideMessageBubble" | ||||||
|  |     :show-popout-button="showPopoutButton" | ||||||
|   /> |   /> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -21,6 +22,7 @@ import { setHeader } from 'widget/helpers/axios'; | |||||||
| import { IFrameHelper } from 'widget/helpers/utils'; | import { IFrameHelper } from 'widget/helpers/utils'; | ||||||
|  |  | ||||||
| import Router from './views/Router'; | import Router from './views/Router'; | ||||||
|  | import { getLocale } from './helpers/urlParamsHelper'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'App', |   name: 'App', | ||||||
| @@ -33,6 +35,7 @@ export default { | |||||||
|       isMobile: false, |       isMobile: false, | ||||||
|       hideMessageBubble: false, |       hideMessageBubble: false, | ||||||
|       widgetPosition: 'right', |       widgetPosition: 'right', | ||||||
|  |       showPopoutButton: false, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
| @@ -49,74 +52,25 @@ export default { | |||||||
|       const isLeft = this.widgetPosition === 'left'; |       const isLeft = this.widgetPosition === 'left'; | ||||||
|       return isLeft; |       return isLeft; | ||||||
|     }, |     }, | ||||||
|  |     isIFrame() { | ||||||
|  |       return IFrameHelper.isIFrame(); | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     const { websiteToken, locale } = window.chatwootWebChannel; |     const { websiteToken, locale } = window.chatwootWebChannel; | ||||||
|     this.setLocale(locale); |     this.setLocale(locale); | ||||||
|  |     if (this.isIFrame) { | ||||||
|     if (IFrameHelper.isIFrame()) { |       this.registerListeners(); | ||||||
|       IFrameHelper.sendMessage({ |       this.sendLoadedEvent(); | ||||||
|         event: 'loaded', |  | ||||||
|         config: { |  | ||||||
|           authToken: window.authToken, |  | ||||||
|           channelConfig: window.chatwootWebChannel, |  | ||||||
|         }, |  | ||||||
|       }); |  | ||||||
|       setHeader('X-Auth-Token', window.authToken); |       setHeader('X-Auth-Token', window.authToken); | ||||||
|  |     } else { | ||||||
|  |       setHeader('X-Auth-Token', window.authToken); | ||||||
|  |       this.fetchOldConversations(); | ||||||
|  |       this.fetchAvailableAgents(websiteToken); | ||||||
|  |       this.setLocale(getLocale(window.location.search)); | ||||||
|     } |     } | ||||||
|     this.setWidgetColor(window.chatwootWebChannel); |  | ||||||
|  |  | ||||||
|     window.addEventListener('message', e => { |  | ||||||
|       const wootPrefix = 'chatwoot-widget:'; |  | ||||||
|       const isDataNotString = typeof e.data !== 'string'; |  | ||||||
|       const isNotFromWoot = isDataNotString || e.data.indexOf(wootPrefix) !== 0; |  | ||||||
|  |  | ||||||
|       if (isNotFromWoot) return; |  | ||||||
|  |  | ||||||
|       const message = JSON.parse(e.data.replace(wootPrefix, '')); |  | ||||||
|       if (message.event === 'config-set') { |  | ||||||
|         this.setLocale(message.locale); |  | ||||||
|         this.setBubbleLabel(); |  | ||||||
|         this.setPosition(message.position); |  | ||||||
|         this.fetchOldConversations().then(() => { |  | ||||||
|           this.setUnreadView(); |  | ||||||
|         }); |  | ||||||
|         this.fetchAvailableAgents(websiteToken); |  | ||||||
|         this.setHideMessageBubble(message.hideMessageBubble); |  | ||||||
|       } else if (message.event === 'widget-visible') { |  | ||||||
|         this.scrollConversationToBottom(); |  | ||||||
|       } else if (message.event === 'set-current-url') { |  | ||||||
|         window.refererURL = message.refererURL; |  | ||||||
|       } else if (message.event === 'toggle-close-button') { |  | ||||||
|         this.isMobile = message.showClose; |  | ||||||
|       } else if (message.event === 'push-event') { |  | ||||||
|         this.createWidgetEvents(message); |  | ||||||
|       } else if (message.event === 'set-label') { |  | ||||||
|         this.$store.dispatch('conversationLabels/create', message.label); |  | ||||||
|       } else if (message.event === 'remove-label') { |  | ||||||
|         this.$store.dispatch('conversationLabels/destroy', message.label); |  | ||||||
|       } else if (message.event === 'set-user') { |  | ||||||
|         this.$store.dispatch('contacts/update', message); |  | ||||||
|       } else if (message.event === 'set-custom-attributes') { |  | ||||||
|         this.$store.dispatch( |  | ||||||
|           'contacts/setCustomAttributes', |  | ||||||
|           message.customAttributes |  | ||||||
|         ); |  | ||||||
|       } else if (message.event === 'delete-custom-attribute') { |  | ||||||
|         this.$store.dispatch('contacts/setCustomAttributes', { |  | ||||||
|           [message.customAttribute]: null, |  | ||||||
|         }); |  | ||||||
|       } else if (message.event === 'set-locale') { |  | ||||||
|         this.setLocale(message.locale); |  | ||||||
|         this.setBubbleLabel(); |  | ||||||
|       } else if (message.event === 'set-unread-view') { |  | ||||||
|         this.showUnreadView = true; |  | ||||||
|       } else if (message.event === 'unset-unread-view') { |  | ||||||
|         this.showUnreadView = false; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     this.$store.dispatch('conversationAttributes/get'); |     this.$store.dispatch('conversationAttributes/get'); | ||||||
|  |     this.setWidgetColor(window.chatwootWebChannel); | ||||||
|     this.registerUnreadEvents(); |     this.registerUnreadEvents(); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
| @@ -147,15 +101,23 @@ export default { | |||||||
|       this.hideMessageBubble = !!hideBubble; |       this.hideMessageBubble = !!hideBubble; | ||||||
|     }, |     }, | ||||||
|     registerUnreadEvents() { |     registerUnreadEvents() { | ||||||
|       bus.$on('on-agent-message-recieved', () => this.setUnreadView()); |       bus.$on('on-agent-message-recieved', () => { | ||||||
|  |         if (!this.isIFrame) { | ||||||
|  |           this.setUserLastSeen(); | ||||||
|  |         } | ||||||
|  |         this.setUnreadView(); | ||||||
|  |       }); | ||||||
|       bus.$on('on-unread-view-clicked', () => { |       bus.$on('on-unread-view-clicked', () => { | ||||||
|         this.unsetUnreadView(); |         this.unsetUnreadView(); | ||||||
|         this.setUserLastSeen(); |         this.setUserLastSeen(); | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|  |     setPopoutDisplay(showPopoutButton) { | ||||||
|  |       this.showPopoutButton = showPopoutButton; | ||||||
|  |     }, | ||||||
|     setUnreadView() { |     setUnreadView() { | ||||||
|       const { unreadMessageCount } = this; |       const { unreadMessageCount } = this; | ||||||
|       if (IFrameHelper.isIFrame() && unreadMessageCount > 0) { |       if (this.isIFrame && unreadMessageCount > 0) { | ||||||
|         IFrameHelper.sendMessage({ |         IFrameHelper.sendMessage({ | ||||||
|           event: 'setUnreadMode', |           event: 'setUnreadMode', | ||||||
|           unreadMessageCount, |           unreadMessageCount, | ||||||
| @@ -163,7 +125,7 @@ export default { | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     unsetUnreadView() { |     unsetUnreadView() { | ||||||
|       if (IFrameHelper.isIFrame()) { |       if (this.isIFrame) { | ||||||
|         IFrameHelper.sendMessage({ event: 'resetUnreadMode' }); |         IFrameHelper.sendMessage({ event: 'resetUnreadMode' }); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
| @@ -176,6 +138,63 @@ export default { | |||||||
|       this.setUserLastSeen(); |       this.setUserLastSeen(); | ||||||
|       this.$store.dispatch('events/create', { name: eventName }); |       this.$store.dispatch('events/create', { name: eventName }); | ||||||
|     }, |     }, | ||||||
|  |     registerListeners() { | ||||||
|  |       const { websiteToken } = window.chatwootWebChannel; | ||||||
|  |       window.addEventListener('message', e => { | ||||||
|  |         if (!IFrameHelper.isAValidEvent(e)) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         const message = IFrameHelper.getMessage(e); | ||||||
|  |         if (message.event === 'config-set') { | ||||||
|  |           this.setLocale(message.locale); | ||||||
|  |           this.setBubbleLabel(); | ||||||
|  |           this.setPosition(message.position); | ||||||
|  |           this.fetchOldConversations().then(() => this.setUnreadView()); | ||||||
|  |           this.setPopoutDisplay(message.showPopoutButton); | ||||||
|  |           this.fetchAvailableAgents(websiteToken); | ||||||
|  |           this.setHideMessageBubble(message.hideMessageBubble); | ||||||
|  |         } else if (message.event === 'widget-visible') { | ||||||
|  |           this.scrollConversationToBottom(); | ||||||
|  |         } else if (message.event === 'set-current-url') { | ||||||
|  |           window.refererURL = message.refererURL; | ||||||
|  |         } else if (message.event === 'toggle-close-button') { | ||||||
|  |           this.isMobile = message.showClose; | ||||||
|  |         } else if (message.event === 'push-event') { | ||||||
|  |           this.createWidgetEvents(message); | ||||||
|  |         } else if (message.event === 'set-label') { | ||||||
|  |           this.$store.dispatch('conversationLabels/create', message.label); | ||||||
|  |         } else if (message.event === 'remove-label') { | ||||||
|  |           this.$store.dispatch('conversationLabels/destroy', message.label); | ||||||
|  |         } else if (message.event === 'set-user') { | ||||||
|  |           this.$store.dispatch('contacts/update', message); | ||||||
|  |         } else if (message.event === 'set-custom-attributes') { | ||||||
|  |           this.$store.dispatch( | ||||||
|  |             'contacts/setCustomAttributes', | ||||||
|  |             message.customAttributes | ||||||
|  |           ); | ||||||
|  |         } else if (message.event === 'delete-custom-attribute') { | ||||||
|  |           this.$store.dispatch('contacts/setCustomAttributes', { | ||||||
|  |             [message.customAttribute]: null, | ||||||
|  |           }); | ||||||
|  |         } else if (message.event === 'set-locale') { | ||||||
|  |           this.setLocale(message.locale); | ||||||
|  |           this.setBubbleLabel(); | ||||||
|  |         } else if (message.event === 'set-unread-view') { | ||||||
|  |           this.showUnreadView = true; | ||||||
|  |         } else if (message.event === 'unset-unread-view') { | ||||||
|  |           this.showUnreadView = false; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     sendLoadedEvent() { | ||||||
|  |       IFrameHelper.sendMessage({ | ||||||
|  |         event: 'loaded', | ||||||
|  |         config: { | ||||||
|  |           authToken: window.authToken, | ||||||
|  |           channelConfig: window.chatwootWebChannel, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -58,4 +58,14 @@ $button-border-width: 1px; | |||||||
|   &.block { |   &.block { | ||||||
|     width: 100%; |     width: 100%; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   &.transparent { | ||||||
|  |     background: transparent; | ||||||
|  |     border: 0; | ||||||
|  |     height: auto; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &.compact { | ||||||
|  |     padding: 0; | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,6 +11,8 @@ html, | |||||||
| body { | body { | ||||||
|   font-family: $font-family; |   font-family: $font-family; | ||||||
|   font-size: 10px; |   font-size: 10px; | ||||||
|  |   -moz-osx-font-smoothing: grayscale; | ||||||
|  |   -webkit-font-smoothing: antialiased; | ||||||
|   height: 100%; |   height: 100%; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -18,36 +20,15 @@ body { | |||||||
|   height: 100%; |   height: 100%; | ||||||
| } | } | ||||||
|  |  | ||||||
| .close-button { |  | ||||||
|   cursor: pointer; |  | ||||||
|   position: relative; |  | ||||||
|   width: $space-two; |  | ||||||
|  |  | ||||||
|   &::before, |  | ||||||
|   &::after { |  | ||||||
|     background-color: $color-heading; |  | ||||||
|     content: ' '; |  | ||||||
|     height: $space-normal; |  | ||||||
|     left: $space-small; |  | ||||||
|     position: absolute; |  | ||||||
|     top: $space-micro; |  | ||||||
|     width: 2px; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &::before { |  | ||||||
|     transform: rotate(45deg); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &::after { |  | ||||||
|     transform: rotate(-45deg); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .is-mobile { | .is-mobile { | ||||||
|   .header-wrap { |   .actions { | ||||||
|     .close-button { |     .close-button { | ||||||
|       display: block !important; |       display: block !important; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     .new-window--button { | ||||||
|  |       display: none !important; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,16 +4,18 @@ | |||||||
|       <img v-if="avatarUrl" :src="avatarUrl" alt="avatar" /> |       <img v-if="avatarUrl" :src="avatarUrl" alt="avatar" /> | ||||||
|       <h2 class="title" v-html="title"></h2> |       <h2 class="title" v-html="title"></h2> | ||||||
|     </div> |     </div> | ||||||
|     <span class="close-button" @click="closeWindow"></span> |     <header-actions :show-popout-button="showPopoutButton" /> | ||||||
|   </header> |   </header> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { mapGetters } from 'vuex'; | import { mapGetters } from 'vuex'; | ||||||
| import { IFrameHelper } from 'widget/helpers/utils'; | import HeaderActions from './HeaderActions'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'ChatHeader', |   name: 'ChatHeader', | ||||||
|  |   components: { | ||||||
|  |     HeaderActions, | ||||||
|  |   }, | ||||||
|   props: { |   props: { | ||||||
|     avatarUrl: { |     avatarUrl: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -23,21 +25,16 @@ export default { | |||||||
|       type: String, |       type: String, | ||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
|  |     showPopoutButton: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapGetters({ |     ...mapGetters({ | ||||||
|       widgetColor: 'appConfig/getWidgetColor', |       widgetColor: 'appConfig/getWidgetColor', | ||||||
|     }), |     }), | ||||||
|   }, |   }, | ||||||
|   methods: { |  | ||||||
|     closeWindow() { |  | ||||||
|       if (IFrameHelper.isIFrame()) { |  | ||||||
|         IFrameHelper.sendMessage({ |  | ||||||
|           event: 'toggleBubble', |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| @@ -73,9 +70,5 @@ export default { | |||||||
|     width: 24px; |     width: 24px; | ||||||
|     margin-right: $space-small; |     margin-right: $space-small; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .close-button { |  | ||||||
|     display: none; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
| <template> | <template> | ||||||
|   <header class="header-expanded"> |   <header class="header-expanded"> | ||||||
|     <img v-if="avatarUrl" class="logo" :src="avatarUrl" /> |     <div class="header--row"> | ||||||
|     <span class="close close-button" @click="closeWindow"></span> |       <img v-if="avatarUrl" class="logo" :src="avatarUrl" /> | ||||||
|  |       <header-actions :show-popout-button="showPopoutButton" /> | ||||||
|  |     </div> | ||||||
|     <h2 class="title" v-html="introHeading"></h2> |     <h2 class="title" v-html="introHeading"></h2> | ||||||
|     <p class="body" v-html="introBody"></p> |     <p class="body" v-html="introBody"></p> | ||||||
|   </header> |   </header> | ||||||
| @@ -9,10 +11,12 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { mapGetters } from 'vuex'; | import { mapGetters } from 'vuex'; | ||||||
| import { IFrameHelper } from 'widget/helpers/utils'; | import HeaderActions from './HeaderActions'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'ChatHeaderExpanded', |   name: 'ChatHeaderExpanded', | ||||||
|  |   components: { | ||||||
|  |     HeaderActions, | ||||||
|  |   }, | ||||||
|   props: { |   props: { | ||||||
|     avatarUrl: { |     avatarUrl: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -26,21 +30,16 @@ export default { | |||||||
|       type: String, |       type: String, | ||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
|  |     showPopoutButton: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapGetters({ |     ...mapGetters({ | ||||||
|       widgetColor: 'appConfig/getWidgetColor', |       widgetColor: 'appConfig/getWidgetColor', | ||||||
|     }), |     }), | ||||||
|   }, |   }, | ||||||
|   methods: { |  | ||||||
|     closeWindow() { |  | ||||||
|       if (IFrameHelper.isIFrame()) { |  | ||||||
|         IFrameHelper.sendMessage({ |  | ||||||
|           event: 'toggleBubble', |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| @@ -59,12 +58,6 @@ export default { | |||||||
|     height: 56px; |     height: 56px; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .close { |  | ||||||
|     position: absolute; |  | ||||||
|     right: $space-medium; |  | ||||||
|     top: $space-medium; |  | ||||||
|     display: none; |  | ||||||
|   } |  | ||||||
|   .title { |   .title { | ||||||
|     color: $color-heading; |     color: $color-heading; | ||||||
|     font-size: $font-size-mega; |     font-size: $font-size-mega; | ||||||
| @@ -79,4 +72,10 @@ export default { | |||||||
|     line-height: 1.5; |     line-height: 1.5; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .header--row { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: flex-start; | ||||||
|  |   justify-content: space-between; | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										89
									
								
								app/javascript/widget/components/HeaderActions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								app/javascript/widget/components/HeaderActions.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | <template> | ||||||
|  |   <div v-if="isIframe" class="actions"> | ||||||
|  |     <button | ||||||
|  |       v-if="showPopoutButton" | ||||||
|  |       class="button transparent compact new-window--button" | ||||||
|  |       @click="popoutWindow" | ||||||
|  |     > | ||||||
|  |       <span class="ion-android-open"></span> | ||||||
|  |     </button> | ||||||
|  |     <button class="button transparent compact close-button"> | ||||||
|  |       <span class="ion-android-close" @click="closeWindow"></span> | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | <script> | ||||||
|  | import { IFrameHelper } from 'widget/helpers/utils'; | ||||||
|  | import { buildPopoutURL } from '../helpers/urlParamsHelper'; | ||||||
|  | import Vue from 'vue'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'HeaderActions', | ||||||
|  |   props: { | ||||||
|  |     showPopoutButton: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     isIframe() { | ||||||
|  |       return IFrameHelper.isIFrame(); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     popoutWindow() { | ||||||
|  |       this.closeWindow(); | ||||||
|  |       const { | ||||||
|  |         location: { origin }, | ||||||
|  |         chatwootWebChannel: { websiteToken }, | ||||||
|  |         authToken, | ||||||
|  |       } = window; | ||||||
|  |  | ||||||
|  |       const popoutWindowURL = buildPopoutURL({ | ||||||
|  |         origin, | ||||||
|  |         websiteToken, | ||||||
|  |         locale: Vue.config.lang, | ||||||
|  |         conversationCookie: authToken, | ||||||
|  |       }); | ||||||
|  |       const popoutWindow = window.open( | ||||||
|  |         popoutWindowURL, | ||||||
|  |         `webwidget_session_${websiteToken}`, | ||||||
|  |         'resizable=off,width=400,height=600' | ||||||
|  |       ); | ||||||
|  |       popoutWindow.focus(); | ||||||
|  |     }, | ||||||
|  |     closeWindow() { | ||||||
|  |       if (IFrameHelper.isIFrame()) { | ||||||
|  |         IFrameHelper.sendMessage({ | ||||||
|  |           event: 'toggleBubble', | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | @import '~widget/assets/scss/variables.scss'; | ||||||
|  |  | ||||||
|  | .actions { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |  | ||||||
|  |   button { | ||||||
|  |     margin-left: $space-normal; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   span { | ||||||
|  |     color: $color-heading; | ||||||
|  |     font-size: $font-size-large; | ||||||
|  |  | ||||||
|  |     &.ion-android-close { | ||||||
|  |       font-size: $font-size-big; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .close-button { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -12,3 +12,5 @@ export const MESSAGE_TYPE = { | |||||||
|   ACTIVITY: 2, |   ACTIVITY: 2, | ||||||
|   TEMPLATE: 3, |   TEMPLATE: 3, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const WOOT_PREFIX = 'chatwoot-widget:'; | ||||||
|   | |||||||
| @@ -1,4 +1,8 @@ | |||||||
| import { buildSearchParamsWithLocale } from '../urlParamsHelper'; | import { | ||||||
|  |   buildSearchParamsWithLocale, | ||||||
|  |   getLocale, | ||||||
|  |   buildPopoutURL, | ||||||
|  | } from '../urlParamsHelper'; | ||||||
|  |  | ||||||
| jest.mock('vue', () => ({ | jest.mock('vue', () => ({ | ||||||
|   config: { |   config: { | ||||||
| @@ -14,3 +18,29 @@ describe('#buildSearchParamsWithLocale', () => { | |||||||
|     expect(buildSearchParamsWithLocale('')).toEqual('?locale=el'); |     expect(buildSearchParamsWithLocale('')).toEqual('?locale=el'); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | describe('#getLocale', () => { | ||||||
|  |   it('returns correct locale', () => { | ||||||
|  |     expect(getLocale('?test=1&cw_conv=2&locale=fr')).toEqual('fr'); | ||||||
|  |     expect(getLocale('?test=1&locale=fr')).toEqual('fr'); | ||||||
|  |     expect(getLocale('?test=1&cw_conv=2&website_token=3&locale=fr')).toEqual( | ||||||
|  |       'fr' | ||||||
|  |     ); | ||||||
|  |     expect(getLocale('')).toEqual(undefined); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | describe('#buildPopoutURL', () => { | ||||||
|  |   it('returns popout URL', () => { | ||||||
|  |     expect( | ||||||
|  |       buildPopoutURL({ | ||||||
|  |         origin: 'https://chatwoot.com', | ||||||
|  |         conversationCookie: 'random-jwt-token', | ||||||
|  |         websiteToken: 'random-website-token', | ||||||
|  |         locale: 'ar', | ||||||
|  |       }) | ||||||
|  |     ).toEqual( | ||||||
|  |       'https://chatwoot.com/widget?cw_conversation=random-jwt-token&website_token=random-website-token&locale=ar' | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								app/javascript/widget/helpers/specs/utils.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/javascript/widget/helpers/specs/utils.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | import { IFrameHelper } from '../utils'; | ||||||
|  |  | ||||||
|  | jest.mock('vue', () => ({ | ||||||
|  |   config: { | ||||||
|  |     lang: 'el', | ||||||
|  |   }, | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | describe('#IFrameHelper', () => { | ||||||
|  |   describe('#isAValidEvent', () => { | ||||||
|  |     it('returns if the event is valid', () => { | ||||||
|  |       expect( | ||||||
|  |         IFrameHelper.isAValidEvent({ | ||||||
|  |           data: | ||||||
|  |             'chatwoot-widget:{"event":"config-set","locale":"fr","position":"left","hideMessageBubble":false,"showPopoutButton":true}', | ||||||
|  |         }) | ||||||
|  |       ).toEqual(true); | ||||||
|  |       expect( | ||||||
|  |         IFrameHelper.isAValidEvent({ | ||||||
|  |           data: | ||||||
|  |             '{"event":"config-set","locale":"fr","position":"left","hideMessageBubble":false,"showPopoutButton":true}', | ||||||
|  |         }) | ||||||
|  |       ).toEqual(false); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   describe('#getMessage', () => { | ||||||
|  |     it('returns parsed message', () => { | ||||||
|  |       expect( | ||||||
|  |         IFrameHelper.getMessage({ | ||||||
|  |           data: | ||||||
|  |             'chatwoot-widget:{"event":"config-set","locale":"fr","position":"left","hideMessageBubble":false,"showPopoutButton":true}', | ||||||
|  |         }) | ||||||
|  |       ).toEqual({ | ||||||
|  |         event: 'config-set', | ||||||
|  |         locale: 'fr', | ||||||
|  |         position: 'left', | ||||||
|  |         hideMessageBubble: false, | ||||||
|  |         showPopoutButton: true, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
|  |  | ||||||
| export const buildSearchParamsWithLocale = search => { | export const buildSearchParamsWithLocale = search => { | ||||||
|   const locale = Vue.config.lang; |   const locale = Vue.config.lang; | ||||||
|   if (search) { |   if (search) { | ||||||
| @@ -8,3 +9,23 @@ export const buildSearchParamsWithLocale = search => { | |||||||
|   } |   } | ||||||
|   return search; |   return search; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const getLocale = (search = '') => { | ||||||
|  |   const searchParamKeyValuePairs = search.split('&'); | ||||||
|  |   return searchParamKeyValuePairs.reduce((acc, keyValuePair) => { | ||||||
|  |     const [key, value] = keyValuePair.split('='); | ||||||
|  |     if (key === 'locale') { | ||||||
|  |       return value; | ||||||
|  |     } | ||||||
|  |     return acc; | ||||||
|  |   }, undefined); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const buildPopoutURL = ({ | ||||||
|  |   origin, | ||||||
|  |   conversationCookie, | ||||||
|  |   websiteToken, | ||||||
|  |   locale, | ||||||
|  | }) => { | ||||||
|  |   return `${origin}/widget?cw_conversation=${conversationCookie}&website_token=${websiteToken}&locale=${locale}`; | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | import { WOOT_PREFIX } from './constants'; | ||||||
|  |  | ||||||
| export const isEmptyObject = obj => | export const isEmptyObject = obj => | ||||||
|   Object.keys(obj).length === 0 && obj.constructor === Object; |   Object.keys(obj).length === 0 && obj.constructor === Object; | ||||||
|  |  | ||||||
| @@ -16,4 +18,11 @@ export const IFrameHelper = { | |||||||
|       '*' |       '*' | ||||||
|     ); |     ); | ||||||
|   }, |   }, | ||||||
|  |   isAValidEvent: e => { | ||||||
|  |     const isDataAString = typeof e.data === 'string'; | ||||||
|  |     const isAValidWootEvent = | ||||||
|  |       isDataAString && e.data.indexOf(WOOT_PREFIX) === 0; | ||||||
|  |     return isAValidWootEvent; | ||||||
|  |   }, | ||||||
|  |   getMessage: e => JSON.parse(e.data.replace(WOOT_PREFIX, '')), | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -6,11 +6,13 @@ | |||||||
|         :intro-heading="introHeading" |         :intro-heading="introHeading" | ||||||
|         :intro-body="introBody" |         :intro-body="introBody" | ||||||
|         :avatar-url="channelConfig.avatarUrl" |         :avatar-url="channelConfig.avatarUrl" | ||||||
|  |         :show-popout-button="showPopoutButton" | ||||||
|       /> |       /> | ||||||
|       <ChatHeader |       <ChatHeader | ||||||
|         v-else |         v-else | ||||||
|         :title="channelConfig.websiteName" |         :title="channelConfig.websiteName" | ||||||
|         :avatar-url="channelConfig.avatarUrl" |         :avatar-url="channelConfig.avatarUrl" | ||||||
|  |         :show-popout-button="showPopoutButton" | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|     <AvailableAgents v-if="showAvailableAgents" :agents="availableAgents" /> |     <AvailableAgents v-if="showAvailableAgents" :agents="availableAgents" /> | ||||||
| @@ -69,6 +71,10 @@ export default { | |||||||
|       type: Number, |       type: Number, | ||||||
|       default: 0, |       default: 0, | ||||||
|     }, |     }, | ||||||
|  |     showPopoutButton: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     isOpen() { |     isOpen() { | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ | |||||||
|       :has-fetched="hasFetched" |       :has-fetched="hasFetched" | ||||||
|       :conversation-attributes="conversationAttributes" |       :conversation-attributes="conversationAttributes" | ||||||
|       :unread-message-count="unreadMessageCount" |       :unread-message-count="unreadMessageCount" | ||||||
|  |       :show-popout-button="showPopoutButton" | ||||||
|     /> |     /> | ||||||
|     <unread |     <unread | ||||||
|       v-else |       v-else | ||||||
| @@ -81,6 +82,10 @@ export default { | |||||||
|       type: Number, |       type: Number, | ||||||
|       default: 0, |       default: 0, | ||||||
|     }, |     }, | ||||||
|  |     showPopoutButton: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -5,8 +5,9 @@ | |||||||
| window.chatwootSettings = { | window.chatwootSettings = { | ||||||
|   hideMessageBubble: false, |   hideMessageBubble: false, | ||||||
|   position: 'left', |   position: 'left', | ||||||
|   locale: 'en', |   locale: 'fr', | ||||||
|   type: 'expanded_bubble', |   type: 'expanded_bubble', | ||||||
|  |   showPopoutButton: true, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| (function(d,t) { | (function(d,t) { | ||||||
|   | |||||||
| @@ -46,6 +46,17 @@ window.chatwootSettings = { | |||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ### To enable popout window | ||||||
|  |  | ||||||
|  | Inorder to enable the popout window, add the following configuration to `chatwootSettings`. This option is disabled by default. | ||||||
|  |  | ||||||
|  | ```js | ||||||
|  | window.chatwootSettings = { | ||||||
|  |   // ...Other Config | ||||||
|  |   showPopoutButton: true, | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ### To trigger widget without displaying bubble | ### To trigger widget without displaying bubble | ||||||
|  |  | ||||||
| ```js | ```js | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Pranav Raj S
					Pranav Raj S