mirror of
https://github.com/Fabio286/antares.git
synced 2025-02-08 07:48:40 +01:00
feat(UI): new BaseSelect component
This commit is contained in:
parent
5cb8ec65ad
commit
745d551cc9
251
src/renderer/components/BaseSelect.vue
Normal file
251
src/renderer/components/BaseSelect.vue
Normal file
@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div
|
||||
ref="el"
|
||||
class="select"
|
||||
:class="{'select-open': isOpen}"
|
||||
role="combobox"
|
||||
:tabindex="searchable ? -1 : tabindex"
|
||||
@focus="activate()"
|
||||
@blur="searchable ? false : deactivate()"
|
||||
@keyup.esc="deactivate()"
|
||||
@keydown.self.down.prevent="moveDown()"
|
||||
@keydown.self.up.prevent="moveUp"
|
||||
>
|
||||
<div class="select__item-text">
|
||||
<input
|
||||
v-if="searchable"
|
||||
ref="searchInput"
|
||||
class="select__search-input"
|
||||
:style="searchInputStyle"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
:tabindex="tabindex"
|
||||
:value="searchText"
|
||||
@input="searchText = $event.target.value"
|
||||
@focus.prevent="activate()"
|
||||
@blur.prevent="deactivate()"
|
||||
@keyup.esc="deactivate()"
|
||||
@keydown.down.prevent="moveDown()"
|
||||
@keydown.up.prevent="moveUp()"
|
||||
@keypress.enter.prevent.stop.self="select(filteredOptions[hightlightedIndex])"
|
||||
>
|
||||
<span v-if="searchable && !isOpen || !searchable">{{ currentOptionLabel }}</span>
|
||||
</div>
|
||||
<div v-if="isOpen" class="select__list-wrapper">
|
||||
<ul class="select__list" @mousedown.prevent>
|
||||
<li
|
||||
v-for="(opt, index) of filteredOptions"
|
||||
:key="getOptionValue(opt)"
|
||||
:class="{
|
||||
'select__option--highlight': index === hightlightedIndex,
|
||||
'select__option--selected': isSelected(opt)
|
||||
}"
|
||||
@click.stop="select(opt)"
|
||||
@mouseenter.self="hightlightedIndex = index"
|
||||
>
|
||||
<slot
|
||||
name="option"
|
||||
:option="opt"
|
||||
:index="index"
|
||||
>
|
||||
{{ getOptionLabel(opt) }}
|
||||
</slot>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, computed, ref, watch } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BaseSelect',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Object]
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
preserveSearch: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
optionTrackBy: {
|
||||
type: [String, Function],
|
||||
default: (opt) => opt.id || opt.value
|
||||
},
|
||||
optionLabel: {
|
||||
type: [String, Function],
|
||||
default: (opt) => opt.label
|
||||
},
|
||||
closeOnSelect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['select', 'open', 'close', 'update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
const hightlightedIndex = ref(0);
|
||||
const isOpen = ref(false);
|
||||
const el = ref(null);
|
||||
const searchInput = ref(null);
|
||||
const searchText = ref('');
|
||||
const filteredOptions = computed(() => {
|
||||
const normalizedSearch = (searchText.value || '').toLowerCase().trim();
|
||||
const options = [...props.options];
|
||||
|
||||
return normalizedSearch
|
||||
? options.filter(opt => getOptionLabel(opt).trim().toLowerCase().indexOf(normalizedSearch) !== -1)
|
||||
: options;
|
||||
});
|
||||
|
||||
const searchInputStyle = computed(() => {
|
||||
if (props.searchable)
|
||||
// just hide the input and give the ability to receive focus
|
||||
return isOpen.value ? { with: '100%' } : { width: 0, position: 'absolute', padding: 0, margin: 0 };
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
watch(filteredOptions, (options) => {
|
||||
if (hightlightedIndex.value >= options.length -1)
|
||||
hightlightedIndex.value = options.length ? options.length -1 : 0;
|
||||
else
|
||||
hightlightedIndex.value = 0;
|
||||
});
|
||||
|
||||
const getOptionValue = (opt) => {
|
||||
const key = typeof props.optionTrackBy === 'function' ? props.optionTrackBy() : props.optionTrackBy;
|
||||
return opt[key];
|
||||
};
|
||||
|
||||
const getOptionLabel = (opt) => {
|
||||
const key = typeof props.optionLabel === 'function' ? props.optionLabel() : props.optionLabel;
|
||||
return opt[key];
|
||||
};
|
||||
|
||||
const currentOptionLabel = computed(() => {
|
||||
if (props.modelValue) {
|
||||
const opt = props.options.find(d => getOptionValue(d) === props.modelValue);
|
||||
return getOptionLabel(opt);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const select = (opt) => {
|
||||
emit('select', opt);
|
||||
emit('update:modelValue', getOptionValue(opt));
|
||||
|
||||
if (props.closeOnSelect)
|
||||
deactivate();
|
||||
};
|
||||
|
||||
const isSelected = (opt) => {
|
||||
return props.modelValue === getOptionValue(opt);
|
||||
};
|
||||
|
||||
const activate = () => {
|
||||
if (isOpen.value) return;
|
||||
|
||||
if (props.searchable)
|
||||
searchInput.value.focus();
|
||||
|
||||
else
|
||||
el.value.focus();
|
||||
|
||||
isOpen.value = true;
|
||||
|
||||
emit('open');
|
||||
};
|
||||
|
||||
const deactivate = () => {
|
||||
if (!isOpen.value) return;
|
||||
|
||||
isOpen.value = false;
|
||||
|
||||
if (props.searchable)
|
||||
searchInput.value.blur();
|
||||
|
||||
else
|
||||
el.value.blur();
|
||||
|
||||
if (!props.preserveSearch) searchText.value = '';
|
||||
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const moveUp = () => {
|
||||
if (hightlightedIndex.value > 0)
|
||||
hightlightedIndex.value--;
|
||||
};
|
||||
const moveDown = () => {
|
||||
if (hightlightedIndex.value < filteredOptions.value.length -1)
|
||||
hightlightedIndex.value++;
|
||||
};
|
||||
|
||||
return {
|
||||
el,
|
||||
searchInput,
|
||||
searchText,
|
||||
searchInputStyle,
|
||||
filteredOptions,
|
||||
getOptionValue,
|
||||
getOptionLabel,
|
||||
currentOptionLabel,
|
||||
activate,
|
||||
deactivate,
|
||||
select,
|
||||
isSelected,
|
||||
moveUp,
|
||||
moveDown,
|
||||
isOpen,
|
||||
hightlightedIndex
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
&__search-input {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
&__list-wrapper {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
z-index: 50;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
left: 0;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user