PronounsPage/components/AutocompleteSelect.vue
2024-11-03 13:55:07 +01:00

121 lines
3.0 KiB
Vue

<script setup lang="ts">
defineOptions({
inheritAttrs: false,
});
const props = defineProps<{
options: Record<string, string>;
freeform?: boolean;
}>();
const modelValue = defineModel<string | null>({ required: true });
const filter = ref((modelValue.value !== null ? props.options[modelValue.value] : null) ?? modelValue.value);
const shown = ref(false);
const highlighted = ref(-1);
const visibleOptions = computed((): Record<string, string> => {
return Object.fromEntries(Object.entries(props.options).filter(([option, display]) => {
return !filter.value ||
option.toLowerCase().includes(filter.value.toLowerCase()) ||
display.toLowerCase().includes(filter.value.toLowerCase());
}));
});
const highlightedOption = computed(() => {
return Object.keys(visibleOptions.value)[highlighted.value];
});
watch(modelValue, () => {
filter.value = (modelValue.value !== null ? props.options[modelValue.value] : null) ?? modelValue.value;
highlighted.value = -1;
});
watch(filter, () => {
if (props.freeform) {
modelValue.value = filter.value;
}
});
const select = (option: string) => {
modelValue.value = option;
hide();
highlighted.value = -1;
};
const show = () => {
shown.value = true;
};
const hide = () => {
setTimeout(() => {
shown.value = false;
}, 100);
};
const filterKeydown = (event: KeyboardEvent) => {
if (!shown.value) {
show();
}
switch (event.key) {
case 'ArrowUp':
highlighted.value--;
event.preventDefault();
event.stopPropagation();
break;
case 'ArrowDown':
highlighted.value++;
event.preventDefault();
event.stopPropagation();
break;
case 'Enter':
if (highlightedOption.value) {
select(highlightedOption.value);
event.preventDefault();
event.stopPropagation();
}
break;
default:
break;
}
};
</script>
<template>
<div class="select flex-grow-1">
<input
v-model="filter"
type="text"
class="form-control"
autocomplete="off"
v-bind="$attrs"
@focus="show"
@blur="hide"
@keydown="filterKeydown"
>
<div v-show="shown" class="list-group shadow">
<button
v-for="(display, option) in visibleOptions"
:key="option"
type="button"
:class="['list-group-item', 'list-group-item-action', highlightedOption === option ? 'active' : '']"
@click.prevent="select(option)"
>
{{ display }}
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.select {
position: relative;
.list-group {
position: absolute;
top: 100%;
max-height: 300px;
width: min(300px, 100%);
overflow-y: auto;
z-index: 999;
}
}
</style>