Skip to content

Commit fad2720

Browse files
author
vvo
committed
feat: pagination component
Mostly based on the work in ui-kit. Differences: - default padding is 3 not 5 - labels are strings and merged with the default ones - there is no wrapping <nav> around, only the <ul> - <, <<, >, >> are hidden if not available instead of disabled. Showing a no-click sign felt frustrating. If not available, not shown, not needed. Still needed: - hitsPerPage param - maxPages param - page href reads the state and generate a real link
1 parent 84f325d commit fad2720

File tree

8 files changed

+298
-1
lines changed

8 files changed

+298
-1
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,27 @@ npm run test:coverage
8383
})
8484
);
8585
```
86+
87+
## pagination
88+
89+
```html
90+
<div id="pagination"></div>
91+
```
92+
93+
```js
94+
instant.addWidget(
95+
instantsearch.widgets.pagination({
96+
container: '#pagination',
97+
// cssClass: 'pagination', // no default
98+
// padding: 3, // number of page numbers to show before/after current
99+
// showFirstLast: true, // show or hide first and last links
100+
// labels: {
101+
// previous: '‹', // &lsaquo;
102+
// next: '›', // &rsaquo;
103+
// first: '«', // &laquo;
104+
// last: '»' // &raquo;
105+
// }
106+
})
107+
);
108+
```
109+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
var React = require('react');
4+
5+
class PaginationLink extends React . Component {
6+
render() {
7+
var label = this.props.label;
8+
var ariaLabel = this.props.ariaLabel;
9+
var href = this.props.href;
10+
var page = this.props.page;
11+
var className = this.props.className;
12+
13+
return (
14+
<li className={className}>
15+
<a href={href} aria-label={ariaLabel} onClick={this.click.bind(this, page)}>
16+
{label}
17+
</a>
18+
</li>
19+
);
20+
}
21+
22+
clickDisabled(e) {
23+
e.preventDefault();
24+
}
25+
26+
click(page, e) {
27+
e.preventDefault();
28+
this.props.setCurrentPage(page).search();
29+
}
30+
}
31+
32+
PaginationLink.propTypes = {
33+
ariaLabel: React.PropTypes.oneOfType([
34+
React.PropTypes.string,
35+
React.PropTypes.number
36+
]).isRequired,
37+
className: React.PropTypes.string,
38+
href: React.PropTypes.string.isRequired,
39+
label: React.PropTypes.oneOfType([
40+
React.PropTypes.string,
41+
React.PropTypes.number
42+
]).isRequired,
43+
page: React.PropTypes.number.isRequired,
44+
setCurrentPage: React.PropTypes.func.isRequired
45+
};
46+
47+
module.exports = PaginationLink;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
var range = require('lodash/utility/range');
4+
5+
class Paginator {
6+
constructor(params) {
7+
this.currentPage = params.currentPage;
8+
this.total = params.total;
9+
this.padding = params.padding;
10+
}
11+
12+
pages() {
13+
var current = this.currentPage;
14+
var padding = this.padding;
15+
var paddingLeft = this.calculatePaddingLeft(current, padding, this.total);
16+
var paddingRight = Math.min(2 * padding + 1, this.total) - paddingLeft;
17+
var first = current - paddingLeft;
18+
var last = current + paddingRight;
19+
return range(first, last);
20+
}
21+
22+
calculatePaddingLeft(current, padding, total) {
23+
if (current <= padding) {
24+
return current;
25+
}
26+
27+
if (current >= (total - padding)) {
28+
return 2 * padding + 1 - (total - current);
29+
}
30+
31+
return padding;
32+
}
33+
34+
isLastPage() {
35+
return this.currentPage === this.total - 1;
36+
}
37+
38+
isFirstPage() {
39+
return this.currentPage === 0;
40+
}
41+
}
42+
43+
module.exports = Paginator;

components/Pagination/index.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
'use strict';
2+
3+
var React = require('react');
4+
var forEach = require('lodash/collection/forEach');
5+
var defaultsDeep = require('lodash/object/defaultsDeep');
6+
7+
var Paginator = require('./Paginator');
8+
var PaginationLink = require('./PaginationLink');
9+
10+
var bem = require('../BemHelper')('as-pagination');
11+
var cx = require('classnames');
12+
13+
class Pagination extends React.Component {
14+
constructor(props) {
15+
super(defaultsDeep(props, Pagination.defaultProps));
16+
}
17+
18+
render() {
19+
if (this.props.nbHits === 0) {
20+
return null;
21+
}
22+
23+
var pager = new Paginator({
24+
currentPage: this.props.currentPage,
25+
total: this.props.nbPages,
26+
padding: this.props.padding
27+
});
28+
29+
var classNames = cx(bem('ul'), this.props.cssClass);
30+
31+
return (
32+
<ul className={classNames}>
33+
{this.props.showFirstLast ? this.firstPageLink(pager) : null}
34+
{this.previousPageLink(pager)}
35+
{this.pages(pager)}
36+
{this.nextPageLink(pager)}
37+
{this.props.showFirstLast ? this.lastPageLink(pager) : null}
38+
</ul>
39+
);
40+
}
41+
42+
previousPageLink(pager) {
43+
if (pager.isFirstPage()) return null;
44+
45+
return (
46+
<PaginationLink
47+
href="#"
48+
label={this.props.labels.prev} ariaLabel="Previous"
49+
setCurrentPage={this.props.setCurrentPage}
50+
page={pager.currentPage - 1} />
51+
);
52+
}
53+
54+
nextPageLink(pager) {
55+
if (pager.isLastPage()) return null;
56+
57+
return (
58+
<PaginationLink
59+
href="#"
60+
label={this.props.labels.next} ariaLabel="Next"
61+
setCurrentPage={this.props.setCurrentPage}
62+
page={pager.currentPage + 1} />
63+
);
64+
}
65+
66+
firstPageLink(pager) {
67+
if (pager.isFirstPage()) return null;
68+
69+
return (
70+
<PaginationLink
71+
href="#"
72+
label={this.props.labels.first}
73+
ariaLabel="First"
74+
setCurrentPage={this.props.setCurrentPage}
75+
page={0} />
76+
);
77+
}
78+
79+
lastPageLink(pager) {
80+
if (pager.isLastPage()) return null;
81+
82+
return (
83+
<PaginationLink
84+
href="#"
85+
label={this.props.labels.last}
86+
ariaLabel="Last"
87+
setCurrentPage={this.props.setCurrentPage}
88+
page={pager.total - 1} />
89+
);
90+
}
91+
92+
pages(pager) {
93+
var elements = [];
94+
95+
forEach(pager.pages(), function(pageNumber) {
96+
var className = pageNumber === pager.currentPage ? 'active' : '';
97+
98+
elements.push(
99+
<PaginationLink
100+
href="#"
101+
label={pageNumber + 1}
102+
ariaLabel={pageNumber + 1}
103+
setCurrentPage={this.props.setCurrentPage}
104+
page={pageNumber}
105+
key={pageNumber}
106+
className={className} />
107+
);
108+
}, this);
109+
110+
return elements;
111+
}
112+
}
113+
114+
Pagination.propTypes = {
115+
nbHits: React.PropTypes.number,
116+
currentPage: React.PropTypes.number,
117+
nbPages: React.PropTypes.number,
118+
labels: React.PropTypes.shape({
119+
prev: React.PropTypes.string,
120+
next: React.PropTypes.string,
121+
first: React.PropTypes.string,
122+
last: React.PropTypes.string
123+
}),
124+
showFirstLast: React.PropTypes.bool,
125+
padding: React.PropTypes.number,
126+
setCurrentPage: React.PropTypes.func.isRequired,
127+
cssClass: React.PropTypes.oneOfType([
128+
React.PropTypes.string,
129+
React.PropTypes.array
130+
])
131+
};
132+
133+
Pagination.defaultProps = {
134+
nbHits: 0,
135+
currentPage: 0,
136+
nbPages: 0,
137+
labels: {
138+
prev: '‹', // &lsaquo;
139+
next: '›', // &rsaquo;
140+
first: '«', // &laquo;
141+
last: '»' // &raquo;
142+
},
143+
showFirstLast: true,
144+
padding: 3
145+
};
146+
147+
module.exports = Pagination;

example/app.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,11 @@ instant.addWidget(
2626
})
2727
);
2828

29+
instant.addWidget(
30+
instantsearch.widgets.pagination({
31+
container: '#pagination',
32+
cssClass: 'pagination'
33+
})
34+
);
35+
2936
instant.start();

example/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ <h1>Instant search demo <small>using instantsearch.js</small></h1>
1919
</div>
2020
<div class="col-md-9">
2121
<div id="hits"></div>
22+
<div id="pagination" class="text-center"></div>
2223
</div>
2324
</div>
2425
</div>

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = {
44
InstantSearch: require('./lib/InstantSearch'),
55
widgets: {
66
searchBox: require('./widgets/search-box/'),
7-
results: require('./widgets/hits/')
7+
results: require('./widgets/hits/'),
8+
pagination: require('./widgets/pagination/')
89
}
910
};

widgets/pagination/index.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
3+
var React = require('react');
4+
5+
var utils = require('../../lib/widgetUtils.js');
6+
7+
function hits(params) {
8+
var Pagination = require('../../components/Pagination/');
9+
var containerNode = utils.getContainerNode(params.container);
10+
11+
return {
12+
render: function(results, state, helper) {
13+
React.render(
14+
<Pagination
15+
nbHits={results.nbHits}
16+
currentPage={results.page}
17+
nbPages={results.nbPages}
18+
setCurrentPage={helper.setCurrentPage.bind(helper)}
19+
cssClass={params.cssClass}
20+
labels={params.labels} />,
21+
containerNode
22+
);
23+
}
24+
};
25+
}
26+
27+
module.exports = hits;

0 commit comments

Comments
 (0)