|
@@ -0,0 +1,162 @@
|
|
|
+<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>
|