Dev branch
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<script setup>
|
||||
import { Link } from "@inertiajs/vue3";
|
||||
import { computed } from "vue";
|
||||
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: () => [] },
|
||||
@@ -11,190 +13,361 @@ const props = defineProps({
|
||||
|
||||
const num = props.links?.length || 0;
|
||||
|
||||
const prevLink = computed(() => (num > 0 ? props.links[0] : null));
|
||||
const nextLink = computed(() => (num > 1 ? props.links[num - 1] : null));
|
||||
const numericLinks = computed(() => {
|
||||
if (num < 3) return [];
|
||||
return props.links
|
||||
.slice(1, num - 1)
|
||||
.map((l) => ({
|
||||
...l,
|
||||
page: Number.parseInt(String(l.label).replace(/[^0-9]/g, ""), 10),
|
||||
}))
|
||||
.filter((l) => !Number.isNaN(l.page));
|
||||
});
|
||||
const currentPage = computed(() => numericLinks.value.find((l) => l.active)?.page || 1);
|
||||
const lastPage = computed(() =>
|
||||
numericLinks.value.length ? Math.max(...numericLinks.value.map((l) => l.page)) : 1
|
||||
);
|
||||
const linkByPage = computed(() => {
|
||||
const m = new Map();
|
||||
for (const l of numericLinks.value) m.set(l.page, l);
|
||||
return m;
|
||||
});
|
||||
const windowItems = computed(() => {
|
||||
const items = [];
|
||||
const cur = currentPage.value;
|
||||
const last = lastPage.value;
|
||||
const show = new Set([1, last, cur - 1, cur, cur + 1]);
|
||||
if (cur <= 3) {
|
||||
show.add(2);
|
||||
show.add(3);
|
||||
const prevLink = computed(() => {
|
||||
if (num > 0 && props.links && Array.isArray(props.links) && props.links[0]) {
|
||||
return props.links[0];
|
||||
}
|
||||
if (cur >= last - 2) {
|
||||
show.add(last - 1);
|
||||
show.add(last - 2);
|
||||
return null;
|
||||
});
|
||||
const nextLink = computed(() => {
|
||||
if (num > 1 && props.links && Array.isArray(props.links) && props.links[num - 1]) {
|
||||
return props.links[num - 1];
|
||||
}
|
||||
|
||||
// Prev
|
||||
items.push({ kind: "prev", link: prevLink.value });
|
||||
|
||||
// Pages with ellipses
|
||||
let inGap = false;
|
||||
for (let p = 1; p <= last; p++) {
|
||||
if (show.has(p)) {
|
||||
items.push({
|
||||
kind: "page",
|
||||
link: linkByPage.value.get(p) || {
|
||||
url: null,
|
||||
label: String(p),
|
||||
active: p === cur,
|
||||
},
|
||||
});
|
||||
inGap = false;
|
||||
} else if (!inGap) {
|
||||
items.push({ kind: "ellipsis" });
|
||||
inGap = true;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Next
|
||||
items.push({ kind: "next", link: nextLink.value });
|
||||
|
||||
return items;
|
||||
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>
|
||||
<div class="flex items-center justify-between bg-white px-4 py-3 sm:px-6">
|
||||
<!-- Mobile: Prev / Next -->
|
||||
<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">
|
||||
<component
|
||||
:is="links?.[0]?.url ? Link : 'span'"
|
||||
:href="links?.[0]?.url"
|
||||
:aria-disabled="!links?.[0]?.url"
|
||||
:tabindex="!links?.[0]?.url ? -1 : 0"
|
||||
class="relative inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium"
|
||||
:class="
|
||||
links?.[0]?.url
|
||||
? 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
|
||||
: 'border-gray-200 bg-gray-100 text-gray-400'
|
||||
"
|
||||
<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
|
||||
</component>
|
||||
<component
|
||||
:is="links?.[num - 1]?.url ? Link : 'span'"
|
||||
:href="links?.[num - 1]?.url"
|
||||
:aria-disabled="!links?.[num - 1]?.url"
|
||||
:tabindex="!links?.[num - 1]?.url ? -1 : 0"
|
||||
class="relative ml-3 inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium"
|
||||
:class="
|
||||
links?.[num - 1]?.url
|
||||
? 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
|
||||
: 'border-gray-200 bg-gray-100 text-gray-400'
|
||||
"
|
||||
</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
|
||||
</component>
|
||||
</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">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700">
|
||||
Showing
|
||||
<span class="font-medium">{{ from }}</span>
|
||||
to
|
||||
<span class="font-medium">{{ to }}</span>
|
||||
of
|
||||
<span class="font-medium">{{ total }}</span>
|
||||
results
|
||||
</p>
|
||||
<!-- 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>
|
||||
<nav
|
||||
class="isolate inline-flex -space-x-px rounded-md shadow-sm"
|
||||
aria-label="Pagination"
|
||||
<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"
|
||||
>
|
||||
<template v-for="(item, idx) in windowItems" :key="idx">
|
||||
<!-- Prev / Next -->
|
||||
<component
|
||||
v-if="item.kind === 'prev' || item.kind === 'next'"
|
||||
:is="item.link?.url ? Link : 'span'"
|
||||
:href="item.link?.url"
|
||||
class="relative inline-flex items-center px-2 py-2 ring-1 ring-inset ring-gray-300 focus:z-20 focus:outline-offset-0"
|
||||
:class="{
|
||||
'rounded-l-md': item.kind === 'prev',
|
||||
'rounded-r-md': item.kind === 'next',
|
||||
'text-gray-900 hover:bg-gray-50': item.link?.url,
|
||||
'text-gray-400 bg-gray-100': !item.link?.url,
|
||||
}"
|
||||
>
|
||||
<span class="sr-only">{{
|
||||
item.kind === "prev" ? "Prejšnja" : "Naslednja"
|
||||
}}</span>
|
||||
<svg
|
||||
v-if="item.kind === 'prev'"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</component>
|
||||
««
|
||||
</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>
|
||||
|
||||
<!-- Ellipsis -->
|
||||
<span
|
||||
v-else-if="item.kind === 'ellipsis'"
|
||||
class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-500 ring-1 ring-inset ring-gray-200 select-none"
|
||||
>…</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>
|
||||
|
||||
<!-- Page number -->
|
||||
<component
|
||||
v-else-if="item.kind === 'page'"
|
||||
:is="item.link?.url ? Link : 'span'"
|
||||
:href="item.link?.url"
|
||||
class="relative inline-flex items-center px-4 py-2 text-sm font-semibold focus:outline-offset-0"
|
||||
:class="{
|
||||
'text-gray-700 ring-1 ring-inset ring-gray-300': !item.link?.url,
|
||||
'text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20':
|
||||
item.link?.url && !item.link?.active,
|
||||
'z-10 bg-blue-600 text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600':
|
||||
item.link?.active,
|
||||
}"
|
||||
>
|
||||
{{ item.link?.label || "" }}
|
||||
</component>
|
||||
</template>
|
||||
</nav>
|
||||
<!-- 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>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user