<template> <div class="pagination-container"> <div v-if="currentPage === pageTotal && total > env.maxMatches" class="more" >Can't find what you're looking for? Narrow down the results using a filter.</div> <nav class="pagination"> <ul class="pages nolist"> <li> <Link :href="getPath(1)" :class="{ disabled: !hasPrevPage }" class="page first nolink" @click="(event) => go(1, event)" ><Icon icon="first2" /></Link> </li> <li> <Link :href="hasPrevPage ? getPath(currentPage - 1) : null" :class="{ disabled: !hasPrevPage }" class="page prev nolink" @click="(event) => hasPrevPage && go(currentPage - 1, event)" ><Icon icon="arrow-left" /></Link> </li> </ul> <div class="index"> <ul class="pages before wrap nolist"> <li v-for="prevPage in prevPages" :key="`page-${prevPage}`" > <Link :href="getPath(prevPage)" :class="{ active: prevPage === currentPage }" class="page nolink" @click="(event) => go(prevPage, event)" >{{ prevPage }}</Link> </li> </ul> <ul class="pages nolist"> <li> <div class="page active">{{ currentPage }}</div> </li> </ul> <ul class="pages after wrap nolist"> <li v-for="nextPage in nextPages" :key="`page-${nextPage}`" > <Link :href="getPath(nextPage)" :class="{ active: nextPage === currentPage }" class="page nolink" @click="(event) => go(nextPage, event)" >{{ nextPage }}</Link> </li> </ul> </div> <ul class="pages nolist"> <li> <Link :href="hasNextPage ? getPath(currentPage + 1) : null" :class="{ disabled: !hasNextPage }" class="page next nolink" @click="(event) => hasNextPage && go(currentPage + 1, event)" ><Icon icon="arrow-right" /></Link> </li> <li> <Link :href="getPath(pageTotal)" :class="{ disabled: !hasNextPage }" class="page last nolink" @click="(event) => go(pageTotal, event)" ><Icon icon="last2" /></Link> </li> </ul> </nav> </div> </template> <script setup> import { computed, inject } from 'vue'; import { parse } from 'path-to-regexp'; const props = defineProps({ page: { type: Number, default: null, }, total: { type: Number, default: null, }, redirect: { type: Boolean, default: true, }, query: { type: String, default: null, }, includeQuery: { type: Boolean, default: true, }, useMaxMatches: { type: Boolean, default: true, }, }); const emit = defineEmits(['navigation']); const pageContext = inject('pageContext'); const { routeParams, urlParsed, pageProps, env, } = pageContext; const currentPage = computed(() => props.page || Number(routeParams?.page)); const limit = computed(() => props.limit || Number(pageProps.limit) || 30); const total = computed(() => props.total || Number(pageProps.total)); const pageTotal = computed(() => Math.ceil((props.useMaxMatches ? Math.min(total.value, env.maxMatches) : total.value) / limit.value)); const hasNextPage = computed(() => currentPage.value + 1 <= pageTotal.value); const hasPrevPage = computed(() => currentPage.value - 1 >= 1); const prevPages = computed(() => Array.from({ length: 4 }, (value, index) => { const page = currentPage.value - index - 1; if (page < 1) { return null; } return page; }).filter(Boolean)); const nextPages = computed(() => Array.from({ length: 4 }, (value, index) => { const page = currentPage.value + index + 1; if (page > pageTotal.value) { return null; } return page; }).filter(Boolean)); function go(page, event) { if (!props.redirect) { event.preventDefault(); history.pushState({}, '', event.target.href); // eslint-disable-line no-restricted-globals } emit('navigation', { href: event.target.href, page, }); } function getPath(page) { const query = typeof window === 'undefined' ? urlParsed.searchOriginal : window.location.search; if (!routeParams.path && props.includeQuery && query) { return `${pageContext.urlParsed.pathname}${page}${query}`; } if (!routeParams.path) { return `${pageContext.urlParsed.pathname}${page}`; } const path = parse(routeParams.path) .map((segment) => { if (segment.name === 'page') { return `/${page}`; } return `${segment.prefix || ''}${routeParams[segment.name] || segment}`; }) .join(''); if (props.includeQuery && query) { return `${path}${query}`; } return path; } </script> <style scoped> .pagination-container { display: flex; flex-direction: column; justify-content: center; } .pagination { height: 5rem; display: flex; justify-content: center; flex-shrink: 0; box-sizing: border-box; padding: 1rem; margin-bottom: 1rem; font-size: 0; overflow: hidden; } .pages { display: flex; gap: 0 .25rem; } .wrap { flex-wrap: wrap; } .before { flex-direction: row-reverse; margin-right: .25rem; } .after { margin-left: .25rem; } .index { display: flex; justify-content: center; } .page { width: 3rem; height: 3rem; display: inline-flex; justify-content: center; align-items: center; border-radius: .5rem; margin-bottom: 1rem; background: var(--background); box-shadow: 0 0 3px var(--shadow-weak-30); color: var(--shadow); font-weight: bold; font-size: 1rem; .icon { width: .9rem; height: .9rem; fill: var(--shadow); } &.active { background: var(--primary); color: var(--text-light); } &.disabled .icon { fill: var(--shadow-weak-20); } } .prev { margin-right: .5rem; } .next { margin-left: .5rem; } .more { padding: 2rem; text-align: center; color: var(--shadow-strong-10); font-size: 1.1rem; } @media (--small-30) { .pagination { height: 4.5rem; } .page { width: 2.25rem; height: 2.5rem; } } </style>