initial commit
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user