integrate withLoadMore HOC into List
This commit is contained in:
parent
4da16acc27
commit
503309890f
16 changed files with 449 additions and 434 deletions
58
src/components/list/list.css
Normal file
58
src/components/list/list.css
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
.List {
|
||||
--__line-height: 1.5em;
|
||||
--__horizontal-gap: 0.75em;
|
||||
--__vertical-gap: 0.5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dotted var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--__vertical-gap) var(--__horizontal-gap);
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: var(--border);
|
||||
|
||||
.actions {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0.9em;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
padding-right: var(--__horizontal-gap);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
&.-scrollable {
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/components/list/list.js
Normal file
152
src/components/list/list.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { isEmpty } from 'lodash'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const List = {
|
||||
props: {
|
||||
boxOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
fetchFunction: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
itemsFunction: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
getKey: {
|
||||
type: Function,
|
||||
default: (item) => item.id,
|
||||
},
|
||||
getClass: {
|
||||
type: Function,
|
||||
default: () => '',
|
||||
},
|
||||
nonInteractive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Checkbox,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
bottomedOut: false,
|
||||
error: false,
|
||||
dynamicItems: this.itemsFunction ? [] : null,
|
||||
selected: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allKeys() {
|
||||
return this.actualItems.map(this.getKey)
|
||||
},
|
||||
filteredSelected() {
|
||||
return this.allKeys.filter((key) => this.selected.indexOf(key) !== -1)
|
||||
},
|
||||
allSelected() {
|
||||
return this.filteredSelected.length === this.actualItems.length
|
||||
},
|
||||
noneSelected() {
|
||||
return this.filteredSelected.length === 0
|
||||
},
|
||||
someSelected() {
|
||||
return !this.allSelected && !this.noneSelected
|
||||
},
|
||||
actualItems() {
|
||||
return this.dynamicItems || this.actualItems
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.fetchFunction) {
|
||||
window.addEventListener('scroll', this.scrollLoad)
|
||||
|
||||
if (this.dynamicItems.length === 0) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('scroll', this.scrollLoad)
|
||||
},
|
||||
methods: {
|
||||
// Entries is not a computed because computed can't track the dynamic
|
||||
// selector for changes and won't trigger after fetch.
|
||||
updateEntries(newEntries) {
|
||||
this.dynamicItems = this.itemsFunction(newEntries)
|
||||
},
|
||||
fetchEntries() {
|
||||
if (!this.loading) {
|
||||
this.loading = true
|
||||
this.error = false
|
||||
this.fetchFunction()
|
||||
.then((newEntries) => {
|
||||
this.loading = false
|
||||
this.bottomedOut = isEmpty(newEntries)
|
||||
return newEntries
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading = false
|
||||
this.error = error
|
||||
})
|
||||
.finally((newEntries) => {
|
||||
this.updateEntries(newEntries)
|
||||
})
|
||||
}
|
||||
},
|
||||
scrollLoad(e) {
|
||||
if (this.fetchFunction) {
|
||||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
const height = Math.max(bodyBRect.height, -bodyBRect.y)
|
||||
if (
|
||||
this.loading === false &&
|
||||
this.bottomedOut === false &&
|
||||
this.$el.offsetHeight > 0 &&
|
||||
window.innerHeight + window.pageYOffset >= height - 750
|
||||
) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected(item) {
|
||||
return this.filteredSelected.indexOf(this.getKey(item)) !== -1
|
||||
},
|
||||
toggle(checked, item) {
|
||||
console.log('TOGGLE', checked, item)
|
||||
const key = this.getKey(item)
|
||||
const oldChecked = this.isSelected(key)
|
||||
if (checked !== oldChecked) {
|
||||
if (checked) {
|
||||
this.selected.push(key)
|
||||
} else {
|
||||
this.selected.splice(this.selected.indexOf(key), 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleAll(value) {
|
||||
if (value) {
|
||||
this.selected = this.allKeys.slice(0)
|
||||
} else {
|
||||
this.selected = []
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default List
|
||||
|
|
@ -1,49 +1,91 @@
|
|||
<template>
|
||||
<div
|
||||
class="list"
|
||||
role="list"
|
||||
class="List"
|
||||
:class="{ '-scrollable': scrollable }"
|
||||
>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="getKey(item)"
|
||||
class="list-item"
|
||||
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
|
||||
role="listitem"
|
||||
v-if="selectable"
|
||||
class="header"
|
||||
>
|
||||
<slot
|
||||
name="item"
|
||||
:item="item"
|
||||
/>
|
||||
<div class="checkbox-wrapper">
|
||||
<Checkbox
|
||||
:model-value="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
@update:model-value="toggleAll"
|
||||
>
|
||||
{{ $t('selectable_list.select_all') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<slot
|
||||
name="header"
|
||||
:selected="filteredSelected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="items.length === 0 && !!$slots.empty"
|
||||
class="list-empty-content faint"
|
||||
class="list"
|
||||
role="list"
|
||||
>
|
||||
<slot name="empty" />
|
||||
<slot name="load" />
|
||||
<div
|
||||
v-for="item in actualItems"
|
||||
:key="getKey(item)"
|
||||
class="list-item"
|
||||
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
v-if="selectable"
|
||||
class="checkbox-wrapper"
|
||||
>
|
||||
<Checkbox
|
||||
:model-value="isSelected(item)"
|
||||
@update:model-value="checked => toggle(checked, item)"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
<slot
|
||||
name="item"
|
||||
:item="item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="actualItems.length === 0 && !!$slots.empty"
|
||||
class="list-empty-content faint"
|
||||
>
|
||||
<slot name="empty" />
|
||||
<slot name="load" />
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
v-if="error"
|
||||
class="button-unstyled -link -fullwidth alert error"
|
||||
@click="fetchEntries"
|
||||
>
|
||||
{{ $t('general.generic_error') }}
|
||||
{{ error }}
|
||||
</button>
|
||||
<FAIcon
|
||||
v-else-if="loading"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
<a
|
||||
v-else-if="!bottomedOut"
|
||||
@click="fetchEntries"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
{{ $t('general.more') }}
|
||||
</a>
|
||||
<span v-else>
|
||||
{{ $t('general.no_more') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
getKey: {
|
||||
type: Function,
|
||||
default: (item) => item.id,
|
||||
},
|
||||
getClass: {
|
||||
type: Function,
|
||||
default: () => '',
|
||||
},
|
||||
nonInteractive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<script src="./list.js"></script>
|
||||
|
||||
<style src="./list.css"></style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue