Files
chatwoot/app/javascript/portal/portalHelpers.js
Sivin Varghese cc8866a84e feat: Sync Popular Articles locale with widget locale (#11754)
# Pull Request Template

## Description

This PR includes the following improvements:
* **Popular Articles Locale Selection based on Widget Locale**
  * Implements priority-based locale matching:
    * Exact locale match (e.g., "fr" === "fr")
    * Base language match (e.g., "fr" when selected is "fr_CA")
    * Variant match (e.g., "fr_BE" when selected is "fr")
* Removes default locale fallback - if no locale match is found, popular
articles section is hidden.
    
* Fixed **API** filter issue where the locale parameter was previously
ignored

* Hides Popular Articles section completely when no locale match is
found and Only shows relevant articles in the user's language


* **RTL Direction Handling Improvements**
* Now directly reads the `lang` attribute from HTML element `<html
lang="en">` instead of relying on `.locale-switcher` and sets direction
attribute based on language.

* Adds `data-dir-applied` attribute to prevent overlapping direction
settings between global helpers and components (eg case: Insert article
in editor dashboard)

* Update `IframeLoader.vue` to Composition API and improve the **dir**
logic

Fixes 
1.
[CW-4505](https://linear.app/chatwoot/issue/CW-4505/popular-articles-not-displayed-based-on-user-locale-in-live-chat),
https://github.com/chatwoot/chatwoot/issues/11745
2. RTL direction is not working in widget article view after merging
this PR https://github.com/chatwoot/chatwoot/pull/11692

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Loom video

**Popular Articles**

https://www.loom.com/share/7cecbaaa77eb48e19263398b6ba8ddef?sid=a2452b8e-7d7e-46a3-b5c8-aed5ab5bc801

**RTL improvements**

https://www.loom.com/share/3ccad77174a0412097e802641df5f3e0?sid=e10ac57f-5c49-4084-84d3-5ad58aee54fa

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-06-24 19:52:44 +05:30

154 lines
5.1 KiB
JavaScript

import { createApp } from 'vue';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
import { directive as onClickaway } from 'vue3-click-away';
import { isSameHost } from '@chatwoot/utils';
import slugifyWithCounter from '@sindresorhus/slugify';
import PublicArticleSearch from './components/PublicArticleSearch.vue';
import TableOfContents from './components/TableOfContents.vue';
import { initializeTheme } from './portalThemeHelper.js';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
export const getHeadingsfromTheArticle = () => {
const rows = [];
const articleElement = document.getElementById('cw-article-content');
articleElement.querySelectorAll('h1, h2, h3').forEach(element => {
const slug = slugifyWithCounter(element.innerText);
element.id = slug;
element.className = 'scroll-mt-24 heading';
element.innerHTML += `<a class="permalink text-slate-600 ml-3" href="#${slug}" title="${element.innerText}" data-turbolinks="false">#</a>`;
rows.push({
slug,
title: element.innerText,
tag: element.tagName.toLowerCase(),
});
});
return rows;
};
export const openExternalLinksInNewTab = () => {
const { customDomain, hostURL } = window.portalConfig;
const isOnArticlePage =
document.querySelector('#cw-article-content') !== null;
document.addEventListener('click', event => {
if (!isOnArticlePage) return;
const link = event.target.closest('a');
if (link) {
const currentLocation = window.location.href;
const linkHref = link.href;
// Check against current location and custom domains
const isInternalLink =
isSameHost(linkHref, currentLocation) ||
(customDomain && isSameHost(linkHref, customDomain)) ||
(hostURL && isSameHost(linkHref, hostURL));
if (!isInternalLink) {
link.target = '_blank';
link.rel = 'noopener noreferrer'; // Security and performance benefits
// Prevent default if you want to stop the link from opening in the current tab
event.stopPropagation();
}
}
});
};
export const InitializationHelpers = {
navigateToLocalePage: () => {
document.addEventListener('change', e => {
const localeSwitcher = e.target.closest('.locale-switcher');
if (!localeSwitcher) return;
const { portalSlug } = localeSwitcher.dataset;
window.location.href = `/hc/${encodeURIComponent(portalSlug)}/${encodeURIComponent(localeSwitcher.value)}/`;
});
},
initializeSearch: () => {
const isSearchContainerAvailable = document.querySelector('#search-wrap');
if (isSearchContainerAvailable) {
// eslint-disable-next-line vue/one-component-per-file
const app = createApp({
components: { PublicArticleSearch },
template: '<PublicArticleSearch />',
});
app.use(VueDOMPurifyHTML, domPurifyConfig);
app.directive('on-clickaway', onClickaway);
app.mount('#search-wrap');
}
},
initializeTableOfContents: () => {
const isOnArticlePage = document.querySelector('#cw-hc-toc');
if (isOnArticlePage) {
// eslint-disable-next-line vue/one-component-per-file
const app = createApp({
components: { TableOfContents },
data() {
return { rows: getHeadingsfromTheArticle() };
},
template: '<table-of-contents :rows="rows" />',
});
app.use(VueDOMPurifyHTML, domPurifyConfig);
app.mount('#cw-hc-toc');
}
},
appendPlainParamToURLs: () => {
[...document.getElementsByTagName('a')].forEach(aTagElement => {
if (aTagElement.href && aTagElement.href.includes('/hc/')) {
const url = new URL(aTagElement.href);
url.searchParams.set('show_plain_layout', 'true');
aTagElement.setAttribute('href', url);
}
});
},
setDirectionAttribute: () => {
const htmlElement = document.querySelector('html');
// If direction is already applied through props, do not apply again (iframe case)
const hasDirApplied = htmlElement.getAttribute('data-dir-applied');
if (!htmlElement || hasDirApplied) return;
const localeFromHtml = htmlElement.lang;
htmlElement.dir =
localeFromHtml && getLanguageDirection(localeFromHtml) ? 'rtl' : 'ltr';
},
initializeThemesInPortal: initializeTheme,
initialize: () => {
openExternalLinksInNewTab();
InitializationHelpers.setDirectionAttribute();
if (window.portalConfig.isPlainLayoutEnabled === 'true') {
InitializationHelpers.appendPlainParamToURLs();
} else {
InitializationHelpers.initializeThemesInPortal();
InitializationHelpers.navigateToLocalePage();
InitializationHelpers.initializeSearch();
InitializationHelpers.initializeTableOfContents();
}
},
onLoad: () => {
InitializationHelpers.initialize();
if (window.location.hash) {
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual';
}
const a = document.createElement('a');
a.href = window.location.hash;
a['data-turbolinks'] = false;
a.click();
}
},
};