harb/web-app/src/components/fcomponents/FSelect.vue

213 lines
5.9 KiB
Vue
Raw Normal View History

2025-09-23 14:18:04 +02:00
<template>
<div class="o-select" @click="clickSelect" ref="componentRef">
<FInput
hide-details
:clearable="props.clearable"
:label="props.label ?? undefined"
:selectedable="false"
:focus="showList"
:modelValue="`${selectedYear} % yearly`"
readonly
>
<template #info v-if="slots.info">
<slot name="info"></slot>
</template>
<template #suffix>
<Icon class="toggle-collapse" v-if="showList" icon="mdi:chevron-down"></Icon>
<Icon class="toggle-collapse" v-else icon="mdi:chevron-up"></Icon>
</template>
</FInput>
<div class="select-list-wrapper" v-show="showList" ref="selectList">
<div class="select-list-inner" @click.stop>
<div
class="select-list-item"
v-for="(item, index) in props.items"
:key="item.index"
:class="{ active: selectedIndex === item.index, hovered: activeIndex === index }"
@click.stop="clickItem(item)"
@mouseenter="mouseEnter($event, index)"
@mouseleave="mouseLeave($event, index)"
>
<div class="circle">
<div class="active" v-if="selectedIndex === item.index"></div>
<div class="hovered" v-else-if="activeIndex === index"></div>
</div>
<div class="yearly">
<div class="value">{{ item.year }} %</div>
<div class="label">yearly</div>
</div>
<div class="daily">
<div class="value">{{ item.daily.toFixed(4) }} %</div>
<div class="label">daily</div>
</div>
2025-09-23 14:18:04 +02:00
</div>
</div>
2025-09-23 14:18:04 +02:00
</div>
</div>
<slot :style="[!props.editor ? { display: 'none' } : {}]"></slot>
2025-09-23 14:18:04 +02:00
</template>
<script setup lang="ts">
import { ref, computed, onMounted, useSlots, withDefaults } from 'vue';
import FInput from '@/components/fcomponents/FInput.vue';
import useClickOutside from '@/composables/useClickOutside';
import { Icon } from '@iconify/vue';
2025-09-23 14:18:04 +02:00
interface Item {
index: number;
year: number;
daily: number;
}
interface Props {
items?: Item[];
clearable?: boolean;
itemTitle?: string;
itemValue?: string;
label?: string | null;
modelValue?: number | null;
value?: string | null;
editor?: boolean;
2025-09-23 14:18:04 +02:00
}
const props = withDefaults(defineProps<Props>(), {
items: () => [],
clearable: false,
itemTitle: 'label',
itemValue: 'value',
label: null,
modelValue: null,
value: null,
editor: false,
2025-09-23 14:18:04 +02:00
});
interface Emits {
(e: 'update:modelValue', value: number): void;
}
const emit = defineEmits<Emits>();
2025-09-23 14:18:04 +02:00
const slots = useSlots();
2025-09-23 14:18:04 +02:00
const showList = ref<boolean>(false);
const _slotElements = ref<unknown[]>([]);
const componentRef = ref<HTMLElement | null>(null);
const selectList = ref<HTMLElement | null>(null);
const activeIndex = ref();
2025-09-23 14:18:04 +02:00
useClickOutside(componentRef, () => {
showList.value = false;
});
const selectedIndex = computed({
2025-09-23 14:18:04 +02:00
get() {
if (typeof props.modelValue === 'number') {
return props.modelValue;
}
return props.items[0]?.index ?? 0;
2025-09-23 14:18:04 +02:00
},
set(newValue: number) {
emit('update:modelValue', newValue);
},
});
2025-09-23 14:18:04 +02:00
const selectedOption = computed(() => props.items.find(item => item.index === selectedIndex.value) ?? props.items[0]);
const selectedYear = computed(() => selectedOption.value?.year ?? 0);
function mouseEnter(event: MouseEvent, index: number) {
const target = event.target as HTMLElement;
activeIndex.value = index;
target.classList.add('active');
2025-09-23 14:18:04 +02:00
}
function mouseLeave(_event: MouseEvent, _index: number) {
const elements = selectList.value?.querySelectorAll('.select-list-item');
elements?.forEach(element => {
(element as HTMLElement).classList.remove('active');
});
2025-09-23 14:18:04 +02:00
}
onMounted(() => {});
2025-09-23 14:18:04 +02:00
function clickSelect(_event: unknown) {
showList.value = !showList.value;
2025-09-23 14:18:04 +02:00
}
function clickItem(item: { index: number }) {
selectedIndex.value = item.index;
showList.value = false;
// emit('input', item)
2025-09-23 14:18:04 +02:00
}
</script>
<style lang="sass">
.o-select
position: relative
display: inline-block
.toggle-collapse
color: black
font-size: 20px
&:active, &:focus, &:hover
cursor: pointer
.select-list-wrapper
position: absolute
box-shadow: 0 1px 6px 0 #20212447
background-color: var(--color-card-bg)
z-index: 10
padding: 16px
border-radius: 16px
border: 1px solid black
right: 0
margin-top: 4px
.select-list-inner
height: 330px
overflow-y: scroll
display: grid
.select-list-item
display: grid
grid-template-columns: 15px 110px 126px
place-content: end
margin-right: 8px
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none
padding: 4px 0
&:active, &:focus, &:hover
cursor: pointer
.daily, .yearly
display: flex
gap: 4px
justify-content: end
.daily
color: var(--color-grey)
.circle
height: 14px
width: 14px
.active
height: 14px
width: 14px
background-color: var(--color-based-blue)
border-radius: 14px
.hovered
height: 14px
width: 14px
background-color: var(--color-based-blue)
opacity: .5
border-radius: 14px
.select-list-inner
&::-webkit-scrollbar-track
border-radius: 10px
&::-webkit-scrollbar
width: 5px
&::-webkit-scrollbar-thumb
border-radius: 10px
background-color: lightgrey
</style>