bootstrap-vue
Version:
With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extens
286 lines (275 loc) • 9.21 kB
JavaScript
import Vue from '../../vue'
import { NAME_PAGINATION_NAV } from '../../constants/components'
import looseEqual from '../../utils/loose-equal'
import { BvEvent } from '../../utils/bv-event.class'
import { getComponentConfig } from '../../utils/config'
import { attemptBlur, requestAF } from '../../utils/dom'
import { isBrowser } from '../../utils/env'
import { isArray, isUndefined, isFunction, isObject } from '../../utils/inspect'
import { mathMax } from '../../utils/math'
import { toInteger } from '../../utils/number'
import { omit } from '../../utils/object'
import { pluckProps } from '../../utils/props'
import { computeHref, parseQuery } from '../../utils/router'
import { toString } from '../../utils/string'
import { warn } from '../../utils/warn'
import paginationMixin from '../../mixins/pagination'
import { props as BLinkProps } from '../link/link'
// --- Props ---
const linkProps = omit(BLinkProps, ['event', 'routerTag'])
const props = {
size: {
type: String,
default: () => getComponentConfig(NAME_PAGINATION_NAV, 'size')
},
numberOfPages: {
type: [Number, String],
default: 1,
validator(value) /* istanbul ignore next */ {
const number = toInteger(value, 0)
if (number < 1) {
warn('Prop "number-of-pages" must be a number greater than "0"', NAME_PAGINATION_NAV)
return false
}
return true
}
},
baseUrl: {
type: String,
default: '/'
},
useRouter: {
type: Boolean,
default: false
},
linkGen: {
type: Function
// default: null
},
pageGen: {
type: Function
// default: null
},
pages: {
// Optional array of page links
type: Array
// default: null
},
noPageDetect: {
// Disable auto page number detection if true
type: Boolean,
default: false
},
...linkProps
}
// --- Utility methods ---
// Sanitize the provided number of pages (converting to a number)
export const sanitizeNumberOfPages = value => mathMax(toInteger(value, 0), 1)
// --- Main component ---
// The render function is brought in via the pagination mixin
// @vue/component
export const BPaginationNav = /*#__PURE__*/ Vue.extend({
name: NAME_PAGINATION_NAV,
mixins: [paginationMixin],
props,
computed: {
// Used by render function to trigger wrapping in '<nav>' element
isNav() {
return true
},
computedValue() {
// Returns the value prop as a number or `null` if undefined or < 1
const value = toInteger(this.value, 0)
return value < 1 ? null : value
}
},
watch: {
numberOfPages() {
this.$nextTick(() => {
this.setNumberOfPages()
})
},
pages() {
this.$nextTick(() => {
this.setNumberOfPages()
})
}
},
created() {
this.setNumberOfPages()
},
mounted() {
if (this.$router) {
// We only add the watcher if vue router is detected
this.$watch('$route', () => {
this.$nextTick(() => {
requestAF(() => {
this.guessCurrentPage()
})
})
})
}
},
methods: {
setNumberOfPages() {
if (isArray(this.pages) && this.pages.length > 0) {
this.localNumberOfPages = this.pages.length
} else {
this.localNumberOfPages = sanitizeNumberOfPages(this.numberOfPages)
}
this.$nextTick(() => {
this.guessCurrentPage()
})
},
onClick(evt, pageNumber) {
// Dont do anything if clicking the current active page
if (pageNumber === this.currentPage) {
return
}
const target = evt.currentTarget || evt.target
// Emit a user-cancelable `page-click` event
const clickEvt = new BvEvent('page-click', {
cancelable: true,
vueTarget: this,
target
})
this.$emit(clickEvt.type, clickEvt, pageNumber)
if (clickEvt.defaultPrevented) {
return
}
// Update the `v-model`
// Done in in requestAF() to allow browser to complete the
// native browser click handling of a link
requestAF(() => {
this.currentPage = pageNumber
this.$emit('change', pageNumber)
})
// Emulate native link click page reloading behaviour by blurring the
// paginator and returning focus to the document
// Done in a `nextTick()` to ensure rendering complete
this.$nextTick(() => {
attemptBlur(target)
})
},
getPageInfo(pageNum) {
if (!isArray(this.pages) || this.pages.length === 0 || isUndefined(this.pages[pageNum - 1])) {
const link = `${this.baseUrl}${pageNum}`
return {
link: this.useRouter ? { path: link } : link,
text: toString(pageNum)
}
}
const info = this.pages[pageNum - 1]
if (isObject(info)) {
const link = info.link
return {
// Normalize link for router use
link: isObject(link) ? link : this.useRouter ? { path: link } : link,
// Make sure text has a value
text: toString(info.text || pageNum)
}
} else {
return { link: toString(info), text: toString(pageNum) }
}
},
makePage(pageNum) {
const info = this.getPageInfo(pageNum)
if (this.pageGen && isFunction(this.pageGen)) {
return this.pageGen(pageNum, info)
}
return info.text
},
makeLink(pageNum) {
const info = this.getPageInfo(pageNum)
if (this.linkGen && isFunction(this.linkGen)) {
return this.linkGen(pageNum, info)
}
return info.link
},
linkProps(pageNum) {
const props = pluckProps(linkProps, this)
const link = this.makeLink(pageNum)
if (this.useRouter || isObject(link)) {
props.to = link
} else {
props.href = link
}
return props
},
resolveLink(to = '') {
// Given a to (or href string), convert to normalized route-like structure
// Works only client side!!
let link
try {
// Convert the `to` to a HREF via a temporary `a` tag
link = document.createElement('a')
link.href = computeHref({ to }, '/', '/')
// We need to add the anchor to the document to make sure the
// `pathname` is correctly detected in any browser (i.e. IE)
document.body.appendChild(link)
// Once href is assigned, the link will be normalized to the full URL bits
const { pathname, hash, search } = link
// Remove link from document
document.body.removeChild(link)
// Return the location in a route-like object
return { path: pathname, hash, query: parseQuery(search) }
} catch (e) {
/* istanbul ignore next */
try {
link && link.parentNode && link.parentNode.removeChild(link)
} catch {}
/* istanbul ignore next */
return {}
}
},
resolveRoute(to = '') {
// Given a to (or href string), convert to normalized route location structure
// works only when router available!!
try {
const route = this.$router.resolve(to, this.$route).route
return { path: route.path, hash: route.hash, query: route.query }
} catch (e) {
/* istanbul ignore next */
return {}
}
},
guessCurrentPage() {
let guess = this.computedValue
const $router = this.$router
const $route = this.$route
// This section only occurs if we are client side, or server-side with $router
/* istanbul ignore else */
if (!this.noPageDetect && !guess && (isBrowser || (!isBrowser && $router))) {
// Current route (if router available)
const currRoute =
$router && $route ? { path: $route.path, hash: $route.hash, query: $route.query } : {}
// Current page full HREF (if client side). Can't be done as a computed prop!
const loc = isBrowser ? window.location || document.location : null
const currLink = loc
? { path: loc.pathname, hash: loc.hash, query: parseQuery(loc.search) }
: /* istanbul ignore next */ {}
// Loop through the possible pages looking for a match until found
for (let page = 1; !guess && page <= this.localNumberOfPages; page++) {
const to = this.makeLink(page)
if ($router && (isObject(to) || this.useRouter)) {
// Resolve the page via the $router
guess = looseEqual(this.resolveRoute(to), currRoute) ? page : null
} else if (isBrowser) {
// If no $router available (or !this.useRouter when `to` is a string)
// we compare using parsed URIs
guess = looseEqual(this.resolveLink(to), currLink) ? page : null
} else {
// probably SSR, but no $router so we can't guess, so lets break out of
// the loop early
/* istanbul ignore next */
guess = -1
}
}
}
// We set currentPage to 0 to trigger an $emit('input', null)
// As the default for this.currentPage is -1 when no value is specified
// And valid page numbers are greater than 0
this.currentPage = guess > 0 ? guess : 0
}
}
})