525 lines
19 KiB
Vue
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>
|