1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-05-01 07:39:50 +00:00
bramw_baserow/web-frontend/modules/core/components/PaginatedDropdown.vue
2023-01-16 14:40:12 +00:00

217 lines
5.6 KiB
Vue

<template>
<div
class="dropdown"
:class="{
'dropdown--floating': !showInput,
'dropdown--disabled': disabled,
}"
:tabindex="realTabindex"
@focusin="show()"
@focusout="focusout($event)"
>
<a v-if="showInput" class="dropdown__selected" @click="show()">
<template v-if="displayName !== null">
{{ displayName }}
</template>
<template v-else>{{
notSelectedText === null ? $t('action.makeChoice') : notSelectedText
}}</template>
<i class="dropdown__toggle-icon fas fa-caret-down"></i>
</a>
<div class="dropdown__items" :class="{ hidden: !open }">
<div v-if="showSearch" class="select__search">
<i class="select__search-icon fas fa-search"></i>
<input
ref="search"
v-model="query"
type="text"
class="select__search-input"
tabindex="0"
:placeholder="searchText === null ? $t('action.search') : searchText"
@input="search"
/>
</div>
<ul
ref="items"
v-auto-overflow-scroll
class="select__items"
tabindex=""
@scroll="scroll"
>
<DropdownItem
v-if="addEmptyItem"
:name="emptyItemDisplayName"
:value="null"
></DropdownItem>
<DropdownItem
v-for="result in results"
:key="result[idName]"
:name="result[valueName]"
:value="result[idName]"
></DropdownItem>
<div v-if="loading" class="select__items-loading"></div>
</ul>
</div>
</div>
</template>
<script>
import debounce from 'lodash/debounce'
import dropdown from '@baserow/modules/core/mixins/dropdown'
import { notifyIf } from '@baserow/modules/core/utils/error'
export default {
name: 'Dropdown',
mixins: [dropdown],
props: {
fetchPage: {
type: Function,
required: true,
},
// The attribute name that contains the identifier in the fetched results.
idName: {
type: String,
required: false,
default: 'id',
},
// The attribute name that contains the display value in the fetched results.
valueName: {
type: String,
required: false,
default: 'value',
},
fetchOnOpen: {
type: Boolean,
required: false,
default: false,
},
addEmptyItem: {
type: Boolean,
required: false,
default: true,
},
emptyItemDisplayName: {
type: [String],
default: '',
},
notSelectedText: {
type: [String, null],
required: false,
default: null,
},
initialDisplayName: {
type: [String, null],
required: false,
default: null,
},
},
data() {
return {
fetched: false,
displayName: this.initialDisplayName,
count: 0,
page: 1,
loading: false,
results: [],
}
},
/**
* When the component is first created, we immediately fetch the first page.
*/
async fetch() {
if (!this.fetchOnOpen) {
this.fetched = true
this.results = await this.fetch(this.page, this.query)
}
},
methods: {
clear() {
this.displayName = this.initialDisplayName
},
/**
* Because the dropdown items could be destroyed in case of a search and because we
* don't need reactivity, we store a copy of the name as display name as soon as it
* has changed.
*/
select(value) {
const displayName = this.getSelectedProperty(value, 'name')
dropdown.methods.select.call(this, { id: value, value: displayName })
this.displayName = displayName
},
async fetch(page = 1, search = null) {
this.page = page
this.loading = true
try {
const { data } = await this.fetchPage(page, search)
this.count = data.count
this.loading = false
return data.results
} catch (e) {
this.loading = false
notifyIf(e)
return []
}
},
/**
* Because the results change when you search, we need to reset the state before
* searching. Otherwise there could be conflicting results.
*/
search() {
this.results = []
this.page = 1
this.count = 0
this.loading = true
this._search()
},
/**
* Small debounce when searching to prevent a lot of requests to the backend.
*/
_search: debounce(async function () {
this.results = await this.fetch(this.page, this.query)
}, 400),
/**
* When the user scrolls in the results, we can check if the user is near the end
* and if so a new page will be loaded.
*/
async scroll() {
const items = this.$refs.items
const max = items.scrollHeight - items.clientHeight
if (
!this.loading &&
this.results.length < this.count &&
items.scrollTop > max - 30
) {
this.results.push(...(await this.fetch(this.page + 1, this.query)))
}
},
async show(...args) {
dropdown.methods.show.call(this, ...args)
if (!this.fetched) {
this.fetched = true
this.results = await this.fetch(this.page, this.query)
}
},
/**
* Normally, when the dropdown hides, the search is reset, but in this case we
* don't want to do that because otherwise results are refreshed everytime the
* user closes dropdown.
*/
hide() {
this.open = false
this.$emit('hide')
this.$el.clickOutsideEventCancel()
document.body.removeEventListener('keydown', this.$el.keydownEvent)
},
reset() {
this.fetched = false
this.open = false
this.displayName = null
this.query = ''
this.results = []
},
},
}
</script>