Последняя версия с сервера прошлого разработчика
This commit is contained in:
332
nova/resources/js/components/GlobalSearch.vue
Executable file
332
nova/resources/js/components/GlobalSearch.vue
Executable file
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<div v-on-clickaway="closeSearch" class="relative z-50 w-full max-w-xs">
|
||||
<div class="relative">
|
||||
<!-- Search -->
|
||||
<div class="relative">
|
||||
<icon type="search" class="absolute search-icon-center ml-3 text-80" />
|
||||
|
||||
<input
|
||||
dusk="global-search"
|
||||
ref="input"
|
||||
@input.stop="search"
|
||||
@keydown.stop=""
|
||||
@keydown.enter.stop="goToCurrentlySelectedResource"
|
||||
@keydown.esc.stop="closeSearch"
|
||||
@focus="openSearch"
|
||||
@keydown.down.prevent="move(1)"
|
||||
@keydown.up.prevent="move(-1)"
|
||||
v-model="searchTerm"
|
||||
type="search"
|
||||
:placeholder="__('Press / to search')"
|
||||
class="pl-search w-full form-global-search"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loader -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="bg-white py-3 overflow-hidden absolute rounded-lg shadow-lg w-full mt-2 max-h-search overflow-y-auto"
|
||||
>
|
||||
<loader class="text-60" width="40" />
|
||||
</div>
|
||||
|
||||
<!-- No Results Found -->
|
||||
<div
|
||||
v-if="shouldShowNoResults"
|
||||
class="bg-white overflow-hidden absolute rounded-lg shadow-lg w-full mt-2 max-h-search overflow-y-auto"
|
||||
>
|
||||
<h3 class="text-xs uppercase tracking-wide text-80 bg-40 py-4 px-3">
|
||||
{{ __('No Results Found.') }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div
|
||||
v-if="shouldShowResults"
|
||||
class="overflow-hidden absolute rounded-lg shadow-lg w-full mt-2 max-h-search overflow-y-auto"
|
||||
ref="container"
|
||||
>
|
||||
<div v-for="group in formattedResults">
|
||||
<h3 class="text-xs uppercase tracking-wide text-80 bg-40 py-2 px-3">
|
||||
{{ group.resourceTitle }}
|
||||
</h3>
|
||||
|
||||
<ul class="list-reset">
|
||||
<li
|
||||
v-for="item in group.items"
|
||||
:key="item.resourceName + ' ' + item.index"
|
||||
:ref="item.index === highlightedResultIndex ? 'selected' : null"
|
||||
>
|
||||
<a
|
||||
:dusk="item.resourceName + ' ' + item.index"
|
||||
@click.prevent="navigate(item.index)"
|
||||
class="cursor-pointer flex items-center hover:bg-20 block py-2 px-3 no-underline font-normal"
|
||||
:class="{
|
||||
'bg-white': highlightedResultIndex != item.index,
|
||||
'bg-20': highlightedResultIndex == item.index,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="item.avatar"
|
||||
:src="item.avatar"
|
||||
class="h-8 w-8 mr-3"
|
||||
:class="{
|
||||
'rounded-full': item.rounded,
|
||||
rounded: !item.rounded,
|
||||
}"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p class="text-90">{{ item.title }}</p>
|
||||
<p v-if="item.subTitle" class="text-xs mt-1 text-80">
|
||||
{{ item.subTitle }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Minimum } from 'laravel-nova'
|
||||
import { mixin as clickaway } from 'vue-clickaway'
|
||||
import { CancelToken, Cancel } from 'axios'
|
||||
|
||||
export default {
|
||||
mixins: [clickaway],
|
||||
|
||||
data: () => ({
|
||||
debouncer: null,
|
||||
canceller: null,
|
||||
loading: false,
|
||||
currentlySearching: false,
|
||||
searchTerm: '',
|
||||
results: [],
|
||||
highlightedResultIndex: 0,
|
||||
}),
|
||||
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.closeSearch()
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.debouncer = _.debounce(callback => callback(), Nova.config.debounce)
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Open search menu if the user types '/'
|
||||
Nova.addShortcut('/', () => {
|
||||
this.openSearch()
|
||||
|
||||
return false
|
||||
})
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
Nova.disableShortcut('/')
|
||||
},
|
||||
|
||||
methods: {
|
||||
isNotInputElement(event) {
|
||||
const tagName = event.target.tagName
|
||||
return Boolean(tagName !== 'INPUT' && tagName !== 'TEXTAREA')
|
||||
},
|
||||
|
||||
openSearch() {
|
||||
this.clearSearch()
|
||||
this.$refs.input.focus()
|
||||
this.currentlySearching = true
|
||||
this.clearResults()
|
||||
},
|
||||
|
||||
closeSearch() {
|
||||
this.clearSearch()
|
||||
this.clearResults()
|
||||
this.$refs.input.blur()
|
||||
this.currentlySearching = false
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.searchTerm = ''
|
||||
},
|
||||
|
||||
clearResults() {
|
||||
this.results = []
|
||||
},
|
||||
|
||||
search(event) {
|
||||
this.highlightedResultIndex = 0
|
||||
this.loading = true
|
||||
|
||||
if (this.searchTerm == '') {
|
||||
if (this.canceller !== null) this.canceller()
|
||||
|
||||
this.loading = false
|
||||
this.results = []
|
||||
} else {
|
||||
this.debouncer(() => {
|
||||
if (this.canceller !== null) this.canceller()
|
||||
|
||||
this.fetchResults(event.target.value)
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
|
||||
async fetchResults(search) {
|
||||
this.results = []
|
||||
|
||||
if (search !== '') {
|
||||
try {
|
||||
const { data: results } = await Minimum(
|
||||
Nova.request().get('/nova-api/search', {
|
||||
params: { search },
|
||||
cancelToken: new CancelToken(canceller => {
|
||||
this.canceller = canceller
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
this.results = results
|
||||
|
||||
this.loading = false
|
||||
} catch (e) {
|
||||
this.loading = false
|
||||
if (e instanceof Cancel) {
|
||||
return
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Move the highlighted results
|
||||
*/
|
||||
move(offset) {
|
||||
if (this.results.length) {
|
||||
let newIndex = this.highlightedResultIndex + offset
|
||||
|
||||
if (newIndex < 0) {
|
||||
this.highlightedResultIndex = this.results.length - 1
|
||||
this.updateScrollPosition()
|
||||
} else if (newIndex > this.results.length - 1) {
|
||||
this.highlightedResultIndex = 0
|
||||
this.updateScrollPosition()
|
||||
} else if (newIndex >= 0 && newIndex < this.results.length) {
|
||||
this.highlightedResultIndex = newIndex
|
||||
this.updateScrollPosition()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateScrollPosition() {
|
||||
const selection = this.$refs.selected
|
||||
const container = this.$refs.container
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (selection) {
|
||||
if (
|
||||
selection[0].offsetTop >
|
||||
container.scrollTop +
|
||||
container.clientHeight -
|
||||
selection[0].clientHeight
|
||||
) {
|
||||
container.scrollTop =
|
||||
selection[0].offsetTop +
|
||||
selection[0].clientHeight -
|
||||
container.clientHeight
|
||||
}
|
||||
if (selection[0].offsetTop < container.scrollTop) {
|
||||
container.scrollTop = selection[0].offsetTop
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
navigate(index) {
|
||||
this.highlightedResultIndex = index
|
||||
this.goToCurrentlySelectedResource()
|
||||
},
|
||||
|
||||
goToCurrentlySelectedResource() {
|
||||
const resource = _.find(
|
||||
this.indexedResults,
|
||||
res => res.index == this.highlightedResultIndex
|
||||
)
|
||||
|
||||
this.$router.push({
|
||||
name: resource.linksTo,
|
||||
params: {
|
||||
resourceName: resource.resourceName,
|
||||
resourceId: resource.resourceId,
|
||||
},
|
||||
})
|
||||
|
||||
this.closeSearch()
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasResults() {
|
||||
return this.results.length > 0
|
||||
},
|
||||
|
||||
hasSearchTerm() {
|
||||
return this.searchTerm !== ''
|
||||
},
|
||||
|
||||
shouldShowNoResults() {
|
||||
return (
|
||||
this.currentlySearching &&
|
||||
!this.loading &&
|
||||
!this.hasResults &&
|
||||
this.hasSearchTerm
|
||||
)
|
||||
},
|
||||
|
||||
shouldShowResults() {
|
||||
return this.currentlySearching && this.hasResults && !this.loading
|
||||
},
|
||||
|
||||
indexedResults() {
|
||||
return _.map(this.results, (item, index) => {
|
||||
return { index, ...item }
|
||||
})
|
||||
},
|
||||
|
||||
formattedGroups() {
|
||||
return _.chain(this.indexedResults)
|
||||
.map(item => {
|
||||
return {
|
||||
resourceName: item.resourceName,
|
||||
resourceTitle: item.resourceTitle,
|
||||
}
|
||||
})
|
||||
.uniqBy('resourceName')
|
||||
.value()
|
||||
},
|
||||
|
||||
formattedResults() {
|
||||
return _.map(this.formattedGroups, group => {
|
||||
return {
|
||||
resourceName: group.resourceName,
|
||||
resourceTitle: group.resourceTitle,
|
||||
items: _.filter(
|
||||
this.indexedResults,
|
||||
item => item.resourceName == group.resourceName
|
||||
),
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user