From 38744708f6e0910127221dbdcdd31b82274f0b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 14 Jan 2020 16:26:32 +0100 Subject: [PATCH 01/12] feat(b-pagination): add page button class props and option to show first/last page numbers --- src/components/dropdown/dropdown.js | 6 +- src/mixins/dropdown.js | 2 +- src/mixins/pagination.js | 161 ++++++++++++++++++---------- src/utils/config-set.js | 7 +- 4 files changed, 114 insertions(+), 62 deletions(-) diff --git a/src/components/dropdown/dropdown.js b/src/components/dropdown/dropdown.js index 11ed0a4ed28..1c119d5c1dc 100644 --- a/src/components/dropdown/dropdown.js +++ b/src/components/dropdown/dropdown.js @@ -29,7 +29,7 @@ export const props = { default: false }, menuClass: { - type: [String, Array], + type: [String, Array, Object], default: null }, toggleTag: { @@ -37,7 +37,7 @@ export const props = { default: 'button' }, toggleClass: { - type: [String, Array], + type: [String, Array, Object], default: null }, noCaret: { @@ -61,7 +61,7 @@ export const props = { default: () => getComponentConfig(NAME, 'splitVariant') }, splitClass: { - type: [String, Array], + type: [String, Array, Object], default: null }, splitButtonType: { diff --git a/src/mixins/dropdown.js b/src/mixins/dropdown.js index e7906d5cb77..33e6d055f0e 100644 --- a/src/mixins/dropdown.js +++ b/src/mixins/dropdown.js @@ -213,7 +213,7 @@ export default { if (!this.inNavbar) { if (typeof Popper === 'undefined') { /* istanbul ignore next */ - warn('b-dropdown: Popper.js not found. Falling back to CSS positioning.') + warn('Popper.js not found. Falling back to CSS positioning', 'BDropdown') } else { // for dropup with alignment we use the parent element as popper container let element = (this.dropup && this.right) || this.split ? this.$el : this.$refs.toggle diff --git a/src/mixins/pagination.js b/src/mixins/pagination.js index 99ed0bbd9eb..9f3a37fdc76 100644 --- a/src/mixins/pagination.js +++ b/src/mixins/pagination.js @@ -55,9 +55,9 @@ export const props = { type: [Number, String], default: null, validator(value) /* istanbul ignore next */ { - const num = toInteger(value) - if (!isNull(value) && (isNaN(num) || num < 1)) { - warn('pagination: v-model value must be a number greater than 0') + const number = toInteger(value) + if (!isNull(value) && (isNaN(number) || number < 1)) { + warn('"v-model" value must be a number greater than "0"', 'BPagination') return false } return true @@ -67,9 +67,9 @@ export const props = { type: [Number, String], default: DEFAULT_LIMIT, validator(value) /* istanbul ignore next */ { - const num = toInteger(value) - if (isNaN(num) || num < 1) { - warn('pagination: prop "limit" must be a number greater than 0') + const number = toInteger(value) + if (isNaN(number) || number < 1) { + warn('Prop "limit" must be a number greater than "0"', 'BPagination') return false } return true @@ -99,6 +99,14 @@ export const props = { type: String, default: '\u00AB' // '«' }, + firstNumber: { + type: Boolean, + default: false + }, + firstClass: { + type: [String, Array, Object], + default: null + }, labelPrevPage: { type: String, default: 'Go to previous page' @@ -107,6 +115,10 @@ export const props = { type: String, default: '\u2039' // '‹' }, + prevClass: { + type: [String, Array, Object], + default: null + }, labelNextPage: { type: String, default: 'Go to next page' @@ -115,6 +127,10 @@ export const props = { type: String, default: '\u203A' // '›' }, + nextClass: { + type: [String, Array, Object], + default: null + }, labelLastPage: { type: String, default: 'Go to last page' @@ -123,10 +139,22 @@ export const props = { type: String, default: '\u00BB' // '»' }, + lastNumber: { + type: Boolean, + default: false + }, + lastClass: { + type: [String, Array, Object], + default: null + }, labelPage: { type: [String, Function], default: 'Go to page' }, + pageClass: { + type: [String, Array, Object], + default: null + }, hideEllipsis: { type: Boolean, default: false @@ -134,6 +162,10 @@ export const props = { ellipsisText: { type: String, default: '\u2026' // '…' + }, + ellipsisClass: { + type: [String, Array, Object], + default: null } } @@ -165,8 +197,8 @@ export default { } 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. + // 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 '' @@ -289,14 +321,13 @@ export default { }, methods: { handleKeyNav(evt) { - const keyCode = evt.keyCode - const shift = evt.shiftKey + const { keyCode, shiftKey } = evt if (keyCode === KeyCodes.LEFT || keyCode === KeyCodes.UP) { evt.preventDefault() - shift ? this.focusFirst() : this.focusPrev() + shiftKey ? this.focusFirst() : this.focusPrev() } else if (keyCode === KeyCodes.RIGHT || keyCode === KeyCodes.DOWN) { evt.preventDefault() - shift ? this.focusLast() : this.focusNext() + shiftKey ? this.focusLast() : this.focusNext() } }, getButtons() { @@ -307,7 +338,7 @@ export default { btn.focus() }, focusCurrent() { - // We do this in next tick to ensure buttons have finished rendering + // We do this in `$nextTick()` to ensure buttons have finished rendering this.$nextTick(() => { const btn = this.getButtons().find( el => toInteger(getAttr(el, 'aria-posinset')) === this.computedCurrentPage @@ -321,7 +352,7 @@ export default { }) }, focusFirst() { - // We do this in next tick to ensure buttons have finished rendering + // We do this in `$nextTick()` to ensure buttons have finished rendering this.$nextTick(() => { const btn = this.getButtons().find(el => !isDisabled(el)) if (btn && btn.focus && btn !== document.activeElement) { @@ -330,7 +361,7 @@ export default { }) }, focusLast() { - // We do this in next tick to ensure buttons have finished rendering + // We do this in `$nextTick()` to ensure buttons have finished rendering this.$nextTick(() => { const btn = this.getButtons() .reverse() @@ -341,7 +372,7 @@ export default { }) }, focusPrev() { - // We do this in next tick to ensure buttons have finished rendering + // We do this in `$nextTick()` to ensure buttons have finished rendering this.$nextTick(() => { const buttons = this.getButtons() const idx = buttons.indexOf(document.activeElement) @@ -351,7 +382,7 @@ export default { }) }, focusNext() { - // We do this in next tick to ensure buttons have finished rendering + // We do this in `$nextTick()` to ensure buttons have finished rendering this.$nextTick(() => { const buttons = this.getButtons() const idx = buttons.indexOf(document.activeElement) @@ -365,6 +396,7 @@ export default { render(h) { const buttons = [] const numberOfPages = this.localNumberOfPages + const pageNumbers = this.pageList.map(p => p.number) const disabled = this.disabled const { showFirstDots, showLastDots } = this.paginationParams const currentPage = this.computedCurrentPage @@ -372,12 +404,12 @@ export default { // Helper function and flag const isActivePage = pageNum => pageNum === currentPage - const noCurrPage = this.currentPage < 1 + const noCurrentPage = this.currentPage < 1 // Factory function for prev/next/first/last buttons - const makeEndBtn = (linkTo, ariaLabel, btnSlot, btnText, pageTest, key) => { + const makeEndBtn = (linkTo, ariaLabel, btnSlot, btnText, btnClass, pageTest, key) => { const isDisabled = - disabled || isActivePage(pageTest) || noCurrPage || linkTo < 1 || linkTo > numberOfPages + disabled || isActivePage(pageTest) || noCurrentPage || linkTo < 1 || linkTo > numberOfPages const pageNum = linkTo < 1 ? 1 : linkTo > numberOfPages ? numberOfPages : linkTo const scope = { disabled: isDisabled, page: pageNum, index: pageNum - 1 } const btnContent = this.normalizeSlot(btnSlot, scope) || toString(btnText) || h() @@ -409,7 +441,7 @@ export default { { key, staticClass: 'page-item', - class: { disabled: isDisabled, 'flex-fill': fill }, + class: [{ disabled: isDisabled, 'flex-fill': fill }, btnClass], attrs: { role: 'presentation', 'aria-hidden': isDisabled ? 'true' : null @@ -426,7 +458,7 @@ export default { { key: `ellipsis-${isLast ? 'last' : 'first'}`, staticClass: 'page-item', - class: ['disabled', 'bv-d-xs-down-none', fill ? 'flex-fill' : ''], + class: ['disabled', 'bv-d-xs-down-none', fill ? 'flex-fill' : '', this.ellipsisClass], attrs: { role: 'separator' } }, [ @@ -437,33 +469,47 @@ export default { ) } - // Goto First Page button bookend - buttons.push( - this.hideGotoEndButtons + // Goto first page button bookend + // Don't render button when `hideGotoEndButtons` is set or when + // `firstNumber` is enabled and the first page is in the page list + const $firstPageBtn = + this.hideGotoEndButtons || (this.firstNumber && pageNumbers.indexOf(1) !== -1) ? h() - : makeEndBtn(1, this.labelFirstPage, 'first-text', this.firstText, 1, 'bookend-goto-first') + : makeEndBtn( + 1, + this.labelFirstPage, + 'first-text', + this.firstNumber ? '1' : this.firstText, + this.firstClass, + 1, + 'bookend-goto-first' + ) + + // Goto previous page button bookend + const $prevPageBtn = makeEndBtn( + currentPage - 1, + this.labelPrevPage, + 'prev-text', + this.prevText, + this.prevClass, + 1, + 'bookend-goto-prev' ) - // Goto Previous page button bookend + // When `firstNumber` prop is set we move the previous page button + // before the first page button buttons.push( - makeEndBtn( - currentPage - 1, - this.labelPrevPage, - 'prev-text', - this.prevText, - 1, - 'bookend-goto-prev' - ) + ...(this.firstNumber ? [$prevPageBtn, $firstPageBtn] : [$firstPageBtn, $prevPageBtn]) ) // First Ellipsis Bookend buttons.push(showFirstDots ? makeEllipsis(false) : h()) - // Individual Page links + // Individual page links this.pageList.forEach((page, idx) => { - const active = isActivePage(page.number) && !noCurrPage + const active = isActivePage(page.number) && !noCurrentPage // Active page will have tabindex of 0, or if no current page and first page button - const tabIndex = disabled ? null : active || (noCurrPage && idx === 0) ? '0' : '-1' + const tabIndex = disabled ? null : active || (noCurrentPage && idx === 0) ? '0' : '-1' const attrs = { role: 'menuitemradio', 'aria-disabled': disabled ? 'true' : null, @@ -508,7 +554,7 @@ export default { { key: `page-${page.number}`, staticClass: 'page-item', - class: [{ disabled, active, 'flex-fill': fill }, page.classes], + class: [{ disabled, active, 'flex-fill': fill }, page.classes, this.pageClass], attrs: { role: 'presentation' } }, [inner] @@ -516,34 +562,39 @@ export default { ) }) - // Last Ellipsis Bookend + // Last ellipsis bookend buttons.push(showLastDots ? makeEllipsis(true) : h()) - // Goto Next page button bookend - buttons.push( - makeEndBtn( - currentPage + 1, - this.labelNextPage, - 'next-text', - this.nextText, - numberOfPages, - 'bookend-goto-next' - ) + // Goto next page button bookend + const $nextPageBtn = makeEndBtn( + currentPage + 1, + this.labelNextPage, + 'next-text', + this.nextText, + this.nextClass, + numberOfPages, + 'bookend-goto-next' ) - // Goto Last Page button bookend - buttons.push( - this.hideGotoEndButtons + // Goto last page button bookend + // Don't render button when `hideGotoEndButtons` is set or when + // `lastNumber` is enabled and the last page is in the page list + const $lastPageBtn = + this.hideGotoEndButtons || (this.lastNumber && pageNumbers.indexOf(numberOfPages) !== -1) ? h() : makeEndBtn( numberOfPages, this.labelLastPage, 'last-text', - this.lastText, + this.lastNumber ? toString(numberOfPages) : this.lastText, + this.lastClass, numberOfPages, 'bookend-goto-last' ) - ) + + // When `lastNumber` prop is set we move the next page button + // after the last page button + buttons.push(...(this.lastNumber ? [$lastPageBtn, $nextPageBtn] : [$nextPageBtn, $lastPageBtn])) // Assemble the pagination buttons const pagination = h( diff --git a/src/utils/config-set.js b/src/utils/config-set.js index f74fc069bbd..4a19d872696 100644 --- a/src/utils/config-set.js +++ b/src/utils/config-set.js @@ -8,6 +8,7 @@ import DEFAULTS from './config-defaults' // --- Constants --- +const NAME = 'BvConfig' const PROP_NAME = '$bvConfig' // Config manager class @@ -42,7 +43,7 @@ class BvConfig { configKeys.forEach(cmpName => { /* istanbul ignore next */ if (!hasOwnProperty(DEFAULTS, cmpName)) { - warn(`config: unknown config property "${cmpName}"`) + warn(`Unknown config property "${cmpName}"`, NAME) return } const cmpConfig = config[cmpName] @@ -55,7 +56,7 @@ class BvConfig { breakpoints.length < 2 || breakpoints.some(b => !isString(b) || b.length === 0) ) { - warn('config: "breakpoints" must be an array of at least 2 breakpoint names') + warn('"breakpoints" must be an array of at least 2 breakpoint names', NAME) } else { this.$_config.breakpoints = cloneDeep(breakpoints) } @@ -65,7 +66,7 @@ class BvConfig { props.forEach(prop => { /* istanbul ignore if */ if (!hasOwnProperty(DEFAULTS[cmpName], prop)) { - warn(`config: unknown config property "${cmpName}.${prop}"`) + warn(`Unknown config property "${cmpName}.${prop}"`, NAME) } else { // TODO: If we pre-populate the config with defaults, we can skip this line this.$_config[cmpName] = this.$_config[cmpName] || {} From baf5b6f46a04b566d1d7fce2614a02c649b3a2d8 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 14 Jan 2020 20:26:49 -0400 Subject: [PATCH 02/12] Update package.json --- src/components/pagination/package.json | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/pagination/package.json b/src/components/pagination/package.json index ced5d34854b..4ca4cb5f775 100644 --- a/src/components/pagination/package.json +++ b/src/components/pagination/package.json @@ -88,6 +88,36 @@ { "prop": "lastText", "description": "Content to place in the goto last page button" + }, + { + "prop": "firstClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to first page' button" + }, + { + "prop": "prevClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to previous page' button" + }, + { + "prop": "pageClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to page #' buttons" + }, + { + "prop": "nextClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to next page' button" + }, + { + "prop": "lastClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to last page' button" + }, + { + "prop": "ellipsisClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'ellipsis' placeholders" } ], "events": [ From 62a6448eb4326b4c90274f6acd774fb89f85f73e Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 14 Jan 2020 20:28:39 -0400 Subject: [PATCH 03/12] Update package.json --- src/components/pagination-nav/package.json | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/pagination-nav/package.json b/src/components/pagination-nav/package.json index 1731e51b01d..7e65b3f6bcc 100644 --- a/src/components/pagination-nav/package.json +++ b/src/components/pagination-nav/package.json @@ -104,6 +104,36 @@ { "prop": "lastText", "description": "Content to place in the goto last page button" + }, + { + "prop": "firstClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to first page' button" + }, + { + "prop": "prevClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to previous page' button" + }, + { + "prop": "pageClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to page #' buttons" + }, + { + "prop": "nextClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to next page' button" + }, + { + "prop": "lastClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'Go to last page' button" + }, + { + "prop": "ellipsisClass", + "version": "2.3.0", + "description": "Class(es) to appply to the 'ellipsis' placeholders" } ], "events": [ From 9d24362e77a624f3a07ced90682003853e215600 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 14 Jan 2020 20:38:10 -0400 Subject: [PATCH 04/12] Update package.json --- src/components/pagination-nav/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/pagination-nav/package.json b/src/components/pagination-nav/package.json index 7e65b3f6bcc..9eeb09173e7 100644 --- a/src/components/pagination-nav/package.json +++ b/src/components/pagination-nav/package.json @@ -61,6 +61,10 @@ "prop": "hideEllipsis", "description": "Do not show ellipsis buttons" }, + { + "prop": "ellipsisText", + "description": "Content to place in the ellipsis placeholder" + }, { "prop": "size", "description": "Size of the rendered buttons: 'sm', 'md' (default), or 'lg'" From dd7eb5261068b01c76ea6f3bbf418fb3ecd6823e Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 14 Jan 2020 20:38:37 -0400 Subject: [PATCH 05/12] Update package.json --- src/components/pagination/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/pagination/package.json b/src/components/pagination/package.json index 4ca4cb5f775..045f7d89f6f 100644 --- a/src/components/pagination/package.json +++ b/src/components/pagination/package.json @@ -45,6 +45,10 @@ "prop": "hideEllipsis", "description": "Do not show ellipsis buttons" }, + { + "prop": "ellipsisText", + "description": "Content to place in the ellipsis placeholder" + }, { "prop": "size", "description": "Size of the rendered buttons: 'sm', 'md' (default), or 'lg'" From 538ea8e868bbcd0c4350ab2f28e9adafe6f80341 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Thu, 23 Jan 2020 17:47:39 -0400 Subject: [PATCH 06/12] chore(pagination): tweaks to pagination PR #4622 (#4667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jacob Müller --- src/components/pagination/pagination.spec.js | 124 ++++++++++ src/mixins/pagination.js | 225 +++++++++++-------- 2 files changed, 255 insertions(+), 94 deletions(-) diff --git a/src/components/pagination/pagination.spec.js b/src/components/pagination/pagination.spec.js index 67950191b7e..a11fa24ce9f 100644 --- a/src/components/pagination/pagination.spec.js +++ b/src/components/pagination/pagination.spec.js @@ -814,6 +814,130 @@ describe('pagination', () => { wrapper.destroy() }) + it('fist-number and last-number props work', async () => { + // To be added... + + const wrapper = mount(BPagination, { + propsData: { + value: 1, + totalRows: 10, + perPage: 1, + limit: 5, + firstNumber: true, + lastNumber: true + } + }) + + wrapper.setProps({ + value: 2 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 3 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 4 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 5 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 6 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 7 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 8 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 9 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 10 + }) + await waitNT(wrapper.vm) + + wrapper.destroy() + }) + + it('fist-number and last-number props work with limit <=3', async () => { + // To be added... + + const wrapper = mount(BPagination, { + propsData: { + value: 1, + totalRows: 10, + perPage: 1, + limit: 3, + firstNumber: true, + lastNumber: true + } + }) + + wrapper.setProps({ + value: 2 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 3 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 4 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 5 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 6 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 7 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 8 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 9 + }) + await waitNT(wrapper.vm) + + wrapper.setProps({ + value: 10 + }) + await waitNT(wrapper.vm) + + wrapper.destroy() + }) + // These tests are wrapped in a new describe to limit the scope of the getBCR Mock describe('pagination keyboard navigation', () => { const origGetBCR = Element.prototype.getBoundingClientRect diff --git a/src/mixins/pagination.js b/src/mixins/pagination.js index 9f3a37fdc76..81e5d4da46e 100644 --- a/src/mixins/pagination.js +++ b/src/mixins/pagination.js @@ -9,7 +9,9 @@ import normalizeSlotMixin from '../mixins/normalize-slot' import { BLink } from '../components/link/link' // Common props, computed, data, render function, and methods -// for and +// for `` and `` + +// --- Constants --- // Threshold of limit size when we start/stop showing ellipsis const ELLIPSIS_THRESHOLD = 3 @@ -17,6 +19,8 @@ const ELLIPSIS_THRESHOLD = 3 // Default # of buttons limit const DEFAULT_LIMIT = 5 +// --- Helper methods --- + // Make an array of N to N+X const makePageArray = (startNumber, numberOfPages) => range(numberOfPages).map((val, i) => ({ number: startNumber + i, classes: null })) @@ -46,6 +50,7 @@ const onSpaceKey = evt => { } } +// --- Props --- export const props = { disabled: { type: Boolean, @@ -211,46 +216,70 @@ export default { }, paginationParams() { // Determine if we should show the the ellipsis - const limit = this.limit + const limit = this.localLimit const numberOfPages = this.localNumberOfPages const currentPage = this.computedCurrentPage const hideEllipsis = this.hideEllipsis + const firstNumber = this.firstNumber + const lastNumber = this.lastNumber let showFirstDots = false let showLastDots = false let numberOfLinks = limit let startNumber = 1 if (numberOfPages <= limit) { - // Special Case: Less pages available than the limit of displayed pages + // Special case: Less pages available than the limit of displayed pages numberOfLinks = numberOfPages } else if (currentPage < limit - 1 && limit > ELLIPSIS_THRESHOLD) { - // We are near the beginning of the page list - if (!hideEllipsis) { + if (!hideEllipsis || lastNumber) { showLastDots = true - numberOfLinks = limit - 1 + numberOfLinks = limit - (firstNumber ? 0 : 1) } + numberOfLinks = Math.min(numberOfLinks, limit) } else if (numberOfPages - currentPage + 2 < limit && limit > ELLIPSIS_THRESHOLD) { - // We are near the end of the list - if (!hideEllipsis) { - numberOfLinks = limit - 1 + 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 && !hideEllipsis) { + if (limit > ELLIPSIS_THRESHOLD) { numberOfLinks = limit - 2 - showFirstDots = showLastDots = true + showFirstDots = !!(!hideEllipsis || firstNumber) + showLastDots = !!(!hideEllipsis || lastNumber) } startNumber = currentPage - Math.floor(numberOfLinks / 2) } // Sanity checks + /* istanbul ignore if */ if (startNumber < 1) { - /* istanbul ignore next */ 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 + } + const 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 = Math.min(numberOfLinks + 1, numberOfPages, limit + 1) + } else if (lastNumber && numberOfPages === startNumber + numberOfLinks - 1) { + startNumber = Math.max(startNumber - 1, 1) + numberOfLinks = Math.min(numberOfPages - startNumber + 1, numberOfPages, limit + 1) + } } + numberOfLinks = Math.min(numberOfLinks, numberOfPages - startNumber + 1) return { showFirstDots, showLastDots, numberOfLinks, startNumber } }, pageList() { @@ -469,44 +498,8 @@ export default { ) } - // Goto first page button bookend - // Don't render button when `hideGotoEndButtons` is set or when - // `firstNumber` is enabled and the first page is in the page list - const $firstPageBtn = - this.hideGotoEndButtons || (this.firstNumber && pageNumbers.indexOf(1) !== -1) - ? h() - : makeEndBtn( - 1, - this.labelFirstPage, - 'first-text', - this.firstNumber ? '1' : this.firstText, - this.firstClass, - 1, - 'bookend-goto-first' - ) - - // Goto previous page button bookend - const $prevPageBtn = makeEndBtn( - currentPage - 1, - this.labelPrevPage, - 'prev-text', - this.prevText, - this.prevClass, - 1, - 'bookend-goto-prev' - ) - - // When `firstNumber` prop is set we move the previous page button - // before the first page button - buttons.push( - ...(this.firstNumber ? [$prevPageBtn, $firstPageBtn] : [$firstPageBtn, $prevPageBtn]) - ) - - // First Ellipsis Bookend - buttons.push(showFirstDots ? makeEllipsis(false) : h()) - - // Individual page links - this.pageList.forEach((page, idx) => { + // Page button factory + const makePageButton = (page, idx) => { const active = isActivePage(page.number) && !noCurrentPage // Active page will have tabindex of 0, or if no current page and first page button const tabIndex = disabled ? null : active || (noCurrentPage && idx === 0) ? '0' : '-1' @@ -548,56 +541,100 @@ export default { }, [this.normalizeSlot('page', scope) || btnContent] ) - buttons.push( - h( - 'li', - { - key: `page-${page.number}`, - staticClass: 'page-item', - class: [{ disabled, active, 'flex-fill': fill }, page.classes, this.pageClass], - attrs: { role: 'presentation' } - }, - [inner] - ) + return h( + 'li', + { + key: `page-${page.number}`, + staticClass: 'page-item', + class: [{ disabled, active, 'flex-fill': fill }, page.classes, this.pageClass], + attrs: { role: 'presentation' } + }, + [inner] + ) + } + + // Goto first page button + // Don't render button when `hideGotoEndButtons` or `firstNumber` is set + let $firstPageBtn = h() + if (!this.firstNumber && !this.hideGotoEndButtons) { + $firstPageBtn = makeEndBtn( + 1, + this.labelFirstPage, + 'first-text', + this.firstText, + this.firstClass, + 1, + 'pagination-goto-first' + ) + } + buttons.push($firstPageBtn) + + // Goto previous page button + buttons.push( + makeEndBtn( + currentPage - 1, + this.labelPrevPage, + '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((page, idx) => { + const offset = showFirstDots && this.firstNumber && pageNumbers[0] !== 1 ? 1 : 0 + buttons.push(makePageButton(page, idx + offset)) }) - // Last ellipsis bookend + // Last ellipsis buttons.push(showLastDots ? makeEllipsis(true) : h()) - // Goto next page button bookend - const $nextPageBtn = makeEndBtn( - currentPage + 1, - this.labelNextPage, - 'next-text', - this.nextText, - this.nextClass, - numberOfPages, - 'bookend-goto-next' + // Show last page button? + buttons.push( + this.lastNumber && pageNumbers[pageNumbers.length - 1] !== numberOfPages + ? makePageButton({ number: numberOfPages }, -1) + : h() ) - // Goto last page button bookend - // Don't render button when `hideGotoEndButtons` is set or when - // `lastNumber` is enabled and the last page is in the page list - const $lastPageBtn = - this.hideGotoEndButtons || (this.lastNumber && pageNumbers.indexOf(numberOfPages) !== -1) - ? h() - : makeEndBtn( - numberOfPages, - this.labelLastPage, - 'last-text', - this.lastNumber ? toString(numberOfPages) : this.lastText, - this.lastClass, - numberOfPages, - 'bookend-goto-last' - ) + // Goto next page button + buttons.push( + makeEndBtn( + currentPage + 1, + this.labelNextPage, + 'next-text', + this.nextText, + this.nextClass, + numberOfPages, + 'pagination-goto-next' + ) + ) - // When `lastNumber` prop is set we move the next page button - // after the last page button - buttons.push(...(this.lastNumber ? [$lastPageBtn, $nextPageBtn] : [$nextPageBtn, $lastPageBtn])) + // Goto last page button + // Don't render button when `hideGotoEndButtons` or `lastNumber` is set + let $lastPageBtn = h() + if (!this.lastNumber && !this.hideGotoEndButtons) { + $lastPageBtn = makeEndBtn( + numberOfPages, + this.labelLastPage, + 'last-text', + this.lastText, + this.lastClass, + numberOfPages, + 'pagination-goto-last' + ) + } + buttons.push($lastPageBtn) // Assemble the pagination buttons - const pagination = h( + const $pagination = h( 'ul', { ref: 'ul', @@ -613,7 +650,7 @@ export default { buttons ) - // if we are pagination-nav, wrap in '