library_components/components/Ml/Paginator/MlPaginator.vue
2024-07-09 11:00:50 +02:00

525 lines
19 KiB
Vue

<script setup lang="ts">
import { toRef, type PropType, computed, ref, watch, onMounted, defineProps, defineEmits } from 'vue';
import { twMerge } from 'tailwind-merge';
import type { PTAttribs } from './types';
const props = defineProps({
pt: {
type: Object,
default: function () {}
},
items: {
type: Object,
required: true
},
itemsPerPage: {
type: Number,
default: 10,
validator: (value: number) => value > 0 || 'itemsPerPage must be greater than 0.'
},
currentPage: {
type: Number,
default: 10
},
modelValue: {
type: Number,
required: true,
validator: (value: number) => value > 0 || 'v-model must be greater than 0.'
},
maxPagesShown: {
type: Number,
default: 5,
validator: (value: number) => value > 0 || 'maxPagesShown must be greater than 0.'
},
dir: {
type: String as PropType<'ltr' | 'rtl'>,
default: 'ltr',
validator: (value: string) => ['ltr', 'rtl'].includes(value) || 'dir must be either "ltr" or "rtl".'
},
type: {
type: String as PropType<'link' | 'button'>,
default: 'button',
validator: (value: string) => ['link', 'button'].includes(value) || 'type must be "link" or "button".'
},
onClick: Function,
locale: {
type: String as PropType<'en' | 'ar' | 'ir'>,
default: 'en',
validator: (value: string) => ['en', 'ar', 'ir'].includes(value) || 'locale must be "en", "ar", or "ir".'
},
prevButtonContent: {
type: String,
default: '<'
},
nextButtonContent: {
type: String,
default: '>'
},
hidePrevNext: {
type: Boolean,
default: false
},
hidePrevNextWhenEnds: {
type: Boolean,
default: false
},
showBreakpointButtons: {
type: Boolean,
default: true
},
disableBreakpointButtons: {
type: Boolean,
default: false
},
startingBreakpointContent: {
type: String,
default: '...'
},
endingBreakpointButtonContent: {
type: String,
default: '...'
},
showJumpButtons: {
type: Boolean,
default: false
},
linkUrl: {
type: String,
default: '#'
},
backwardJumpButtonContent: {
type: String,
default: '<<'
},
forwardJumpButtonContent: {
type: String,
default: '>>'
},
disablePagination: {
type: Boolean,
default: false
},
showEndingButtons: {
type: Boolean,
default: false
},
firstPageContent: {
type: String,
default: 'First'
},
lastPageContent: {
type: String,
default: 'Last'
},
// Class props
backButtonClass: String,
nextButtonClass: {
type: String,
default: 'next-button'
},
firstButtonClass: {
type: String,
default: 'first-button'
},
lastButtonClass: {
type: String,
default: 'last-button'
},
numberButtonsClass: {
type: String,
default: 'number-buttons'
},
startingBreakpointButtonClass: {
type: String,
default: 'starting-breakpoint-button'
},
endingBreakPointButtonClass: {
type: String,
default: 'ending-breakpoint-button'
},
firstPageButtonClass: {
type: String,
default: 'first-page-button'
},
lastPageButtonClass: {
type: String,
default: 'last-page-button'
},
paginateButtonsClass: {
type: String,
default: 'px-3 py-1 rounded-full font-[1000] text-sm'
},
disabledPaginateButtonsClass: {
type: String,
default: 'disabled-paginate-buttons'
},
disabledBreakPointButtonClass: String,
backwardJumpButtonClass: String,
forwardJumpButtonClass: String,
disabledBackwardJumpButtonClass: String,
disabledBackButtonClass: String,
disabledFirstButtonClass: String,
disabledLastButtonClass: String,
disabledNextButtonClass: String,
disabledForwardJumpButtonClass: String
});
const classes = ref({
root: { class: `` },
paginator: { class: `` },
activePaginator: { class: '' }
} as PTAttribs);
const rootClasses = computed(() => twMerge(classes.value.root?.class, props.pt?.root?.class));
const paginatorClasses = computed(() => twMerge(classes.value.paginator?.class, props.pt?.paginator?.class));
const activePaginator = computed(() => twMerge(classes.value.activePaginator?.class, props.pt?.activePaginator?.class));
if (props.currentPage && !props.modelValue) {
throw new Error('currentPage/current-page is deprecated, use v-model instead to set the current page.');
}
if (!props.modelValue) {
throw new TypeError('v-model is required for the paginate component.');
}
const currentPageRef = toRef(props, 'modelValue');
const emit = defineEmits(['update:modelValue', 'click', 'update:paginatedItems']);
const onClickHandler = (number: number) => {
if (number === currentPageRef.value || number > totalPages.value || number < 1 || props.disablePagination) return;
emit('update:modelValue', number);
emit('click', number);
};
const NumbersLocale = (number: number) => {
switch (props.locale) {
case 'ar':
return number.toLocaleString('ar-SA');
case 'ir':
return number.toLocaleString('fa-IR');
default:
return number;
}
};
const navigationHandler = (page: number) => {
if (props.type !== 'link') return '';
return props.linkUrl.replace('[page]', page.toString());
};
const totalPages = computed(() => Math.ceil(props.items.length / props.itemsPerPage));
const paginatedItems = computed(() => {
const start = (currentPageRef.value - 1) * props.itemsPerPage;
const end = start + props.itemsPerPage;
return props.items.slice(start, end);
});
watch(currentPageRef, emitPaginatedItems);
onMounted(emitPaginatedItems);
function emitPaginatedItems() {
emit('update:paginatedItems', paginatedItems.value);
}
const paginate = computed(() => {
let startPage: number, endPage: number;
if (totalPages.value <= props.maxPagesShown) {
startPage = 1;
endPage = totalPages.value;
} else {
const maxPagesShownBeforeCurrentPage = Math.floor(props.maxPagesShown / 2);
const maxPagesShownAfterCurrentPage = Math.ceil(props.maxPagesShown / 2) - 1;
if (currentPageRef.value <= maxPagesShownBeforeCurrentPage) {
startPage = 1;
endPage = props.maxPagesShown;
} else if (currentPageRef.value + maxPagesShownAfterCurrentPage >= totalPages.value) {
startPage = totalPages.value - props.maxPagesShown + 1;
endPage = totalPages.value;
} else {
startPage = currentPageRef.value - maxPagesShownBeforeCurrentPage;
endPage = currentPageRef.value + maxPagesShownAfterCurrentPage;
}
}
let pages = Array.from(Array(endPage + 1 - startPage).keys()).map((i) => startPage + i);
if (props.dir === 'rtl') {
pages = pages.reverse();
}
return { currentPage: currentPageRef.value, itemsPerPage: props.itemsPerPage, totalPages: totalPages, startPage, endPage, pages };
});
const isRtl = computed(() => props.dir === 'rtl');
const backButtonIfCondition = computed(() =>
isRtl.value ? !props.hidePrevNextWhenEnds || currentPageRef.value !== totalPages.value : !props.hidePrevNextWhenEnds || currentPageRef.value !== 1
);
const nextButtonIfCondition = computed(() =>
isRtl.value ? !props.hidePrevNextWhenEnds || currentPageRef.value !== 1 : !props.hidePrevNextWhenEnds || currentPageRef.value !== totalPages.value
);
const startingBreakPointButtonIfCondition = computed(() => (isRtl.value ? paginate.value.pages[0] < totalPages.value - 1 : paginate.value.pages[0] >= 3));
const endingBreakPointButtonIfCondition = computed(() =>
isRtl.value ? paginate.value.pages[paginate.value.pages.length - 1] >= 3 : paginate.value.pages[paginate.value.pages.length - 1] < totalPages.value - 1
);
const firstButtonIfCondition = computed(() => (isRtl.value ? paginate.value.pages[0] < totalPages.value : paginate.value.pages[0] >= 2));
const lastButtonIfCondition = computed(() =>
isRtl.value ? paginate.value.pages[paginate.value.pages.length - 1] >= 2 : paginate.value.pages[paginate.value.pages.length - 1] < totalPages.value
);
const firstPageButtonIfCondition = computed(() => currentPageRef.value !== 1);
const lastPageButtonIfCondition = computed(() => currentPageRef.value !== totalPages.value);
if (props.type === 'link' && (props.linkUrl === '#' || !props.linkUrl.includes('[page]'))) {
throw new TypeError('linkUrl must contain "[page]" if type is "link".');
}
</script>
<template>
<ul id="componentContainer" :class="rootClasses">
<!-- Go back to first page Button -->
<li v-if="showEndingButtons && firstPageButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="navigationHandler(isRtl ? totalPages : 1)"
:class="[firstPageButtonClass, paginatorClasses, disablePagination ? disabledPaginateButtonsClass : '']"
:disabled="disablePagination"
@click="emitPaginatedItems()"
@click.prevent="onClickHandler(isRtl ? totalPages : 1)"
>
<slot name="first-page-button">
{{ firstPageContent }}
</slot>
</component>
</li>
<!-- Backward Jump Button -->
<li v-if="showJumpButtons && startingBreakPointButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="navigationHandler(isRtl ? currentPageRef + Math.ceil(maxPagesShown / 2) : currentPageRef - Math.ceil(maxPagesShown / 2))"
:class="[
backwardJumpButtonClass,
paginateButtonsClass,
disablePagination ? disabledPaginateButtonsClass : '',
disablePagination ? disabledBackwardJumpButtonClass : ''
]"
:disabled="disablePagination"
@click.prevent="onClickHandler(isRtl ? currentPageRef + Math.ceil(maxPagesShown / 2) : currentPageRef - Math.ceil(maxPagesShown / 2))"
@click="emitPaginatedItems()"
>
<slot name="backward-jump-button">
{{ backwardJumpButtonContent }}
</slot>
</component>
</li>
<!-- Back Button -->
<li v-if="!hidePrevNext && backButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="navigationHandler(isRtl ? currentPageRef + 1 : currentPageRef - 1)"
:class="[
backButtonClass,
paginateButtonsClass,
disablePagination ? disabledPaginateButtonsClass : '',
disablePagination ? disabledBackButtonClass : ''
]"
:disabled="disablePagination"
@click.prevent="onClickHandler(isRtl ? currentPageRef + 1 : currentPageRef - 1)"
@click="emitPaginatedItems()"
>
<slot name="prev-button">
{{ prevButtonContent }}
</slot>
</component>
</li>
<!-- First Button before Starting Breakpoint Button -->
<li v-if="showBreakpointButtons && firstButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="navigationHandler(isRtl ? totalPages : 1)"
:class="[
firstButtonClass,
paginateButtonsClass,
disablePagination ? disabledPaginateButtonsClass : '',
disablePagination ? disabledFirstButtonClass : ''
]"
:disabled="disablePagination"
@click.prevent="onClickHandler(isRtl ? totalPages : 1)"
@click="emitPaginatedItems()"
>
{{ isRtl ? NumbersLocale(totalPages) : NumbersLocale(1) }}
</component>
</li>
<!-- Starting Breakpoint Button -->
<li v-if="showBreakpointButtons && startingBreakPointButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="
navigationHandler(
disableBreakpointButtons
? currentPageRef
: isRtl
? currentPageRef + Math.ceil(maxPagesShown / 2)
: currentPageRef - Math.ceil(maxPagesShown / 2)
)
"
:disabled="disableBreakpointButtons || disablePagination"
:class="[
startingBreakpointButtonClass,
paginateButtonsClass,
disableBreakpointButtons || disablePagination ? `${disabledPaginateButtonsClass} ${disabledBreakPointButtonClass}` : ''
]"
@click="emitPaginatedItems()"
@click.prevent="
onClickHandler(
disableBreakpointButtons
? currentPageRef
: isRtl
? currentPageRef + Math.ceil(maxPagesShown / 2)
: currentPageRef - Math.ceil(maxPagesShown / 2)
)
"
>
<slot name="starting-breakpoint-button">
{{ startingBreakpointContent }}
</slot>
</component>
</li>
<!-- Numbers Buttons -->
<li v-for="(page, index) in paginate.pages" :key="index">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="navigationHandler(page)"
:class="[
paginateButtonsClass,
numberButtonsClass,
page === currentPageRef ? activePaginator : '',
disablePagination ? disabledPaginateButtonsClass : ''
]"
:disabled="disablePagination"
@click.prevent="() => onClickHandler(page)"
@click="emitPaginatedItems()"
>
{{ NumbersLocale(page) }}
</component>
</li>
<!-- Ending Breakpoint Button -->
<li v-if="showBreakpointButtons && endingBreakPointButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="
navigationHandler(
disableBreakpointButtons
? currentPageRef
: isRtl
? currentPageRef - Math.ceil(maxPagesShown / 2)
: currentPageRef + Math.ceil(maxPagesShown / 2)
)
"
:disabled="disableBreakpointButtons || disablePagination"
:class="[
endingBreakPointButtonClass,
paginateButtonsClass,
disableBreakpointButtons || disablePagination ? `${disabledPaginateButtonsClass} ${disabledBreakPointButtonClass}` : ''
]"
@click="emitPaginatedItems()"
@click.prevent="
onClickHandler(
disableBreakpointButtons
? currentPageRef
: isRtl
? currentPageRef - Math.ceil(maxPagesShown / 2)
: currentPageRef + Math.ceil(maxPagesShown / 2)
)
"
>
<slot name="ending-breakpoint-button">
{{ endingBreakpointButtonContent }}
</slot>
</component>
</li>
<!-- Last Button after Ending Breakingpoint Button-->
<li v-if="showBreakpointButtons && lastButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="navigationHandler(isRtl ? 1 : totalPages)"
:class="[
lastButtonClass,
paginateButtonsClass,
disablePagination ? disabledPaginateButtonsClass : '',
disablePagination ? disabledLastButtonClass : ''
]"
:disabled="disablePagination"
@click="emitPaginatedItems()"
@click.prevent="onClickHandler(isRtl ? 1 : totalPages)"
>
{{ isRtl ? NumbersLocale(1) : NumbersLocale(totalPages) }}
</component>
</li>
<!-- Next Button -->
<li v-if="!hidePrevNext && nextButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="navigationHandler(isRtl ? currentPageRef - 1 : currentPageRef + 1)"
:class="[
paginateButtonsClass,
nextButtonClass,
disablePagination ? disabledPaginateButtonsClass : '',
disablePagination ? disabledNextButtonClass : ''
]"
:disabled="disablePagination"
@click.prevent="onClickHandler(isRtl ? currentPageRef - 1 : currentPageRef + 1)"
@click="emitPaginatedItems()"
>
<slot name="next-button">
{{ nextButtonContent }}
</slot>
</component>
</li>
<!-- Forward Jump Button -->
<li v-if="showJumpButtons && endingBreakPointButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="navigationHandler(isRtl ? currentPageRef - Math.ceil(maxPagesShown / 2) : currentPageRef + Math.ceil(maxPagesShown / 2))"
:class="[
forwardJumpButtonClass,
paginateButtonsClass,
disablePagination ? disabledPaginateButtonsClass : '',
disablePagination ? disabledForwardJumpButtonClass : ''
]"
:disabled="disablePagination"
@click.prevent="onClickHandler(isRtl ? currentPageRef - Math.ceil(maxPagesShown / 2) : currentPageRef + Math.ceil(maxPagesShown / 2))"
@click="emitPaginatedItems()"
>
<slot name="forward-jump-button">
{{ forwardJumpButtonContent }}
</slot>
</component>
</li>
<!-- Go forward to last page -->
<li v-if="showEndingButtons && lastPageButtonIfCondition">
<component
:is="type === 'button' ? 'button' : 'a'"
:href="navigationHandler(isRtl ? 1 : totalPages)"
:class="[lastPageButtonClass, paginateButtonsClass, disablePagination ? disabledPaginateButtonsClass : '']"
:disabled="disablePagination"
@click.prevent="onClickHandler(isRtl ? 1 : totalPages)"
@click="emitPaginatedItems()"
>
<slot name="last-page-button">
{{ lastPageContent }}
</slot>
</component>
</li>
</ul>
</template>