Skip to content

Commit

Permalink
Add pagination component
Browse files Browse the repository at this point in the history
  • Loading branch information
skaptox committed Nov 17, 2021
1 parent 849ec7f commit e67c753
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 2 deletions.
3 changes: 2 additions & 1 deletion helper/classes.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,6 @@
},
"VUpload": {
"is-fullwidth": ["expanded"]
}
},
"VPagination": {}
}
17 changes: 16 additions & 1 deletion helper/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,6 @@
"VUpload": {
"always": [
"file",
"is-fullwidth",
"file-label",
"file-input",
"file-cta",
Expand All @@ -783,5 +782,21 @@

],
"unstable": ["has-name"]
},
"VPagination": {
"always": [
"pagination",
"pagination-previous",
"pagination-next",
"pagination-list",
"pagination-ellipsis",
"pagination-link",
"is-current"
],
"optional": [
"is-simple",
"is-rounded"
],
"unstable": ["has-name"]
}
}
234 changes: 234 additions & 0 deletions src/components/compounds/Pagination/Pagination.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<script>
import { watchEffect, computed, nextTick } from 'vue'
import PaginationItem from './PaginationItem.vue'
export default {
name: 'VPagination',
components: {
PaginationItem,
},
props: {
total: [Number, String],
perPage: {
type: [Number, String],
default: 20,
},
current: {
type: [Number, String],
default: 1,
},
rangeBefore: {
type: [Number, String],
default: 1,
},
rangeAfter: {
type: [Number, String],
default: 1,
},
size: String,
simple: Boolean,
rounded: Boolean,
order: String,
ariaNextLabel: String,
ariaPreviousLabel: String,
ariaPageLabel: String,
ariaCurrentLabel: String,
},
emits: ['update:current', 'change'],
setup(props, { emit, slots }) {
const beforeCurrent = computed(() => {
return Number.parseInt(props.rangeBefore)
})
const afterCurrent = computed(() => {
return Number.parseInt(props.rangeAfter)
})
const pageCount = computed(() => {
return Math.ceil(props.total / props.perPage)
})
const firstItem = computed(() => {
const _firstItem = props.current * props.perPage - props.perPage + 1
return _firstItem >= 0 ? _firstItem : 0
})
const hasPrev = computed(() => {
return props.current > 1
})
const hasFirst = computed(() => {
return props.current >= 2 + beforeCurrent.value
})
const hasFirstEllipsis = computed(() => {
return props.current >= beforeCurrent.value + 4
})
const hasLast = computed(() => {
return props.current <= pageCount.value - (1 + afterCurrent.value)
})
const hasLastEllipsis = computed(() => {
return props.current < pageCount.value - (2 + afterCurrent.value)
})
const hasNext = computed(() => {
return props.current < pageCount.value
})
const pagesInRange = computed(() => {
if (props.simple) return null
let left = Math.max(1, props.current - beforeCurrent.value)
if (left - 1 === 2) {
left--
}
let right = Math.min(props.current + afterCurrent.value, pageCount.value)
if (pageCount.value - right === 2) {
right++
}
const pages = []
for (let i = left; i <= right; i++) {
pages.push(getPage(i))
}
return pages
})
watchEffect(() => {
if (props.current > pageCount.value) last()
})
function changePage(num, e) {
if (props.current === num || num < 1 || num > pageCount.value) return
emit('update:current', num)
emit('change', num)
if (e && e.target) {
nextTick(() => e.target.focus())
}
}
function last(e) {
changePage(pageCount.value, e)
}
function getPage(num, options = {}) {
return {
number: num,
isCurrent: props.current === num,
click: e => changePage(num, e),
disabled: options.disabled || false,
class: options.class || '',
'aria-label': options['aria-label'] || getAriaPageLabel(num, props.current === num),
}
}
function getAriaPageLabel(pageNumber, isCurrent) {
if (props.ariaPageLabel && (!isCurrent || !props.ariaCurrentLabel)) {
return props.ariaPageLabel + ' ' + pageNumber + '.'
} else if (props.ariaPageLabel && isCurrent && props.ariaCurrentLabel) {
return props.ariaCurrentLabel + ', ' + props.ariaPageLabel + ' ' + pageNumber + '.'
}
return null
}
return {
pageCount,
firstItem,
hasPrev,
hasFirst,
hasFirstEllipsis,
hasLast,
hasLastEllipsis,
hasNext,
pagesInRange,
getPage,
slots,
}
},
}
</script>

