UNPKG

bootstrap-vue

Version:

BootstrapVue, with over 40 plugins and more than 75 custom components, provides one of the most comprehensive implementations of Bootstrap v4 components and grid system for Vue.js. With extensive and automated WAI-ARIA accessibility markup.

572 lines (504 loc) 17.6 kB
/* * Common props, computed, data, render function, and methods * for <b-pagination> and <b-pagination-nav> */ import KeyCodes from '../utils/key-codes'; import range from '../utils/range'; import toString from '../utils/to-string'; import warn from '../utils/warn'; import { isFunction, isNull } from '../utils/inspect'; import { isVisible, isDisabled, selectAll, getAttr } from '../utils/dom'; import normalizeSlotMixin from '../mixins/normalize-slot'; import { BLink } from '../components/link/link'; // Threshold of limit size when we start/stop showing ellipsis var ELLIPSIS_THRESHOLD = 3; // Default # of buttons limit var DEFAULT_LIMIT = 5; // Make an array of N to N+X function makePageArray(startNum, numPages) { return range(numPages).map(function (value, index) { return { number: index + startNum, classes: null }; }); } // Sanitize the provided Limit value (converting to a number) function sanitizeLimit(value) { var limit = parseInt(value, 10) || 1; return limit < 1 ? DEFAULT_LIMIT : limit; } // Sanitize the provided current page number (converting to a number) function sanitizeCurPage(value, numPages) { var page = parseInt(value, 10) || 1; return page > numPages ? numPages : page < 1 ? 1 : page; } // Links don't normally respond to SPACE, so we add that functionality via this handler function onSpaceKey(evt) { if (evt.keyCode === KeyCodes.SPACE) { evt.preventDefault(); // Stop page from scrolling evt.stopImmediatePropagation(); evt.stopPropagation(); // Trigger the click event on the link evt.currentTarget.click(); return false; } } // Props object var props = { disabled: { type: Boolean, default: false }, value: { type: [Number, String], default: null, validator: function validator(value) { var num = parseInt(value, 10); /* istanbul ignore if */ if (!isNull(value) && (isNaN(num) || num < 1)) { warn('pagination: v-model value must be a number greater than 0'); return false; } return true; } }, limit: { type: [Number, String], default: DEFAULT_LIMIT, validator: function validator(value) { var num = parseInt(value, 10); /* istanbul ignore if */ if (isNaN(num) || num < 1) { warn('pagination: prop "limit" must be a number greater than 0'); return false; } return true; } }, size: { type: String, default: 'md' }, align: { type: String, default: 'left' }, hideGotoEndButtons: { type: Boolean, default: false }, ariaLabel: { type: String, default: 'Pagination' }, labelFirstPage: { type: String, default: 'Go to first page' }, firstText: { type: String, default: "\xAB" // '«' }, labelPrevPage: { type: String, default: 'Go to previous page' }, prevText: { type: String, default: "\u2039" // '‹' }, labelNextPage: { type: String, default: 'Go to next page' }, nextText: { type: String, default: "\u203A" // '›' }, labelLastPage: { type: String, default: 'Go to last page' }, lastText: { type: String, default: "\xBB" // '»' }, labelPage: { type: [String, Function], default: 'Go to page' }, hideEllipsis: { type: Boolean, default: false }, ellipsisText: { type: String, default: "\u2026" // '…' } // @vue/component }; export default { mixins: [normalizeSlotMixin], model: { prop: 'value', event: 'input' }, props: props, data: function data() { var curr = parseInt(this.value, 10); return { // -1 signifies no page initially selected currentPage: curr > 0 ? curr : -1, localNumPages: 1, localLimit: DEFAULT_LIMIT }; }, computed: { btnSize: function btnSize() { return this.size ? "pagination-".concat(this.size) : ''; }, alignment: function alignment() { var align = this.align; if (align === 'center') { return 'justify-content-center'; } else if (align === 'end' || align === 'right') { return 'justify-content-end'; } else if (align === 'fill') { // The page-items will also have 'flex-fill' added. // We ad text centering to make the button appearance better in fill mode. return 'text-center'; } return ''; }, computedCurrentPage: function computedCurrentPage() { return sanitizeCurPage(this.currentPage, this.localNumPages); }, paginationParams: function paginationParams() { // Determine if we should show the the ellipsis var limit = this.limit; var numPages = this.localNumPages; var curPage = this.computedCurrentPage; var hideEllipsis = this.hideEllipsis; var showFirstDots = false; var showLastDots = false; var numLinks = limit; var startNum = 1; if (numPages <= limit) { // Special Case: Less pages available than the limit of displayed pages numLinks = numPages; } else if (curPage < limit - 1 && limit > ELLIPSIS_THRESHOLD) { // We are near the beginning of the page list if (!hideEllipsis) { showLastDots = true; numLinks = limit - 1; } } else if (numPages - curPage + 2 < limit && limit > ELLIPSIS_THRESHOLD) { // We are near the end of the list if (!hideEllipsis) { numLinks = limit - 1; showFirstDots = true; } startNum = numPages - numLinks + 1; } else { // We are somewhere in the middle of the page list if (limit > ELLIPSIS_THRESHOLD && !hideEllipsis) { numLinks = limit - 2; showFirstDots = showLastDots = true; } startNum = curPage - Math.floor(numLinks / 2); } // Sanity checks if (startNum < 1) { /* istanbul ignore next */ startNum = 1; } else if (startNum > numPages - numLinks) { startNum = numPages - numLinks + 1; } return { showFirstDots: showFirstDots, showLastDots: showLastDots, numLinks: numLinks, startNum: startNum }; }, pageList: function pageList() { // Generates the pageList array var _this$paginationParam = this.paginationParams, numLinks = _this$paginationParam.numLinks, startNum = _this$paginationParam.startNum; var currPage = this.computedCurrentPage; // Generate list of page numbers var pages = makePageArray(startNum, numLinks); // We limit to a total of 3 page buttons on XS screens // So add classes to page links to hide them for XS breakpoint // Note: Ellipsis will also be hidden on XS screens // TODO: Make this visual limit configurable based on breakpoint(s) if (pages.length > 3) { var idx = currPage - startNum; // THe following is a bootstrap-vue custom utility class var classes = 'bv-d-xs-down-none'; if (idx === 0) { // Keep leftmost 3 buttons visible when current page is first page for (var i = 3; i < pages.length; i++) { pages[i].classes = classes; } } else if (idx === pages.length - 1) { // Keep rightmost 3 buttons visible when current page is last page for (var _i = 0; _i < pages.length - 3; _i++) { pages[_i].classes = classes; } } else { // Hide all except current page, current page - 1 and current page + 1 for (var _i2 = 0; _i2 < idx - 1; _i2++) { // hide some left button(s) pages[_i2].classes = classes; } for (var _i3 = pages.length - 1; _i3 > idx + 1; _i3--) { // hide some right button(s) pages[_i3].classes = classes; } } } return pages; } }, watch: { value: function value(newValue, oldValue) { if (newValue !== oldValue) { this.currentPage = sanitizeCurPage(newValue, this.localNumPages); } }, currentPage: function currentPage(newValue, oldValue) { if (newValue !== oldValue) { // Emit null if no page selected this.$emit('input', newValue > 0 ? newValue : null); } }, limit: function limit(newValue, oldValue) { if (newValue !== oldValue) { this.localLimit = sanitizeLimit(newValue); } } }, created: function created() { var _this = this; // Set our default values in data this.localLimit = sanitizeLimit(this.limit); this.$nextTick(function () { // Sanity check _this.currentPage = _this.currentPage > _this.localNumPages ? _this.localNumPages : _this.currentPage; }); }, methods: { getButtons: function getButtons() { // Return only buttons that are visible return selectAll('a.page-link', this.$el).filter(function (btn) { return isVisible(btn); }); }, setBtnFocus: function setBtnFocus(btn) { btn.focus(); }, focusCurrent: function focusCurrent() { var _this2 = this; // We do this in next tick to ensure buttons have finished rendering this.$nextTick(function () { var btn = _this2.getButtons().find(function (el) { return parseInt(getAttr(el, 'aria-posinset'), 10) === _this2.computedCurrentPage; }); if (btn && btn.focus) { _this2.setBtnFocus(btn); } else { // Fallback if current page is not in button list _this2.focusFirst(); } }); }, focusFirst: function focusFirst() { var _this3 = this; // We do this in next tick to ensure buttons have finished rendering this.$nextTick(function () { var btn = _this3.getButtons().find(function (el) { return !isDisabled(el); }); if (btn && btn.focus && btn !== document.activeElement) { _this3.setBtnFocus(btn); } }); }, focusLast: function focusLast() { var _this4 = this; // We do this in next tick to ensure buttons have finished rendering this.$nextTick(function () { var btn = _this4.getButtons().reverse().find(function (el) { return !isDisabled(el); }); if (btn && btn.focus && btn !== document.activeElement) { _this4.setBtnFocus(btn); } }); }, focusPrev: function focusPrev() { var _this5 = this; // We do this in next tick to ensure buttons have finished rendering this.$nextTick(function () { var buttons = _this5.getButtons(); var idx = buttons.indexOf(document.activeElement); if (idx > 0 && !isDisabled(buttons[idx - 1]) && buttons[idx - 1].focus) { _this5.setBtnFocus(buttons[idx - 1]); } }); }, focusNext: function focusNext() { var _this6 = this; // We do this in next tick to ensure buttons have finished rendering this.$nextTick(function () { var buttons = _this6.getButtons(); var idx = buttons.indexOf(document.activeElement); var cnt = buttons.length - 1; if (idx < cnt && !isDisabled(buttons[idx + 1]) && buttons[idx + 1].focus) { _this6.setBtnFocus(buttons[idx + 1]); } }); } }, render: function render(h) { var _this7 = this; var buttons = []; var numberOfPages = this.localNumPages; var disabled = this.disabled; var _this$paginationParam2 = this.paginationParams, showFirstDots = _this$paginationParam2.showFirstDots, showLastDots = _this$paginationParam2.showLastDots; var currPage = this.computedCurrentPage; var fill = this.align === 'fill'; // Helper function and flag var isActivePage = function isActivePage(pageNum) { return pageNum === currPage; }; var noCurrPage = this.currentPage < 1; // Factory function for prev/next/first/last buttons var makeEndBtn = function makeEndBtn(linkTo, ariaLabel, btnSlot, btnText, pageTest, key) { var isDisabled = disabled || isActivePage(pageTest) || noCurrPage || linkTo < 1 || linkTo > numberOfPages; var pageNum = linkTo < 1 ? 1 : linkTo > numberOfPages ? numberOfPages : linkTo; var scope = { disabled: isDisabled, page: pageNum, index: pageNum - 1 }; var btnContent = _this7.normalizeSlot(btnSlot, scope) || toString(btnText) || h(false); var inner = h(isDisabled ? 'span' : BLink, { staticClass: 'page-link', props: isDisabled ? {} : _this7.linkProps(linkTo), attrs: { role: 'menuitem', tabindex: isDisabled ? null : '-1', 'aria-label': ariaLabel, 'aria-controls': _this7.ariaControls || null, 'aria-disabled': isDisabled ? 'true' : null }, on: isDisabled ? {} : { click: function click(evt) { _this7.onClick(linkTo, evt); }, keydown: onSpaceKey } }, [btnContent]); return h('li', { key: key, staticClass: 'page-item', class: { disabled: isDisabled, 'flex-fill': fill }, attrs: { role: 'none presentation', 'aria-hidden': isDisabled ? 'true' : null } }, [inner]); }; // Ellipsis factory var makeEllipsis = function makeEllipsis(isLast) { return h('li', { key: "ellipsis-".concat(isLast ? 'last' : 'first'), staticClass: 'page-item', class: ['disabled', 'bv-d-xs-down-none', fill ? 'flex-fill' : ''], attrs: { role: 'separator' } }, [h('span', { staticClass: 'page-link' }, [_this7.normalizeSlot('ellipsis-text', {}) || toString(_this7.ellipsisText) || h(false)])]); }; // Goto First Page button bookend buttons.push(this.hideGotoEndButtons ? h(false) : makeEndBtn(1, this.labelFirstPage, 'first-text', this.firstText, 1, 'bookend-goto-first')); // Goto Previous page button bookend buttons.push(makeEndBtn(currPage - 1, this.labelPrevPage, 'prev-text', this.prevText, 1, 'bookend-goto-prev')); // First Ellipsis Bookend buttons.push(showFirstDots ? makeEllipsis(false) : h(false)); // Individual Page links this.pageList.forEach(function (page, idx) { var active = isActivePage(page.number) && !noCurrPage; // Active page will have tabindex of 0, or if no current page and first page button var tabIndex = disabled ? null : active || noCurrPage && idx === 0 ? '0' : '-1'; var attrs = { role: 'menuitemradio', 'aria-disabled': disabled ? 'true' : null, 'aria-controls': _this7.ariaControls || null, 'aria-label': isFunction(_this7.labelPage) ? _this7.labelPage(page.number) : "".concat(_this7.labelPage, " ").concat(page.number), 'aria-checked': active ? 'true' : 'false', 'aria-posinset': page.number, 'aria-setsize': numberOfPages, // ARIA "roving tabindex" method tabindex: tabIndex }; var btnContent = toString(_this7.makePage(page.number)); var scope = { page: page.number, index: page.number - 1, content: btnContent, active: active, disabled: disabled }; var inner = h(disabled ? 'span' : BLink, { props: disabled ? {} : _this7.linkProps(page.number), staticClass: 'page-link', attrs: attrs, on: disabled ? {} : { click: function click(evt) { _this7.onClick(page.number, evt); }, keydown: onSpaceKey } }, [_this7.normalizeSlot('page', scope) || btnContent]); buttons.push(h('li', { key: "page-".concat(page.number), staticClass: 'page-item', class: [{ disabled: disabled, active: active, 'flex-fill': fill }, page.classes], attrs: { role: 'none presentation' } }, [inner])); }); // Last Ellipsis Bookend buttons.push(showLastDots ? makeEllipsis(true) : h(false)); // Goto Next page button bookend buttons.push(makeEndBtn(currPage + 1, this.labelNextPage, 'next-text', this.nextText, numberOfPages, 'bookend-goto-next')); // Goto Last Page button bookend buttons.push(this.hideGotoEndButtons ? h(false) : makeEndBtn(numberOfPages, this.labelLastPage, 'last-text', this.lastText, numberOfPages, 'bookend-goto-last')); // Assemble the pagination buttons var pagination = h('ul', { ref: 'ul', staticClass: 'pagination', class: ['b-pagination', this.btnSize, this.alignment], attrs: { role: 'menubar', 'aria-disabled': disabled ? 'true' : 'false', 'aria-label': this.ariaLabel || null }, on: { keydown: function keydown(evt) { var keyCode = evt.keyCode; var shift = evt.shiftKey; if (keyCode === KeyCodes.LEFT) { evt.preventDefault(); shift ? _this7.focusFirst() : _this7.focusPrev(); } else if (keyCode === KeyCodes.RIGHT) { evt.preventDefault(); shift ? _this7.focusLast() : _this7.focusNext(); } } } }, buttons); // if we are pagination-nav, wrap in '<nav>' wrapper if (this.isNav) { return h('nav', { attrs: { 'aria-disabled': disabled ? 'true' : null, 'aria-hidden': disabled ? 'true' : 'false' } }, [pagination]); } else { return pagination; } } };