Bug 32474: Infinite scroll v-selects
This patch is an example ajax based v-select. The v-select will load the first 20 items and then continue to load paginated sections of 20 items as the user scrolls down. The v-select also offers ajax based searches (unpaginated) and will return to 20 item pagination if the search is cleared. Currently the pagination just works with an Intersection Observer based on scrolling - the main issue with this is that the size of the v-select window changes every time new data is added to the list and this causes the scrollbar to jump before resetting at the correct size. This can be a bit annoying, especially when scrolling quickly. The only way round this will either be to paginate using buttons i.e. (previous/next page) or to limit the data to 20 items at all times and re-paginate when scrolling back up - interested to hear thoughts/suggestions on this or whether anyone has a magic CSS fix that solves it ;) The new v-select is only in one location so far as a test - Agreement Licenses Test plan: 1) You will need to add multiple licenses in order to see the pagination, attached is a script that will create 100 dummy licenses at a time if you wish to use that 2) Once licenses are created, apply patch and run yarn build 3) Navigate to Agreements and click the New Agreement button 4) Scroll down to the Add new license option and click the button 5) The License select is the InfiniteScrollSelect and should display the licenses you have added 6) Open the dropdown and 20 items will be listed 7) Scroll down and as you scroll, more items will be loaded (this can be seen in the Network tab in developer tools) 8) Enter a search query and the results should reflect the search query 9) Delete the search query and the dropdown should return to the first 20 paginated items and pagination will work again when scrolling 10) Try submitting the form with paginate/searched options and the form should still work as intended Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com> Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
This commit is contained in:
parent
5fbdc2d7d3
commit
28078a6e25
2 changed files with 175 additions and 14 deletions
|
@ -19,22 +19,12 @@
|
|||
<label :for="`license_id_${counter}`" class="required"
|
||||
>{{ $__("License") }}:</label
|
||||
>
|
||||
<v-select
|
||||
<InfiniteScrollSelect
|
||||
:id="`license_id_${counter}`"
|
||||
v-model="agreement_license.license_id"
|
||||
label="name"
|
||||
:reduce="l => l.license_id"
|
||||
:options="licenses"
|
||||
>
|
||||
<template #search="{ attributes, events }">
|
||||
<input
|
||||
:required="!agreement_license.license_id"
|
||||
class="vs__search"
|
||||
v-bind="attributes"
|
||||
v-on="events"
|
||||
/>
|
||||
</template>
|
||||
</v-select>
|
||||
dataType="licenses"
|
||||
:required="true"
|
||||
/>
|
||||
<span class="required">{{ $__("Required") }}</span>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -102,6 +92,7 @@
|
|||
|
||||
<script>
|
||||
import { APIClient } from "../../fetch/api-client.js"
|
||||
import InfiniteScrollSelect from "../InfiniteScrollSelect.vue"
|
||||
|
||||
export default {
|
||||
name: "AgreementLicenses",
|
||||
|
@ -139,5 +130,8 @@ export default {
|
|||
this.agreement_licenses.splice(counter, 1)
|
||||
},
|
||||
},
|
||||
components: {
|
||||
InfiniteScrollSelect,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<v-select
|
||||
v-bind:id="id"
|
||||
v-model="model"
|
||||
:label="queryProperty"
|
||||
:options="search ? data : paginated"
|
||||
:reduce="item => item[dataIdentifier]"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
@search="searchFilter($event)"
|
||||
>
|
||||
<template #list-footer>
|
||||
<li v-show="hasNextPage && !this.search" ref="load">
|
||||
{{ $__("Loading more options...") }}
|
||||
</li>
|
||||
</template>
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { APIClient } from "../fetch/api-client.js"
|
||||
|
||||
export default {
|
||||
created() {
|
||||
this.fetchInitialData(this.dataType)
|
||||
switch (this.dataType) {
|
||||
case "vendors":
|
||||
this.dataIdentifier = "id"
|
||||
this.queryProperty = "name"
|
||||
break
|
||||
case "agreements":
|
||||
this.dataIdentifier = "agreement_id"
|
||||
this.queryProperty = "name"
|
||||
break
|
||||
case "licenses":
|
||||
this.dataIdentifier = "license_id"
|
||||
this.queryProperty = "name"
|
||||
break
|
||||
case "localPackages":
|
||||
this.dataIdentifier = "package_id"
|
||||
this.queryProperty = "name"
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
props: {
|
||||
id: String,
|
||||
dataType: String,
|
||||
modelValue: Number,
|
||||
required: Boolean,
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
observer: null,
|
||||
dataIdentifier: null,
|
||||
queryProperty: null,
|
||||
limit: null,
|
||||
search: "",
|
||||
scrollPage: null,
|
||||
data: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value)
|
||||
},
|
||||
},
|
||||
filtered() {
|
||||
return this.data.filter(item =>
|
||||
item[this.queryProperty].includes(this.search)
|
||||
)
|
||||
},
|
||||
paginated() {
|
||||
return this.filtered.slice(0, this.limit)
|
||||
},
|
||||
hasNextPage() {
|
||||
return this.paginated.length < this.filtered.length
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.observer = new IntersectionObserver(this.infiniteScroll)
|
||||
},
|
||||
methods: {
|
||||
async fetchInitialData(dataType) {
|
||||
const client = APIClient.erm
|
||||
await client[dataType]
|
||||
.getAll("_page=1&_per_page=20&_match=contains")
|
||||
.then(
|
||||
items => {
|
||||
this.data = items
|
||||
this.search = ""
|
||||
this.limit = 19
|
||||
this.scrollPage = 1
|
||||
},
|
||||
error => {}
|
||||
)
|
||||
},
|
||||
async searchFilter(e) {
|
||||
if (e) {
|
||||
this.observer.disconnect()
|
||||
this.data = []
|
||||
this.search = e
|
||||
const client = APIClient.erm
|
||||
await client[this.dataType]
|
||||
.getAll(
|
||||
`q={"me.${this.queryProperty}":{"like":"%${e}%"}}&_per_page=-1`
|
||||
)
|
||||
.then(
|
||||
items => {
|
||||
this.data = items
|
||||
},
|
||||
error => {}
|
||||
)
|
||||
} else {
|
||||
await this.fetchInitialData(this.dataType)
|
||||
await this.resetSelect()
|
||||
}
|
||||
},
|
||||
async onOpen() {
|
||||
await this.fetchInitialData(this.dataType)
|
||||
if (this.hasNextPage) {
|
||||
await this.$nextTick()
|
||||
this.observer.observe(this.$refs.load)
|
||||
}
|
||||
},
|
||||
onClose() {
|
||||
this.observer.disconnect()
|
||||
this.search = ""
|
||||
},
|
||||
async infiniteScroll([{ isIntersecting, target }]) {
|
||||
if (isIntersecting) {
|
||||
const ul = target.offsetParent
|
||||
const scrollTop = target.offsetParent.scrollTop
|
||||
this.limit += 20
|
||||
this.scrollPage++
|
||||
await this.$nextTick()
|
||||
const client = APIClient.erm
|
||||
await client[this.dataType]
|
||||
.getAll(
|
||||
`_page=${this.scrollPage}&_per_page=20&_match=contains`
|
||||
)
|
||||
.then(
|
||||
items => {
|
||||
const existingData = [...this.data]
|
||||
this.data = [...existingData, ...items]
|
||||
},
|
||||
error => {}
|
||||
)
|
||||
ul.scrollTop = scrollTop
|
||||
}
|
||||
},
|
||||
async resetSelect() {
|
||||
if (this.hasNextPage) {
|
||||
await this.$nextTick()
|
||||
this.observer.observe(this.$refs.load)
|
||||
}
|
||||
},
|
||||
},
|
||||
name: "InfiniteScrollSelect",
|
||||
}
|
||||
</script>
|
Loading…
Reference in a new issue