feat(datatable) : pagination compacte avec saut de page (Page [n] / N)

This commit is contained in:
2026-06-09 15:06:10 +02:00
parent b2c6f33e38
commit f797c1c8a0
+50 -55
View File
@@ -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>