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
634 lines (549 loc) • 24.3 kB
JavaScript
var _watch;
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import { extend } from '../vue';
import { NAME_PAGINATION } from '../constants/components';
import { CODE_DOWN, CODE_LEFT, CODE_RIGHT, CODE_SPACE, CODE_UP } from '../constants/key-codes';
import { PROP_TYPE_ARRAY_OBJECT_STRING, PROP_TYPE_BOOLEAN, PROP_TYPE_BOOLEAN_NUMBER_STRING, PROP_TYPE_FUNCTION_STRING, PROP_TYPE_NUMBER_STRING, PROP_TYPE_STRING } from '../constants/props';
import { SLOT_NAME_ELLIPSIS_TEXT, SLOT_NAME_FIRST_TEXT, SLOT_NAME_LAST_TEXT, SLOT_NAME_NEXT_TEXT, SLOT_NAME_PAGE, SLOT_NAME_PREV_TEXT } from '../constants/slots';
import { createArray } from '../utils/array';
import { attemptFocus, getActiveElement, getAttr, isDisabled, isVisible, selectAll } from '../utils/dom';
import { stopEvent } from '../utils/events';
import { isFunction, isNull } from '../utils/inspect';
import { mathFloor, mathMax, mathMin } from '../utils/math';
import { makeModelMixin } from '../utils/model';
import { toInteger } from '../utils/number';
import { sortKeys } from '../utils/object';
import { hasPropFunction, makeProp, makePropsConfigurable } from '../utils/props';
import { safeVueInstance } from '../utils/safe-vue-instance';
import { toString } from '../utils/string';
import { warn } from '../utils/warn';
import { normalizeSlotMixin } from '../mixins/normalize-slot';
import { BLink } from '../components/link/link'; // Common props, computed, data, render function, and methods
// for `<b-pagination>` and `<b-pagination-nav>`
// --- Constants ---
var _makeModelMixin = makeModelMixin('value', {
type: PROP_TYPE_BOOLEAN_NUMBER_STRING,
defaultValue: null,
/* istanbul ignore next */
validator: function validator(value) {
if (!isNull(value) && toInteger(value, 0) < 1) {
warn('"v-model" value must be a number greater than "0"', NAME_PAGINATION);
return false;
}
return true;
}
}),
modelMixin = _makeModelMixin.mixin,
modelProps = _makeModelMixin.props,
MODEL_PROP_NAME = _makeModelMixin.prop,
MODEL_EVENT_NAME = _makeModelMixin.event;
export { MODEL_PROP_NAME, MODEL_EVENT_NAME }; // Threshold of limit size when we start/stop showing ellipsis
var ELLIPSIS_THRESHOLD = 3; // Default # of buttons limit
var DEFAULT_LIMIT = 5; // --- Helper methods ---
// Make an array of N to N+X
var makePageArray = function makePageArray(startNumber, numberOfPages) {
return createArray(numberOfPages, function (_, i) {
return {
number: startNumber + i,
classes: null
};
});
}; // Sanitize the provided limit value (converting to a number)
var sanitizeLimit = function sanitizeLimit(value) {
var limit = toInteger(value) || 1;
return limit < 1 ? DEFAULT_LIMIT : limit;
}; // Sanitize the provided current page number (converting to a number)
var sanitizeCurrentPage = function sanitizeCurrentPage(val, numberOfPages) {
var page = toInteger(val) || 1;
return page > numberOfPages ? numberOfPages : page < 1 ? 1 : page;
}; // Links don't normally respond to SPACE, so we add that
// functionality via this handler
var onSpaceKey = function onSpaceKey(event) {
if (event.keyCode === CODE_SPACE) {
// Stop page from scrolling
stopEvent(event, {
immediatePropagation: true
}); // Trigger the click event on the link
event.currentTarget.click();
return false;
}
}; // --- Props ---
export var props = makePropsConfigurable(sortKeys(_objectSpread(_objectSpread({}, modelProps), {}, {
align: makeProp(PROP_TYPE_STRING, 'left'),
ariaLabel: makeProp(PROP_TYPE_STRING, 'Pagination'),
disabled: makeProp(PROP_TYPE_BOOLEAN, false),
ellipsisClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING),
ellipsisText: makeProp(PROP_TYPE_STRING, "\u2026"),
// '…'
firstClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING),
firstNumber: makeProp(PROP_TYPE_BOOLEAN, false),
firstText: makeProp(PROP_TYPE_STRING, "\xAB"),
// '«'
hideEllipsis: makeProp(PROP_TYPE_BOOLEAN, false),
hideGotoEndButtons: makeProp(PROP_TYPE_BOOLEAN, false),
labelFirstPage: makeProp(PROP_TYPE_STRING, 'Go to first page'),
labelLastPage: makeProp(PROP_TYPE_STRING, 'Go to last page'),
labelNextPage: makeProp(PROP_TYPE_STRING, 'Go to next page'),
labelPage: makeProp(PROP_TYPE_FUNCTION_STRING, 'Go to page'),
labelPrevPage: makeProp(PROP_TYPE_STRING, 'Go to previous page'),
lastClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING),
lastNumber: makeProp(PROP_TYPE_BOOLEAN, false),
lastText: makeProp(PROP_TYPE_STRING, "\xBB"),
// '»'
limit: makeProp(PROP_TYPE_NUMBER_STRING, DEFAULT_LIMIT,
/* istanbul ignore next */
function (value) {
if (toInteger(value, 0) < 1) {
warn('Prop "limit" must be a number greater than "0"', NAME_PAGINATION);
return false;
}
return true;
}),
nextClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING),
nextText: makeProp(PROP_TYPE_STRING, "\u203A"),
// '›'
pageClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING),
pills: makeProp(PROP_TYPE_BOOLEAN, false),
prevClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING),
prevText: makeProp(PROP_TYPE_STRING, "\u2039"),
// '‹'
size: makeProp(PROP_TYPE_STRING)
})), 'pagination'); // --- Mixin ---
// @vue/component
export var paginationMixin = extend({
mixins: [modelMixin, normalizeSlotMixin],
props: props,
data: function data() {
// `-1` signifies no page initially selected
var currentPage = toInteger(this[MODEL_PROP_NAME], 0);
currentPage = currentPage > 0 ? currentPage : -1;
return {
currentPage: currentPage,
localNumberOfPages: 1,
localLimit: DEFAULT_LIMIT
};
},
computed: {
btnSize: function btnSize() {
var size = this.size;
return size ? "pagination-".concat(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 add text centering to make the button appearance better in fill mode
return 'text-center';
}
return '';
},
styleClass: function styleClass() {
return this.pills ? 'b-pagination-pills' : '';
},
computedCurrentPage: function computedCurrentPage() {
return sanitizeCurrentPage(this.currentPage, this.localNumberOfPages);
},
paginationParams: function paginationParams() {
// Determine if we should show the the ellipsis
var limit = this.localLimit,
numberOfPages = this.localNumberOfPages,
currentPage = this.computedCurrentPage,
hideEllipsis = this.hideEllipsis,
firstNumber = this.firstNumber,
lastNumber = this.lastNumber;
var showFirstDots = false;
var showLastDots = false;
var numberOfLinks = limit;
var startNumber = 1;
if (numberOfPages <= limit) {
// Special case: Less pages available than the limit of displayed pages
numberOfLinks = numberOfPages;
} else if (currentPage < limit - 1 && limit > ELLIPSIS_THRESHOLD) {
if (!hideEllipsis || lastNumber) {
showLastDots = true;
numberOfLinks = limit - (firstNumber ? 0 : 1);
}
numberOfLinks = mathMin(numberOfLinks, limit);
} else if (numberOfPages - currentPage + 2 < limit && limit > ELLIPSIS_THRESHOLD) {
if (!hideEllipsis || firstNumber) {
showFirstDots = true;
numberOfLinks = limit - (lastNumber ? 0 : 1);
}
startNumber = numberOfPages - numberOfLinks + 1;
} else {
// We are somewhere in the middle of the page list
if (limit > ELLIPSIS_THRESHOLD) {
numberOfLinks = limit - (hideEllipsis ? 0 : 2);
showFirstDots = !!(!hideEllipsis || firstNumber);
showLastDots = !!(!hideEllipsis || lastNumber);
}
startNumber = currentPage - mathFloor(numberOfLinks / 2);
} // Sanity checks
/* istanbul ignore if */
if (startNumber < 1) {
startNumber = 1;
showFirstDots = false;
} else if (startNumber > numberOfPages - numberOfLinks) {
startNumber = numberOfPages - numberOfLinks + 1;
showLastDots = false;
}
if (showFirstDots && firstNumber && startNumber < 4) {
numberOfLinks = numberOfLinks + 2;
startNumber = 1;
showFirstDots = false;
}
var lastPageNumber = startNumber + numberOfLinks - 1;
if (showLastDots && lastNumber && lastPageNumber > numberOfPages - 3) {
numberOfLinks = numberOfLinks + (lastPageNumber === numberOfPages - 2 ? 2 : 3);
showLastDots = false;
} // Special handling for lower limits (where ellipsis are never shown)
if (limit <= ELLIPSIS_THRESHOLD) {
if (firstNumber && startNumber === 1) {
numberOfLinks = mathMin(numberOfLinks + 1, numberOfPages, limit + 1);
} else if (lastNumber && numberOfPages === startNumber + numberOfLinks - 1) {
startNumber = mathMax(startNumber - 1, 1);
numberOfLinks = mathMin(numberOfPages - startNumber + 1, numberOfPages, limit + 1);
}
}
numberOfLinks = mathMin(numberOfLinks, numberOfPages - startNumber + 1);
return {
showFirstDots: showFirstDots,
showLastDots: showLastDots,
numberOfLinks: numberOfLinks,
startNumber: startNumber
};
},
pageList: function pageList() {
// Generates the pageList array
var _this$paginationParam = this.paginationParams,
numberOfLinks = _this$paginationParam.numberOfLinks,
startNumber = _this$paginationParam.startNumber;
var currentPage = this.computedCurrentPage; // Generate list of page numbers
var pages = makePageArray(startNumber, numberOfLinks); // 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 = currentPage - startNumber; // 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: (_watch = {}, _defineProperty(_watch, MODEL_PROP_NAME, function (newValue, oldValue) {
if (newValue !== oldValue) {
this.currentPage = sanitizeCurrentPage(newValue, this.localNumberOfPages);
}
}), _defineProperty(_watch, "currentPage", function currentPage(newValue, oldValue) {
if (newValue !== oldValue) {
// Emit `null` if no page selected
this.$emit(MODEL_EVENT_NAME, newValue > 0 ? newValue : null);
}
}), _defineProperty(_watch, "limit", function limit(newValue, oldValue) {
if (newValue !== oldValue) {
this.localLimit = sanitizeLimit(newValue);
}
}), _watch),
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.localNumberOfPages ? _this.localNumberOfPages : _this.currentPage;
});
},
methods: {
handleKeyNav: function handleKeyNav(event) {
var keyCode = event.keyCode,
shiftKey = event.shiftKey;
/* istanbul ignore if */
if (this.isNav) {
// We disable left/right keyboard navigation in `<b-pagination-nav>`
return;
}
if (keyCode === CODE_LEFT || keyCode === CODE_UP) {
stopEvent(event, {
propagation: false
});
shiftKey ? this.focusFirst() : this.focusPrev();
} else if (keyCode === CODE_RIGHT || keyCode === CODE_DOWN) {
stopEvent(event, {
propagation: false
});
shiftKey ? this.focusLast() : this.focusNext();
}
},
getButtons: function getButtons() {
// Return only buttons that are visible
return selectAll('button.page-link, a.page-link', this.$el).filter(function (btn) {
return isVisible(btn);
});
},
focusCurrent: function focusCurrent() {
var _this2 = this;
// We do this in `$nextTick()` to ensure buttons have finished rendering
this.$nextTick(function () {
var btn = _this2.getButtons().find(function (el) {
return toInteger(getAttr(el, 'aria-posinset'), 0) === _this2.computedCurrentPage;
});
if (!attemptFocus(btn)) {
// Fallback if current page is not in button list
_this2.focusFirst();
}
});
},
focusFirst: function focusFirst() {
var _this3 = this;
// We do this in `$nextTick()` to ensure buttons have finished rendering
this.$nextTick(function () {
var btn = _this3.getButtons().find(function (el) {
return !isDisabled(el);
});
attemptFocus(btn);
});
},
focusLast: function focusLast() {
var _this4 = this;
// We do this in `$nextTick()` to ensure buttons have finished rendering
this.$nextTick(function () {
var btn = _this4.getButtons().reverse().find(function (el) {
return !isDisabled(el);
});
attemptFocus(btn);
});
},
focusPrev: function focusPrev() {
var _this5 = this;
// We do this in `$nextTick()` to ensure buttons have finished rendering
this.$nextTick(function () {
var buttons = _this5.getButtons();
var index = buttons.indexOf(getActiveElement());
if (index > 0 && !isDisabled(buttons[index - 1])) {
attemptFocus(buttons[index - 1]);
}
});
},
focusNext: function focusNext() {
var _this6 = this;
// We do this in `$nextTick()` to ensure buttons have finished rendering
this.$nextTick(function () {
var buttons = _this6.getButtons();
var index = buttons.indexOf(getActiveElement());
if (index < buttons.length - 1 && !isDisabled(buttons[index + 1])) {
attemptFocus(buttons[index + 1]);
}
});
}
},
render: function render(h) {
var _this7 = this;
var _safeVueInstance = safeVueInstance(this),
disabled = _safeVueInstance.disabled,
labelPage = _safeVueInstance.labelPage,
ariaLabel = _safeVueInstance.ariaLabel,
isNav = _safeVueInstance.isNav,
numberOfPages = _safeVueInstance.localNumberOfPages,
currentPage = _safeVueInstance.computedCurrentPage;
var pageNumbers = this.pageList.map(function (p) {
return p.number;
});
var _this$paginationParam2 = this.paginationParams,
showFirstDots = _this$paginationParam2.showFirstDots,
showLastDots = _this$paginationParam2.showLastDots;
var fill = this.align === 'fill';
var $buttons = []; // Helper function and flag
var isActivePage = function isActivePage(pageNumber) {
return pageNumber === currentPage;
};
var noCurrentPage = this.currentPage < 1; // Factory function for prev/next/first/last buttons
var makeEndBtn = function makeEndBtn(linkTo, ariaLabel, btnSlot, btnText, btnClass, pageTest, key) {
var isDisabled = disabled || isActivePage(pageTest) || noCurrentPage || linkTo < 1 || linkTo > numberOfPages;
var pageNumber = linkTo < 1 ? 1 : linkTo > numberOfPages ? numberOfPages : linkTo;
var scope = {
disabled: isDisabled,
page: pageNumber,
index: pageNumber - 1
};
var $btnContent = _this7.normalizeSlot(btnSlot, scope) || toString(btnText) || h();
var $inner = h(isDisabled ? 'span' : isNav ? BLink : 'button', {
staticClass: 'page-link',
class: {
'flex-grow-1': !isNav && !isDisabled && fill
},
props: isDisabled || !isNav ? {} : _this7.linkProps(linkTo),
attrs: {
role: isNav ? null : 'menuitem',
type: isNav || isDisabled ? null : 'button',
tabindex: isDisabled || isNav ? null : '-1',
'aria-label': ariaLabel,
'aria-controls': safeVueInstance(_this7).ariaControls || null,
'aria-disabled': isDisabled ? 'true' : null
},
on: isDisabled ? {} : {
'!click': function click(event) {
_this7.onClick(event, linkTo);
},
keydown: onSpaceKey
}
}, [$btnContent]);
return h('li', {
key: key,
staticClass: 'page-item',
class: [{
disabled: isDisabled,
'flex-fill': fill,
'd-flex': fill && !isNav && !isDisabled
}, btnClass],
attrs: {
role: isNav ? null : 'presentation',
'aria-hidden': isDisabled ? 'true' : null
}
}, [$inner]);
}; // Ellipsis factory
var makeEllipsis = function makeEllipsis(isLast) {
return h('li', {
staticClass: 'page-item',
class: ['disabled', 'bv-d-xs-down-none', fill ? 'flex-fill' : '', _this7.ellipsisClass],
attrs: {
role: 'separator'
},
key: "ellipsis-".concat(isLast ? 'last' : 'first')
}, [h('span', {
staticClass: 'page-link'
}, [_this7.normalizeSlot(SLOT_NAME_ELLIPSIS_TEXT) || toString(_this7.ellipsisText) || h()])]);
}; // Page button factory
var makePageButton = function makePageButton(page, idx) {
var pageNumber = page.number;
var active = isActivePage(pageNumber) && !noCurrentPage; // Active page will have tabindex of 0, or if no current page and first page button
var tabIndex = disabled ? null : active || noCurrentPage && idx === 0 ? '0' : '-1';
var attrs = {
role: isNav ? null : 'menuitemradio',
type: isNav || disabled ? null : 'button',
'aria-disabled': disabled ? 'true' : null,
'aria-controls': safeVueInstance(_this7).ariaControls || null,
'aria-label': hasPropFunction(labelPage) ?
/* istanbul ignore next */
labelPage(pageNumber) : "".concat(isFunction(labelPage) ? labelPage() : labelPage, " ").concat(pageNumber),
'aria-checked': isNav ? null : active ? 'true' : 'false',
'aria-current': isNav && active ? 'page' : null,
'aria-posinset': isNav ? null : pageNumber,
'aria-setsize': isNav ? null : numberOfPages,
// ARIA "roving tabindex" method (except in `isNav` mode)
tabindex: isNav ? null : tabIndex
};
var btnContent = toString(_this7.makePage(pageNumber));
var scope = {
page: pageNumber,
index: pageNumber - 1,
content: btnContent,
active: active,
disabled: disabled
};
var $inner = h(disabled ? 'span' : isNav ? BLink : 'button', {
props: disabled || !isNav ? {} : _this7.linkProps(pageNumber),
staticClass: 'page-link',
class: {
'flex-grow-1': !isNav && !disabled && fill
},
attrs: attrs,
on: disabled ? {} : {
'!click': function click(event) {
_this7.onClick(event, pageNumber);
},
keydown: onSpaceKey
}
}, [_this7.normalizeSlot(SLOT_NAME_PAGE, scope) || btnContent]);
return h('li', {
staticClass: 'page-item',
class: [{
disabled: disabled,
active: active,
'flex-fill': fill,
'd-flex': fill && !isNav && !disabled
}, page.classes, _this7.pageClass],
attrs: {
role: isNav ? null : 'presentation'
},
key: "page-".concat(pageNumber)
}, [$inner]);
}; // Goto first page button
// Don't render button when `hideGotoEndButtons` or `firstNumber` is set
var $firstPageBtn = h();
if (!this.firstNumber && !this.hideGotoEndButtons) {
$firstPageBtn = makeEndBtn(1, this.labelFirstPage, SLOT_NAME_FIRST_TEXT, this.firstText, this.firstClass, 1, 'pagination-goto-first');
}
$buttons.push($firstPageBtn); // Goto previous page button
$buttons.push(makeEndBtn(currentPage - 1, this.labelPrevPage, SLOT_NAME_PREV_TEXT, this.prevText, this.prevClass, 1, 'pagination-goto-prev')); // Show first (1) button?
$buttons.push(this.firstNumber && pageNumbers[0] !== 1 ? makePageButton({
number: 1
}, 0) : h()); // First ellipsis
$buttons.push(showFirstDots ? makeEllipsis(false) : h()); // Individual page links
this.pageList.forEach(function (page, idx) {
var offset = showFirstDots && _this7.firstNumber && pageNumbers[0] !== 1 ? 1 : 0;
$buttons.push(makePageButton(page, idx + offset));
}); // Last ellipsis
$buttons.push(showLastDots ? makeEllipsis(true) : h()); // Show last page button?
$buttons.push(this.lastNumber && pageNumbers[pageNumbers.length - 1] !== numberOfPages ? makePageButton({
number: numberOfPages
}, -1) : h()); // Goto next page button
$buttons.push(makeEndBtn(currentPage + 1, this.labelNextPage, SLOT_NAME_NEXT_TEXT, this.nextText, this.nextClass, numberOfPages, 'pagination-goto-next')); // Goto last page button
// Don't render button when `hideGotoEndButtons` or `lastNumber` is set
var $lastPageBtn = h();
if (!this.lastNumber && !this.hideGotoEndButtons) {
$lastPageBtn = makeEndBtn(numberOfPages, this.labelLastPage, SLOT_NAME_LAST_TEXT, this.lastText, this.lastClass, numberOfPages, 'pagination-goto-last');
}
$buttons.push($lastPageBtn); // Assemble the pagination buttons
var $pagination = h('ul', {
staticClass: 'pagination',
class: ['b-pagination', this.btnSize, this.alignment, this.styleClass],
attrs: {
role: isNav ? null : 'menubar',
'aria-disabled': disabled ? 'true' : 'false',
'aria-label': isNav ? null : ariaLabel || null
},
// We disable keyboard left/right nav when `<b-pagination-nav>`
on: isNav ? {} : {
keydown: this.handleKeyNav
},
ref: 'ul'
}, $buttons); // If we are `<b-pagination-nav>`, wrap in `<nav>` wrapper
if (isNav) {
return h('nav', {
attrs: {
'aria-disabled': disabled ? 'true' : null,
'aria-hidden': disabled ? 'true' : 'false',
'aria-label': isNav ? ariaLabel || null : null
}
}, [$pagination]);
}
return $pagination;
}
});