UNPKG

@gitlab/ui

Version:
418 lines (405 loc) 13.8 kB
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 };