276 lines
5.4 KiB
Vue
276 lines
5.4 KiB
Vue
<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) {
|
|
if (!routeParams.path && props.includeQuery) {
|
|
return `${pageContext.urlParsed.pathname}${page}${urlParsed.searchOriginal}`;
|
|
}
|
|
|
|
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 && urlParsed.searchOriginal) {
|
|
return `${path}${urlParsed.searchOriginal}`;
|
|
}
|
|
|
|
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;
|
|
font-size: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.pages {
|
|
display: flex;
|
|
}
|
|
|
|
.wrap {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.before {
|
|
flex-direction: row-reverse;
|
|
}
|
|
|
|
.index {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.page {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
display: inline-flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
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 {
|
|
color: var(--primary);
|
|
}
|
|
|
|
&.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;
|
|
}
|
|
</style>
|