first commit
This commit is contained in:
524
components/Ml/Paginator/MlPaginator.vue
Normal file
524
components/Ml/Paginator/MlPaginator.vue
Normal file
@ -0,0 +1,524 @@
|
||||
<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>
|
11
components/Ml/Paginator/types.ts
Normal file
11
components/Ml/Paginator/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export type PTAttribs = {
|
||||
root?: {
|
||||
class: string;
|
||||
};
|
||||
paginator?: {
|
||||
class: string;
|
||||
};
|
||||
activePaginator?: {
|
||||
class: string;
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user