mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-05-01 07:39:50 +00:00
217 lines
5.6 KiB
Vue
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>
|