Files
online.tbbank.gov.tm-larave…/nova/resources/js/components/Inputs/SearchInput.vue
2024-09-01 18:54:23 +05:00

316 lines
8.4 KiB
Vue

<template>
<div v-bind="$attrs" class="relative" :dusk="dusk" ref="searchInputContainer">
<div
ref="input"
@click.stop="open"
@keydown.space.prevent="open"
@keydown.down.prevent="open"
@keydown.up.prevent="open"
:class="{
'ring dark:border-gray-500 dark:ring-gray-700': show,
'form-input-border-error': error,
'bg-gray-50 dark:bg-gray-700': disabled || readOnly,
}"
class="relative flex items-center form-control form-input form-control-bordered form-select pr-6"
:tabindex="show ? -1 : 0"
:aria-expanded="show === true ? 'true' : 'false'"
:dusk="`${dusk}-selected`"
>
<IconArrow
v-if="shouldShowDropdownArrow && !disabled"
class="pointer-events-none form-select-arrow text-gray-700"
/>
<slot name="default">
<div class="text-gray-400 dark:text-gray-400">
{{ __('Click to choose') }}
</div>
</slot>
</div>
<button
type="button"
@click="clear"
v-if="!shouldShowDropdownArrow && !disabled"
tabindex="-1"
class="absolute p-2 inline-block right-[4px]"
style="top: 6px"
:dusk="`${dusk}-clear-button`"
>
<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>
<teleport to="body">
<div
v-if="show"
ref="dropdown"
class="rounded-lg px-0 bg-white dark:bg-gray-900 shadow border border-gray-200 dark:border-gray-700 absolute top-0 left-0 my-1 overflow-hidden"
:style="{ width: inputWidth + 'px', zIndex: 2000 }"
:dusk="`${dusk}-dropdown`"
>
<!-- Search Input -->
<input
:disabled="disabled || readOnly"
v-model="searchValue"
ref="search"
@keydown.enter.prevent="chooseSelected"
@keydown.down.prevent="move(1)"
@keydown.up.prevent="move(-1)"
class="h-10 outline-none w-full px-3 text-sm leading-normal bg-white dark:bg-gray-700 rounded-t border-b border-gray-200 dark:border-gray-800"
tabindex="-1"
type="search"
:placeholder="__('Search')"
spellcheck="false"
/>
<!-- Search Results -->
<div
ref="container"
class="relative overflow-y-scroll text-sm"
tabindex="-1"
style="max-height: 155px"
:dusk="`${dusk}-results`"
>
<div
v-for="(option, index) in data"
:dusk="`${dusk}-result-${index}`"
:key="getTrackedByKey(option)"
:ref="index === selectedOptionIndex ? 'selected' : 'unselected'"
@click.stop="choose(option)"
class="px-3 py-1.5 cursor-pointer z-[50]"
:class="{
'border-t border-gray-100 dark:border-gray-700': index !== 0,
[`search-input-item-${index}`]: true,
'hover:bg-gray-100 dark:hover:bg-gray-800':
index !== selectedOptionIndex,
'bg-primary-500 text-white dark:text-gray-900':
index === selectedOptionIndex,
}"
>
<slot
name="option"
:option="option"
:selected="index === selectedOptionIndex"
/>
</div>
</div>
</div>
<Backdrop @click="close" :show="show" :style="{ zIndex: 1999 }" />
</teleport>
</template>
<script>
import debounce from 'lodash/debounce'
import findIndex from 'lodash/findIndex'
import get from 'lodash/get'
import { createPopper } from '@popperjs/core'
import { mapProps } from '@/mixins'
export default {
emits: ['clear', 'input', 'shown', 'closed', 'selected'],
inheritAttrs: false,
props: {
dusk: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
readOnly: {
type: Boolean,
default: false,
},
value: {},
data: {},
trackBy: {},
error: {
type: Boolean,
default: false,
},
boundary: {},
debounce: {
type: Number,
default: 500,
},
clearable: {
type: Boolean,
default: true,
},
...mapProps(['mode']),
},
data: () => ({
debouncer: null,
show: false,
searchValue: '',
selectedOptionIndex: 0,
popper: null,
inputWidth: null,
}),
watch: {
searchValue(search) {
this.selectedOptionIndex = 0
if (this.$refs.container) {
this.$refs.container.scrollTop = 0
} else {
this.$nextTick(() => {
this.$refs.container.scrollTop = 0
})
}
this.debouncer(() => {
this.$emit('input', search)
})
},
show(show) {
if (show) {
let selected = findIndex(this.data, [
this.trackBy,
get(this.value, this.trackBy),
])
if (selected !== -1) this.selectedOptionIndex = selected
this.inputWidth = this.$refs.input.offsetWidth
Nova.$emit('disable-focus-trap')
this.$nextTick(() => {
this.popper = createPopper(this.$refs.input, this.$refs.dropdown, {
placement: 'bottom-start',
onFirstUpdate: state => {
this.$refs.container.scrollTop = this.$refs.container.scrollHeight
this.updateScrollPosition()
this.$refs.search.focus()
},
})
})
} else {
this.$refs.search.blur()
if (this.popper) this.popper.destroy()
Nova.$emit('enable-focus-trap')
}
},
},
created() {
this.debouncer = debounce(callback => callback(), this.debounce)
},
mounted() {
document.addEventListener('keydown', this.handleEscape)
},
beforeUnmount() {
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.readOnly) {
this.show = true
this.searchValue = ''
this.$emit('shown')
}
},
close() {
this.show = false
this.$emit('closed')
},
clear() {
if (!this.disabled) {
this.selectedOptionIndex = null
this.$emit('clear', null)
}
},
move(offset) {
let newIndex = this.selectedOptionIndex + offset
if (newIndex >= 0 && newIndex < this.data.length) {
this.selectedOptionIndex = newIndex
this.updateScrollPosition()
}
},
updateScrollPosition() {
this.$nextTick(() => {
if (this.$refs.selected && this.$refs.selected[0]) {
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(event) {
if (event.isComposing || event.keyCode === 229) return
if (this.data[this.selectedOptionIndex] !== undefined) {
this.$emit('selected', this.data[this.selectedOptionIndex])
this.$refs.input.focus()
this.$nextTick(() => this.close())
}
},
choose(option) {
this.selectedOptionIndex = findIndex(this.data, [
this.trackBy,
get(option, this.trackBy),
])
this.$emit('selected', option)
this.$refs.input.blur()
this.$nextTick(() => this.close())
},
},
computed: {
shouldShowDropdownArrow() {
return this.value == '' || this.value == null || !this.clearable
},
},
}
</script>