diff --git a/docs/examples/.eslintrc b/docs/examples/.eslintrc index 358c61473e..6f2fa1bdf5 100644 --- a/docs/examples/.eslintrc +++ b/docs/examples/.eslintrc @@ -39,6 +39,7 @@ "PageHeader", "PageItem", "Pager", + "Pagination", "Panel", "PanelGroup", "Popover", diff --git a/docs/examples/PaginationAdvanced.js b/docs/examples/PaginationAdvanced.js new file mode 100644 index 0000000000..c2a71a21a5 --- /dev/null +++ b/docs/examples/PaginationAdvanced.js @@ -0,0 +1,30 @@ +const PaginationAdvanced = React.createClass({ + getInitialState() { + return { + activePage: 1 + }; + }, + + handleSelect(event, selectedEvent) { + this.setState({ + activePage: selectedEvent.eventKey + }); + }, + + render() { + return ( + + ); + } +}); + +React.render(, mountNode); diff --git a/docs/examples/PaginationBasic.js b/docs/examples/PaginationBasic.js new file mode 100644 index 0000000000..852188333a --- /dev/null +++ b/docs/examples/PaginationBasic.js @@ -0,0 +1,41 @@ +const PaginationBasic = React.createClass({ + getInitialState() { + return { + activePage: 1 + }; + }, + + handleSelect(event, selectedEvent){ + this.setState({ + activePage: selectedEvent.eventKey + }); + }, + + render() { + return ( +
+ +
+ + +
+ + +
+ ); + } +}); + +React.render(, mountNode); diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js index ea408c7037..3a9de9e102 100644 --- a/docs/src/ComponentsPage.js +++ b/docs/src/ComponentsPage.js @@ -445,6 +445,19 @@ const ComponentsPage = React.createClass({ + {/* Pagination */} +
+

Pagination Pagination

+

Example pagination

+ +

Provide pagination links for your site or app with the multi-page pagination component. Set items to the number of pages. activePage prop dictates which page is active

+ + +

More options such as first, last, previous, next and ellipsis.

+ + +
+ {/* Alerts */}

Alert messages Alert

@@ -654,19 +667,20 @@ const ComponentsPage = React.createClass({ Navbars Togglable tabs Pager - Alerts - Carousels - Grids - Thumbnail - List group - Labels - Badges - Jumbotron - Page Header - Wells - Glyphicons - Tables - Input + Pagination + Alerts + Carousels + Grids + Thumbnail + List group + Labels + Badges + Jumbotron + Page Header + Wells + Glyphicons + Tables + Input Back to top diff --git a/docs/src/ReactPlayground.js b/docs/src/ReactPlayground.js index 097fd47fb5..0b33a304a3 100644 --- a/docs/src/ReactPlayground.js +++ b/docs/src/ReactPlayground.js @@ -32,6 +32,7 @@ import * as modOverlayMixin from '../../src/OverlayMixin'; import * as modPageHeader from '../../src/PageHeader'; import * as modPageItem from '../../src/PageItem'; import * as modPager from '../../src/Pager'; +import * as modPagination from '../../src/Pagination'; import * as modPanel from '../../src/Panel'; import * as modPanelGroup from '../../src/PanelGroup'; import * as modPopover from '../../src/Popover'; @@ -83,6 +84,7 @@ const OverlayTrigger = modOverlayTrigger.default; const OverlayMixin = modOverlayMixin.default; const PageHeader = modPageHeader.default; const PageItem = modPageItem.default; +const Pagination = modPagination.default; const Pager = modPager.default; const Panel = modPanel.default; const PanelGroup = modPanelGroup.default; diff --git a/docs/src/Samples.js b/docs/src/Samples.js index a9dcb3b496..c9b0aee582 100644 --- a/docs/src/Samples.js +++ b/docs/src/Samples.js @@ -14,7 +14,7 @@ export default { ButtonGroupNested: require('fs').readFileSync(__dirname + '/../examples/ButtonGroupNested.js', 'utf8'), ButtonGroupVertical: require('fs').readFileSync(__dirname + '/../examples/ButtonGroupVertical.js', 'utf8'), ButtonGroupJustified: require('fs').readFileSync(__dirname + '/../examples/ButtonGroupJustified.js', 'utf8'), - ButtonGroupBlock: require('fs').readFileSync(__dirname + '/../examples/ButtonGroupBlock.js', 'utf8'), + ButtonGroupBlock: require('fs').readFileSync(__dirname + '/../examples/ButtonGroupBlock.js', 'utf8'), DropdownButtonBasic: require('fs').readFileSync(__dirname + '/../examples/DropdownButtonBasic.js', 'utf8'), SplitButtonBasic: require('fs').readFileSync(__dirname + '/../examples/SplitButtonBasic.js', 'utf8'), DropdownButtonSizes: require('fs').readFileSync(__dirname + '/../examples/DropdownButtonSizes.js', 'utf8'), @@ -65,6 +65,8 @@ export default { PagerDefault: require('fs').readFileSync(__dirname + '/../examples/PagerDefault.js', 'utf8'), PagerAligned: require('fs').readFileSync(__dirname + '/../examples/PagerAligned.js', 'utf8'), PagerDisabled: require('fs').readFileSync(__dirname + '/../examples/PagerDisabled.js', 'utf8'), + PaginationBasic: require('fs').readFileSync(__dirname + '/../examples/PaginationBasic.js', 'utf8'), + PaginationAdvanced: require('fs').readFileSync(__dirname + '/../examples/PaginationAdvanced.js', 'utf8'), AlertBasic: require('fs').readFileSync(__dirname + '/../examples/AlertBasic.js', 'utf8'), AlertDismissable: require('fs').readFileSync(__dirname + '/../examples/AlertDismissable.js', 'utf8'), AlertAutoDismissable: require('fs').readFileSync(__dirname + '/../examples/AlertAutoDismissable.js', 'utf8'), diff --git a/src/Pagination.js b/src/Pagination.js new file mode 100644 index 0000000000..30a23991ff --- /dev/null +++ b/src/Pagination.js @@ -0,0 +1,167 @@ +import React from 'react'; +import classNames from 'classnames'; +import BootstrapMixin from './BootstrapMixin'; +import PaginationButton from './PaginationButton'; + +const Pagination = React.createClass({ + mixins: [BootstrapMixin], + + propTypes: { + activePage: React.PropTypes.number, + items: React.PropTypes.number, + maxButtons: React.PropTypes.number, + ellipsis: React.PropTypes.bool, + first: React.PropTypes.bool, + last: React.PropTypes.bool, + prev: React.PropTypes.bool, + next: React.PropTypes.bool, + onSelect: React.PropTypes.func + }, + + getDefaultProps() { + return { + activePage: 1, + items: 1, + maxButtons: 0, + first: false, + last: false, + prev: false, + next: false, + ellipsis: true, + bsClass: 'pagination' + }; + }, + + renderPageButtons() { + let pageButtons = []; + let startPage, endPage, hasHiddenPagesBefore, hasHiddenPagesAfter; + let { + maxButtons, + activePage, + items, + onSelect, + ellipsis + } = this.props; + + if(maxButtons){ + let hiddenPagesBefore = activePage - parseInt(maxButtons / 2); + startPage = hiddenPagesBefore > 1 ? hiddenPagesBefore : 1; + hasHiddenPagesAfter = startPage + maxButtons <= items; + + if(!hasHiddenPagesAfter){ + endPage = items; + startPage = items - maxButtons + 1; + } else { + endPage = startPage + maxButtons - 1; + } + } else { + startPage = 1; + endPage = items; + } + + for(let pagenumber = startPage; pagenumber <= endPage; pagenumber++){ + pageButtons.push( + + {pagenumber} + + ); + } + + if(maxButtons && hasHiddenPagesAfter && ellipsis){ + pageButtons.push( + + ... + + ); + } + + return pageButtons; + }, + + renderPrev() { + if(!this.props.prev){ + return null; + } + + return ( + + + + ); + }, + + renderNext() { + if(!this.props.next){ + return null; + } + + return ( + + + + ); + }, + + renderFirst() { + if(!this.props.first){ + return null; + } + + return ( + + « + + ); + }, + + renderLast() { + if(!this.props.last){ + return null; + } + + return ( + + » + + ); + }, + + render() { + let classes = this.getBsClassSet(); + return ( +
    + {this.renderFirst()} + {this.renderPrev()} + {this.renderPageButtons()} + {this.renderNext()} + {this.renderLast()} +
+ ); + } +}); + +export default Pagination; diff --git a/src/PaginationButton.js b/src/PaginationButton.js new file mode 100644 index 0000000000..0238b5b9b7 --- /dev/null +++ b/src/PaginationButton.js @@ -0,0 +1,51 @@ +import React from 'react'; +import classNames from 'classnames'; +import BootstrapMixin from './BootstrapMixin'; +import createSelectedEvent from './utils/createSelectedEvent'; + +const PaginationButton = React.createClass({ + mixins: [BootstrapMixin], + + propTypes: { + className: React.PropTypes.string, + eventKey: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number + ]), + onSelect: React.PropTypes.func, + disabled: React.PropTypes.bool, + active: React.PropTypes.bool + }, + + getDefaultProps() { + return { + active: false, + disabled: false + }; + }, + + handleClick(event) { + // This would go away once SafeAnchor is available + event.preventDefault(); + + if (this.props.onSelect) { + let selectedEvent = createSelectedEvent(this.props.eventKey); + this.props.onSelect(event, selectedEvent); + } + }, + + render() { + let classes = this.getBsClassSet(); + + classes.active = this.props.active; + classes.disabled = this.props.disabled; + + return ( +
  • + {this.props.children} +
  • + ); + } +}); + +export default PaginationButton; diff --git a/src/index.js b/src/index.js index c44952e71d..7d2d8e8e85 100644 --- a/src/index.js +++ b/src/index.js @@ -35,6 +35,7 @@ import ModalTrigger from './ModalTrigger'; import OverlayTrigger from './OverlayTrigger'; import OverlayMixin from './OverlayMixin'; import PageHeader from './PageHeader'; +import Pagination from './Pagination'; import Panel from './Panel'; import PanelGroup from './PanelGroup'; import PageItem from './PageItem'; @@ -95,6 +96,7 @@ export default { PanelGroup, PageItem, Pager, + Pagination, Popover, ProgressBar, Row, diff --git a/src/styleMaps.js b/src/styleMaps.js index 54694e58ed..a200286d33 100644 --- a/src/styleMaps.js +++ b/src/styleMaps.js @@ -13,6 +13,7 @@ const styleMaps = { 'list-group-item': 'list-group-item', 'panel': 'panel', 'panel-group': 'panel-group', + 'pagination': 'pagination', 'progress-bar': 'progress-bar', 'nav': 'nav', 'navbar': 'navbar', diff --git a/test/PaginationSpec.js b/test/PaginationSpec.js new file mode 100644 index 0000000000..d611ce4adc --- /dev/null +++ b/test/PaginationSpec.js @@ -0,0 +1,77 @@ +import React from 'react'; +import ReactTestUtils from 'react/lib/ReactTestUtils'; +import Pagination from '../src/Pagination'; + +describe('Pagination', function () { + it('Should have class', function () { + let instance = ReactTestUtils.renderIntoDocument( + Item content + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'pagination')); + }); + + it('Should show the correct active button', function () { + let instance = ReactTestUtils.renderIntoDocument( + + ); + let pageButtons = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'li'); + assert.equal(pageButtons.length, 5); + React.findDOMNode(pageButtons[2]).className.should.match(/\bactive\b/); + }); + + it('Should call onSelect when page button is selected', function (done) { + function onSelect(event, selectedEvent) { + assert.equal(selectedEvent.eventKey, 2); + done(); + } + + let instance = ReactTestUtils.renderIntoDocument( + + ); + + ReactTestUtils.Simulate.click( + ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'a')[1] + ); + }); + + it('Should only show part of buttons and active button in the middle of buttons when given maxButtons', function () { + let instance = ReactTestUtils.renderIntoDocument( + + ); + let pageButtons = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'li'); + // 9 visible page buttons and 1 ellipsis button + assert.equal(pageButtons.length, 10); + + // active button is the second one + assert.equal(React.findDOMNode(pageButtons[0]).firstChild.innerText, '6'); + React.findDOMNode(pageButtons[4]).className.should.match(/\bactive\b/); + }); + + it('Should show the ellipsis, first, last, prev and next button', function () { + let instance = ReactTestUtils.renderIntoDocument( + + ); + let pageButtons = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'li'); + // add first, last, prev, next and ellipsis button + assert.equal(pageButtons.length, 8); + + assert.equal(React.findDOMNode(pageButtons[0]).innerText, '«'); + assert.equal(React.findDOMNode(pageButtons[1]).innerText, '‹'); + assert.equal(React.findDOMNode(pageButtons[5]).innerText, '...'); + assert.equal(React.findDOMNode(pageButtons[6]).innerText, '›'); + assert.equal(React.findDOMNode(pageButtons[7]).innerText, '»'); + + }); +});