Teren-app/resources/js/Components/Pagination.vue
Simon Pocrnjič 63e0958b66 Dev branch
2025-11-02 12:31:01 +01:00

374 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { Link, router } from "@inertiajs/vue3";
import { computed, ref } from "vue";
import { Input } from '@/Components/ui/input';
import { Button } from '@/Components/ui/button';
const props = defineProps({
links: { type: Array, default: () => [] },
from: { type: Number, default: 0 },
to: { type: Number, default: 0 },
total: { type: Number, default: 0 },
});
const num = props.links?.length || 0;
const prevLink = computed(() => {
if (num > 0 && props.links && Array.isArray(props.links) && props.links[0]) {
return props.links[0];
}
return null;
});
const nextLink = computed(() => {
if (num > 1 && props.links && Array.isArray(props.links) && props.links[num - 1]) {
return props.links[num - 1];
}
return null;
});
const firstLink = computed(() => {
if (num < 3 || !props.links || !Array.isArray(props.links)) return null;
// Find the first numeric link (page 1)
for (let i = 1; i < num - 1; i++) {
const link = props.links[i];
if (!link) continue;
const page = Number.parseInt(String(link?.label || "").replace(/[^0-9]/g, ""), 10);
if (page === 1) return link;
}
return null;
});
const lastLink = computed(() => {
if (num < 3 || !props.links || !Array.isArray(props.links)) return null;
// Find the last numeric link
let maxPage = 0;
let maxLink = null;
for (let i = 1; i < num - 1; i++) {
const link = props.links[i];
if (!link) continue;
const page = Number.parseInt(String(link?.label || "").replace(/[^0-9]/g, ""), 10);
if (!Number.isNaN(page) && page > maxPage) {
maxPage = page;
maxLink = link;
}
}
return maxLink;
});
const numericLinks = computed(() => {
if (num < 3 || !props.links || !Array.isArray(props.links)) return [];
return props.links
.slice(1, num - 1)
.filter((l) => l != null)
.map((l) => ({
...l,
page: Number.parseInt(String(l?.label || "").replace(/[^0-9]/g, ""), 10),
}))
.filter((l) => !Number.isNaN(l.page) && l.page != null);
});
const currentPage = computed(() => {
const active = numericLinks.value.find((l) => l?.active);
return active?.page || 1;
});
const lastPage = computed(() => {
if (!numericLinks.value.length) return 1;
const pages = numericLinks.value.map((l) => l?.page).filter(p => p != null);
return pages.length ? Math.max(...pages) : 1;
});
const linkByPage = computed(() => {
const m = new Map();
for (const l of numericLinks.value) {
if (l?.page != null) {
m.set(l.page, l);
}
}
return m;
});
// Generate visible page numbers with ellipsis (similar to DataTableClient)
const visiblePages = computed(() => {
const pages = [];
const total = lastPage.value;
const current = currentPage.value;
const maxVisible = 5; // Match DataTableClient default
if (total <= maxVisible) {
for (let i = 1; i <= total; i++) {
pages.push(i);
}
return pages;
}
// Calculate window around current page
const half = Math.floor(maxVisible / 2);
let start = Math.max(1, current - half);
let end = Math.min(total, start + maxVisible - 1);
start = Math.max(1, Math.min(start, end - maxVisible + 1));
// Add pages in window
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
});
const gotoInput = ref("");
// Handle scroll on navigation
function handleLinkClick(event) {
// Prevent default scroll behavior
event.preventDefault();
const href = event.currentTarget.getAttribute('href');
if (href) {
router.visit(href, {
preserveScroll: false,
onSuccess: () => {
// Scroll to top of table after navigation completes
setTimeout(() => {
const tableElement = document.querySelector('[data-table-container]');
if (tableElement) {
tableElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}, 100);
},
});
}
}
function goToPage() {
const raw = String(gotoInput.value || "").trim();
const n = Number(raw);
if (!Number.isFinite(n) || n < 1 || n > lastPage.value) {
gotoInput.value = "";
return;
}
const targetLink = linkByPage.value.get(Math.floor(n));
if (targetLink?.url) {
router.visit(targetLink.url, {
preserveScroll: false,
onSuccess: () => {
// Scroll to top of table when page changes
const tableElement = document.querySelector('[data-table-container]');
if (tableElement) {
tableElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
},
});
} else {
// If link not found, try to construct URL manually
gotoInput.value = "";
}
}
function handleKeyPress(event) {
if (event.key === "Enter") {
goToPage();
}
}
</script>
<template>
<nav
class="flex flex-wrap items-center justify-between gap-3 border-t border-gray-200 bg-white px-4 py-3 text-sm text-gray-700 sm:px-6"
aria-label="Pagination"
>
<!-- Mobile: Simple prev/next -->
<div class="flex flex-1 justify-between sm:hidden">
<Link
v-if="prevLink?.url"
:href="prevLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
>
Prejšnja
</Link>
<span
v-else
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-400 cursor-not-allowed opacity-50"
>
Prejšnja
</span>
<Link
v-if="nextLink?.url"
:href="nextLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
>
Naslednja
</Link>
<span
v-else
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-400 cursor-not-allowed opacity-50"
>
Naslednja
</span>
</div>
<!-- Desktop: Full pagination -->
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<!-- Page stats -->
<div v-if="total > 0">
<span class="text-sm text-gray-700">
Prikazano: <span class="font-medium">{{ from || 0 }}</span><span class="font-medium">{{ to || 0 }}</span> od
<span class="font-medium">{{ total || 0 }}</span>
</span>
</div>
<div v-else>
<span class="text-sm text-gray-700">Ni zadetkov</span>
</div>
<!-- Pagination controls -->
<div class="flex items-center gap-1">
<!-- First -->
<Link
v-if="firstLink?.url && currentPage > 1"
:href="firstLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-2 py-1 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
aria-label="Prva stran"
>
««
</Link>
<span
v-else
class="px-2 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
aria-label="Prva stran"
>
««
</span>
<!-- Prev -->
<Link
v-if="prevLink?.url"
:href="prevLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-2 py-1 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
aria-label="Prejšnja stran"
>
«
</Link>
<span
v-else
class="px-2 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
aria-label="Prejšnja stran"
>
«
</span>
<!-- Leading ellipsis / first page when window doesn't include 1 -->
<Link
v-if="visiblePages[0] > 1"
:href="firstLink?.url || '#'"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-900 hover:bg-gray-50 transition-colors"
>
1
</Link>
<span v-if="visiblePages[0] > 2" class="px-1 text-gray-700">…</span>
<!-- Page numbers -->
<template v-for="p in visiblePages" :key="p">
<Link
v-if="linkByPage.get(p)?.url"
:href="linkByPage.get(p).url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-3 py-1 rounded border transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
:class="
p === currentPage
? 'border-primary-600 bg-primary-600 text-white'
: 'border-gray-300 bg-white text-gray-900 hover:bg-gray-50'
"
:aria-current="p === currentPage ? 'page' : undefined"
>
{{ p }}
</Link>
<span
v-else
class="px-3 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
>
{{ p }}
</span>
</template>
<!-- Trailing ellipsis / last page when window doesn't include last -->
<span v-if="visiblePages[visiblePages.length - 1] < lastPage - 1" class="px-1 text-gray-700">…</span>
<Link
v-if="visiblePages[visiblePages.length - 1] < lastPage && lastLink?.url"
:href="lastLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-900 hover:bg-gray-50 transition-colors"
>
{{ lastPage }}
</Link>
<!-- Next -->
<Link
v-if="nextLink?.url"
:href="nextLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-2 py-1 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
aria-label="Naslednja stran"
>
»
</Link>
<span
v-else
class="px-2 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
aria-label="Naslednja stran"
>
»
</span>
<!-- Last -->
<Link
v-if="lastLink?.url && currentPage < lastPage"
:href="lastLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-2 py-1 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
aria-label="Zadnja stran"
>
»»
</Link>
<span
v-else
class="px-2 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
aria-label="Zadnja stran"
>
»»
</span>
<!-- Goto page input -->
<div class="ms-2 flex items-center gap-1">
<Input
v-model="gotoInput"
type="number"
min="1"
:max="lastPage"
inputmode="numeric"
class="w-16 text-sm"
:placeholder="String(currentPage)"
aria-label="Pojdi na stran"
@keyup.enter="goToPage"
@blur="goToPage"
/>
<span class="text-sm text-gray-500">/ {{ lastPage }}</span>
</div>
</div>
</div>
</nav>
</template>