Valentyne Stigloher 10180aa6a3 (refactor) use #shared alias instead of ~~/shared
the #shared alias used by Nuxt cannot be easily disabled and to prevent breackage with jiti, we make use of it
2025-08-17 18:56:02 +02:00

125 lines
3.3 KiB
Vue

<script setup lang="ts">
import { onKeyStroke, useDebounce } from '@vueuse/core';
import { normaliseQuery, validateQuery } from '#shared/search.ts';
const emit = defineEmits<{
selected: [];
}>();
const query = ref('');
const normalisedQuery = computed(() => {
return normaliseQuery(query.value);
});
const debouncedQuery = useDebounce(normalisedQuery);
const queryValidation = computed(() => {
return validateQuery(normalisedQuery.value);
});
const searchAsyncData = useAsyncData('search', async () => {
selected.value = null;
if (queryValidation.value !== undefined) {
return [];
}
return await $fetch('/api/search', {
query: {
query: debouncedQuery.value,
},
});
}, {
watch: [debouncedQuery],
});
const isLoading = computed(() => {
return queryValidation.value === undefined &&
(normalisedQuery.value !== debouncedQuery.value || searchAsyncData.status.value === 'pending');
});
const hasNoResults = computed(() => {
return !isLoading.value && searchAsyncData.data.value?.length === 0;
});
onKeyStroke('k', (event) => {
const isCtrlKeyPressed = isMac.value ? event.metaKey : event.ctrlKey;
if (isCtrlKeyPressed) {
event.preventDefault();
open();
}
});
const selected = ref<number | null>(null);
const searchItems = useTemplateRef('searchItems');
onKeyStroke('ArrowDown', () => {
selected.value = Math.min(
(selected.value ?? -1) + 1,
(searchAsyncData.data?.value?.length ?? 0) - 1,
);
});
onKeyStroke('ArrowUp', () => {
selected.value = Math.max(
(selected.value ?? -1) - 1,
0,
);
});
onKeyStroke('Enter', () => {
if (selected.value === null) {
return;
}
const document = searchAsyncData.data.value?.[selected.value];
if (!document) {
return;
}
const link = searchItems.value?.[selected.value]?.firstElementChild as HTMLElement | undefined;
link?.click();
});
const reset = () => {
query.value = '';
searchAsyncData.clear();
selected.value = null;
};
defineExpose({ reset });
</script>
<template>
<div class="input-group mb-4">
<span class="input-group-text">
<Spinner v-if="isLoading" size="1.25em" />
<Icon v-else v="search" />
</span>
<input
v-model="query"
type="search"
autofocus
class="form-control border-primary"
:placeholder="$t('crud.search')"
>
<button type="button" class="btn btn-outline-danger" @click="emit('selected')">
<Icon v="times" />
</button>
</div>
<div v-if="queryValidation !== undefined" class="alert alert-info">
<T>search.{{ queryValidation }}</T>
</div>
<ul
v-else-if="searchAsyncData.data.value?.length ?? 0 > 0"
class="list-group"
>
<li
v-for="(document, index) of searchAsyncData.data.value"
:key="`${document.kind}-${document.id}`"
ref="searchItems"
:class="['list-group-item list-group-item-action py-0 pe-0', selected === index ? 'list-group-item-active' : '']"
>
<SearchItem :document="document" @click="emit('selected')" />
</li>
</ul>
<div v-else-if="hasNoResults" class="alert alert-info">
<T :params="{ query }">search.noResults</T>
</div>
</template>