From 1869e6a1482daf9381d9ac2244bf0aeffa758edc Mon Sep 17 00:00:00 2001 From: Giulio Ganci Date: Mon, 9 May 2022 17:31:58 +0200 Subject: [PATCH] feat(UI): BaseSelect supports option groups --- src/renderer/components/BaseSelect.vue | 125 ++++++++++++++++------ src/renderer/scss/main.scss | 1 + src/renderer/scss/themes/dark-theme.scss | 5 + src/renderer/scss/themes/light-theme.scss | 5 + 4 files changed, 102 insertions(+), 34 deletions(-) diff --git a/src/renderer/components/BaseSelect.vue b/src/renderer/components/BaseSelect.vue index 6105b8af..27ff8929 100644 --- a/src/renderer/components/BaseSelect.vue +++ b/src/renderer/components/BaseSelect.vue @@ -44,11 +44,12 @@ @@ -105,13 +106,19 @@ export default defineComponent({ type: [String, Function], default: () => (opt) => opt.label ? 'label' : undefined }, + groupLabel: { + type: String + }, + groupValues: { + type: String + }, closeOnSelect: { type: Boolean, default: true }, dropdownContainer: { type: String, - default: '#main-content' + default: '#window-content' }, dropdownOffsets: { type: Object, @@ -131,13 +138,61 @@ export default defineComponent({ const optionList = ref(null); const optionRefs = []; 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 normalizedSearch = (searchText.value || '').toLowerCase().trim(); - const options = [...props.options]; return normalizedSearch - ? options.filter(opt => getOptionLabel(opt).trim().toLowerCase().indexOf(normalizedSearch) !== -1) - : options; + ? flattenOptions.value.filter(opt => opt.$type === 'group' || opt.label.trim().toLowerCase().indexOf(normalizedSearch) !== -1) + : flattenOptions.value; }); const searchInputStyle = computed(() => { @@ -155,29 +210,16 @@ export default defineComponent({ hightlightedIndex.value = 0; }); - const getOptionValue = (opt) => { - const key = typeof props.optionTrackBy === 'function' ? props.optionTrackBy(opt) : props.optionTrackBy; - 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 currentOptionLabel = computed(() => + flattenOptions.value.find(d => d.value === props.modelValue)?.label + ); const select = (opt) => { - internalValue.value = opt; + if (opt.$type === 'group') return; + + internalValue.value = opt.value; emit('select', opt); - emit('update:modelValue', getOptionValue(opt)); + emit('update:modelValue', opt.value); emit('change', opt); if (props.closeOnSelect) @@ -185,12 +227,13 @@ export default defineComponent({ }; const isSelected = (opt) => { - return internalValue.value === getOptionValue(opt); + return internalValue.value === opt.value; }; const activate = () => { if (isOpen.value) return; isOpen.value = true; + hightlightedIndex.value = flattenOptions.value.findIndex(el => el.value === internalValue.value) || 0; if (props.searchable) searchInput.value.focus(); @@ -198,7 +241,10 @@ export default defineComponent({ else el.value.focus(); - nextTick(() => adjustListPosition()); + nextTick(() => { + adjustListPosition(); + scrollTo(optionRefs[hightlightedIndex.value]); + }); emit('open'); }; @@ -230,11 +276,22 @@ export default defineComponent({ const keyArrows = (direction) => { const sum = direction === 'down' ? +1 : -1; - const index = hightlightedIndex.value + sum; - hightlightedIndex.value = Math.max(0, index > filteredOptions.value.length - 1 ? filteredOptions.value.length - 1 : index); + let index = hightlightedIndex.value + sum; + 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]; + if (!optEl) + return; + scrollTo(optEl); + }; + + const scrollTo = (optEl) => { + if (!optEl) return; const visMin = optionList.value.scrollTop; const visMax = optionList.value.scrollTop + optionList.value.clientHeight - optEl.clientHeight; @@ -263,8 +320,6 @@ export default defineComponent({ searchText, searchInputStyle, filteredOptions, - getOptionValue, - getOptionLabel, currentOptionLabel, activate, deactivate, @@ -295,6 +350,7 @@ export default defineComponent({ } &__list-wrapper { + cursor: pointer; position: fixed; display: block; z-index: 5; @@ -308,5 +364,6 @@ export default defineComponent({ &__list { list-style: none; } + } diff --git a/src/renderer/scss/main.scss b/src/renderer/scss/main.scss index 065090c1..17683722 100644 --- a/src/renderer/scss/main.scss +++ b/src/renderer/scss/main.scss @@ -318,6 +318,7 @@ option:checked { } .select__list-wrapper { + z-index: 401 !important; border: 1px solid transparent; 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); diff --git a/src/renderer/scss/themes/dark-theme.scss b/src/renderer/scss/themes/dark-theme.scss index 8cb4f1b4..7416e21e 100644 --- a/src/renderer/scss/themes/dark-theme.scss +++ b/src/renderer/scss/themes/dark-theme.scss @@ -126,6 +126,11 @@ border-color: $bg-color-gray; 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] { diff --git a/src/renderer/scss/themes/light-theme.scss b/src/renderer/scss/themes/light-theme.scss index 63a129df..a547d1d8 100644 --- a/src/renderer/scss/themes/light-theme.scss +++ b/src/renderer/scss/themes/light-theme.scss @@ -24,6 +24,11 @@ background-color: $body-bg; } + &__group { + background: $bg-color-light-gray; + color: $unknown-color; + } + &__option--highlight { color: $light-color; }