@gitlab/ui
Version:
GitLab UI Components
418 lines (405 loc) • 13.8 kB
JavaScript
import debounce from 'lodash/debounce';
import isFunction from 'lodash/isFunction';
import range from 'lodash/range';
import { breakpoints, GlBreakpointInstance } from '../../../utils/breakpoints';
import { alignOptions, resizeDebounceTime } from '../../../utils/constants';
import { translate, sprintf } from '../../../utils/i18n';
import GlIcon from '../icon/icon';
import GlLink from '../link/link';
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
//
const pageRange = (from, to) => range(from, to + 1, 1);
var script = {
name: 'Pagination',
components: {
GlLink,
GlIcon
},
model: {
prop: 'value',
event: 'input'
},
props: {
value: {
type: Number,
required: false,
default: 1,
validator: x => x > 0
},
/**
* Number of items per page
*/
perPage: {
type: Number,
required: false,
default: 20,
validator: x => x > 0
},
/**
* Total number of items
*/
totalItems: {
type: Number,
required: false,
default: 0
},
/**
* The object must contain the xs, sm, md and default keys
*/
limits: {
type: Object,
required: false,
default: () => ({
xs: 0,
sm: 3,
md: 9,
default: 9
}),
validator: value => {
const missingSizes = Object.keys(breakpoints).filter(size => !value[size]).length;
return missingSizes === 0 ? true : value.default;
}
},
/**
* A function that receives the page number and that returns a string representing the page URL
*/
linkGen: {
type: Function,
required: false,
default: null
},
/**
* When using the compact pagination, use this prop to pass the previous page number
*/
prevPage: {
type: Number,
required: false,
default: null
},
/**
* Text for the previous button (overridden by "previous" slot)
*/
prevText: {
type: String,
required: false,
default: translate('GlPagination.prevText', 'Previous')
},
/**
* When using the compact pagination, use this prop to pass the next page number
*/
nextPage: {
type: Number,
required: false,
default: null
},
/**
* Text for the next button (overridden by "next" slot)
*/
nextText: {
type: String,
required: false,
default: translate('GlPagination.nextText', 'Next')
},
/**
* Text for the ellipsis (overridden by "ellipsis-left" and "ellipsis-right" slots)
*/
ellipsisText: {
type: String,
required: false,
default: '…'
},
/**
* aria-label for the nav
*/
labelNav: {
type: String,
required: false,
default: translate('GlPagination.nav', 'Pagination')
},
/**
* aria-label for the first page item
*/
labelFirstPage: {
type: String,
required: false,
default: translate('GlPagination.labelFirstPage', 'Go to first page')
},
/**
* aria-label for the previous page item
*/
labelPrevPage: {
type: String,
required: false,
default: translate('GlPagination.labelPrevPage', 'Go to previous page')
},
/**
* aria-label for the next page item
*/
labelNextPage: {
type: String,
required: false,
default: translate('GlPagination.labelNextPage', 'Go to next page')
},
/**
* aria-label for the last page item
*/
labelLastPage: {
type: String,
required: false,
default: translate('GlPagination.labelLastPage', 'Go to last page')
},
/**
* aria-label getter for numbered page items, defaults to "Go to page <page_number>"
*/
labelPage: {
// note: `Function` support is for legacy reasons
type: [Function, String],
required: false,
default: translate('GlPagination.labelPage', 'Go to page %{page}')
},
/**
* Controls the component\'s horizontal alignment, value should be one of "left", "center", "right" or "fill"
*/
align: {
type: String,
required: false,
default: alignOptions.left,
validator: value => Object.keys(alignOptions).includes(value)
},
disabled: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
breakpoint: GlBreakpointInstance.getBreakpointSize(),
// If total pages count is below or equal to minTotalPagesToCollapse, collapsing is disabled
minTotalPagesToCollapse: 4
};
},
computed: {
isVisible() {
return this.totalPages > 1 || this.isCompactPagination;
},
isLinkBased() {
return isFunction(this.linkGen);
},
paginationLimit() {
return typeof this.limits[this.breakpoint] !== 'undefined' ? this.limits[this.breakpoint] : this.limits.default;
},
maxAdjacentPages() {
return Math.max(Math.ceil((this.paginationLimit - 1) / 2), 0);
},
totalPages() {
return Math.ceil(this.totalItems / this.perPage);
},
isFillAlign() {
return this.align === alignOptions.fill;
},
wrapperClasses() {
const classes = [];
if (this.align === alignOptions.center) {
classes.push('gl-justify-center');
}
if (this.align === alignOptions.right) {
classes.push('gl-justify-end');
}
if (this.isFillAlign) {
classes.push('gl-text-center');
}
return classes;
},
shouldCollapseLeftSide() {
const diff = this.value - this.maxAdjacentPages;
// Magic 3: prevents collapsing a single page on the left side
return diff >= this.maxAdjacentPages && diff > 3 && this.totalPages > this.minTotalPagesToCollapse;
},
shouldCollapseRightSide() {
// Magic 2: prevents collapsing a single page on the right side
const diff = this.totalPages - 2 - this.value;
return diff > this.maxAdjacentPages && this.totalPages > this.minTotalPagesToCollapse;
},
visibleItems() {
let items = [];
if (!this.isCompactPagination) {
let firstPage = this.shouldCollapseLeftSide ? this.value - this.maxAdjacentPages : 1;
// If we're on last page, show at least one page to the left
firstPage = Math.min(firstPage, this.totalPages - 1);
let lastPage = this.shouldCollapseRightSide ? this.value + this.maxAdjacentPages : this.totalPages;
// If we're on first page, show at least one page to the right
lastPage = Math.max(lastPage, 2);
// Default numbered items
items = pageRange(firstPage, lastPage).map(page => this.getPageItem(page));
if (this.shouldCollapseLeftSide) {
items.splice(0, 0, this.getPageItem(1, this.labelFirstPage), this.getEllipsisItem('left'));
}
if (this.shouldCollapseRightSide) {
items.push(this.getEllipsisItem('right'), this.getPageItem(this.totalPages, this.labelLastPage));
}
}
return items;
},
isCompactPagination() {
return Boolean(!this.totalItems && (this.prevPage || this.nextPage));
},
prevPageIsDisabled() {
return this.pageIsDisabled(this.value - 1);
},
nextPageIsDisabled() {
return this.pageIsDisabled(this.value + 1);
},
prevPageAriaLabel() {
return this.prevPageIsDisabled ? false : this.labelPrevPage || this.labelForPage(this.value - 1);
},
nextPageAriaLabel() {
return this.nextPageIsDisabled ? false : this.labelNextPage || this.labelForPage(this.value + 1);
},
prevPageHref() {
if (this.prevPageIsDisabled) return false;
if (this.isLinkBased) return this.linkGen(this.value - 1);
return '#';
},
nextPageHref() {
if (this.nextPageIsDisabled) return false;
if (this.isLinkBased) return this.linkGen(this.value + 1);
return '#';
}
},
created() {
window.addEventListener('resize', debounce(this.setBreakpoint, resizeDebounceTime));
},
beforeDestroy() {
window.removeEventListener('resize', debounce(this.setBreakpoint, resizeDebounceTime));
},
methods: {
labelForPage(page) {
if (isFunction(this.labelPage)) {
return this.labelPage(page);
}
return sprintf(this.labelPage, {
page
});
},
setBreakpoint() {
this.breakpoint = GlBreakpointInstance.getBreakpointSize();
},
pageIsDisabled(page) {
return this.disabled || page < 1 || this.isCompactPagination && page > this.value && !this.nextPage || !this.isCompactPagination && page > this.totalPages;
},
getPageItem(page) {
let label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
const commonAttrs = {
'aria-label': label || this.labelForPage(page),
href: '#',
class: []
};
const isActivePage = page === this.value;
const isDisabled = this.pageIsDisabled(page);
const attrs = {
...commonAttrs
};
const listeners = {};
if (isActivePage) {
attrs.class.push('active');
attrs['aria-current'] = 'page';
}
// Disable previous and/or next buttons if needed
if (this.isLinkBased) {
attrs.href = this.linkGen(page);
}
listeners.click = e => this.handleClick(e, page);
return {
content: page,
component: isDisabled ? 'span' : GlLink,
disabled: isDisabled,
key: `page_${page}`,
slot: 'page-number',
slotData: {
page,
active: isActivePage,
disabled: isDisabled
},
attrs,
listeners
};
},
getEllipsisItem(side) {
return {
content: this.ellipsisText,
key: `ellipsis_${side}`,
slot: `ellipsis-${side}`,
component: 'span',
disabled: true,
slotData: {},
listeners: {}
};
},
handleClick(event, value) {
if (!this.isLinkBased) {
event.preventDefault();
/**
* Emitted when the page changes
* @event input
* @arg {number} value The page that just got loaded
*/
this.$emit('input', value);
}
},
handlePrevious(event, value) {
this.handleClick(event, value);
/**
* Emitted when the "previous" button is clicked
* @event previous
*/
this.$emit('previous');
},
handleNext(event, value) {
this.handleClick(event, value);
/**
* Emitted when the "next" button is clicked
* @event next
*/
this.$emit('next');
}
}
};
/* script */
const __vue_script__ = script;
/* template */
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.isVisible)?_c('nav',{staticClass:"gl-pagination",attrs:{"aria-label":_vm.labelNav}},[_c('ul',{class:_vm.wrapperClasses},[_c('li',{class:{
disabled: _vm.prevPageIsDisabled,
'gl-flex-auto': _vm.isFillAlign,
},attrs:{"aria-disabled":_vm.prevPageIsDisabled,"aria-hidden":_vm.prevPageIsDisabled,"data-testid":"gl-pagination-li"}},[_c(_vm.prevPageIsDisabled ? 'span' : 'a',{tag:"component",staticClass:"gl-pagination-item",attrs:{"data-testid":"gl-pagination-prev","aria-label":_vm.prevPageAriaLabel,"href":_vm.prevPageHref},on:{"click":function($event){!_vm.prevPageIsDisabled ? _vm.handlePrevious($event, _vm.value - 1) : null;}}},[_vm._t("previous",function(){return [_c('gl-icon',{attrs:{"name":"chevron-left"}}),_vm._v(" "),_c('span',{staticClass:"gl-hidden sm:gl-block"},[_vm._v(_vm._s(_vm.prevText))])]},null,{ page: _vm.value - 1, disabled: _vm.prevPageIsDisabled })],2)],1),_vm._v(" "),_vm._l((_vm.visibleItems),function(item){return _c('li',{key:item.key,class:{
disabled: item.disabled,
'gl-flex-auto': _vm.isFillAlign,
},attrs:{"data-testid":"gl-pagination-li"}},[_c(item.component,_vm._g(_vm._b({tag:"component",staticClass:"gl-pagination-item",attrs:{"data-testid":"gl-pagination-item","size":"md","aria-disabled":item.disabled}},'component',item.attrs,false),item.listeners),[_vm._t(item.slot,function(){return [_vm._v(_vm._s(item.content))]},null,item.slotData)],2)],1)}),_vm._v(" "),_c('li',{class:{
disabled: _vm.nextPageIsDisabled,
'gl-flex-auto': _vm.isFillAlign,
},attrs:{"aria-disabled":_vm.nextPageIsDisabled,"aria-hidden":_vm.nextPageIsDisabled,"data-testid":"gl-pagination-li"}},[_c(_vm.nextPageIsDisabled ? 'span' : 'a',{tag:"component",staticClass:"gl-pagination-item",attrs:{"data-testid":"gl-pagination-next","aria-label":_vm.nextPageAriaLabel,"href":_vm.nextPageHref},on:{"click":function($event){!_vm.nextPageIsDisabled ? _vm.handleNext($event, _vm.value + 1) : null;}}},[_vm._t("next",function(){return [_c('span',{staticClass:"gl-hidden sm:gl-block"},[_vm._v(_vm._s(_vm.nextText))]),_vm._v(" "),_c('gl-icon',{attrs:{"name":"chevron-right"}})]},null,{ page: _vm.value + 1, disabled: _vm.nextPageIsDisabled })],2)],1)],2)]):_vm._e()};
var __vue_staticRenderFns__ = [];
/* style */
const __vue_inject_styles__ = undefined;
/* scoped */
const __vue_scope_id__ = undefined;
/* module identifier */
const __vue_module_identifier__ = undefined;
/* functional template */
const __vue_is_functional_template__ = false;
/* style inject */
/* style inject SSR */
/* style inject shadow dom */
const __vue_component__ = /*#__PURE__*/__vue_normalize__(
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
__vue_inject_styles__,
__vue_script__,
__vue_scope_id__,
__vue_is_functional_template__,
__vue_module_identifier__,
false,
undefined,
undefined,
undefined
);
export { __vue_component__ as default };