feat: allow control for expand and collapse
This commit is contained in:
@@ -25,6 +25,7 @@ const ARTICLE_VIEW_RESIZE_DURATION = 180;
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
import { useArticleView } from 'widget/composables/useArticleView';
|
||||
import { SDK_SET_BUBBLE_VISIBILITY } from '../shared/constants/sharedFrameEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import {
|
||||
@@ -42,14 +43,22 @@ export default {
|
||||
const { prefersDarkMode } = useDarkMode();
|
||||
const router = useRouter();
|
||||
const { isInWorkingHours } = useAvailability();
|
||||
const { isArticleView, isWidgetExpanded, setArticleView } =
|
||||
useArticleView();
|
||||
|
||||
return { prefersDarkMode, router, isInWorkingHours };
|
||||
return {
|
||||
prefersDarkMode,
|
||||
router,
|
||||
isInWorkingHours,
|
||||
isArticleView,
|
||||
isWidgetExpanded,
|
||||
setArticleView,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isMobile: false,
|
||||
campaignsSnoozedTill: undefined,
|
||||
isArticleView: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -76,6 +85,11 @@ export default {
|
||||
? getLanguageDirection(this.$root.$i18n.locale)
|
||||
: false;
|
||||
},
|
||||
shouldExpandArticleView() {
|
||||
// The widget only widens on article pages, and only when the user has
|
||||
// opted in via the header toggle (persisted, collapsed by default).
|
||||
return this.isArticleView && this.isWidgetExpanded;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeCampaign() {
|
||||
@@ -88,15 +102,25 @@ export default {
|
||||
},
|
||||
},
|
||||
'$route.name'(routeName, previousRouteName) {
|
||||
// Expansion is driven by the rendered portal page (see the
|
||||
// 'portalPageLoaded' handler) so the widget only widens for article pages.
|
||||
// Here we just collapse when leaving the article view, since the iframe is
|
||||
// torn down then and can no longer signal.
|
||||
if (this.isIFrame && previousRouteName === 'article-viewer') {
|
||||
// Leaving the article view tears down the iframe, so reset the flag; the
|
||||
// watcher below collapses the widget if it was expanded.
|
||||
if (previousRouteName === 'article-viewer') {
|
||||
this.isArticleView = false;
|
||||
IFrameHelper.sendMessage({ event: 'collapseWidget' });
|
||||
}
|
||||
},
|
||||
shouldExpandArticleView(shouldExpand) {
|
||||
if (!this.isIFrame) return;
|
||||
// Resize the host widget and mask the iframe while it reflows off-screen,
|
||||
// revealing it once the size transition settles.
|
||||
IFrameHelper.sendMessage({
|
||||
event: shouldExpand ? 'expandWidget' : 'collapseWidget',
|
||||
});
|
||||
emitter.emit(ON_ARTICLE_VIEW_RESIZING, true);
|
||||
setTimeout(
|
||||
() => emitter.emit(ON_ARTICLE_VIEW_RESIZING, false),
|
||||
ARTICLE_VIEW_RESIZE_DURATION
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { websiteToken, locale, widgetColor } = window.chatwootWebChannel;
|
||||
@@ -200,29 +224,6 @@ export default {
|
||||
query: { link, v: Date.now() },
|
||||
});
|
||||
},
|
||||
setArticleView(isArticle) {
|
||||
// portalPageLoaded is delivered asynchronously and can arrive after we've
|
||||
// already left the article view; ignore it unless we're still on that
|
||||
// route so the expanded width can't leak onto the listing or other views.
|
||||
if (!this.isIFrame || this.$route.name !== 'article-viewer') return;
|
||||
|
||||
// Re-assert the width on every page load (the SDK toggles are idempotent)
|
||||
// so the holder always reflects the current page, then only mask the
|
||||
// reflow when the width actually changes to avoid a needless flash.
|
||||
const didChange = isArticle !== this.isArticleView;
|
||||
this.isArticleView = isArticle;
|
||||
IFrameHelper.sendMessage({
|
||||
event: isArticle ? 'expandWidget' : 'collapseWidget',
|
||||
});
|
||||
|
||||
if (didChange) {
|
||||
emitter.emit(ON_ARTICLE_VIEW_RESIZING, true);
|
||||
setTimeout(
|
||||
() => emitter.emit(ON_ARTICLE_VIEW_RESIZING, false),
|
||||
ARTICLE_VIEW_RESIZE_DURATION
|
||||
);
|
||||
}
|
||||
},
|
||||
registerUnreadEvents() {
|
||||
emitter.on(ON_AGENT_MESSAGE_RECEIVED, () => {
|
||||
const { name: routeName } = this.$route;
|
||||
@@ -382,7 +383,12 @@ export default {
|
||||
} else if (message.event === 'open-article') {
|
||||
this.openArticle(message.slug);
|
||||
} else if (message.event === 'portalPageLoaded') {
|
||||
this.setArticleView(message.isArticle);
|
||||
// portalPageLoaded is delivered asynchronously and can arrive after
|
||||
// we've left the article view; ignore it unless we're still there so
|
||||
// the expanded width can't leak onto the listing or other views.
|
||||
if (this.$route.name === 'article-viewer') {
|
||||
this.setArticleView(message.isArticle);
|
||||
}
|
||||
} else if (message.event === 'toggle-open') {
|
||||
this.$store.dispatch('appConfig/toggleWidgetOpen', message.isOpen);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup>
|
||||
import { toRef } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import HeaderActions from './HeaderActions.vue';
|
||||
import AvailabilityContainer from 'widget/components/Availability/AvailabilityContainer.vue';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
import { useArticleView } from 'widget/composables/useArticleView';
|
||||
|
||||
const props = defineProps({
|
||||
avatarUrl: { type: String, default: '' },
|
||||
@@ -17,7 +19,10 @@ const props = defineProps({
|
||||
const availableAgents = toRef(props, 'availableAgents');
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const { isOnline } = useAvailability(availableAgents);
|
||||
const { isArticleView, isWidgetExpanded, toggleWidgetExpanded } =
|
||||
useArticleView();
|
||||
|
||||
const onBackButtonClick = () => {
|
||||
router.replace({ name: 'home' });
|
||||
@@ -58,6 +63,25 @@ const onBackButtonClick = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HeaderActions :show-popout-button="showPopoutButton" />
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
v-if="isArticleView"
|
||||
class="button transparent compact"
|
||||
:title="
|
||||
isWidgetExpanded
|
||||
? t('PORTAL.COLLAPSE_ARTICLE')
|
||||
: t('PORTAL.EXPAND_ARTICLE')
|
||||
"
|
||||
@click="toggleWidgetExpanded"
|
||||
>
|
||||
<span
|
||||
class="size-4 text-n-slate-12"
|
||||
:class="
|
||||
isWidgetExpanded ? 'i-lucide-minimize-2' : 'i-lucide-maximize-2'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
<HeaderActions :show-popout-button="showPopoutButton" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ref } from 'vue';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
const EXPANDED_STORAGE_KEY = 'chatwoot:widget:articleViewExpanded';
|
||||
|
||||
// Module-level singletons so the header toggle and the resize logic in App.vue
|
||||
// share a single source of truth.
|
||||
//
|
||||
// `isArticleView` - whether the iframe is currently showing an article page.
|
||||
// `isWidgetExpanded`- the user's persisted expand/collapse preference. Defaults
|
||||
// to collapsed and only ever applies on article pages.
|
||||
const isArticleView = ref(false);
|
||||
const isWidgetExpanded = ref(LocalStorage.get(EXPANDED_STORAGE_KEY) === true);
|
||||
|
||||
export function useArticleView() {
|
||||
const setArticleView = value => {
|
||||
isArticleView.value = value;
|
||||
};
|
||||
|
||||
const toggleWidgetExpanded = () => {
|
||||
isWidgetExpanded.value = !isWidgetExpanded.value;
|
||||
LocalStorage.set(EXPANDED_STORAGE_KEY, isWidgetExpanded.value);
|
||||
};
|
||||
|
||||
return {
|
||||
isArticleView,
|
||||
isWidgetExpanded,
|
||||
setArticleView,
|
||||
toggleWidgetExpanded,
|
||||
};
|
||||
}
|
||||
@@ -125,7 +125,9 @@
|
||||
"PORTAL": {
|
||||
"POPULAR_ARTICLES": "Popular Articles",
|
||||
"VIEW_ALL_ARTICLES": "View all articles",
|
||||
"IFRAME_LOAD_ERROR": "There was an error loading the article, please refresh the page and try again."
|
||||
"IFRAME_LOAD_ERROR": "There was an error loading the article, please refresh the page and try again.",
|
||||
"EXPAND_ARTICLE": "Expand",
|
||||
"COLLAPSE_ARTICLE": "Collapse"
|
||||
},
|
||||
"ATTACHMENTS": {
|
||||
"image": {
|
||||
|
||||
Reference in New Issue
Block a user