Skip to content

Commit e67c753

Browse files
committed
Add pagination component
1 parent 849ec7f commit e67c753

File tree

5 files changed

+307
-2
lines changed

5 files changed

+307
-2
lines changed

helper/classes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,6 @@
133133
},
134134
"VUpload": {
135135
"is-fullwidth": ["expanded"]
136-
}
136+
},
137+
"VPagination": {}
137138
}

helper/mappings.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,6 @@
768768
"VUpload": {
769769
"always": [
770770
"file",
771-
"is-fullwidth",
772771
"file-label",
773772
"file-input",
774773
"file-cta",
@@ -783,5 +782,21 @@
783782

784783
],
785784
"unstable": ["has-name"]
785+
},
786+
"VPagination": {
787+
"always": [
788+
"pagination",
789+
"pagination-previous",
790+
"pagination-next",
791+
"pagination-list",
792+
"pagination-ellipsis",
793+
"pagination-link",
794+
"is-current"
795+
],
796+
"optional": [
797+
"is-simple",
798+
"is-rounded"
799+
],
800+
"unstable": ["has-name"]
786801
}
787802
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
<script>
2+
import { watchEffect, computed, nextTick } from 'vue'
3+
import PaginationItem from './PaginationItem.vue'
4+
5+
export default {
6+
name: 'VPagination',
7+
components: {
8+
PaginationItem,
9+
},
10+
props: {
11+
total: [Number, String],
12+
perPage: {
13+
type: [Number, String],
14+
default: 20,
15+
},
16+
current: {
17+
type: [Number, String],
18+
default: 1,
19+
},
20+
rangeBefore: {
21+
type: [Number, String],
22+
default: 1,
23+
},
24+
rangeAfter: {
25+
type: [Number, String],
26+
default: 1,
27+
},
28+
size: String,
29+
simple: Boolean,
30+
rounded: Boolean,
31+
order: String,
32+
ariaNextLabel: String,
33+
ariaPreviousLabel: String,
34+
ariaPageLabel: String,
35+
ariaCurrentLabel: String,
36+
},
37+
emits: ['update:current', 'change'],
38+
39+
setup(props, { emit, slots }) {
40+
const beforeCurrent = computed(() => {
41+
return Number.parseInt(props.rangeBefore)
42+
})
43+
const afterCurrent = computed(() => {
44+
return Number.parseInt(props.rangeAfter)
45+
})
46+
47+
const pageCount = computed(() => {
48+
return Math.ceil(props.total / props.perPage)
49+
})
50+
51+
const firstItem = computed(() => {
52+
const _firstItem = props.current * props.perPage - props.perPage + 1
53+
return _firstItem >= 0 ? _firstItem : 0
54+
})
55+
56+
const hasPrev = computed(() => {
57+
return props.current > 1
58+
})
59+
60+
const hasFirst = computed(() => {
61+
return props.current >= 2 + beforeCurrent.value
62+
})
63+
64+
const hasFirstEllipsis = computed(() => {
65+
return props.current >= beforeCurrent.value + 4
66+
})
67+
68+
const hasLast = computed(() => {
69+
return props.current <= pageCount.value - (1 + afterCurrent.value)
70+
})
71+
72+
const hasLastEllipsis = computed(() => {
73+
return props.current < pageCount.value - (2 + afterCurrent.value)
74+
})
75+
76+
const hasNext = computed(() => {
77+
return props.current < pageCount.value
78+
})
79+
80+
const pagesInRange = computed(() => {
81+
if (props.simple) return null
82+
let left = Math.max(1, props.current - beforeCurrent.value)
83+
if (left - 1 === 2) {
84+
left--
85+
}
86+
87+
let right = Math.min(props.current + afterCurrent.value, pageCount.value)
88+
if (pageCount.value - right === 2) {
89+
right++
90+
}
91+
const pages = []
92+
93+
for (let i = left; i <= right; i++) {
94+
pages.push(getPage(i))
95+
}
96+
97+
return pages
98+
})
99+
100+
watchEffect(() => {
101+
if (props.current > pageCount.value) last()
102+
})
103+
104+
function changePage(num, e) {
105+
if (props.current === num || num < 1 || num > pageCount.value) return
106+
emit('update:current', num)
107+
emit('change', num)
108+
if (e && e.target) {
109+
nextTick(() => e.target.focus())
110+
}
111+
}
112+
113+
function last(e) {
114+
changePage(pageCount.value, e)
115+
}
116+
function getPage(num, options = {}) {
117+
return {
118+
number: num,
119+
isCurrent: props.current === num,
120+
click: e => changePage(num, e),
121+
disabled: options.disabled || false,
122+
class: options.class || '',
123+
'aria-label': options['aria-label'] || getAriaPageLabel(num, props.current === num),
124+
}
125+
}
126+
127+
function getAriaPageLabel(pageNumber, isCurrent) {
128+
if (props.ariaPageLabel && (!isCurrent || !props.ariaCurrentLabel)) {
129+
return props.ariaPageLabel + ' ' + pageNumber + '.'
130+
} else if (props.ariaPageLabel && isCurrent && props.ariaCurrentLabel) {
131+
return props.ariaCurrentLabel + ', ' + props.ariaPageLabel + ' ' + pageNumber + '.'
132+
}
133+
return null
134+
}
135+
return {
136+
pageCount,
137+
firstItem,
138+
hasPrev,
139+
hasFirst,
140+
hasFirstEllipsis,
141+
hasLast,
142+
hasLastEllipsis,
143+
hasNext,
144+
pagesInRange,
145+
getPage,
146+
slots,
147+
}
148+
},
149+
}
150+
</script>
151+
152+
<template>
153+
<!-- eslint-disable @pathscale/vue3/v-directive -->
154+
<nav
155+
class="pagination"
156+
:class="[
157+
order,
158+
size,
159+
{
160+
'is-simple': simple,
161+
'is-rounded': rounded,
162+
},
163+
]">
164+
<slot
165+
v-if="slots.previous"
166+
name="previous"
167+
:page="
168+
getPage(current - 1, {
169+
disabled: !hasPrev,
170+
class: 'pagination-previous',
171+
'aria-label': ariaPreviousLabel,
172+
})
173+
">
174+
&laquo;
175+
</slot>
176+
<pagination-item
177+
v-else
178+
class="pagination-previous"
179+
:disabled="!hasPrev"
180+
:page="getPage(current - 1)"
181+
:aria-label="ariaPreviousLabel">
182+
&laquo;
183+
</pagination-item>
184+
<slot
185+
v-if="slots.next"
186+
name="next"
187+
:page="
188+
getPage(current + 1, {
189+
disabled: !hasNext,
190+
class: 'pagination-next',
191+
'aria-label': ariaNextLabel,
192+
})
193+
">
194+
&raquo;
195+
</slot>
196+
<pagination-item
197+
v-else
198+
class="pagination-next"
199+
:disabled="!hasNext"
200+
:page="getPage(current + 1)"
201+
:aria-label="ariaNextLabel">
202+
&raquo;
203+
</pagination-item>
204+
205+
<small class="info" v-if="simple">
206+
<template v-if="perPage == 1"> {{ firstItem }} / {{ total }} </template>
207+
<template v-else>
208+
{{ firstItem }}-{{ Math.min(current * perPage, total) }} / {{ total }}
209+
</template>
210+
</small>
211+
<ul class="pagination-list" v-else>
212+
<li v-if="hasFirst">
213+
<slot v-if="slots.default" :page="getPage(1)" />
214+
<pagination-item v-else :page="getPage(1)" />
215+
</li>
216+
<li v-if="hasFirstEllipsis">
217+
<span class="pagination-ellipsis">&hellip;</span>
218+
</li>
219+
220+
<li v-for="page in pagesInRange" :key="page.number">
221+
<slot v-if="slots.default" :page="page" />
222+
<pagination-item v-else :page="page" />
223+
</li>
224+
225+
<li v-if="hasLastEllipsis">
226+
<span class="pagination-ellipsis">&hellip;</span>
227+
</li>
228+
<li v-if="hasLast">
229+
<slot v-if="slots.default" :page="getPage(pageCount)" />
230+
<pagination-item v-else :page="getPage(pageCount)" />
231+
</li>
232+
</ul>
233+
</nav>
234+
</template>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
2+
3+
<script>
4+
import { computed } from 'vue'
5+
6+
export default {
7+
name: 'VPaginationItem',
8+
props: {
9+
page: {
10+
type: Object,
11+
required: true,
12+
},
13+
tag: {
14+
type: String,
15+
default: 'a',
16+
},
17+
disabled: Boolean,
18+
},
19+
20+
setup(props) {
21+
const href = computed(() => {
22+
return props.tag === 'a' ? '#' : null
23+
})
24+
25+
const computedDisabled = computed(() => {
26+
return props.disabled || props.page.disabled ? true : null
27+
})
28+
29+
return {
30+
href,
31+
computedDisabled,
32+
}
33+
},
34+
}
35+
</script>
36+
37+
<template>
38+
<component
39+
:is="tag"
40+
role="button"
41+
:href="href"
42+
:disabled="computedDisabled"
43+
class="pagination-link"
44+
:class="{ 'is-current': page.isCurrent, [page.class]: true }"
45+
v-bind="$attrs"
46+
@click.prevent="page.click"
47+
:aria-label="page['aria-label']"
48+
:aria-current="page.isCurrent">
49+
<slot>{{ page.number }}</slot>
50+
</component>
51+
</template>

src/components/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,8 @@ export { default as VTable } from './compounds/Table/Table.vue'
9696

9797
export { default as DataGrid } from './compounds/Table/DataGrid.ts'
9898

99+
export { default as VPagination } from './compounds/Pagination/Pagination.vue'
100+
101+
export { default as VPaginationItem } from './compounds/Pagination/PaginationItem.vue'
102+
99103
export * from './global-settings'

0 commit comments

Comments
 (0)