Files
sori.studio/components/site/SitePagination.vue

146 lines
5.1 KiB
Vue

<script setup>
const props = defineProps({
page: {
type: Number,
default: 1
},
totalPages: {
type: Number,
default: 1
},
label: {
type: String,
default: '페이지'
},
basePath: {
type: String,
required: true
}
})
const normalizedTotalPages = computed(() => Math.max(1, Number(props.totalPages) || 1))
const currentPage = computed(() => Math.max(1, Math.min(Number(props.page) || 1, normalizedTotalPages.value)))
const previousPage = computed(() => Math.max(1, currentPage.value - 1))
const nextPage = computed(() => Math.min(normalizedTotalPages.value, currentPage.value + 1))
const canGoPrevious = computed(() => currentPage.value > 1)
const canGoNext = computed(() => currentPage.value < normalizedTotalPages.value)
/**
* 연속 페이지 번호를 만든다.
* @param {number} start - 시작 페이지
* @param {number} end - 끝 페이지
* @returns {Array<number>} 페이지 번호 목록
*/
const range = (start, end) => {
const pages = []
for (let page = start; page <= end; page += 1) {
pages.push(page)
}
return pages
}
const pageItems = computed(() => {
const total = normalizedTotalPages.value
const current = currentPage.value
if (total <= 8) {
return range(1, total).map((page) => ({ type: 'page', page }))
}
const pages = new Set([1, total])
if (current <= 5) {
range(1, Math.min(total - 1, Math.max(4, current + 3))).forEach((page) => pages.add(page))
} else if (current >= total - 4) {
range(Math.max(2, total - 7), total).forEach((page) => pages.add(page))
} else {
range(Math.max(2, current - 3), Math.min(total - 1, current + 3)).forEach((page) => pages.add(page))
}
const sortedPages = [...pages].sort((left, right) => left - right)
const items = []
sortedPages.forEach((page, index) => {
const previous = sortedPages[index - 1]
if (previous && page - previous > 1) {
items.push({
type: 'ellipsis',
key: `ellipsis-${previous}-${page}`
})
}
items.push({
type: 'page',
page
})
})
return items
})
/**
* 페이지 링크 대상을 만든다.
* @param {number} page - 페이지 번호
* @returns {Object} Nuxt 링크 대상
*/
const getPageLinkTo = (page) => ({
path: props.basePath,
query: page > 1 ? { page } : {}
})
</script>
<template>
<nav class="site-pagination site-section" :aria-label="label">
<div class="site-pagination__body site-section-body mx-auto flex max-w-[720px] items-center justify-start gap-3 overflow-x-auto text-sm sm:justify-center sm:gap-4">
<NuxtLink
class="site-pagination__arrow inline-flex size-10 items-center justify-center rounded-full border border-[var(--site-line)] text-[var(--site-muted)] transition hover:border-[var(--site-soft)] hover:text-[var(--site-text)]"
:class="!canGoPrevious ? 'pointer-events-none opacity-35' : ''"
:to="getPageLinkTo(previousPage)"
:aria-disabled="!canGoPrevious"
:aria-label="`${label} 이전으로 이동`"
>
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m15 18-6-6 6-6" />
</svg>
</NuxtLink>
<ol class="site-pagination__pages flex min-w-max items-center justify-center gap-1 sm:gap-2">
<li v-for="item in pageItems" :key="item.type === 'page' ? `page-${item.page}` : item.key">
<span
v-if="item.type === 'ellipsis'"
class="site-pagination__ellipsis inline-flex min-w-9 items-center justify-center px-1 text-base font-semibold text-[var(--site-muted)]"
aria-hidden="true"
>
...
</span>
<NuxtLink
v-else
class="site-pagination__page inline-flex min-w-9 items-center justify-center rounded-full px-2 py-2 text-base font-semibold text-[var(--site-muted)] transition hover:text-[var(--site-text)]"
:class="item.page === currentPage ? 'site-pagination__page--current pointer-events-none !bg-[var(--site-accent)] !text-white shadow-sm' : ''"
:to="getPageLinkTo(item.page)"
:aria-current="item.page === currentPage ? 'page' : undefined"
:aria-label="`${item.page}페이지로 이동`"
>
{{ item.page }}
</NuxtLink>
</li>
</ol>
<NuxtLink
class="site-pagination__arrow inline-flex size-10 items-center justify-center rounded-full border border-[var(--site-line)] text-[var(--site-muted)] transition hover:border-[var(--site-soft)] hover:text-[var(--site-text)]"
:class="!canGoNext ? 'pointer-events-none opacity-35' : ''"
:to="getPageLinkTo(nextPage)"
:aria-disabled="!canGoNext"
:aria-label="`${label} 다음으로 이동`"
>
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m9 18 6-6-6-6" />
</svg>
</NuxtLink>
</div>
</nav>
</template>