mirror of https://github.com/Fabio286/antares.git
feat(UI): BaseSelect supports option groups
This commit is contained in:
parent
302c66457d
commit
1869e6a148
|
@ -44,11 +44,12 @@
|
||||||
<ul class="select__list" @mousedown.prevent>
|
<ul class="select__list" @mousedown.prevent>
|
||||||
<li
|
<li
|
||||||
v-for="(opt, index) of filteredOptions"
|
v-for="(opt, index) of filteredOptions"
|
||||||
:key="getOptionValue(opt)"
|
:key="opt.id"
|
||||||
:ref="(el) => optionRefs[index] = el"
|
:ref="(el) => optionRefs[index] = el"
|
||||||
:class="{
|
:class="{
|
||||||
'select__option--highlight': index === hightlightedIndex,
|
'select__group': opt.$type === 'group',
|
||||||
'select__option--selected': isSelected(opt)
|
'select__option--highlight': opt.$type === 'option' && index === hightlightedIndex,
|
||||||
|
'select__option--selected': opt.$type === 'option' && isSelected(opt)
|
||||||
}"
|
}"
|
||||||
@click.stop="select(opt)"
|
@click.stop="select(opt)"
|
||||||
@mouseenter.self="hightlightedIndex = index"
|
@mouseenter.self="hightlightedIndex = index"
|
||||||
|
@ -58,7 +59,7 @@
|
||||||
:option="opt"
|
:option="opt"
|
||||||
:index="index"
|
:index="index"
|
||||||
>
|
>
|
||||||
{{ getOptionLabel(opt) }}
|
{{ opt.label }}
|
||||||
</slot>
|
</slot>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -105,13 +106,19 @@ export default defineComponent({
|
||||||
type: [String, Function],
|
type: [String, Function],
|
||||||
default: () => (opt) => opt.label ? 'label' : undefined
|
default: () => (opt) => opt.label ? 'label' : undefined
|
||||||
},
|
},
|
||||||
|
groupLabel: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
groupValues: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
closeOnSelect: {
|
closeOnSelect: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
dropdownContainer: {
|
dropdownContainer: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '#main-content'
|
default: '#window-content'
|
||||||
},
|
},
|
||||||
dropdownOffsets: {
|
dropdownOffsets: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -131,13 +138,61 @@ export default defineComponent({
|
||||||
const optionList = ref(null);
|
const optionList = ref(null);
|
||||||
const optionRefs = [];
|
const optionRefs = [];
|
||||||
const searchText = ref('');
|
const searchText = ref('');
|
||||||
|
|
||||||
|
const getOptionValue = (opt) => _guess('optionTrackBy', opt);
|
||||||
|
const getOptionLabel = (opt) => _guess('optionLabel', opt);
|
||||||
|
const _guess = (name, item) => {
|
||||||
|
const prop = props[name];
|
||||||
|
const key = typeof prop === 'function' ? prop(item) : prop;
|
||||||
|
return key ? item[key] : item;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenOptions = computed(() => {
|
||||||
|
return [...props.options].reduce((prev, curr) => {
|
||||||
|
if (curr[props.groupValues] && curr[props.groupValues].length) {
|
||||||
|
prev.push({
|
||||||
|
$type: 'group',
|
||||||
|
label: curr[props.groupLabel],
|
||||||
|
id: `group-${curr[props.groupLabel]}`,
|
||||||
|
count: curr[props.groupLabel].length
|
||||||
|
});
|
||||||
|
|
||||||
|
return prev.concat(curr[props.groupValues].map(el => {
|
||||||
|
const value = getOptionValue(el);
|
||||||
|
return {
|
||||||
|
$type: 'option',
|
||||||
|
label: getOptionLabel(el),
|
||||||
|
id: `option-${value}`,
|
||||||
|
value,
|
||||||
|
$data: {
|
||||||
|
...el
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const value = getOptionValue(curr);
|
||||||
|
prev.push({
|
||||||
|
$type: 'option',
|
||||||
|
label: getOptionLabel(curr),
|
||||||
|
id: `option-${value}`,
|
||||||
|
value,
|
||||||
|
$data: {
|
||||||
|
...curr
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
}, []);
|
||||||
|
});
|
||||||
|
|
||||||
const filteredOptions = computed(() => {
|
const filteredOptions = computed(() => {
|
||||||
const normalizedSearch = (searchText.value || '').toLowerCase().trim();
|
const normalizedSearch = (searchText.value || '').toLowerCase().trim();
|
||||||
const options = [...props.options];
|
|
||||||
|
|
||||||
return normalizedSearch
|
return normalizedSearch
|
||||||
? options.filter(opt => getOptionLabel(opt).trim().toLowerCase().indexOf(normalizedSearch) !== -1)
|
? flattenOptions.value.filter(opt => opt.$type === 'group' || opt.label.trim().toLowerCase().indexOf(normalizedSearch) !== -1)
|
||||||
: options;
|
: flattenOptions.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchInputStyle = computed(() => {
|
const searchInputStyle = computed(() => {
|
||||||
|
@ -155,29 +210,16 @@ export default defineComponent({
|
||||||
hightlightedIndex.value = 0;
|
hightlightedIndex.value = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOptionValue = (opt) => {
|
const currentOptionLabel = computed(() =>
|
||||||
const key = typeof props.optionTrackBy === 'function' ? props.optionTrackBy(opt) : props.optionTrackBy;
|
flattenOptions.value.find(d => d.value === props.modelValue)?.label
|
||||||
return key ? opt[key] : opt;
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const getOptionLabel = (opt) => {
|
|
||||||
const key = typeof props.optionLabel === 'function' ? props.optionLabel(opt) : props.optionLabel;
|
|
||||||
return key ? opt[key] : opt;
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentOptionLabel = computed(() => {
|
|
||||||
if (props.modelValue) {
|
|
||||||
const opt = props.options.find(d => getOptionValue(d) === props.modelValue);
|
|
||||||
return getOptionLabel(opt);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const select = (opt) => {
|
const select = (opt) => {
|
||||||
internalValue.value = opt;
|
if (opt.$type === 'group') return;
|
||||||
|
|
||||||
|
internalValue.value = opt.value;
|
||||||
emit('select', opt);
|
emit('select', opt);
|
||||||
emit('update:modelValue', getOptionValue(opt));
|
emit('update:modelValue', opt.value);
|
||||||
emit('change', opt);
|
emit('change', opt);
|
||||||
|
|
||||||
if (props.closeOnSelect)
|
if (props.closeOnSelect)
|
||||||
|
@ -185,12 +227,13 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSelected = (opt) => {
|
const isSelected = (opt) => {
|
||||||
return internalValue.value === getOptionValue(opt);
|
return internalValue.value === opt.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const activate = () => {
|
const activate = () => {
|
||||||
if (isOpen.value) return;
|
if (isOpen.value) return;
|
||||||
isOpen.value = true;
|
isOpen.value = true;
|
||||||
|
hightlightedIndex.value = flattenOptions.value.findIndex(el => el.value === internalValue.value) || 0;
|
||||||
|
|
||||||
if (props.searchable)
|
if (props.searchable)
|
||||||
searchInput.value.focus();
|
searchInput.value.focus();
|
||||||
|
@ -198,7 +241,10 @@ export default defineComponent({
|
||||||
else
|
else
|
||||||
el.value.focus();
|
el.value.focus();
|
||||||
|
|
||||||
nextTick(() => adjustListPosition());
|
nextTick(() => {
|
||||||
|
adjustListPosition();
|
||||||
|
scrollTo(optionRefs[hightlightedIndex.value]);
|
||||||
|
});
|
||||||
|
|
||||||
emit('open');
|
emit('open');
|
||||||
};
|
};
|
||||||
|
@ -230,11 +276,22 @@ export default defineComponent({
|
||||||
|
|
||||||
const keyArrows = (direction) => {
|
const keyArrows = (direction) => {
|
||||||
const sum = direction === 'down' ? +1 : -1;
|
const sum = direction === 'down' ? +1 : -1;
|
||||||
const index = hightlightedIndex.value + sum;
|
let index = hightlightedIndex.value + sum;
|
||||||
hightlightedIndex.value = Math.max(0, index > filteredOptions.value.length - 1 ? filteredOptions.value.length - 1 : index);
|
index = Math.max(0, index > filteredOptions.value.length - 1 ? filteredOptions.value.length - 1 : index);
|
||||||
|
if (filteredOptions.value[index].$type === 'group')
|
||||||
|
index=Math.max(1, index+sum);
|
||||||
|
|
||||||
|
hightlightedIndex.value = index;
|
||||||
|
|
||||||
const optEl = optionRefs[hightlightedIndex.value];
|
const optEl = optionRefs[hightlightedIndex.value];
|
||||||
|
if (!optEl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
scrollTo(optEl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollTo = (optEl) => {
|
||||||
|
if (!optEl) return;
|
||||||
const visMin = optionList.value.scrollTop;
|
const visMin = optionList.value.scrollTop;
|
||||||
const visMax = optionList.value.scrollTop + optionList.value.clientHeight - optEl.clientHeight;
|
const visMax = optionList.value.scrollTop + optionList.value.clientHeight - optEl.clientHeight;
|
||||||
|
|
||||||
|
@ -263,8 +320,6 @@ export default defineComponent({
|
||||||
searchText,
|
searchText,
|
||||||
searchInputStyle,
|
searchInputStyle,
|
||||||
filteredOptions,
|
filteredOptions,
|
||||||
getOptionValue,
|
|
||||||
getOptionLabel,
|
|
||||||
currentOptionLabel,
|
currentOptionLabel,
|
||||||
activate,
|
activate,
|
||||||
deactivate,
|
deactivate,
|
||||||
|
@ -295,6 +350,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
&__list-wrapper {
|
&__list-wrapper {
|
||||||
|
cursor: pointer;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: block;
|
display: block;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
@ -308,5 +364,6 @@ export default defineComponent({
|
||||||
&__list {
|
&__list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -318,6 +318,7 @@ option:checked {
|
||||||
}
|
}
|
||||||
|
|
||||||
.select__list-wrapper {
|
.select__list-wrapper {
|
||||||
|
z-index: 401 !important;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
box-shadow: 0px 8px 17px 0px rgba(0, 0, 0, 0.2), 0px 6px 20px 0px rgba(0, 0, 0, 0.19);
|
box-shadow: 0px 8px 17px 0px rgba(0, 0, 0, 0.2), 0px 6px 20px 0px rgba(0, 0, 0, 0.19);
|
||||||
|
|
|
@ -126,6 +126,11 @@
|
||||||
border-color: $bg-color-gray;
|
border-color: $bg-color-gray;
|
||||||
background-color: $bg-color-light-dark;
|
background-color: $bg-color-light-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__group {
|
||||||
|
background: rgba($bg-color-gray, 0.65);
|
||||||
|
color: rgba($bg-color-light-gray, 0.7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input[readonly] {
|
.form-input[readonly] {
|
||||||
|
|
|
@ -24,6 +24,11 @@
|
||||||
background-color: $body-bg;
|
background-color: $body-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__group {
|
||||||
|
background: $bg-color-light-gray;
|
||||||
|
color: $unknown-color;
|
||||||
|
}
|
||||||
|
|
||||||
&__option--highlight {
|
&__option--highlight {
|
||||||
color: $light-color;
|
color: $light-color;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue