rejuvallife/resources/js/@core/AppBarSearch.vue
2024-10-25 01:02:11 +05:00

397 lines
10 KiB
Vue

<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import {
VList,
VListItem,
VListSubheader,
} from 'vuetify/components/VList'
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
searchQuery: {
type: String,
required: true,
},
searchResults: {
type: Array,
required: true,
},
suggestions: {
type: Array,
required: false,
},
noDataSuggestion: {
type: Array,
required: false,
},
})
const emit = defineEmits([
'update:isDialogVisible',
'update:searchQuery',
'itemSelected',
])
const { ctrl_k, meta_k } = useMagicKeys({
passive: false,
onEventFired(e) {
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown')
e.preventDefault()
},
})
const refSearchList = ref()
const searchQuery = ref(structuredClone(toRaw(props.searchQuery)))
const refSearchInput = ref()
const isLocalDialogVisible = ref(structuredClone(toRaw(props.isDialogVisible)))
const searchResults = ref(structuredClone(toRaw(props.searchResults)))
// 👉 Watching props change
watch(props, () => {
isLocalDialogVisible.value = structuredClone(toRaw(props.isDialogVisible))
searchResults.value = structuredClone(toRaw(props.searchResults))
searchQuery.value = structuredClone(toRaw(props.searchQuery))
})
watch([
ctrl_k,
meta_k,
], () => {
isLocalDialogVisible.value = true
emit('update:isDialogVisible', true)
})
// 👉 clear search result and close the dialog
const clearSearchAndCloseDialog = () => {
emit('update:isDialogVisible', false)
emit('update:searchQuery', '')
}
watchEffect(() => {
if (!searchQuery.value.length)
searchResults.value = []
})
const getFocusOnSearchList = e => {
if (e.key === 'ArrowDown') {
e.preventDefault()
refSearchList.value?.focus('next')
} else if (e.key === 'ArrowUp') {
e.preventDefault()
refSearchList.value?.focus('prev')
}
}
const dialogModelValueUpdate = val => {
emit('update:isDialogVisible', val)
emit('update:searchQuery', '')
}
const resolveCategories = val => {
if (val === 'dashboards')
return 'Dashboards'
if (val === 'appsPages')
return 'Apps & Pages'
if (val === 'userInterface')
return 'User Interface'
if (val === 'formsTables')
return 'Forms Tables'
if (val === 'chartsMisc')
return 'Charts Misc'
return 'Misc'
}
</script>
<template>
<VDialog
max-width="600"
:model-value="isLocalDialogVisible"
:height="$vuetify.display.smAndUp ? '550' : '100%'"
:fullscreen="$vuetify.display.width < 600"
class="app-bar-search-dialog"
@update:model-value="dialogModelValueUpdate"
@keyup.esc="clearSearchAndCloseDialog"
>
<VCard
height="100%"
width="100%"
class="position-relative"
>
<VCardText
class="pt-1"
style="min-block-size: 65px;"
>
<!-- 👉 Search Input -->
<VTextField
ref="refSearchInput"
v-model="searchQuery"
autofocus
density="comfortable"
variant="plain"
class="app-bar-autocomplete-box"
@keyup.esc="clearSearchAndCloseDialog"
@keydown="getFocusOnSearchList"
@update:model-value="$emit('update:searchQuery', searchQuery)"
>
<!-- 👉 Prepend Inner -->
<template #prepend-inner>
<div class="d-flex align-center text-high-emphasis me-1">
<VIcon
size="22"
icon="tabler-search"
class="mt-1"
style="opacity: 1;"
/>
</div>
</template>
<!-- 👉 Append Inner -->
<template #append-inner>
<div class="d-flex align-center">
<div
class="text-base text-disabled cursor-pointer me-1"
@click="clearSearchAndCloseDialog"
>
[esc]
</div>
<IconBtn
size="small"
@click="clearSearchAndCloseDialog"
>
<VIcon icon="tabler-x" />
</IconBtn>
</div>
</template>
</VTextField>
</VCardText>
<!-- 👉 Divider -->
<VDivider />
<!-- 👉 Perfect Scrollbar -->
<PerfectScrollbar
:options="{ wheelPropagation: false, suppressScrollX: true }"
class="h-100"
>
<!-- 👉 Search List -->
<VList
v-show="searchQuery.length && !!searchResults.length"
ref="refSearchList"
density="compact"
class="app-bar-search-list"
>
<!-- 👉 list Item /List Sub header -->
<template
v-for="item in searchResults"
:key="item.title"
>
<VListSubheader
v-if="'header' in item"
class="text-disabled"
>
{{ resolveCategories(item.title) }}
</VListSubheader>
<template v-else>
<slot
name="searchResult"
:item="item"
>
<VListItem
link
@click="$emit('itemSelected', item)"
>
<template #prepend>
<VIcon
size="20"
:icon="item.icon"
class="me-3"
/>
</template>
<template #append>
<VIcon
size="20"
icon="tabler-corner-down-left"
class="enter-icon text-disabled"
/>
</template>
<VListItemTitle>
{{ item.title }}
</VListItemTitle>
</VListItem>
</slot>
</template>
</template>
</VList>
<!-- 👉 Suggestions -->
<div
v-show="!!searchResults && !searchQuery"
class="h-100"
>
<slot name="suggestions">
<VCardText class="app-bar-search-suggestions h-100 pa-10">
<VRow
v-if="props.suggestions"
class="gap-y-4"
>
<VCol
v-for="suggestion in props.suggestions"
:key="suggestion.title"
cols="12"
sm="6"
class="ps-6"
>
<p class="text-xs text-disabled text-uppercase">
{{ suggestion.title }}
</p>
<VList class="card-list">
<VListItem
v-for="item in suggestion.content"
:key="item.title"
link
:title="item.title"
class="app-bar-search-suggestion"
@click="$emit('itemSelected', item)"
>
<template #prepend>
<VIcon
:icon="item.icon"
size="20"
class="me-2"
/>
</template>
</VListItem>
</VList>
</VCol>
</VRow>
</VCardText>
</slot>
</div>
<!-- 👉 No Data found -->
<div
v-show="!searchResults.length && searchQuery.length"
class="h-100"
>
<slot name="noData">
<VCardText class="h-100">
<div class="app-bar-search-suggestions d-flex flex-column align-center justify-center text-high-emphasis h-100">
<VIcon
size="75"
icon="tabler-file-x"
/>
<div class="d-flex align-center flex-wrap justify-center gap-2 text-h6 my-3">
<span>No Result For </span>
<span>"{{ searchQuery }}"</span>
</div>
<div
v-if="props.noDataSuggestion"
class="mt-8"
>
<span class="d-flex justify-center text-disabled">Try searching for</span>
<h6
v-for="suggestion in props.noDataSuggestion"
:key="suggestion.title"
class="app-bar-search-suggestion text-sm font-weight-regular cursor-pointer mt-3"
@click="$emit('itemSelected', suggestion)"
>
<VIcon
size="20"
:icon="suggestion.icon"
class="me-3"
/>
<span class="text-sm">{{ suggestion.title }}</span>
</h6>
</div>
</div>
</VCardText>
</slot>
</div>
</PerfectScrollbar>
</VCard>
</VDialog>
</template>
<style lang="scss">
.app-bar-search-suggestions {
.app-bar-search-suggestion {
&:hover {
color: rgb(var(--v-theme-primary));
}
}
}
.app-bar-autocomplete-box {
.v-field__input {
padding-block-end: 0.425rem;
padding-block-start: 1.16rem;
}
.v-field__append-inner,
.v-field__prepend-inner {
padding-block-start: 0.95rem;
}
.v-field__field input {
text-align: start !important;
}
}
.app-bar-search-dialog {
.v-overlay__scrim {
backdrop-filter: blur(4px);
}
.v-list-item-title {
font-size: 0.875rem !important;
}
.app-bar-search-list {
.v-list-item,
.v-list-subheader {
font-size: 0.75rem;
padding-inline: 1.5rem !important;
}
.v-list-item {
.v-list-item__append {
.enter-icon {
visibility: hidden;
}
}
&:hover,
&:active,
&:focus {
.v-list-item__append {
.enter-icon {
visibility: visible;
}
}
}
}
.v-list-subheader {
line-height: 1;
min-block-size: auto;
padding-block: 0.6875rem 0.3125rem;
text-transform: uppercase;
}
}
}
</style>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 16px;
}
</style>