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