feat(UI): BaseSelect supports option groups

This commit is contained in:
Giulio Ganci 2022-05-09 17:31:58 +02:00
parent 302c66457d
commit 1869e6a148
4 changed files with 102 additions and 34 deletions

View File

@ -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>

View File

@ -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);

View File

@ -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] {

View File

@ -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;
} }