first commit
This commit is contained in:
273
resources/js/@core/components/AppBarSearch.vue
Normal file
273
resources/js/@core/components/AppBarSearch.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import {
|
||||
VList,
|
||||
VListItem,
|
||||
} from 'vuetify/components/VList'
|
||||
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
searchResults: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDialogVisible',
|
||||
'search',
|
||||
])
|
||||
|
||||
|
||||
// 👉 Hotkey
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
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 refSearchInput = ref()
|
||||
const searchQueryLocal = ref('')
|
||||
|
||||
// 👉 watching control + / to open dialog
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
watch([
|
||||
ctrl_k,
|
||||
meta_k,
|
||||
], () => {
|
||||
emit('update:isDialogVisible', true)
|
||||
})
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
// 👉 clear search result and close the dialog
|
||||
const clearSearchAndCloseDialog = () => {
|
||||
searchQueryLocal.value = ''
|
||||
emit('update:isDialogVisible', false)
|
||||
}
|
||||
|
||||
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 => {
|
||||
searchQueryLocal.value = ''
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
|
||||
watch(() => props.isDialogVisible, () => {
|
||||
searchQueryLocal.value = ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
max-width="600"
|
||||
:model-value="props.isDialogVisible"
|
||||
:height="$vuetify.display.smAndUp ? '537' : '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="py-3 px-4">
|
||||
<!-- 👉 Search Input -->
|
||||
<VTextField
|
||||
ref="refSearchInput"
|
||||
v-model="searchQueryLocal"
|
||||
autofocus
|
||||
density="compact"
|
||||
variant="plain"
|
||||
class="app-bar-search-input"
|
||||
@keyup.esc="clearSearchAndCloseDialog"
|
||||
@keydown="getFocusOnSearchList"
|
||||
@update:model-value="$emit('search', searchQueryLocal)"
|
||||
>
|
||||
<!-- 👉 Prepend Inner -->
|
||||
<template #prepend-inner>
|
||||
<div class="d-flex align-center text-high-emphasis me-1">
|
||||
<VIcon
|
||||
size="24"
|
||||
icon="ri-search-line"
|
||||
style=" margin-block-start:1px; opacity: 1;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 👉 Append Inner -->
|
||||
<template #append-inner>
|
||||
<div class="d-flex align-start">
|
||||
<div
|
||||
class="text-base text-disabled cursor-pointer me-1"
|
||||
@click="clearSearchAndCloseDialog"
|
||||
>
|
||||
[esc]
|
||||
</div>
|
||||
|
||||
<IconBtn
|
||||
class="mt-n2"
|
||||
color="medium-emphasis"
|
||||
@click="clearSearchAndCloseDialog"
|
||||
>
|
||||
<VIcon icon="ri-close-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VTextField>
|
||||
</VCardText>
|
||||
|
||||
<!-- 👉 Divider -->
|
||||
<VDivider />
|
||||
|
||||
<!-- 👉 Perfect Scrollbar -->
|
||||
<PerfectScrollbar
|
||||
:options="{ wheelPropagation: false, suppressScrollX: true }"
|
||||
class="h-100"
|
||||
>
|
||||
<!-- 👉 Search List -->
|
||||
<VList
|
||||
v-show="searchQueryLocal.length && !!props.searchResults.length"
|
||||
ref="refSearchList"
|
||||
density="compact"
|
||||
class="app-bar-search-list py-0"
|
||||
>
|
||||
<!-- 👉 list Item /List Sub header -->
|
||||
<template
|
||||
v-for="item in props.searchResults"
|
||||
:key="item"
|
||||
>
|
||||
<slot
|
||||
name="searchResult"
|
||||
:item="item"
|
||||
>
|
||||
<VListItem>
|
||||
{{ item }}
|
||||
</VListItem>
|
||||
</slot>
|
||||
</template>
|
||||
</VList>
|
||||
|
||||
<!-- 👉 Suggestions -->
|
||||
<div
|
||||
v-show="!!props.searchResults && !searchQueryLocal && $slots.suggestions"
|
||||
class="h-100"
|
||||
>
|
||||
<slot name="suggestions" />
|
||||
</div>
|
||||
|
||||
<!-- 👉 No Data found -->
|
||||
<div
|
||||
v-show="!props.searchResults.length && searchQueryLocal.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 pa-12">
|
||||
<VIcon
|
||||
size="64"
|
||||
icon="ri-file-forbid-line"
|
||||
/>
|
||||
<div class="d-flex align-center flex-wrap justify-center gap-2 text-h5 my-3">
|
||||
<span>No Result For </span>
|
||||
<span>"{{ searchQueryLocal }}"</span>
|
||||
</div>
|
||||
|
||||
<slot name="noDataSuggestion" />
|
||||
</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-search-dialog {
|
||||
.app-bar-search-input{
|
||||
.v-field__input{
|
||||
padding-block-start: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.v-overlay__scrim {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.app-bar-search-list {
|
||||
.v-list-item,
|
||||
.v-list-subheader {
|
||||
font-size: 0.75rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.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: 16px 8px;
|
||||
padding-inline-start: 1rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(:focus-visible){
|
||||
.app-bar-search-dialog {
|
||||
.v-list-item:focus-visible::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
--v-card-list-gap: 16px;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user