Последняя версия с сервера прошлого разработчика
This commit is contained in:
294
nova/resources/js/components/SearchInput.vue
Executable file
294
nova/resources/js/components/SearchInput.vue
Executable file
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div
|
||||
:data-testid="dataTestid"
|
||||
:dusk="dataTestid"
|
||||
:class="{ 'opacity-75': disabled }"
|
||||
v-on-clickaway="close"
|
||||
>
|
||||
<div class="relative">
|
||||
<div
|
||||
ref="input"
|
||||
@click="open"
|
||||
@focus="open"
|
||||
@keydown.down.prevent="open"
|
||||
@keydown.up.prevent="open"
|
||||
:class="{
|
||||
focus: show,
|
||||
'border-danger': error,
|
||||
'form-select': shouldShowDropdownArrow,
|
||||
disabled,
|
||||
}"
|
||||
class="flex items-center form-control form-input form-input-bordered pr-6"
|
||||
:tabindex="show ? -1 : 0"
|
||||
>
|
||||
<div
|
||||
v-if="shouldShowDropdownArrow && !disabled"
|
||||
class="search-input-trigger absolute pin select-box"
|
||||
/>
|
||||
|
||||
<slot name="default">
|
||||
<div class="text-70">{{ __('Click to choose') }}</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click.stop="clear"
|
||||
v-if="!shouldShowDropdownArrow && !disabled"
|
||||
tabindex="-1"
|
||||
class="absolute p-2 inline-block"
|
||||
style="right: 4px; top: 6px"
|
||||
>
|
||||
<svg
|
||||
class="block fill-current icon h-2 w-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="278.046 126.846 235.908 235.908"
|
||||
>
|
||||
<path
|
||||
d="M506.784 134.017c-9.56-9.56-25.06-9.56-34.62 0L396 210.18l-76.164-76.164c-9.56-9.56-25.06-9.56-34.62 0-9.56 9.56-9.56 25.06 0 34.62L361.38 244.8l-76.164 76.165c-9.56 9.56-9.56 25.06 0 34.62 9.56 9.56 25.06 9.56 34.62 0L396 279.42l76.164 76.165c9.56 9.56 25.06 9.56 34.62 0 9.56-9.56 9.56-25.06 0-34.62L430.62 244.8l76.164-76.163c9.56-9.56 9.56-25.06 0-34.62z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="show"
|
||||
ref="dropdown"
|
||||
class="form-input px-0 border border-60 absolute pin-t pin-l my-1 overflow-hidden"
|
||||
:style="{ width: inputWidth + 'px', zIndex: 2000 }"
|
||||
>
|
||||
<div class="p-2 bg-grey-300">
|
||||
<input
|
||||
:disabled="disabled"
|
||||
v-model="search"
|
||||
@input="handleInput"
|
||||
ref="search"
|
||||
@keydown.enter.prevent="chooseSelected"
|
||||
@keydown.down.prevent="move(1)"
|
||||
@keydown.up.prevent="move(-1)"
|
||||
class="outline-none search-input-input w-full px-2 py-1.5 text-sm leading-normal bg-white rounded"
|
||||
tabindex="-1"
|
||||
type="text"
|
||||
:placeholder="__('Search')"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="container"
|
||||
class="search-input-options relative overflow-y-scroll scrolling-touch text-sm"
|
||||
tabindex="-1"
|
||||
style="max-height: 155px"
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in data"
|
||||
:dusk="dataTestid + '-result-' + index"
|
||||
:key="getTrackedByKey(option)"
|
||||
:ref="index === selected ? 'selected' : null"
|
||||
@click="choose(option)"
|
||||
class="px-4 py-2 cursor-pointer"
|
||||
:class="{
|
||||
[`search-input-item-${index}`]: true,
|
||||
'hover:bg-30': index !== selected,
|
||||
'bg-primary text-white': index === selected,
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
name="option"
|
||||
:option="option"
|
||||
:selected="index === selected"
|
||||
></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import Vue from 'vue'
|
||||
import Popper from 'popper.js'
|
||||
import { mixin as clickaway } from 'vue-clickaway'
|
||||
|
||||
export default {
|
||||
mixins: [clickaway],
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
dataTestid: {},
|
||||
disabled: { default: false },
|
||||
value: {},
|
||||
data: {},
|
||||
trackBy: {},
|
||||
error: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
boundary: {},
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
debouncer: null,
|
||||
show: false,
|
||||
search: '',
|
||||
selected: 0,
|
||||
popper: null,
|
||||
inputWidth: null,
|
||||
}),
|
||||
|
||||
watch: {
|
||||
search(search) {
|
||||
this.selected = 0
|
||||
this.$refs.container.scrollTop = 0
|
||||
},
|
||||
show(show) {
|
||||
if (show) {
|
||||
let selected = _.findIndex(this.data, [
|
||||
this.trackBy,
|
||||
_.get(this.value, this.trackBy),
|
||||
])
|
||||
if (selected !== -1) this.selected = selected
|
||||
this.inputWidth = this.$refs.input.offsetWidth
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const vm = this
|
||||
|
||||
this.popper = new Popper(this.$refs.input, this.$refs.dropdown, {
|
||||
placement: 'bottom-start',
|
||||
onCreate() {
|
||||
vm.$refs.container.scrollTop = vm.$refs.container.scrollHeight
|
||||
vm.updateScrollPosition()
|
||||
vm.$refs.search.focus()
|
||||
},
|
||||
modifiers: {
|
||||
preventOverflow: {
|
||||
boundariesElement: this.boundary
|
||||
? this.boundary
|
||||
: 'scrollParent',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
} else {
|
||||
if (this.popper) this.popper.destroy()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.debouncer = _.debounce(callback => callback(), this.debounce)
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.handleEscape)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.handleEscape)
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleEscape(e) {
|
||||
// 'tab' or 'escape'
|
||||
if (this.show && (e.keyCode == 9 || e.keyCode == 27)) {
|
||||
setTimeout(() => this.close(), 50)
|
||||
}
|
||||
},
|
||||
|
||||
getTrackedByKey(option) {
|
||||
return _.get(option, this.trackBy)
|
||||
},
|
||||
|
||||
open() {
|
||||
if (!this.disabled) {
|
||||
this.show = true
|
||||
this.search = ''
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.show = false
|
||||
},
|
||||
|
||||
clear() {
|
||||
if (!this.disabled) {
|
||||
this.selected = null
|
||||
this.$emit('clear', null)
|
||||
}
|
||||
},
|
||||
|
||||
move(offset) {
|
||||
let newIndex = this.selected + offset
|
||||
|
||||
if (newIndex >= 0 && newIndex < this.data.length) {
|
||||
this.selected = newIndex
|
||||
this.updateScrollPosition()
|
||||
}
|
||||
},
|
||||
|
||||
updateScrollPosition() {
|
||||
Vue.nextTick(() => {
|
||||
if (this.$refs.selected) {
|
||||
if (
|
||||
this.$refs.selected[0].offsetTop >
|
||||
this.$refs.container.scrollTop +
|
||||
this.$refs.container.clientHeight -
|
||||
this.$refs.selected[0].clientHeight
|
||||
) {
|
||||
this.$refs.container.scrollTop =
|
||||
this.$refs.selected[0].offsetTop +
|
||||
this.$refs.selected[0].clientHeight -
|
||||
this.$refs.container.clientHeight
|
||||
}
|
||||
|
||||
if (
|
||||
this.$refs.selected[0].offsetTop < this.$refs.container.scrollTop
|
||||
) {
|
||||
this.$refs.container.scrollTop = this.$refs.selected[0].offsetTop
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
chooseSelected() {
|
||||
if (this.data[this.selected] !== undefined) {
|
||||
this.$emit('selected', this.data[this.selected])
|
||||
this.$refs.input.focus()
|
||||
Vue.nextTick(() => this.close())
|
||||
}
|
||||
},
|
||||
|
||||
choose(option) {
|
||||
this.selected = _.findIndex(this.data, [
|
||||
this.trackBy,
|
||||
_.get(option, this.trackBy),
|
||||
])
|
||||
this.$emit('selected', option)
|
||||
this.$refs.input.focus()
|
||||
Vue.nextTick(() => this.close())
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the input event of the search box
|
||||
*/
|
||||
handleInput(e) {
|
||||
this.debouncer(() => {
|
||||
this.$emit('input', e.target.value)
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
shouldShowDropdownArrow() {
|
||||
return this.value == '' || this.value == null || !this.clearable
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user