Files
chatwoot/app/javascript/portal/components/PublicArticleSearch.vue
T

172 lines
4.0 KiB
Vue

<script>
import SearchSuggestions from './SearchSuggestions.vue';
import PublicSearchInput from './PublicSearchInput.vue';
import ArticlesAPI from '../api/article';
export default {
components: {
PublicSearchInput,
SearchSuggestions,
},
props: {
size: {
type: String,
default: 'default',
validator: value => ['small', 'default'].includes(value),
},
showKbd: {
type: Boolean,
default: false,
},
},
emits: ['input', 'blur'],
data() {
return {
searchTerm: '',
isLoading: false,
showSearchBox: false,
searchResults: [],
};
},
computed: {
portalSlug() {
return window.portalConfig.portalSlug;
},
localeCode() {
return window.portalConfig.localeCode;
},
normalizedSearchTerm() {
return this.searchTerm.trim();
},
shouldShowSearchBox() {
return this.normalizedSearchTerm !== '' && this.showSearchBox;
},
searchTranslations() {
const { searchTranslations = {} } = window.portalConfig;
return searchTranslations;
},
kbdLabel() {
if (!this.showKbd) return '';
const isMac = /Mac|iPhone|iPad|iPod/i.test(
navigator.platform || navigator.userAgent
);
return isMac ? '⌘ K' : 'Ctrl K';
},
},
watch: {
currentPage() {
this.clearSearchTerm();
},
},
mounted() {
if (this.showKbd) document.addEventListener('keydown', this.onKeydown);
},
unmounted() {
if (this.showKbd) document.removeEventListener('keydown', this.onKeydown);
clearTimeout(this.typingTimer);
},
methods: {
onUpdateSearchTerm(value) {
this.searchTerm = value;
if (this.typingTimer) {
clearTimeout(this.typingTimer);
}
if (this.normalizedSearchTerm === '') {
this.searchResults = [];
this.isLoading = false;
this.closeSearch();
return;
}
this.openSearch();
this.isLoading = true;
this.typingTimer = setTimeout(() => {
this.fetchArticlesByQuery();
}, 1000);
},
onChange(e) {
this.$emit('input', e.target.value);
},
onBlur(e) {
this.$emit('blur', e.target.value);
},
openSearch() {
this.showSearchBox = true;
},
closeSearch() {
this.showSearchBox = false;
},
clearSearchTerm() {
this.searchTerm = '';
},
onKeydown(e) {
if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
e.preventDefault();
if (this.$refs.searchInput) this.$refs.searchInput.focusInput();
}
if (e.key === 'Escape') {
this.closeSearch();
if (this.$refs.searchInput) this.$refs.searchInput.blurInput();
}
},
async fetchArticlesByQuery() {
const query = this.normalizedSearchTerm;
if (!query) {
this.isLoading = false;
return;
}
try {
this.isLoading = true;
this.searchResults = [];
const { data } = await ArticlesAPI.searchArticles(
this.portalSlug,
this.localeCode,
query
);
this.searchResults = data.payload;
} catch (error) {
// Show something wrong message
} finally {
this.isLoading = false;
}
},
},
};
</script>
<template>
<div v-on-clickaway="closeSearch" class="relative w-full max-w-5xl my-4">
<PublicSearchInput
ref="searchInput"
:search-term="searchTerm"
:search-placeholder="searchTranslations.searchPlaceholder"
:size="size"
:kbd="kbdLabel"
@update:search-term="onUpdateSearchTerm"
@focus="openSearch"
/>
<div
v-if="shouldShowSearchBox"
class="absolute w-full top-14"
@mouseover="openSearch"
>
<SearchSuggestions
:items="searchResults"
:is-loading="isLoading"
:search-term="normalizedSearchTerm"
:empty-placeholder="searchTranslations.emptyPlaceholder"
:results-title="searchTranslations.resultsTitle"
:loading-placeholder="searchTranslations.loadingPlaceholder"
/>
</div>
</div>
</template>