374 lines
12 KiB
Vue
374 lines
12 KiB
Vue
<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>
|