funkwhale/front/src/components/ui/Pagination.vue

120 lines
3.9 KiB
Vue

<script setup lang="ts">
import { useElementSize } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
import { ref, computed } from 'vue'
import { isMobileView } from '~/composables/screen'
import { preventNonNumeric } from '~/utils/event-validators'
import Button from './Button.vue'
import Input from './Input.vue'
const { t } = useI18n()
type NonnegativeInteger<T extends number> =
`${T}` extends `-${any}` | `${any}.${any}` ? never : T
const { pages } = defineProps<{ pages: NonnegativeInteger<number> }>()
const page = defineModel<number>('page', {
required: true,
validator: (value: number) => value > 0
})
const goTo = ref<number>()
const range = (start: number, end: number) => Array.from({ length: end - start + 1 }, (_, i) => i + start)
const renderPages = computed(() => {
const start = range(2, 5)
const end = range(pages - 4, pages - 1)
const pagesArray = [1]
if (page.value < 5) pagesArray.push(...start)
if (page.value >= 5 && page.value <= pages - 4) {
pagesArray.push(page.value - 1)
pagesArray.push(page.value)
pagesArray.push(page.value + 1)
}
if (page.value > pages - 4) pagesArray.push(...end)
pagesArray.push(pages)
return pagesArray.filter((page, index, pages) => pages.indexOf(page) === index)
})
const pagination = ref()
const { width } = useElementSize(pagination)
const isSmall = isMobileView(width)
const setPage = () => {
if (goTo.value == null) return
page.value = Math.min(pages, Math.max(1, goTo.value))
}
</script>
<template>
<nav ref="pagination" :aria-label="t('vui.aria.pagination.nav')" :class="{ 'is-small': isSmall }"
class="funkwhale pagination" role="navigation">
<ul class="pages">
<li>
<Button low-height min-content @click="page -= 1" :disabled="page <= 1" :aria-label="t('vui.aria.pagination.gotoPrevious')"
outline icon="bi-chevron-left">
<span v-if="!isSmall">{{ t('vui.pagination.previous') }}</span>
</Button>
</li>
<template v-if="!isSmall" v-for="(i, index) in renderPages" :key="i">
<li>
<Button square-small v-if="i <= pages && i > 0 && pages > 3" @click="page = i"
:aria-label="page !== i ? t('vui.aria.pagination.gotoPage', i) : t('vui.aria.pagination.currentPage', page)"
:outline="page !== i">
{{ i }}
</Button>
</li>
<li v-if="i + 1 < renderPages[index + 1]">…</li>
</template>
<template v-else>
<li>
<Button square-small @click="page = 1"
:aria-label="page !== 1 ? t('vui.aria.pagination.gotoPage', page) : t('vui.aria.pagination.currentPage', page)"
:outline="page !== 1">
1
</Button>
</li>
<li v-if="page === 1 || page === pages">…</li>
<li v-else>
<Button square-small :aria-label="t('vui.aria.pagination.currentPage', page)" aria-current="true">
{{ page }}
</Button>
</li>
<li>
<Button square-small @click="page = pages"
:aria-label="page !== pages ? t('vui.aria.pagination.gotoPage', page) : t('vui.aria.pagination.currentPage', page)"
:outline="page !== pages">
{{ pages }}
</Button>
</li>
</template>
<li>
<Button low-height min-content @click="page += 1" :disabled="page >= pages" :aria-label="t('vui.aria.pagination.gotoNext')"
outline icon="right bi-chevron-right">
<span v-if="!isSmall">{{ t('vui.pagination.next') }}</span>
</Button>
</li>
</ul>
<div class="goto">
{{ t('vui.go-to') }}
<Input :placeholder="page.toString()" @keyup.enter="setPage" @keydown="preventNonNumeric"
inputmode="numeric" pattern="[0-9]* // TODO: Move number input functionality into `Input` component"
v-model="goTo" />
</div>
</nav>
</template>
<style lang="scss">
@import './pagination.scss'
</style>