<template>
<!-- eslint-disable @pathscale/vue3/v-directive -->
<nav
class="pagination"
:class="[
order,
size,
{
'is-simple': simple,
'is-rounded': rounded,
},
]">
<slot
v-if="slots.previous"
name="previous"
:page="
getPage(current - 1, {
disabled: !hasPrev,
class: 'pagination-previous',
'aria-label': ariaPreviousLabel,
})
">
&laquo;
</slot>
<pagination-item
v-else
class="pagination-previous"
:disabled="!hasPrev"
:page="getPage(current - 1)"
:aria-label="ariaPreviousLabel">
&laquo;
</pagination-item>
<slot
v-if="slots.next"
name="next"
:page="
getPage(current + 1, {
disabled: !hasNext,
class: 'pagination-next',
'aria-label': ariaNextLabel,
})
">
&raquo;
</slot>
<pagination-item
v-else
class="pagination-next"
:disabled="!hasNext"
:page="getPage(current + 1)"
:aria-label="ariaNextLabel">
&raquo;
</pagination-item>

<small class="info" v-if="simple">
<template v-if="perPage == 1"> {{ firstItem }} / {{ total }} </template>
<template v-else>
{{ firstItem }}-{{ Math.min(current * perPage, total) }} / {{ total }}
</template>
</small>
<ul class="pagination-list" v-else>
<li v-if="hasFirst">
<slot v-if="slots.default" :page="getPage(1)" />
<pagination-item v-else :page="getPage(1)" />
</li>
<li v-if="hasFirstEllipsis">
<span class="pagination-ellipsis">&hellip;</span>
</li>

<li v-for="page in pagesInRange" :key="page.number">
<slot v-if="slots.default" :page="page" />
<pagination-item v-else :page="page" />
</li>

<li v-if="hasLastEllipsis">
<span class="pagination-ellipsis">&hellip;</span>
</li>
<li v-if="hasLast">
<slot v-if="slots.default" :page="getPage(pageCount)" />
<pagination-item v-else :page="getPage(pageCount)" />
</li>
</ul>
</nav>
</template>
51 changes: 51 additions & 0 deletions src/components/compounds/Pagination/PaginationItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@


<script>
import { computed } from 'vue'
export default {
name: 'VPaginationItem',
props: {
page: {
type: Object,
required: true,
},
tag: {
type: String,
default: 'a',
},
disabled: Boolean,
},
setup(props) {
const href = computed(() => {
return props.tag === 'a' ? '#' : null
})
const computedDisabled = computed(() => {
return props.disabled || props.page.disabled ? true : null
})
return {
href,
computedDisabled,
}
},
}
</script>

<template>
<component
:is="tag"
role="button"
:href="href"
:disabled="computedDisabled"
class="pagination-link"
:class="{ 'is-current': page.isCurrent, [page.class]: true }"
v-bind="$attrs"
@click.prevent="page.click"
:aria-label="page['aria-label']"
:aria-current="page.isCurrent">
<slot>{{ page.number }}</slot>
</component>
</template>
4 changes: 4 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,8 @@ export { default as VTable } from './compounds/Table/Table.vue'

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

export { default as VPagination } from './compounds/Pagination/Pagination.vue'

export { default as VPaginationItem } from './compounds/Pagination/PaginationItem.vue'

export * from './global-settings'

0 comments on commit e67c753

Please sign in to comment.