BetterSelect.vue 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. <script setup lang="ts">
  2. import { ref, computed, onMounted, ref as elementRef, onUnmounted, watch } from 'vue';
  3. const props = defineProps<{
  4. options: { value: string; label: string }[];
  5. modelValue: string;
  6. placeholder: string;
  7. width: string;
  8. }>();
  9. const emit = defineEmits(['update:modelValue']);
  10. const showOptions = ref(false);
  11. const selectedValueRef = elementRef<HTMLElement | null>(null);
  12. const optionsListRef = elementRef<HTMLElement | null>(null);
  13. const handleClickOutside = (event: MouseEvent) => {
  14. if (selectedValueRef.value && optionsListRef.value && showOptions.value) {
  15. const isClickInside = selectedValueRef.value.contains(event.target as Node) || optionsListRef.value.contains(event.target as Node);
  16. if (!isClickInside) {
  17. showOptions.value = false;
  18. }
  19. }
  20. };
  21. const handleSelect = (option: { value: string; label: string }) => {
  22. emit('update:modelValue', option.value);
  23. showOptions.value = false;
  24. };
  25. const selectedOption = computed(() => {
  26. return props.options.find(option => option.value === props.modelValue);
  27. });
  28. let updateTimer: number | null = null;
  29. const updateOptionsListPosition = () => {
  30. if (updateTimer) {
  31. clearTimeout(updateTimer);
  32. }
  33. updateTimer = setTimeout(() => {
  34. if (selectedValueRef.value && optionsListRef.value && showOptions.value) {
  35. optionsListRef.value.style.top = selectedValueRef.value.getBoundingClientRect().bottom + 'px';
  36. optionsListRef.value.style.left = selectedValueRef.value.getBoundingClientRect().left + 'px';
  37. optionsListRef.value.style.width = selectedValueRef.value.offsetWidth + 'px';
  38. }
  39. updateTimer = null;
  40. }, 300);
  41. };
  42. onMounted(() => {
  43. document.addEventListener('click', handleClickOutside);
  44. window.addEventListener('resize', updateOptionsListPosition);
  45. });
  46. onUnmounted(() => {
  47. document.removeEventListener('click', handleClickOutside);
  48. window.removeEventListener('resize', updateOptionsListPosition);
  49. });
  50. watch(showOptions, (newValue) => {
  51. if (newValue) {
  52. updateOptionsListPosition();
  53. }
  54. });
  55. </script>
  56. <template>
  57. <div class="better-select" :class="showOptions ? ['showing-select'] : []">
  58. <div ref="selectedValueRef" :class="selectedOption ? [] : ['placeholder-showing']" class="selected-value" @click="showOptions = !showOptions">
  59. {{ selectedOption?.label || props.placeholder }}
  60. </div>
  61. <ul
  62. ref="optionsListRef"
  63. class="options-list"
  64. :class="showOptions ? ['showing-options-list'] : []"
  65. :style="{
  66. position: 'fixed',
  67. top: selectedValueRef?.getBoundingClientRect().bottom + 'px',
  68. left: selectedValueRef?.getBoundingClientRect().left + 'px',
  69. width: selectedValueRef?.offsetWidth + 'px'
  70. }"
  71. >
  72. <li
  73. v-for="option in props.options"
  74. :key="option.value"
  75. :class="option.value == selectedOption?.value ? ['active'] : []"
  76. @click="handleSelect(option)"
  77. >
  78. {{ option.label }}
  79. </li>
  80. </ul>
  81. </div>
  82. </template>
  83. <style scoped>
  84. .better-select {
  85. position: relative;
  86. text-align: center;
  87. width: v-bind("props.width");
  88. }
  89. .selected-value:hover {
  90. background-size: 100% 2px;
  91. }
  92. .showing-select .selected-value {
  93. background-size: 100% 2px;
  94. }
  95. .selected-value {
  96. padding: 2px;
  97. cursor: pointer;
  98. background: linear-gradient(to right, var(--text-color), var(--text-color)) no-repeat left bottom;
  99. color: var(--text-color);
  100. background-size: 0 2px;
  101. transition: all .3s;
  102. }
  103. .placeholder-showing {
  104. color: var(--secondary-text-color);
  105. }
  106. .options-list {
  107. margin: 0;
  108. padding: 0;
  109. opacity: 0;
  110. list-style-type: none;
  111. border-top: none;
  112. border-radius: 0 0 4px 4px;
  113. max-height: 200px;
  114. overflow-y: hidden;
  115. background-color: var(--background-color);
  116. z-index: 1000;
  117. transform-origin: top;
  118. transform: scaleY(0%);
  119. transition: all .3s;
  120. }
  121. .showing-options-list {
  122. opacity: 100%;
  123. transform: scaleY(100%);
  124. box-shadow: var(--muted-text-color) 0 0 4px;
  125. }
  126. .options-list li {
  127. padding: 4px 0;
  128. cursor: pointer;
  129. text-align: center;
  130. transition: all .3s;
  131. }
  132. .options-list .active {
  133. background-color: var(--secondary-background-color);
  134. }
  135. .options-list li:hover {
  136. background-color: var(--secondary-background-color);
  137. }
  138. </style>