salix-front/src/components/ui/VnPaginate.vue

292 lines
6.9 KiB
Vue

<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
class: {
type: String,
default: '',
},
autoLoad: {
type: Boolean,
default: false,
},
data: {
type: Array,
default: null,
},
url: {
type: String,
default: '',
},
filter: {
type: Object,
default: null,
},
userFilter: {
type: Object,
default: null,
},
where: {
type: Object,
default: null,
},
order: {
type: [String, Array],
default: '',
},
limit: {
type: Number,
default: 20,
},
userParams: {
type: Object,
default: null,
},
keepOpts: {
type: Array,
default: () => [],
},
offset: {
type: Number,
default: undefined,
},
skeleton: {
type: Boolean,
default: true,
},
exprBuilder: {
type: Function,
default: null,
},
searchUrl: {
type: String,
default: null,
},
disableInfiniteScroll: {
type: Boolean,
default: false,
},
mapKey: {
type: String,
default: '',
},
keyData: {
type: String,
default: undefined,
},
});
const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']);
const isLoading = ref(false);
const mounted = ref(false);
const pagination = ref({
sortBy: props.order,
rowsPerPage: props.limit,
page: 1,
});
const arrayData = useArrayData(props.dataKey, {
url: props.url,
filter: props.filter,
userFilter: props.userFilter,
where: props.where,
limit: props.limit,
order: props.order,
userParams: props.userParams,
exprBuilder: props.exprBuilder,
keepOpts: props.keepOpts,
searchUrl: props.searchUrl,
mapKey: props.mapKey,
});
const store = arrayData.store;
onMounted(async () => {
if (props.autoLoad && !store.data?.length) await fetch();
else emit('onFetch', store.data);
mounted.value = true;
});
onBeforeUnmount(() => {
if (!store.keepData) arrayData.reset(['data']);
arrayData.resetPagination();
});
watch(
() => props.data,
() => {
store.data = props.data;
},
);
watch(
() => store.data,
(data) => {
if (!mounted.value) return;
emit('onChange', data);
},
{ immediate: true },
);
watch(
() => [props.url, props.filter],
([url, filter]) => mounted.value && fetch({ url, filter }),
);
const addFilter = async (filter, params) => {
await arrayData.addFilter({ filter, params });
};
async function fetch(params) {
useArrayData(props.dataKey, params);
arrayData.resetPagination();
await arrayData.fetch({ append: false });
return emitStoreData();
}
async function update(params) {
useArrayData(props.dataKey, params);
const { limit, skip } = store;
store.limit = limit + skip;
store.skip = 0;
await arrayData.fetch({ append: false });
store.limit = limit;
store.skip = skip;
return emitStoreData();
}
function emitStoreData() {
if (!store.hasMoreData) isLoading.value = false;
emit('onFetch', store.data);
return store.data;
}
async function paginate() {
const { page, rowsPerPage, sortBy, descending } = pagination.value;
if (!arrayData.store.url) return;
isLoading.value = true;
await arrayData.loadMore();
if (!store.hasMoreData) {
if (store.userParamsChanged) store.hasMoreData = true;
store.userParamsChanged = false;
endPagination();
return;
}
pagination.value.rowsNumber = store.data.length;
pagination.value.page = page;
pagination.value.rowsPerPage = rowsPerPage;
pagination.value.sortBy = sortBy;
pagination.value.descending = descending;
endPagination();
}
function endPagination() {
isLoading.value = false;
emit('onFetch', store.data);
emit('onPaginate');
}
async function onLoad(index, done) {
if (!store.data || !mounted.value) return done();
if (store.data.length === 0 || !arrayData.store.url) return done(false);
pagination.value.page = pagination.value.page + 1;
await paginate();
let isDone = false;
if (store.userParamsChanged) isDone = !store.hasMoreData;
done(isDone);
}
defineExpose({
fetch,
update,
addFilter,
paginate,
userParams: arrayData.store.userParams,
currentFilter: arrayData.store.currentFilter,
});
</script>
<template>
<div class="full-width">
<div
v-if="!store.data && !store.data?.length && !isLoading"
class="info-row q-pa-md text-center"
>
<h5>
{{ t('No data to display') }}
</h5>
</div>
<div
v-if="props.skeleton && props.autoLoad && !store.data"
class="card-list q-gutter-y-md"
>
<QCard class="card" v-for="$index in props.limit" :key="$index">
<QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<QItemSection class="q-pa-md">
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
</QItemSection>
<QSeparator vertical />
<QCardActions vertical class="justify-between">
<QSkeleton type="circle" class="q-mb-md" size="40px" />
<QSkeleton type="circle" class="q-mb-md" size="40px" />
<QSkeleton type="circle" class="q-mb-md" size="40px" />
</QCardActions>
</QItem>
</QCard>
</div>
</div>
<QInfiniteScroll
v-if="store.data"
@load="onLoad"
:offset="offset"
:class="['full-width', props.class]"
:disable="disableInfiniteScroll || !store.hasMoreData"
v-bind="$attrs"
>
<slot name="body" :rows="keyData ? store.data[keyData] : store.data"></slot>
<div v-if="isLoading" class="spinner info-row q-pa-md text-center">
<QSpinner color="primary" size="md" />
</div>
</QInfiniteScroll>
</template>
<style lang="scss" scoped>
.spinner {
z-index: 1;
align-content: end;
position: absolute;
bottom: 0;
left: 0;
}
.info-row {
width: 100%;
h5 {
margin: 0;
}
}
</style>
<i18n>
es:
No data to display: Sin datos que mostrar
No results found: No se han encontrado resultados
Load more data: Cargar más resultados
</i18n>