conflict-nuxt-4/app/components/auto-import/AutoComplation.vue
2026-02-14 10:06:25 +03:30

971 lines
34 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="relative w-full max-w-xl mx-auto">
<!-- Main Search Container -->
<div class="relative">
<!-- Input Field -->
<div class="relative">
<div
class="relative bg-white dark:bg-dark-primary border border-gray-300 dark:border-dark-primary-700 rounded-lg transition-all duration-200"
>
<!-- Input Row -->
<div class="flex items-center">
<!-- Left Side: Icons + Filter Dropdown -->
<div class="flex items-center space-x-2 space-x-reverse ml-3">
<!-- Search Icon -->
<!-- <UIcon
name="i-heroicons-magnifying-glass"
class="w-5 h-5 text-gray-400 dark:text-gray-500 transition-colors duration-200"
:class="{
'text-blue-500 dark:text-blue-400': isFocused || searchQuery,
}"
/> -->
<!-- Filter Dropdown Trigger -->
<div class="relative">
<!-- Trigger Button با آیکون -->
<button
@click.stop="toggleFilterDropdown"
class="flex items-center gap-1 px-3 py-1.5 text-sm text-primary-700 hover:text-primary-900 hover:bg-primary-50 rounded-lg transition-all duration-200"
>
{{ selectedFilterLabel }}
<!-- آیکون Chevron با انیمیشن -->
<svg
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-180': filterDropdownOpen }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<!-- Dropdown Menu -->
<Transition
enter-active-class="transition ease-out duration-150"
enter-from-class="opacity-0 -translate-y-1"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-1"
>
<div
v-if="filterDropdownOpen"
class="absolute top-full left-0 mt-1.5 w-36 bg-white border border-primary-200 rounded-lg shadow-sm z-50"
>
<div class="py-1.5">
<button
v-for="filter in quickFilters"
:key="filter.value"
@click="selectFilterFromDropdown(filter)"
class="w-full px-4 py-2 text-sm text-primary-700 hover:bg-primary-50 hover:text-primary-900 text-right transition-colors duration-150"
:class="{
'text-blue-600': selectedFilter === filter.value,
}"
>
{{ filter.label }}
</button>
</div>
</div>
</Transition>
</div>
<!-- Selected Filter Badge -->
<!-- <div
v-if="selectedFilter !== 'all'"
class="flex items-center space-x-1 space-x-reverse px-2 py-1 bg-blue-50 dark:bg-blue-900/30 rounded-md animate-fade-in"
>
<UIcon
:name="selectedFilterIcon"
class="w-3 h-3 text-blue-500 dark:text-blue-400"
/>
<span class="text-xs text-blue-600 dark:text-blue-300">{{
selectedFilterLabel
}}</span>
<button
@click.stop="clearFilter"
class="text-blue-400 dark:text-blue-300 hover:text-blue-600 dark:hover:text-blue-100"
>
<UIcon name="i-heroicons-x-mark" class="w-3 h-3" />
</button>
</div> -->
</div>
<!-- Main Input -->
<input
ref="inputRef"
v-model="searchQuery"
:placeholder="isFocused && !searchQuery ? placeholder : ''"
type="text"
autocomplete="off"
spellcheck="false"
class="flex-1 bg-transparent text-dark-light dark:text-white-light placeholder:text-dark-light dark:placeholder:text-white-light focus:outline-none text-sm w-full"
@focus="handleFocus"
@blur="handleBlur"
@input="handleInput"
@keydown="handleKeyDown"
@keyup.enter="handleEnter"
/>
<!-- Right Side Actions -->
<div class="flex items-center space-x-2 space-x-reverse">
<!-- <div v-if="loading" class="flex items-center justify-center">
<div
class="w-4 h-4 border-2 border-gray-300 dark:border-dark-primary-600 border-t-blue-500 dark:border-t-blue-400 rounded-full animate-spin"
></div>
</div> -->
<button
v-if="searchQuery"
@click="clearSearch"
class="flex items-center justify-center w-6 h-6 rounded-full hover:bg-gray-100 dark:hover:bg-dark-primary-800 transition-colors duration-200"
aria-label="پاک کردن"
>
<UIcon
name="i-heroicons-x-mark"
class="w-3 h-3 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
/>
</button>
<button
:disabled="loading"
@click="handleSearch"
class="cursor-pointer flex items-center justify-center h-10 p-2 bg-primary text-white rounded-l-lg hover:bg-primary-400 dark:hover:bg-dark-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 active:scale-95"
aria-label="جستجو"
>
جستجو
<!-- <UIcon name="i-heroicons-arrow-left" class="w-4 h-4" /> -->
</button>
</div>
</div>
</div>
<!-- Character Counter -->
<!-- <div v-if="searchQuery" class="absolute left-3 -bottom-5">
<span class="text-xs text-gray-400 dark:text-gray-500 font-mono">
{{ searchQuery.length }}/{{ resolvedProps.value.maxChars || 100 }}
</span>
</div> -->
</div>
<!-- Results Dropdown -->
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 transform -translate-y-2"
enter-to-class="opacity-100 transform translate-y-0"
leave-active-class="transition-all duration-150 ease-in"
leave-from-class="opacity-100 transform translate-y-0"
leave-to-class="opacity-0 transform -translate-y-2"
>
<div
v-if="hasVisibleResults"
class="absolute z-50 w-full mt-2 bg-white dark:bg-dark-primary border border-gray-200 dark:border-dark-primary-700 rounded-lg shadow-lg backdrop-blur-sm bg-opacity-95 dark:bg-opacity-95 overflow-hidden"
>
<!-- Header -->
<div
class="px-4 py-3 bg-gray-100 dark:bg-dark-primary-800 border-b border-gray-100 dark:border-dark-primary-800"
>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2 space-x-reverse">
<UIcon
:name="
showAutocompleteList
? 'i-heroicons-sparkles'
: 'i-heroicons-clock'
"
class="w-4 h-4 ml-1"
:class="
showAutocompleteList ? 'text-purple-500' : 'text-blue-500'
"
/>
<span
class="text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ showAutocompleteList ? "پیشنهادات" : "تاریخچه" }}
<!-- <span
v-if="selectedFilter !== 'all'"
class="mr-2 inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-300"
>
<UIcon :name="selectedFilterIcon" class="w-3 h-3 ml-1" />
{{ selectedFilterLabel }}
</span> -->
</span>
</div>
<span class="text-xs text-gray-500 dark:text-gray-400"
>{{ totalResults }} مورد</span
>
</div>
</div>
<!-- Results List -->
<div class="max-h-80 overflow-y-auto custom-scrollbar">
<!-- Autocomplete -->
<div v-if="showAutocompleteList">
<div
v-for="(item, index) in autocompleteResults"
:key="`autocomplete-${item.id || index}`"
class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-dark-primary-800 cursor-pointer border-b border-gray-100 dark:border-dark-primary-800 last:border-b-0 transition-colors duration-150"
:class="{
'bg-blue-50 dark:bg-blue-900/20':
selectedIndex === index && activeSection === 'autocomplete',
}"
@click="handleAutocompleteSelect(item)"
@mouseenter="
hoverIndex = index;
activeSection = 'autocomplete';
"
>
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<div
class="flex items-center space-x-2 space-x-reverse mb-1"
>
<UIcon
:name="item.icon || 'i-heroicons-magnifying-glass'"
class="w-4 h-4 text-gray-400 dark:text-gray-500 flex-shrink-0"
/>
<span
class="text-sm font-medium text-gray-800 dark:text-gray-200 truncate"
>
{{ item.label || item.value || item }}
</span>
</div>
<p
v-if="item.description"
class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2"
>
{{ item.description }}
</p>
</div>
<UIcon
name="i-heroicons-chevron-left"
class="w-4 h-4 text-gray-300 dark:text-gray-600 flex-shrink-0"
:class="{
'text-blue-500 dark:text-blue-400':
selectedIndex === index &&
activeSection === 'autocomplete',
}"
/>
</div>
</div>
</div>
<!-- History -->
<div v-if="showHistoryList">
<div
v-for="(item, index) in historyResults"
:key="`history-${item.id || index}`"
class="group px-4 py-3 hover:bg-gray-50 dark:hover:bg-dark-primary-800 cursor-pointer border-b border-gray-100 dark:border-dark-primary-800 last:border-b-0 transition-colors duration-150"
:class="{
'bg-blue-50 dark:bg-blue-900/20':
selectedIndex === autocompleteResults.length + index &&
activeSection === 'history',
}"
@click="handleHistorySelect(item)"
@mouseenter="
hoverIndex = autocompleteResults.length + index;
activeSection = 'history';
"
>
<div class="flex items-center justify-between">
<div
class="flex items-center space-x-3 space-x-reverse flex-1 min-w-0"
>
<div class="relative ml-2">
<div
class="w-8 h-8 bg-gray-100 dark:bg-dark-primary-800 rounded-lg flex items-center justify-center"
>
<UIcon
name="i-heroicons-clock"
class="w-4 h-4 text-gray-400 dark:text-gray-500"
/>
</div>
<div
v-if="item.count && item.count > 1"
class="absolute -top-1 -right-1 w-4 h-4 bg-blue-500 dark:bg-blue-400 rounded-full flex items-center justify-center"
>
<span class="text-[9px] text-white font-medium">{{
item.count
}}</span>
</div>
</div>
<div class="flex-1 min-w-0">
<p
class="text-sm text-gray-800 dark:text-gray-200 truncate"
>
{{ item.label || item.value || item }}
</p>
<p
v-if="item.timestamp"
class="text-xs text-gray-500 dark:text-gray-400 mt-0.5"
>
{{ formatTimeAgo(item.timestamp) }}
</p>
</div>
</div>
<button
@click.stop="removeHistoryItem(item, index)"
class="opacity-0 group-hover:opacity-100 p-1 text-gray-400 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition-all duration-200"
>
<UIcon name="i-heroicons-trash" class="w-4 h-4" />
</button>
</div>
</div>
<!-- Empty History -->
<div
v-if="historyResults.length === 0"
class="px-4 py-8 text-center"
>
<UIcon
name="i-heroicons-clock"
class="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-3"
/>
<p class="text-sm text-gray-500 dark:text-gray-400">
تاریخچه خالی است
</p>
</div>
</div>
<!-- Empty State -->
<div v-if="showEmptyState" class="px-4 py-8 text-center">
<UIcon
name="i-heroicons-magnifying-glass"
class="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-3"
/>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">
نتیجه‌ای یافت نشد
</p>
<p class="text-xs text-gray-500 dark:text-gray-500">
عبارت جستجو را تغییر دهید
</p>
</div>
</div>
<!-- Clear All History -->
<div
v-if="historyResults.length > 0"
class="px-4 py-3 bg-gray-100 dark:bg-dark-primary-800 border-t border-gray-100 dark:border-dark-primary-800"
>
<button
@click="clearAllHistory"
class="w-full cursor-pointer flex items-center justify-center space-x-2 space-x-reverse text-sm text-gray-500 dark:text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors duration-200"
>
<UIcon name="i-heroicons-trash" class="w-4 h-4 ml-1" />
<span>پاک کردن تاریخچه</span>
</button>
</div>
<!-- Footer -->
<!-- <div
v-if="
hasVisibleResults && (showAutocompleteList || showHistoryList)
"
class="px-4 py-2 border-t border-gray-100 dark:border-dark-primary-800 bg-gray-50 dark:bg-dark-primary-800/50"
>
<div
class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
>
<div class="flex items-center space-x-4 space-x-reverse">
<span class="flex items-center space-x-1 space-x-reverse">
<UIcon name="i-heroicons-arrow-up" class="w-3 h-3" />
<UIcon name="i-heroicons-arrow-down" class="w-3 h-3" />
<span>ناوبری</span>
</span>
<span class="flex items-center space-x-1 space-x-reverse">
<UIcon name="i-heroicons-enter" class="w-3 h-3" />
<span>انتخاب</span>
</span>
</div>
<span> Enter برای جستجو</span>
</div>
</div> -->
</div>
</Transition>
</div>
<!-- Recent Searches (خارج از دراپداون) -->
<!-- <div
v-if="
showRecentSearches &&
historyResults.length > 0 &&
!searchQuery &&
!isFocused
"
class="mt-4 animate-fade-in"
>
<div
class="flex items-center space-x-2 space-x-reverse overflow-x-auto py-2"
>
<span class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"
>اخیراً:</span
>
<button
v-for="(item, index) in recentSearches"
:key="index"
@click="selectRecentSearch(item)"
class="flex-shrink-0 px-3 py-1 text-xs bg-gray-100 dark:bg-dark-primary-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-dark-primary-700 rounded-full transition-colors duration-200 truncate max-w-xs"
>
{{ item }}
</button>
</div>
</div> -->
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
const props = defineProps({
autoComplationSchema: {
type: Object,
default: () => ({}),
},
placeholder: { type: String, default: "جستجو در هزاران محتوا" },
autocompleteUrl: { type: String, required: true },
historyUrl: { type: String, required: true },
showSearchButton: { type: Boolean, default: true },
debounceTime: { type: Number, default: 500 },
maxHistoryItems: { type: Number, default: 10 },
minCharsForAutocomplete: { type: Number, default: 2 },
maxChars: { type: Number, default: 100 },
filters: { type: Array, default: () => [] },
});
const resolvedProps = computed(() => ({
placeholder: props.autoComplationSchema.placeholder ?? props.placeholder,
autocompleteUrl:
props.autoComplationSchema.autocompleteUrl ?? props.autocompleteUrl,
historyUrl: props.autoComplationSchema.historyUrl ?? props.historyUrl,
showSearchButton:
props.autoComplationSchema.showSearchButton ?? props.showSearchButton,
debounceTime: props.autoComplationSchema.debounceTime ?? props.debounceTime,
maxHistoryItems:
props.autoComplationSchema.maxHistoryItems ?? props.maxHistoryItems,
minCharsForAutocomplete:
props.autoComplationSchema.minCharsForAutocomplete ??
props.minCharsForAutocomplete,
maxChars: props.autoComplationSchema.maxChars ?? props.maxChars,
filters: props.autoComplationSchema.filters ?? props.filters,
}));
// const emit = defineEmits([
// "search",
// "autocomplete-select",
// "history-select",
// "history-clear",
// "history-remove",
// "focus",
// "blur",
// "clear",
// "filter-selected",
// "complete-search",
// ]);
const emit = defineEmits(["auto-complation-handler"]);
// States
const searchQuery = ref("");
const autocompleteResults = ref([]);
const historyResults = ref([]);
const recentSearches = ref([]);
const showResults = ref(false);
const selectedIndex = ref(-1);
const hoverIndex = ref(-1);
const activeSection = ref(null);
const loading = ref(false);
const isFocused = ref(false);
const inputRef = ref(null);
const selectedFilter = ref("all");
const filterDropdownOpen = ref(false);
const showRecentSearches = ref(true);
const quickFilters = computed(() => [
// { label: "همه", value: "all", icon: "i-heroicons-globe-alt" },
...(resolvedProps.value.filters || []),
]);
// LocalStorage Key
const LOCAL_STORAGE_KEY = "searchHistory";
// لود از localStorage
const loadHistoryFromLocal = () => {
try {
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
historyResults.value = parsed.slice(
0,
resolvedProps.value.maxHistoryItems
);
recentSearches.value = historyResults.value
.slice(0, 5)
.map((i) => i.label);
}
} catch (err) {
console.error("خطا در لود تاریخچه از localStorage:", err);
}
};
// ذخیره در localStorage
const saveHistoryToLocal = () => {
try {
localStorage.setItem(
LOCAL_STORAGE_KEY,
JSON.stringify(historyResults.value)
);
} catch (err) {
console.error("خطا در ذخیره تاریخچه:", err);
}
};
// Computed
const hasVisibleResults = computed(
() =>
showResults.value &&
(showAutocompleteList.value ||
showHistoryList.value ||
showEmptyState.value)
);
const showAutocompleteList = computed(
() =>
showResults.value &&
autocompleteResults.value.length > 0 &&
searchQuery.value.length >= resolvedProps.value.minCharsForAutocomplete
);
const showHistoryList = computed(
() => showResults.value && searchQuery.value.length === 0
);
const showEmptyState = computed(
() =>
showResults.value &&
!showAutocompleteList.value &&
!showHistoryList.value &&
searchQuery.value.length >= resolvedProps.value.minCharsForAutocomplete
);
const totalResults = computed(
() =>
(showAutocompleteList.value ? autocompleteResults.value.length : 0) +
(showHistoryList.value ? historyResults.value.length : 0)
);
const selectedFilterIcon = computed(
() =>
quickFilters.value.find((f) => f.value === selectedFilter.value)?.icon ||
"i-heroicons-funnel"
);
const selectedFilterLabel = computed(
() =>
quickFilters.value.find((f) => f.value === selectedFilter.value)?.label ||
"فیلتر"
);
// فرمت زمان
const formatTimeAgo = (timestamp) => {
if (!timestamp) return "اخیراً";
const now = new Date();
const past = new Date(timestamp);
const diffMs = now - past;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return "همین الان";
if (diffMins < 60) return `${diffMins} دقیقه پیش`;
if (diffHours < 24) return `${diffHours} ساعت پیش`;
if (diffDays < 7) return `${diffDays} روز پیش`;
return past.toLocaleDateString("fa-IR");
};
const { $http: httpService } = useNuxtApp();
// Autocomplete
const fetchAutocomplete = async () => {
if (
!searchQuery.value ||
searchQuery.value.length < resolvedProps.value.minCharsForAutocomplete
) {
autocompleteResults.value = [];
return;
}
loading.value = true;
let url = `${resolvedProps.value.autocompleteUrl}/q=${searchQuery.value}`;
if (selectedFilter.value !== "all") url += `&filter=${selectedFilter.value}`;
try {
const response = await httpService.getRequest(url);
const hits = response?.hits?.hits || [];
autocompleteResults.value = hits
.map((hit) => ({
id: hit._id,
label: hit._source?.title || hit._source?.name || "بدون عنوان",
value: hit._source?.title || hit._source?.name,
description: hit._source?.description || hit._source?.subtitle || "",
icon:
hit._source?.type === "product"
? "i-heroicons-shopping-bag"
: hit._source?.type === "article"
? "i-heroicons-newspaper"
: "i-heroicons-document-text",
}))
.slice(0, 6);
} catch (err) {
console.error("خطا در پیشنهادات:", err);
autocompleteResults.value = [];
} finally {
loading.value = false;
}
};
// History (اولویت با localStorage)
const fetchHistory = async () => {
loadHistoryFromLocal();
// اگر بخوای از سرور هم بگیری، این بخش رو فعال کن
// if (historyResults.value.length === 0) { ... }
};
// اضافه کردن به تاریخچه + ذخیره
const addToHistory = (item) => {
const term =
typeof item === "string" ? item : item.value || item.label || item;
if (!term) return;
const existingIndex = historyResults.value.findIndex((h) => h.value === term);
if (existingIndex > -1) {
const existing = historyResults.value[existingIndex];
existing.count = (existing.count || 0) + 1;
existing.timestamp = new Date().toISOString();
historyResults.value.splice(existingIndex, 1);
historyResults.value.unshift(existing);
} else {
historyResults.value.unshift({
id: Date.now(),
label: term,
value: term,
timestamp: new Date().toISOString(),
count: 1,
});
}
if (historyResults.value.length > resolvedProps.value.maxHistoryItems) {
historyResults.value = historyResults.value.slice(
0,
resolvedProps.value.maxHistoryItems
);
}
recentSearches.value = historyResults.value.slice(0, 5).map((i) => i.label);
saveHistoryToLocal();
};
const removeHistoryItem = (item, index) => {
historyResults.value.splice(index, 1);
recentSearches.value = recentSearches.value.filter((t) => t !== item.label);
saveHistoryToLocal();
// emit("history-remove", item);
emit("auto-complation-handler", { action: "history-remove", item });
};
const clearAllHistory = () => {
historyResults.value = [];
recentSearches.value = [];
localStorage.removeItem(LOCAL_STORAGE_KEY);
// emit("history-clear");
emit("auto-complation-handler", { action: "history-clear", item: null });
};
// UI Functions
const toggleFilterDropdown = () =>
(filterDropdownOpen.value = !filterDropdownOpen.value);
const selectFilterFromDropdown = (filter) => {
selectedFilter.value = filter.value;
// emit("filter-selected", filter);
emit("auto-complation-handler", { action: "filter-selected", item: filter });
filterDropdownOpen.value = false;
if (searchQuery.value) fetchAutocomplete();
};
const clearFilter = () => {
selectedFilter.value = "all";
// emit("filter-selected", {
// label: "همه",
// value: "all",
// icon: "i-heroicons-globe-alt",
// });
emit("auto-complation-handler", {
action: "filter-selected",
item: {
label: "همه",
value: "all",
icon: "i-heroicons-globe-alt",
},
});
if (searchQuery.value) fetchAutocomplete();
};
const clearSearch = () => {
searchQuery.value = "";
autocompleteResults.value = [];
// emit("clear");
emit("auto-complation-handler", { action: "clear", item: null });
};
const handleSearch = () => {
// اگر تایمر debounce فعال بود، لغوش کن
if (searchTimer.value) {
clearTimeout(searchTimer.value);
searchTimer.value = null;
}
const query = searchQuery.value.trim();
// حتی اگر query خالی باشه، ایونت رو بفرست (جستجوی خالی = نمایش همه)
addToHistory(query || ""); // اختیاری: می‌تونی تاریخچه خالی اضافه نکنی
showResults.value = false;
// ارسال ایونت complete-search حتی اگر query خالی باشه
emit("auto-complation-handler", {
action: "complete-search",
item: query || null, // یا "" یا null — بسته به چیزی که والد انتظار داره
});
};
// const handleSearch = () => {
// // اگر تایمر فعال بود، لغوش کن (چون کاربر دستی جستجو کرد)
// if (searchTimer.value) {
// clearTimeout(searchTimer.value);
// searchTimer.value = null;
// }
// if (!searchQuery.value.trim()) return;
// addToHistory(searchQuery.value.trim());
// showResults.value = false;
// // emit("search", searchQuery.value.trim());
// // emit("complete-search", searchQuery.value.trim());
// // emit("auto-complation-handler", { action: "search", item: searchQuery.value.trim() });
// emit("auto-complation-handler", { action: "complete-search", item: searchQuery.value.trim() });
// };
const handleAutocompleteSelect = (item) => {
searchQuery.value = item.value || item.label;
addToHistory(item);
showResults.value = false;
inputRef.value?.blur();
// emit("autocomplete-select", item);
// emit("search", searchQuery.value);
emit("auto-complation-handler", { action: "autocomplete-select", item });
// emit("auto-complation-handler", { action: "search", item: searchQuery.value });
emit("auto-complation-handler", {
action: "complete-search",
item: searchQuery.value.trim(),
});
};
const handleHistorySelect = (item) => {
// console.log("item ==> ", item);
searchQuery.value = item.value || item.label;
showResults.value = false;
inputRef.value?.focus();
// emit("history-select", item);
emit("auto-complation-handler", { action: "history-select", item });
emit("auto-complation-handler", {
action: "complete-search",
item: searchQuery.value.trim(),
});
};
const selectRecentSearch = (term) => {
searchQuery.value = term;
handleSearch();
};
// 3. اصلاح handleFocus برای باز کردن دراپ‌داون
const handleFocus = () => {
isFocused.value = true;
showResults.value = true; // همیشه باز باشه
selectedIndex.value = -1;
activeSection.value = null;
// emit("focus");
emit("auto-complation-handler", { action: "focus", item: null });
if (historyResults.value.length === 0) {
fetchHistory();
}
};
const handleBlur = () => {
setTimeout(() => {
isFocused.value = false;
showResults.value = false;
selectedIndex.value = -1;
hoverIndex.value = -1;
activeSection.value = null;
filterDropdownOpen.value = false;
// emit("blur");
emit("auto-complation-handler", { action: "blur", item: null });
}, 300);
};
const searchTimer = ref(null);
// زمان debounce به ثانیه (۳ ثانیه = 3000 میلی‌ثانیه)
const DEBOUNCE_TIME = computed(() => resolvedProps.value.debounceTime);
const handleInput = () => {
// همیشه پیشنهادات رو سریع بگیر (autocomplete)
fetchAutocomplete();
// ریست تایمر قبلی
if (searchTimer.value) {
clearTimeout(searchTimer.value);
}
// تنظیم تایمر جدید برای جستجوی نهایی
searchTimer.value = setTimeout(() => {
if (
searchQuery.value.trim() &&
searchQuery.value.length >= resolvedProps.value.minCharsForAutocomplete
) {
// جستجوی نهایی بعد از ۳ ثانیه توقف تایپ
addToHistory(searchQuery.value.trim());
// emit("search", searchQuery.value.trim());
// emit("complete-search", searchQuery.value.trim());
// emit("auto-complation-handler", { action: "search", item: searchQuery.value.trim() });
emit("auto-complation-handler", {
action: "complete-search",
item: searchQuery.value.trim(),
});
}
}, DEBOUNCE_TIME.value);
};
// 4. اصلاح handleKeyDown (نسخه بهبود یافته)
const handleKeyDown = (event) => {
if (!hasVisibleResults.value) return;
const autocompleteCount = showAutocompleteList.value
? autocompleteResults.value.length
: 0;
const historyCount = showHistoryList.value ? historyResults.value.length : 0;
const totalItems = autocompleteCount + historyCount;
if (totalItems === 0) return;
switch (event.key) {
case "ArrowDown":
event.preventDefault();
if (selectedIndex.value < totalItems - 1) {
selectedIndex.value++;
} else {
selectedIndex.value = 0; // چرخشی
}
updateActiveSection(autocompleteCount);
break;
case "ArrowUp":
event.preventDefault();
if (selectedIndex.value <= 0) {
selectedIndex.value = totalItems - 1;
} else {
selectedIndex.value--;
}
updateActiveSection(autocompleteCount);
break;
case "Enter":
event.preventDefault();
if (selectedIndex.value >= 0) {
if (activeSection.value === "autocomplete") {
handleAutocompleteSelect(
autocompleteResults.value[selectedIndex.value]
);
} else if (activeSection.value === "history") {
const historyIndex = selectedIndex.value - autocompleteCount;
handleHistorySelect(historyResults.value[historyIndex]);
}
} else {
handleSearch();
}
break;
case "Escape":
showResults.value = false;
selectedIndex.value = -1;
activeSection.value = null;
inputRef.value?.blur();
break;
}
};
const updateActiveSection = (autocompleteCount) => {
activeSection.value =
selectedIndex.value < autocompleteCount ? "autocomplete" : "history";
}; // قابل گسترش برای ناوبری کیبورد
const handleEnter = () => {
handleSearch(); // جستجوی فوری با Enter
};
// Mount
const closeDropdown = () => (filterDropdownOpen.value = false);
onMounted(() => {
document.addEventListener("click", closeDropdown);
});
onUnmounted(() => {
document.removeEventListener("click", closeDropdown);
});
watch(autocompleteResults, (val) => {
if (val.length > 0 && isFocused.value) {
showResults.value = true;
}
});
watch(searchQuery, (newVal) => {
// همیشه دراپ‌داون باز باشه وقتی فوکوس داریم یا چیزی تایپ شده
if (isFocused.value) {
showResults.value = true;
}
// ریست ناوبری کیبورد
selectedIndex.value = -1;
activeSection.value = null;
});
</script>
<style scoped>
.animate-fade-in {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(243, 244, 246, 0.5);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
.dark .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(75, 85, 99, 0.5);
}
</style>