-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added taggable autocompletable query input
- Loading branch information
1 parent
75ef7e0
commit 75c3459
Showing
8 changed files
with
210 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,70 +1,213 @@ | ||
<template> | ||
<div> | ||
<div class="QueryInput"> | ||
<div class="p-inputgroup flex-1"> | ||
<span class="p-inputgroup-addon"> | ||
<i class="pi pi-search"></i> | ||
</span> | ||
<InputText | ||
type="search" | ||
v-model="query" | ||
class="w-full" | ||
:placeholder="t('query-input-component.placeholder')" | ||
/> | ||
<AutoComplete | ||
v-model="currentSelection" | ||
:forceSelection="false" | ||
:panel-class="{ HidePanel: hidePanel }" | ||
:pt="{ token: { class: 'SearchTagChip' } }" | ||
:suggestions="suggestions" | ||
:virtualScrollerOptions="{ itemSize: 50, scrollWidth: '100vw', scrollHeight: '300px' }" | ||
multiple | ||
placeholder="Search for files by string or tag" | ||
@complete="aComplete" | ||
@item-select="clearAfterSelect" | ||
@clear="clearInput" | ||
:class="{ 'p-invalid': !currentRequestValid }" | ||
> | ||
<template #option="slotProps"> | ||
<div class="flex align-options-center"> | ||
<SearchTag :tag="slotProps.option" /> | ||
</div> | ||
</template> | ||
|
||
<template #chip="slotProps"> | ||
<SearchTag :tag="slotProps.value" /> | ||
</template> | ||
|
||
<template #removetokenicon="slotProps"> | ||
<div | ||
@click="slotProps.onClick" | ||
class="inline-flex bg-red-500 align-items-center px-1 text-white cursor-pointer" | ||
> | ||
<i class="pi pi-times"></i> | ||
</div> | ||
</template> | ||
</AutoComplete> | ||
</div> | ||
|
||
<div class="my-2 text-sm" v-if="!isSearching && query !== ''"> | ||
{{ t('query-input-component.hits', { count: searchResultCount }) }} | ||
<div class="Options flex justify-content-between gap-2 mt-2"> | ||
<div class="my-2 text flex gap-2"> | ||
<span>{{ t('query-input-component.hits', { count: currentResultCount }) }}</span> | ||
<span class="text-red-500" v-if="!currentRequestValid">INVALID QUERY</span> | ||
</div> | ||
|
||
<div class="flex justify-content-end gap-2"> | ||
<RouterLink v-slot="{ navigate }" to="/x" custom> | ||
<Button @click="navigate" text size="small" label="examples" /> | ||
</RouterLink> | ||
|
||
<Button @click="clearInput" text size="small" label="clear" /> | ||
</div> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { computed, ref } from 'vue' | ||
import InputText from 'primevue/inputtext' | ||
import { useI18n } from 'vue-i18n' | ||
import { executeQuerySearch } from '@/plugins/search' | ||
import type { Tag } from '@/stores/tags' | ||
import { useTagStore } from '@/stores/tags' | ||
import type { AutoCompleteCompleteEvent } from 'primevue/autocomplete' | ||
import AutoComplete from 'primevue/autocomplete' | ||
import SearchTag from '@/components/structure/SearchTag.vue' | ||
import { useSearchResultStore } from '@/stores/results' | ||
import { useStatStore } from '@/stores/stats' | ||
import { useI18n } from 'vue-i18n' | ||
import { watchDebounced } from '@vueuse/core' | ||
import { executeQuerySearch } from '@/plugins/search' | ||
import Button from 'primevue/button' | ||
import { isEmpty } from 'lodash' | ||
const { t } = useI18n() | ||
const currentSelection = ref<Tag[]>([]) | ||
const suggestions = ref<Tag[]>([]) | ||
const currentPlainValue = ref('') | ||
const hidePanel = ref(false) | ||
const search = async (query: string) => { | ||
useStatStore().isSearching = true | ||
try { | ||
const result = await executeQuerySearch({ | ||
size: 10, | ||
query: { | ||
query: query | ||
}, | ||
fields: ['*'] | ||
}) | ||
useSearchResultStore().overwrite( | ||
result.hits.map((h) => { | ||
h.fields.id = h.id | ||
return h.fields | ||
}) | ||
) | ||
} finally { | ||
useStatStore().isSearching = false | ||
} | ||
const resultStore = useSearchResultStore() | ||
const statStore = useStatStore() | ||
const aComplete = (event: AutoCompleteCompleteEvent) => { | ||
currentPlainValue.value = event.query | ||
suggestions.value = useTagStore().entries.filter( | ||
(entry) => | ||
entry.trans.plain?.toLowerCase().includes(event.query.toLowerCase()) || | ||
entry.id.includes(event.query.toLowerCase()) | ||
) | ||
// Hide the panel if no suggestions have been found | ||
hidePanel.value = suggestions.value.length === 0 | ||
} | ||
const clearAfterSelect = () => { | ||
currentPlainValue.value = '' | ||
} | ||
const clearInput = () => { | ||
currentSelection.value = [] | ||
currentPlainValue.value = '' | ||
currentResultCount.value = 0 | ||
currentRequestValid.value = true | ||
resultStore.clear() | ||
} | ||
const query = ref('') | ||
const currentResultCount = ref(0) | ||
const currentRequestValid = ref(true) | ||
const query = computed(() => { | ||
const plain = currentPlainValue.value | ||
const tags = currentSelection.value.map((tag) => 'tags:"' + tag.id + '"') | ||
return (plain + ' ' + tags.join('')).trim() | ||
}) | ||
watchDebounced( | ||
query, | ||
() => { | ||
search(query.value) | ||
async () => { | ||
if (query.value.trim() === '') return | ||
statStore.isSearching = true | ||
try { | ||
const result = await executeQuerySearch({ | ||
size: 10, | ||
query: { | ||
query: query.value | ||
}, | ||
fields: ['*'] | ||
}) | ||
resultStore.overwrite( | ||
result.hits.map((h) => { | ||
h.fields.id = h.id | ||
return h.fields | ||
}) | ||
) | ||
currentResultCount.value = result.total_hits | ||
currentRequestValid.value = true | ||
} catch { | ||
currentRequestValid.value = false | ||
} finally { | ||
statStore.isSearching = false | ||
} | ||
}, | ||
{ debounce: 200, maxWait: 400 } // maxWait should be lower than the progress indicator "delay" in AppLayout. | ||
{ debounce: 200, maxWait: 500 } | ||
) | ||
</script> | ||
|
||
// setTimeout(() => { | ||
// query.value = 'midi' | ||
// }, 500) | ||
<style lang="scss"> | ||
.SearchTagChip { | ||
border: 0; | ||
padding: 0; | ||
margin: 0; | ||
const isSearching = computed(() => useStatStore().isSearching) | ||
const searchResultCount = computed(() => useStatStore().searchResultCount) | ||
</script> | ||
display: flex; | ||
align-items: stretch; | ||
justify-content: center; | ||
border-top-width: 2px !important; | ||
border-top-style: solid; | ||
border-bottom-width: 2px !important; | ||
border-bottom-style: solid; | ||
border-left-width: 2px !important; | ||
border-left-style: solid; | ||
border-right-width: 2px !important; | ||
border-right-style: solid; | ||
border-radius: var(--border-radius); | ||
.SearchTag { | ||
margin: unset !important; | ||
.Parts { | ||
span { | ||
border: unset !important; | ||
} | ||
span:first-child { | ||
border-left-width: 2px !important; | ||
border-left-style: solid; | ||
border-top-left-radius: var(--border-radius) !important; | ||
border-bottom-left-radius: var(--border-radius) !important; | ||
} | ||
span:last-child { | ||
border-right-width: unset !important; | ||
border-right-style: unset !important; | ||
border-top-right-radius: unset !important; | ||
border-bottom-right-radius: unset !important; | ||
} | ||
} | ||
} | ||
} | ||
.p-autocomplete-panel.HidePanel { | ||
visibility: hidden; | ||
} | ||
.QueryInput { | ||
.Options { | ||
a { | ||
color: var(--gray-500); | ||
text-decoration: underline; | ||
&:hover { | ||
color: var(--gray-700); | ||
text-decoration: none; | ||
} | ||
} | ||
} | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters