- Standardized the use of quotes and spacing in various files. - Removed unnecessary line breaks and trailing spaces in components. - Improved the structure of computed properties and methods for better clarity. - Enhanced the consistency of prop definitions and emit events in Vue components. - Updated the GraphQL composable to streamline error handling and data processing. - Refactored network configuration files for better organization and readability. - Cleaned up model files by removing redundant lines and ensuring consistent formatting. - Adjusted router configuration for improved readability. - Enhanced utility functions for better maintainability and clarity.
245 lines
5.1 KiB
Vue
245 lines
5.1 KiB
Vue
<script setup lang="ts" generic="T">
|
|
import { ref, computed } from "vue";
|
|
import { onClickOutside } from "@vueuse/core";
|
|
import ChevronDown from "@/assets/chevronDown.svg";
|
|
|
|
defineOptions({ name: "UiDropdown" });
|
|
|
|
export interface DropdownItem<T = any> {
|
|
value: T;
|
|
label: string;
|
|
icon?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
items: DropdownItem<T>[];
|
|
modelValue: T;
|
|
placeholder?: string;
|
|
searchable?: boolean;
|
|
disabled?: boolean;
|
|
size?: "sm" | "md" | "lg";
|
|
showIcon?: boolean;
|
|
}>(),
|
|
{
|
|
placeholder: "Selecione...",
|
|
searchable: false,
|
|
disabled: false,
|
|
size: "md",
|
|
showIcon: true,
|
|
},
|
|
);
|
|
|
|
const emit = defineEmits<{
|
|
"update:modelValue": [value: T];
|
|
change: [value: T];
|
|
}>();
|
|
|
|
const isOpen = ref(false);
|
|
const searchQuery = ref("");
|
|
const dropdownRef = ref<HTMLElement | null>(null);
|
|
|
|
const selectedItem = computed(() => {
|
|
return props.items.find((item) => item.value === props.modelValue);
|
|
});
|
|
|
|
const filteredItems = computed(() => {
|
|
if (!props.searchable || !searchQuery.value) {
|
|
return props.items;
|
|
}
|
|
|
|
const query = searchQuery.value.toLowerCase();
|
|
return props.items.filter((item) => item.label.toLowerCase().includes(query));
|
|
});
|
|
|
|
const toggleDropdown = () => {
|
|
if (!props.disabled) {
|
|
isOpen.value = !isOpen.value;
|
|
if (!isOpen.value) {
|
|
searchQuery.value = "";
|
|
}
|
|
}
|
|
};
|
|
|
|
const selectItem = (item: DropdownItem<T>) => {
|
|
if (!item.disabled) {
|
|
emit("update:modelValue", item.value);
|
|
emit("change", item.value);
|
|
isOpen.value = false;
|
|
searchQuery.value = "";
|
|
}
|
|
};
|
|
|
|
onClickOutside(dropdownRef, () => {
|
|
isOpen.value = false;
|
|
searchQuery.value = "";
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="dropdownRef" class="dropdown-container">
|
|
<button
|
|
type="button"
|
|
:class="[
|
|
'dropdown-trigger',
|
|
`size-${size}`,
|
|
{ disabled: disabled, open: isOpen },
|
|
]"
|
|
@click="toggleDropdown"
|
|
>
|
|
<img
|
|
v-if="selectedItem?.icon && showIcon"
|
|
:src="selectedItem.icon"
|
|
:alt="selectedItem.label"
|
|
class="item-icon"
|
|
/>
|
|
<span class="selected-text">
|
|
{{ selectedItem?.label || placeholder }}
|
|
</span>
|
|
<ChevronDown class="chevron" :class="{ rotated: isOpen }" />
|
|
</button>
|
|
|
|
<transition name="dropdown-fade">
|
|
<div v-if="isOpen" class="dropdown-menu">
|
|
<input
|
|
v-if="searchable"
|
|
v-model="searchQuery"
|
|
type="text"
|
|
class="search-input"
|
|
placeholder="Buscar..."
|
|
@click.stop
|
|
/>
|
|
|
|
<div class="items-container">
|
|
<div
|
|
v-for="item in filteredItems"
|
|
:key="String(item.value)"
|
|
:class="[
|
|
'dropdown-item',
|
|
{
|
|
selected: item.value === modelValue,
|
|
disabled: item.disabled,
|
|
},
|
|
]"
|
|
@click="selectItem(item)"
|
|
>
|
|
<img
|
|
v-if="item.icon && showIcon"
|
|
:src="item.icon"
|
|
:alt="item.label"
|
|
class="item-icon"
|
|
/>
|
|
<span class="item-label">{{ item.label }}</span>
|
|
</div>
|
|
|
|
<div v-if="filteredItems.length === 0" class="no-results">
|
|
Nenhum resultado encontrado
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.dropdown-container {
|
|
@apply relative inline-block;
|
|
}
|
|
|
|
.dropdown-trigger {
|
|
@apply flex items-center gap-2 bg-gray-300 hover:bg-gray-200 rounded-3xl transition-colors cursor-pointer border-none outline-none;
|
|
}
|
|
|
|
.dropdown-trigger:focus {
|
|
@apply outline-2 outline-indigo-800;
|
|
}
|
|
|
|
.dropdown-trigger.disabled {
|
|
@apply opacity-50 cursor-not-allowed;
|
|
}
|
|
|
|
.dropdown-trigger.disabled:hover {
|
|
@apply bg-gray-300;
|
|
}
|
|
|
|
.size-sm {
|
|
@apply px-2 py-1 text-sm;
|
|
}
|
|
|
|
.size-md {
|
|
@apply px-3 py-2 text-base;
|
|
}
|
|
|
|
.size-lg {
|
|
@apply px-4 py-3 text-lg;
|
|
}
|
|
|
|
.item-icon {
|
|
@apply sm:w-fit w-4 flex-shrink-0;
|
|
}
|
|
|
|
.selected-text {
|
|
@apply text-gray-900 font-medium min-w-fit;
|
|
}
|
|
|
|
.chevron {
|
|
@apply transition-transform duration-300 invert pr-1;
|
|
}
|
|
|
|
.chevron.rotated {
|
|
@apply rotate-180;
|
|
}
|
|
|
|
.dropdown-menu {
|
|
@apply absolute right-0 mt-2 bg-white rounded-xl border border-gray-300 shadow-md z-50 min-w-max w-full;
|
|
}
|
|
|
|
.search-input {
|
|
@apply w-full px-4 py-3 border-b border-gray-200 outline-none text-gray-900;
|
|
}
|
|
|
|
.search-input:focus {
|
|
@apply border-indigo-800;
|
|
}
|
|
|
|
.items-container {
|
|
@apply max-h-64 overflow-y-auto;
|
|
}
|
|
|
|
.dropdown-item {
|
|
@apply flex items-center gap-2 px-4 py-4 cursor-pointer hover:bg-gray-300 transition-colors text-gray-900 font-semibold text-sm;
|
|
}
|
|
|
|
.dropdown-item.selected {
|
|
@apply bg-gray-100;
|
|
}
|
|
|
|
.dropdown-item.disabled {
|
|
@apply opacity-50 cursor-not-allowed;
|
|
}
|
|
|
|
.dropdown-item.disabled:hover {
|
|
@apply bg-transparent;
|
|
}
|
|
|
|
.item-label {
|
|
@apply text-end;
|
|
}
|
|
|
|
.no-results {
|
|
@apply px-4 py-6 text-center text-gray-500 text-sm;
|
|
}
|
|
|
|
/* Animação */
|
|
.dropdown-fade-enter-active,
|
|
.dropdown-fade-leave-active {
|
|
@apply transition-all duration-200;
|
|
}
|
|
|
|
.dropdown-fade-enter-from,
|
|
.dropdown-fade-leave-to {
|
|
@apply opacity-0 -translate-y-2;
|
|
}
|
|
</style>
|