mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-26 22:43:06 -04:00
148 lines
4.0 KiB
Vue
148 lines
4.0 KiB
Vue
<script setup lang="ts">
|
|
import { onKeyStroke, useDebounce } from '@vueuse/core';
|
|
|
|
import { normaliseQuery, validateQuery } from '~/src/search.ts';
|
|
|
|
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;
|
|
});
|
|
|
|
const dialogue = useTemplateRef('dialogue');
|
|
|
|
const open = () => {
|
|
query.value = '';
|
|
searchAsyncData.clear();
|
|
dialogue.value?.showModal();
|
|
selected.value = null;
|
|
};
|
|
|
|
const close = () => {
|
|
dialogue.value?.close();
|
|
};
|
|
|
|
onKeyStroke('k', (event) => {
|
|
const isCtrlKeyPressed = isMac.value ? event.metaKey : event.ctrlKey;
|
|
if (isCtrlKeyPressed) {
|
|
event.preventDefault();
|
|
open();
|
|
}
|
|
});
|
|
|
|
const onMousedown = (event: MouseEvent) => {
|
|
const rect = dialogue.value?.getBoundingClientRect();
|
|
if (rect && (
|
|
event.clientX < rect.left || event.clientX > rect.right ||
|
|
event.clientY < rect.top || event.clientY > rect.bottom)
|
|
) {
|
|
close();
|
|
}
|
|
};
|
|
|
|
const selected = ref<number | null>(null);
|
|
const router = useRouter();
|
|
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;
|
|
}
|
|
|
|
router.push(document.url);
|
|
close();
|
|
});
|
|
|
|
defineExpose({ open, close });
|
|
</script>
|
|
|
|
<template>
|
|
<dialog ref="dialogue" class="container m-auto h-100 rounded border" @mousedown="onMousedown">
|
|
<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"
|
|
class="form-control border-primary"
|
|
:placeholder="$t('crud.search')"
|
|
>
|
|
<button class="btn btn-outline-danger" @click="close()">
|
|
<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}`"
|
|
:class="['list-group-item list-group-item-action p-0', selected === index ? 'list-group-item-active' : '']"
|
|
>
|
|
<SearchItem :document="document" @click="close()" />
|
|
</li>
|
|
</ul>
|
|
<div v-else-if="hasNoResults" class="alert alert-info">
|
|
<T :params="{ query }">search.noResults</T>
|
|
</div>
|
|
</dialog>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@import "assets/variables";
|
|
|
|
.list-group-item-active {
|
|
background-color: $list-group-hover-bg !important;
|
|
border-left: 3px solid $primary !important;
|
|
margin-inline-start: -2px; /** compensate for the border mark, minus 1px (regular border) */
|
|
}
|
|
</style>
|