Skip to content

Commit d5c5198

Browse files
authored
feat: add NePaginator (#19)
1 parent 9ab72a8 commit d5c5198

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-0
lines changed

src/components/NePaginator.vue

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<!--
2+
Copyright (C) 2023 Nethesis S.r.l.
3+
SPDX-License-Identifier: GPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { computed } from 'vue'
8+
import { range } from 'lodash-es'
9+
import { faChevronLeft as fasChevronLeft } from '@fortawesome/free-solid-svg-icons'
10+
import { faChevronRight as fasChevronRight } from '@fortawesome/free-solid-svg-icons'
11+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
12+
13+
export type NePaginatorProps = {
14+
currentPage: number
15+
totalPages: number
16+
previousLabel: string
17+
nextLabel: string
18+
}
19+
20+
const props = defineProps({
21+
currentPage: {
22+
type: Number,
23+
required: true
24+
},
25+
totalPages: {
26+
type: Number,
27+
required: true
28+
},
29+
previousLabel: {
30+
type: String,
31+
required: true
32+
},
33+
nextLabel: {
34+
type: String,
35+
required: true
36+
}
37+
})
38+
const emit = defineEmits<{
39+
selectPage: [page: number]
40+
}>()
41+
42+
const firstPages = computed(() => props.currentPage <= 4)
43+
const lastPages = computed(() => props.currentPage > props.totalPages - 4)
44+
45+
const cellClass =
46+
'flex h-10 items-center justify-center border border-gray-300 bg-white px-4 leading-tight text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-white'
47+
const cellHoverClasses = 'hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-700'
48+
49+
const currentPageCellClass =
50+
'z-10 flex items-center justify-center px-4 h-10 leading-tight text-primary-700 border border-primary-300 bg-primary-50 dark:border-gray-700 dark:bg-gray-700 dark:text-white'
51+
const currentPageHoverCellClasses = 'hover:bg-primary-100 hover:text-primary-800'
52+
53+
function getCellClass(page: number, disableHoverClasses?: boolean) {
54+
return props.currentPage === page
55+
? [currentPageCellClass, !disableHoverClasses ? currentPageHoverCellClasses : 'cursor-default']
56+
: [cellClass, !disableHoverClasses ? cellHoverClasses : 'cursor-default']
57+
}
58+
59+
function getAriaCurrent(page: number) {
60+
return props.currentPage === page ? 'page' : 'false'
61+
}
62+
63+
function navigateToPage(page: number) {
64+
emit('selectPage', page)
65+
}
66+
</script>
67+
68+
<template>
69+
<nav>
70+
<ul class="flex h-10 items-center -space-x-px text-base">
71+
<li>
72+
<button
73+
:disabled="currentPage === 1"
74+
class="ms-0 flex h-10 items-center justify-center rounded-s-lg border border-e-0 border-gray-300 bg-white px-4 leading-tight text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
75+
@click="navigateToPage(currentPage - 1)"
76+
>
77+
<span class="sr-only">{{ previousLabel }}</span>
78+
<font-awesome-icon :icon="fasChevronLeft" class="h-3 w-3 shrink-0" aria-hidden="true" />
79+
</button>
80+
</li>
81+
<!-- show all page numbers if there are no more than 8 pages in total -->
82+
<template v-if="totalPages <= 8">
83+
<li v-for="i in range(1, totalPages + 1)" :key="i">
84+
<button
85+
:aria-current="getAriaCurrent(i)"
86+
:class="getCellClass(i)"
87+
@click="navigateToPage(i)"
88+
>
89+
{{ i }}
90+
</button>
91+
</li>
92+
</template>
93+
<!-- show a collapsed view of the pages, with start, ending, previous and next page -->
94+
<template v-else>
95+
<li>
96+
<button
97+
:aria-current="getAriaCurrent(1)"
98+
:class="getCellClass(1)"
99+
@click="navigateToPage(1)"
100+
>
101+
1
102+
</button>
103+
</li>
104+
<li>
105+
<button
106+
:aria-current="getAriaCurrent(firstPages ? 2 : -1)"
107+
:class="getCellClass(firstPages ? 2 : -1, !firstPages)"
108+
@click="firstPages ? navigateToPage(2) : undefined"
109+
>
110+
{{ firstPages ? 2 : '...' }}
111+
</button>
112+
</li>
113+
<li v-for="i in range(-1, 2)" :key="i">
114+
<button
115+
:aria-current="
116+
getAriaCurrent(firstPages ? 4 + i : lastPages ? totalPages - 3 + i : currentPage + i)
117+
"
118+
:class="
119+
getCellClass(firstPages ? 4 + i : lastPages ? totalPages - 3 + i : currentPage + i)
120+
"
121+
@click="
122+
navigateToPage(firstPages ? 4 + i : lastPages ? totalPages - 3 + i : currentPage + i)
123+
"
124+
>
125+
{{ firstPages ? 4 + i : lastPages ? totalPages - 3 + i : currentPage + i }}
126+
</button>
127+
</li>
128+
<li>
129+
<button
130+
:aria-current="getAriaCurrent(lastPages ? totalPages - 1 : -1)"
131+
:class="getCellClass(lastPages ? totalPages - 1 : -1, !lastPages)"
132+
@click="lastPages ? navigateToPage(totalPages - 1) : undefined"
133+
>
134+
{{ lastPages ? totalPages - 1 : '...' }}
135+
</button>
136+
</li>
137+
<li>
138+
<button
139+
:aria-current="getAriaCurrent(totalPages)"
140+
:class="getCellClass(totalPages)"
141+
@click="navigateToPage(totalPages)"
142+
>
143+
{{ totalPages }}
144+
</button>
145+
</li>
146+
</template>
147+
<li>
148+
<button
149+
:disabled="currentPage === totalPages"
150+
class="flex h-10 items-center justify-center rounded-e-lg border border-gray-300 bg-white px-4 leading-tight text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
151+
@click="navigateToPage(currentPage + 1)"
152+
>
153+
<span class="sr-only">{{ nextLabel }}</span>
154+
<font-awesome-icon :icon="fasChevronRight" class="h-3 w-3 shrink-0" aria-hidden="true" />
155+
</button>
156+
</li>
157+
</ul>
158+
</nav>
159+
</template>

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export { default as NeCard } from '@/components/NeCard.vue'
2727
export { default as NeLink } from '@/components/NeLink.vue'
2828
export { default as NeFormItemLabel } from '@/components/NeFormItemLabel.vue'
2929
export { default as NeRadioSelection } from '@/components/NeRadioSelection.vue'
30+
export { default as NePaginator } from '@/components/NePaginator.vue'
3031

3132
// types
3233
export type { NeComboboxOption } from '@/components/NeCombobox.vue'
34+
export type { NePaginatorProps } from '@/components/NePaginator.vue'

stories/NePaginator.stories.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (C) 2024 Nethesis S.r.l.
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
import { Meta, StoryObj } from '@storybook/vue3'
5+
import { NePaginator } from '../src/main'
6+
7+
const meta: Meta<typeof NePaginator> = {
8+
title: 'Visual/NePaginator',
9+
component: NePaginator,
10+
tags: ['autodocs'],
11+
args: {}
12+
}
13+
14+
export default meta
15+
type Story = StoryObj<typeof meta>
16+
17+
const template = '<NePaginator v-bind="args" />'
18+
19+
export const Default: Story = {
20+
render: (args) => ({
21+
components: { NePaginator },
22+
setup() {
23+
return { args }
24+
},
25+
template: template
26+
}),
27+
args: {
28+
currentPage: 1,
29+
totalPages: 5,
30+
previousLabel: 'Previous',
31+
nextLabel: 'Next'
32+
}
33+
}
34+
35+
export const WithManyPages: Story = {
36+
render: (args) => ({
37+
components: { NePaginator },
38+
setup() {
39+
return { args }
40+
},
41+
template: template
42+
}),
43+
args: {
44+
currentPage: 5,
45+
totalPages: 12,
46+
previousLabel: 'Previous',
47+
nextLabel: 'Next'
48+
}
49+
}

0 commit comments

Comments
 (0)