vue-paginate
Version:
A simple vue.js plugin to paginate data
345 lines (325 loc) • 9.48 kB
JavaScript
import LimitedLinksGenerator from '../util/LimitedLinksGenerator'
import { LEFT_ARROW, RIGHT_ARROW, ELLIPSES } from '../config/linkTypes'
import { warn } from '../util/debug'
export default {
name: 'paginate-links',
props: {
for: {
type: String,
required: true
},
limit: {
type: Number,
default: 0
},
simple: {
type: Object,
default: null,
validator (obj) {
return obj.prev && obj.next
}
},
stepLinks: {
type: Object,
default: () => {
return {
prev: LEFT_ARROW,
next: RIGHT_ARROW
}
},
validator (obj) {
return obj.prev && obj.next
}
},
showStepLinks: {
type: Boolean
},
hideSinglePage: {
type: Boolean
},
classes: {
type: Object,
default: null
},
async: {
type: Boolean,
default: false
},
container: {
type: Object,
default: null
}
},
data () {
return {
listOfPages: [],
numberOfPages: 0,
target: null
}
},
computed: {
parent () {
return this.container ? this.container.el : this.$parent
},
state () {
return this.container ? this.container.state : this.$parent.paginate[this.for]
},
currentPage: {
get () {
if (this.state) {
return this.state.page
}
},
set (page) {
this.state.page = page
}
}
},
mounted () {
if (this.simple && this.limit) {
warn(`<paginate-links for="${this.for}"> 'simple' and 'limit' props can't be used at the same time. In this case, 'simple' will take precedence, and 'limit' will be ignored.`, this.parent, 'warn')
}
if (this.simple && !this.simple.next) {
warn(`<paginate-links for="${this.for}"> 'simple' prop doesn't contain 'next' value.`, this.parent)
}
if (this.simple && !this.simple.prev) {
warn(`<paginate-links for="${this.for}"> 'simple' prop doesn't contain 'prev' value.`, this.parent)
}
if (this.stepLinks && !this.stepLinks.next) {
warn(`<paginate-links for="${this.for}"> 'step-links' prop doesn't contain 'next' value.`, this.parent)
}
if (this.stepLinks && !this.stepLinks.prev) {
warn(`<paginate-links for="${this.for}"> 'step-links' prop doesn't contain 'prev' value.`, this.parent)
}
this.$nextTick(() => {
this.updateListOfPages()
})
},
watch: {
'state': {
handler () {
this.updateListOfPages()
},
deep: true
},
currentPage (toPage, fromPage) {
this.$emit('change', toPage + 1, fromPage + 1)
}
},
methods: {
updateListOfPages () {
this.target = getTargetPaginateComponent(this.parent.$children, this.for)
if (!this.target) {
if (this.async) return
warn(`<paginate-links for="${this.for}"> can't be used without its companion <paginate name="${this.for}">`, this.parent)
warn(`To fix that issue you may need to use :async="true" on <paginate-links> component to allow for asyncronous rendering`, this.parent, 'warn')
return
}
this.numberOfPages = Math.ceil(this.target.list.length / this.target.per)
this.listOfPages = getListOfPageNumbers(this.numberOfPages)
}
},
render (h) {
if (!this.target && this.async) return null
let links = this.simple
? getSimpleLinks(this, h)
: this.limit > 1
? getLimitedLinks(this, h)
: getFullLinks(this, h)
if (this.hideSinglePage && this.numberOfPages <= 1) {
return null
}
const el = h('ul', {
class: ['paginate-links', this.for]
}, links)
if (this.classes) {
this.$nextTick(() => {
addAdditionalClasses(el.elm, this.classes)
})
}
return el
}
}
function getFullLinks (vm, h) {
const allLinks = vm.showStepLinks
? [vm.stepLinks.prev, ...vm.listOfPages, vm.stepLinks.next]
: vm.listOfPages
return allLinks.map(link => {
const data = {
on: {
click: (e) => {
e.preventDefault()
vm.currentPage = getTargetPageForLink(
link,
vm.limit,
vm.currentPage,
vm.listOfPages,
vm.stepLinks
)
}
}
}
const liClasses = getClassesForLink(
link,
vm.currentPage,
vm.listOfPages.length - 1,
vm.stepLinks
)
const linkText = link === vm.stepLinks.next || link === vm.stepLinks.prev
? link
: link + 1 // it means it's a number
return h('li', { class: liClasses }, [h('a', data, linkText)])
})
}
function getLimitedLinks (vm, h) {
let limitedLinks = new LimitedLinksGenerator(
vm.listOfPages,
vm.currentPage,
vm.limit,
vm.stepLinks
).generate()
limitedLinks = vm.showStepLinks
? [vm.stepLinks.prev, ...limitedLinks, vm.stepLinks.next]
: limitedLinks
const limitedLinksMetadata = getLimitedLinksMetadata(limitedLinks)
return limitedLinks.map((link, index) => {
const data = {
on: {
click: (e) => {
e.preventDefault()
vm.currentPage = getTargetPageForLink(
link,
vm.limit,
vm.currentPage,
vm.listOfPages,
vm.stepLinks,
limitedLinksMetadata[index]
)
}
}
}
const liClasses = getClassesForLink(
link,
vm.currentPage,
vm.listOfPages.length - 1,
vm.stepLinks
)
// If the link is a number,
// then incremented by 1 (since it's 0 based).
// otherwise, do nothing (so, it's a symbol).
const text = (link === parseInt(link, 10)) ? link + 1 : link
return h('li', { class: liClasses }, [h('a', data, text)])
})
}
function getSimpleLinks (vm, h) {
const lastPage = vm.listOfPages.length - 1
const prevData = {
on: {
click: (e) => {
e.preventDefault()
if (vm.currentPage > 0) vm.currentPage -= 1
}
}
}
const nextData = {
on: {
click: (e) => {
e.preventDefault()
if (vm.currentPage < lastPage) vm.currentPage += 1
}
}
}
const nextListData = { class: ['next', vm.currentPage >= lastPage ? 'disabled' : ''] }
const prevListData = { class: ['prev', vm.currentPage <= 0 ? 'disabled' : ''] }
const prevLink = h('li', prevListData, [h('a', prevData, vm.simple.prev)])
const nextLink = h('li', nextListData, [h('a', nextData, vm.simple.next)])
return [prevLink, nextLink]
}
function getTargetPaginateComponent (children, targetName) {
return children
.filter(child => (child.$vnode.componentOptions.tag === 'paginate'))
.find(child => child.name === targetName)
}
function getListOfPageNumbers (numberOfPages) {
// converts number of pages into an array
// that contains each individual page number
// For Example: 4 => [0, 1, 2, 3]
return Array.apply(null, { length: numberOfPages })
.map((val, index) => index)
}
function getClassesForLink(link, currentPage, lastPage, { prev, next }) {
let liClass = []
if (link === prev) {
liClass.push('left-arrow')
} else if (link === next) {
liClass.push('right-arrow')
} else if (link === ELLIPSES) {
liClass.push('ellipses')
} else {
liClass.push('number')
}
if (link === currentPage) {
liClass.push('active')
}
if (link === prev && currentPage <= 0) {
liClass.push('disabled')
} else if (link === next && currentPage >= lastPage) {
liClass.push('disabled')
}
return liClass
}
function getTargetPageForLink (link, limit, currentPage, listOfPages, { prev, next }, metaData = null) {
let currentChunk = Math.floor(currentPage / limit)
if (link === prev) {
return (currentPage - 1) < 0 ? 0 : currentPage - 1
} else if (link === next) {
return (currentPage + 1 > listOfPages.length - 1)
? listOfPages.length - 1
: currentPage + 1
} else if (metaData && metaData === 'right-ellipses') {
return (currentChunk + 1) * limit
} else if (metaData && metaData === 'left-ellipses') {
const chunkContent = listOfPages.slice(currentChunk * limit, currentChunk * limit + limit)
const isLastPage = currentPage === listOfPages.length - 1
if (isLastPage && chunkContent.length === 1) {
currentChunk--
}
return (currentChunk - 1) * limit + limit - 1
}
// which is number
return link
}
/**
* Mainly used here to check whether the displayed
* ellipses is for showing previous or next links
*/
function getLimitedLinksMetadata (limitedLinks) {
return limitedLinks.map((link, index) => {
if (link === ELLIPSES && limitedLinks[index - 1] === 0) {
return 'left-ellipses'
} else if (link === ELLIPSES && limitedLinks[index - 1] !== 0) {
return 'right-ellipses'
}
return link
})
}
function addAdditionalClasses (linksContainer, classes) {
Object.keys(classes).forEach(selector => {
if (selector === 'ul') {
const selectorValue = classes['ul']
if (Array.isArray(selectorValue)) {
selectorValue.forEach(c => linksContainer.classList.add(c))
} else {
linksContainer.classList.add(selectorValue)
}
}
linksContainer.querySelectorAll(selector).forEach(node => {
const selectorValue = classes[selector]
if (Array.isArray(selectorValue)) {
selectorValue.forEach(c => node.classList.add(c))
} else {
node.classList.add(selectorValue)
}
})
})
}