<script setup>
import { onMounted, onUnmounted, ref, useTemplateRef } from "vue";
const props = defineProps({
basePath: {
type: String,
required: false,
},
currentPage: {
type: Number,
default: 1,
},
firstUrl: {
type: String,
required: false,
},
lastUrl: {
type: String,
required: false,
},
nextUrl: {
type: String,
required: false,
},
pages: {
type: Array,
required: true,
},
pageTrigger: {
type: String,
default: "p",
},
prevUrl: {
type: String,
required: false,
},
totalPages: {
type: Number,
required: true,
},
});
const jump = ref(null);
const paginationNav = useTemplateRef("pagination-nav");
const emit = defineEmits(["goToPage"]);
const resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
const pagNav = entry.target;
const pag = pagNav.querySelector(".pagination");
pagNav.classList.add("paginationNav--expanded");
const bBox = pag.getBoundingClientRect();
if (entry.contentRect.width <= bBox.width) {
pagNav.classList.remove("paginationNav--expanded");
}
});
onMounted(() => {
resizeObserver.observe(paginationNav.value);
});
onUnmounted(() => {
resizeObserver.unobserve(paginationNav.value);
});
function getPageUrl(page) {
return `/${props.basePath}/${props.pageTrigger}${page}${window.location.search}`;
}
function handleFormSubmit() {
if (props.basePath) {
_jumpToPage(jump.value);
} else {
emit("goToPage", jump.value);
jump.value = null;
}
}
function throttle(f, delay) {
let timer = 0;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => f.apply(this, args), delay);
};
}
function _jumpToPage(page) {
window.location.href = getPageUrl(page);
}
</script>
<template>
<nav
aria-label="Pagination"
class="u-blockCenter paginationNav"
ref="pagination-nav"
>
<div class="pagination">
<component
v-if="currentPage > 1"
:is="prevUrl ? 'a' : 'button'"
:href="prevUrl ?? null"
v-on:click="$emit('goToPage', currentPage - 1, 'prevPage')"
class="pagination__link pagination__link--prev"
:title="`Go to page ${currentPage - 1}`"
>
<span class="prependedIcon"><i class="fas fa-angle-left"></i></span>Prev
</component>
<div class="pagination__pages">
<template v-if="currentPage > 2 && pages.at(0) > 1">
<component
:is="firstUrl ? 'a' : 'button'"
:href="firstUrl ?? null"
v-on:click="$emit('goToPage', 1, 'first')"
class="pagination__link"
><span class="u-srOnly">Go to page </span>1</component
>
<span class="pagination__sep pagination__sep--first">...</span>
</template>
<template v-for="page in pages">
<span
v-if="page === currentPage"
class="pagination__link pagination__link--active"
><span class="u-srOnly">Current page </span>{{ page }}</span
>
<component
v-else
:is="basePath ? 'a' : 'button'"
:href="basePath ? getPageUrl(page) : null"
v-on:click="$emit('goToPage', page, 'specific')"
class="pagination__link"
><span class="u-srOnly">Go to page </span>{{ page }}</component
>
</template>
<template v-if="pages.at(-1) < totalPages - 1">
<span class="pagination__sep">...</span>
<component
:is="lastUrl ? 'a' : 'button'"
:href="lastUrl ?? null"
v-on:click="$emit('goToPage', totalPages, 'last')"
class="pagination__link"
><span class="u-srOnly">Go to page </span
>{{ totalPages }}</component
>
</template>
</div>
<component
v-if="currentPage < totalPages"
:is="nextUrl ? 'a' : 'button'"
:href="nextUrl ?? null"
v-on:click="$emit('goToPage', currentPage + 1, 'next')"
class="pagination__link pagination__link--next"
:title="`Go to page ${currentPage + 1}`"
>
Next<span class="appendedIcon"
>‍<i class="fas fa-angle-right"></i
></span>
</component>
</div>
<form
v-on:submit.prevent="handleFormSubmit"
class="paginationJump"
aria-label="Jump to page"
>
<label for="jump-to-page">Go to page:</label>
<input
v-model="jump"
type="number"
class="input input--text paginationJump__input"
id="jump-to-page"
size="2"
min="1"
:max="totalPages"
/>
<input
type="submit"
value="Go"
class="button button--primary paginationJump__button"
/>
</form>
</nav>
</template>
<style scoped lang="scss">
@use "../_layout/mixins";
$border: 2px solid var(--neutral--darker);
$gap: 0.5em;
.paginationNav {
container-type: inline-size;
container-name: pagination;
}
.pagination {
display: inline-grid;
grid-template-areas:
"prev next"
"pages pages";
justify-content: center;
align-items: center;
row-gap: 0.5em;
grid-auto-flow: column;
}
.pagination__pages {
grid-area: pages;
display: flex;
flex-wrap: wrap;
align-items: baseline;
}
.pagination__sep--first:has(+ span) {
margin-inline-end: $gap;
}
.pagination__link {
@include mixins.blockLink($underline: false);
padding: 0.2em $gap;
color: var(--color);
cursor: pointer;
}
.pagination__link--active {
color: var(--white);
background-color: var(--blue);
}
.pagination__link--prev {
justify-self: flex-start;
grid-area: prev;
}
.pagination__page {
justify-self: center;
}
.pagination__link--next {
justify-self: flex-end;
grid-area: next;
}
.paginationNav--expanded {
.pagination {
grid-template-areas: "prev pages next";
}
.pagination__link--prev {
justify-self: flex-start;
border-inline-end: $border;
}
.pagination__link--next {
justify-self: flex-end;
border-inline-start: $border;
}
}
.paginationJump {
margin-block-start: 1em;
display: flex;
flex-wrap: wrap;
gap: $gap;
justify-content: center;
align-items: center;
}
.paginationJump__input {
width: 4em;
}
.paginationJump__button {
--button-padding: 0 0.5em;
align-self: stretch;
--button-size: var(--step-0);
}
</style>