feat(datatable) : pagination compacte avec saut de page (Page [n] / N)
This commit is contained in:
@@ -81,43 +81,39 @@
|
||||
<nav aria-label="Pagination" class="flex items-center gap-1" data-test="pagination-nav">
|
||||
<MalioButton
|
||||
variant="tertiary"
|
||||
label="Prev"
|
||||
label="Préc."
|
||||
:disabled="page <= 1"
|
||||
button-class="h-[30px] w-auto min-w-0 px-3 text-sm"
|
||||
aria-label="Page précédente"
|
||||
data-test="prev-button"
|
||||
@click="goToPage(page - 1)"
|
||||
@click="changePage(page - 1)"
|
||||
/>
|
||||
|
||||
<template v-for="(p, idx) in visiblePages" :key="idx">
|
||||
<span
|
||||
v-if="p === '...'"
|
||||
class="px-1 text-sm text-m-muted"
|
||||
aria-hidden="true"
|
||||
>…</span>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="inline-flex h-[30px] min-w-[2.5rem] items-center justify-center rounded px-2 text-sm transition-colors"
|
||||
:class="p === page
|
||||
? 'bg-m-btn-primary text-white font-semibold'
|
||||
: 'text-m-text hover:bg-m-bg'"
|
||||
:aria-current="p === page ? 'page' : undefined"
|
||||
:data-test="`page-${p}`"
|
||||
@click="goToPage(p)"
|
||||
<span class="flex items-center gap-2 text-sm">
|
||||
<label :for="pageInputId" class="text-m-muted">Page</label>
|
||||
<input
|
||||
:id="pageInputId"
|
||||
v-model="pageInput"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
aria-label="Aller à la page"
|
||||
data-test="page-input"
|
||||
class="h-[30px] w-[58px] rounded-malio border border-m-border text-center text-sm text-m-text outline-none focus:border-m-primary"
|
||||
@input="onPageInput"
|
||||
@keydown.enter="commitPageInput"
|
||||
@blur="commitPageInput"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
</template>
|
||||
<span class="text-m-muted">/ <span data-test="total-pages">{{ totalPages }}</span></span>
|
||||
</span>
|
||||
|
||||
<MalioButton
|
||||
variant="tertiary"
|
||||
label="Next"
|
||||
label="Suiv."
|
||||
:disabled="page >= totalPages"
|
||||
button-class="h-[30px] w-auto min-w-0 px-3 text-sm"
|
||||
aria-label="Page suivante"
|
||||
data-test="next-button"
|
||||
@click="goToPage(page + 1)"
|
||||
@click="changePage(page + 1)"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -125,7 +121,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs, useId } from 'vue'
|
||||
import { computed, ref, watch, onBeforeUnmount, useAttrs, useId } from 'vue'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import MalioSelect from '../select/Select.vue'
|
||||
import MalioButton from '../button/Button.vue'
|
||||
@@ -173,6 +169,15 @@ const componentId = computed(() => props.id || `malio-datatable-${generatedId}`)
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(props.totalItems / props.perPage)))
|
||||
|
||||
const PAGE_JUMP_DEBOUNCE = 400
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const pageInputId = computed(() => `${componentId.value}-page-input`)
|
||||
const pageInput = ref(String(props.page))
|
||||
|
||||
watch(() => props.page, (p) => { pageInput.value = String(p) })
|
||||
|
||||
onBeforeUnmount(() => { if (debounceTimer) clearTimeout(debounceTimer) })
|
||||
|
||||
const perPageSelectOptions = computed(() =>
|
||||
props.perPageOptions.map(n => ({ label: String(n), value: n }))
|
||||
)
|
||||
@@ -184,42 +189,32 @@ function onPerPageChange(value: string | number | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
function changePage(page: number) {
|
||||
if (page >= 1 && page <= totalPages.value && page !== props.page) {
|
||||
emit('update:page', page)
|
||||
}
|
||||
}
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const total = totalPages.value
|
||||
const current = props.page
|
||||
|
||||
if (total <= 5) {
|
||||
return Array.from({ length: total }, (_, i) => i + 1)
|
||||
function onPageInput() {
|
||||
pageInput.value = pageInput.value.replace(/[^0-9]/g, '')
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
if (pageInput.value === '') return
|
||||
const n = Number(pageInput.value)
|
||||
if (n >= 1 && n <= totalPages.value) {
|
||||
debounceTimer = setTimeout(() => changePage(n), PAGE_JUMP_DEBOUNCE)
|
||||
}
|
||||
}
|
||||
|
||||
const pages: (number | '...')[] = []
|
||||
pages.push(1)
|
||||
|
||||
if (current > 3) {
|
||||
pages.push('...')
|
||||
function commitPageInput() {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
const raw = pageInput.value.trim()
|
||||
const n = Number(raw)
|
||||
if (raw === '' || n === 0 || Number.isNaN(n)) {
|
||||
pageInput.value = String(props.page)
|
||||
return
|
||||
}
|
||||
|
||||
const start = Math.max(2, current - 1)
|
||||
const end = Math.min(total - 1, current + 1)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
if (current < total - 2) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
if (total > 1) {
|
||||
pages.push(total)
|
||||
}
|
||||
|
||||
return pages
|
||||
})
|
||||
const clamped = Math.min(Math.max(1, Math.round(n)), totalPages.value)
|
||||
changePage(clamped)
|
||||
pageInput.value = String(clamped)
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user