123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- <script setup lang="ts">
- import { ref, computed, onMounted, ref as elementRef, onUnmounted, watch } from 'vue';
- const props = defineProps<{
- options: { value: string; label: string }[];
- modelValue: string;
- placeholder: string;
- width: string;
- }>();
- const emit = defineEmits(['update:modelValue']);
- const showOptions = ref(false);
- const selectedValueRef = elementRef<HTMLElement | null>(null);
- const optionsListRef = elementRef<HTMLElement | null>(null);
- const handleClickOutside = (event: MouseEvent) => {
- if (selectedValueRef.value && optionsListRef.value && showOptions.value) {
- const isClickInside = selectedValueRef.value.contains(event.target as Node) || optionsListRef.value.contains(event.target as Node);
- if (!isClickInside) {
- showOptions.value = false;
- }
- }
- };
- const handleSelect = (option: { value: string; label: string }) => {
- emit('update:modelValue', option.value);
- showOptions.value = false;
- };
- const selectedOption = computed(() => {
- return props.options.find(option => option.value === props.modelValue);
- });
- let updateTimer: number | null = null;
- const updateOptionsListPosition = () => {
- if (updateTimer) {
- clearTimeout(updateTimer);
- }
- updateTimer = setTimeout(() => {
- if (selectedValueRef.value && optionsListRef.value && showOptions.value) {
- optionsListRef.value.style.top = selectedValueRef.value.getBoundingClientRect().bottom + 'px';
- optionsListRef.value.style.left = selectedValueRef.value.getBoundingClientRect().left + 'px';
- optionsListRef.value.style.width = selectedValueRef.value.offsetWidth + 'px';
- }
- updateTimer = null;
- }, 300);
- };
- onMounted(() => {
- document.addEventListener('click', handleClickOutside);
- window.addEventListener('resize', updateOptionsListPosition);
- });
- onUnmounted(() => {
- document.removeEventListener('click', handleClickOutside);
- window.removeEventListener('resize', updateOptionsListPosition);
- });
- watch(showOptions, (newValue) => {
- if (newValue) {
- updateOptionsListPosition();
- }
- });
- </script>
- <template>
- <div class="better-select" :class="showOptions ? ['showing-select'] : []">
- <div ref="selectedValueRef" :class="selectedOption ? [] : ['placeholder-showing']" class="selected-value" @click="showOptions = !showOptions">
- {{ selectedOption?.label || props.placeholder }}
- </div>
- <ul
- ref="optionsListRef"
- class="options-list"
- :class="showOptions ? ['showing-options-list'] : []"
- :style="{
- position: 'fixed',
- top: selectedValueRef?.getBoundingClientRect().bottom + 'px',
- left: selectedValueRef?.getBoundingClientRect().left + 'px',
- width: selectedValueRef?.offsetWidth + 'px'
- }"
- >
- <li
- v-for="option in props.options"
- :key="option.value"
- :class="option.value == selectedOption?.value ? ['active'] : []"
- @click="handleSelect(option)"
- >
- {{ option.label }}
- </li>
- </ul>
- </div>
- </template>
- <style scoped>
- .better-select {
- position: relative;
- text-align: center;
- width: v-bind("props.width");
- }
- .selected-value:hover {
- background-size: 100% 2px;
- }
- .showing-select .selected-value {
- background-size: 100% 2px;
- }
- .selected-value {
- padding: 2px;
- cursor: pointer;
- background: linear-gradient(to right, var(--text-color), var(--text-color)) no-repeat left bottom;
- color: var(--text-color);
- background-size: 0 2px;
- transition: all .3s;
- }
- .placeholder-showing {
- color: var(--secondary-text-color);
- }
- .options-list {
- margin: 0;
- padding: 0;
- opacity: 0;
- list-style-type: none;
- border-top: none;
- border-radius: 0 0 4px 4px;
- max-height: 200px;
- overflow-y: hidden;
- background-color: var(--background-color);
- z-index: 1000;
- transform-origin: top;
- transform: scaleY(0%);
- transition: all .3s;
- }
- .showing-options-list {
- opacity: 100%;
- transform: scaleY(100%);
- box-shadow: var(--muted-text-color) 0 0 4px;
- }
- .options-list li {
- padding: 4px 0;
- cursor: pointer;
- text-align: center;
- transition: all .3s;
- }
- .options-list .active {
- background-color: var(--secondary-background-color);
- }
- .options-list li:hover {
- background-color: var(--secondary-background-color);
- }
- </style>
|