Skip to content

Commit f2a1089

Browse files
authored
resolves #111 add pagination controls to search results (PR #112)
* use flexbox to layout search results footer * add pagination controls to search results footer * store the page in the search state * consolidate transformData function into queryDataCallback handler * extract queryHook logic as bound function * add schema version to search state
1 parent 60bc4c8 commit f2a1089

File tree

2 files changed

+98
-27
lines changed

2 files changed

+98
-27
lines changed

src/css/vendor/docsearch.css

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,40 @@
7171
}
7272

7373
.algolia-autocomplete .ds-footer {
74+
display: flex;
75+
justify-content: space-between;
7476
background-color: rgba(164, 167, 174, 0.1);
7577
border-top: 1px solid rgba(225, 225, 225, 0.5);
7678
padding: 10px 8px 8px;
7779
}
7880

79-
.algolia-autocomplete .ds-footer::after {
80-
content: "";
81-
display: table;
82-
clear: both;
81+
.algolia-autocomplete .ds-pagination {
82+
display: flex;
83+
align-items: center;
84+
gap: 0.5em;
85+
font-size: 0.8em;
86+
line-height: 1;
87+
}
88+
89+
.algolia-autocomplete .ds-pagination a {
90+
color: #174d8c;
91+
}
92+
93+
.algolia-autocomplete .ds-pagination--prev::before,
94+
.algolia-autocomplete .ds-pagination--next::after {
95+
display: inline-block;
96+
width: 0.8333em;
97+
text-align: center;
98+
font-size: 1.2em;
99+
line-height: 0;
100+
}
101+
102+
.algolia-autocomplete .ds-pagination--prev::before {
103+
content: "\2039";
104+
}
105+
106+
.algolia-autocomplete .ds-pagination--next::after {
107+
content: "\203a";
83108
}
84109

85110
.algolia-autocomplete .algolia-docsearch-footer {

src/js/vendor/docsearch.bundle.js

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
var SOLIDUS_KEY_CODE = 191
99
var SEARCH_FILTER_ACTIVE_KEY = 'docs:search-filter-active'
1010
var SAVED_SEARCH_STATE_KEY = 'docs:saved-search-state'
11+
var SAVED_SEARCH_STATE_VERSION = '1'
1112

1213
activateSearch(require('docsearch.js/dist/cdn/docsearch.js'), document.getElementById('search-script').dataset)
1314

1415
function activateSearch (docsearch, config) {
1516
appendStylesheet(config.stylesheet)
1617
var baseAlgoliaOptions = {
17-
hitsPerPage: parseInt(config.maxResults) || 15,
18+
hitsPerPage: parseInt(config.pageSize) || 20, // cannot exceed the hitsPerPage value defined on the index
1819
}
1920
var searchField = document.getElementById(config.searchFieldId || 'search')
2021
searchField.appendChild(Object.assign(document.createElement('div'), { className: 'algolia-autocomplete-results' }))
@@ -32,29 +33,27 @@
3233
autoWidth: false,
3334
templates: {
3435
footer:
35-
'<div class="ds-footer"><div class="algolia-docsearch-footer">' +
36+
'<div class="ds-footer"><div class="ds-pagination">' +
37+
'<span class="ds-pagination--curr">Page 1</span>' +
38+
'<a href="#" class="ds-pagination--prev">Prev</a>' +
39+
'<a href="#" class="ds-pagination--next">Next</a></div>' +
40+
'<div class="algolia-docsearch-footer">' +
3641
'Search by <a class="algolia-docsearch-footer--logo" href="https://www.algolia.com/docsearch" ' +
3742
'target="_blank" rel="noopener">Algolia</a>' +
3843
'</div></div>',
3944
},
4045
},
41-
algoliaOptions: baseAlgoliaOptions,
42-
transformData: protectHitOrder,
43-
queryHook:
44-
searchField.classList.contains('has-filter') &&
45-
function (query) {
46-
controller.algoliaOptions = typeahead.$facetFilterInput.prop('checked')
47-
? Object.assign({}, baseAlgoliaOptions, { facetFilters: [typeahead.$facetFilterInput.data('facetFilter')] })
48-
: baseAlgoliaOptions
49-
},
46+
baseAlgoliaOptions: baseAlgoliaOptions,
5047
})
5148
var input = controller.input
5249
var typeahead = input.data('aaAutocomplete')
5350
var dropdown = typeahead.dropdown
5451
var menu = dropdown.$menu
5552
var dataset = dropdown.datasets[0]
5653
dataset.cache = false
54+
dataset.source = controller.getAutocompleteSource(undefined, processQuery.bind(typeahead, controller))
5755
delete dataset.templates.footer
56+
controller.queryDataCallback = processQueryData.bind(typeahead)
5857
typeahead.setVal() // clear value on page reload
5958
input.on('autocomplete:closed', clearSearch.bind(typeahead))
6059
input.on('autocomplete:cursorchanged autocomplete:cursorremoved', saveSearchState.bind(typeahead))
@@ -71,6 +70,8 @@
7170
.find('.filter input')
7271
.on('change', toggleFilter.bind(typeahead))
7372
.prop('checked', window.localStorage.getItem(SEARCH_FILTER_ACTIVE_KEY) === 'true')
73+
menu.find('.ds-pagination--prev').on('click', paginate.bind(typeahead, -1)).css('visibility', 'hidden')
74+
menu.find('.ds-pagination--next').on('click', paginate.bind(typeahead, 1)).css('visibility', 'hidden')
7475
monitorCtrlKey.call(typeahead)
7576
searchField.addEventListener('click', confineEvent)
7677
document.documentElement.addEventListener('click', clearSearch.bind(typeahead))
@@ -87,6 +88,7 @@
8788
} else if (e.persisted && !isClosed(this)) {
8889
this.$input.focus()
8990
this.$input.val(this.getVal())
91+
this.dropdown.datasets[0].page = this.dropdown.$menu.find('.ds-pagination--curr').data('page')
9092
} else if (window.sessionStorage.getItem('docs:restore-search-on-back') === 'true') {
9193
if (!window.matchMedia('(min-width: 1024px)').matches) document.querySelector('.navbar-burger').click()
9294
restoreSearch.call(this)
@@ -104,7 +106,7 @@
104106
var restoring = dropdown.restoring
105107
delete dropdown.restoring
106108
if (isClosed(this)) return
107-
getScrollableResultsContainer(dropdown).scrollTop(0)
109+
updatePagination.call(dropdown)
108110
if (restoring && restoring.query === this.getVal() && restoring.filter === this.$facetFilterInput.prop('checked')) {
109111
var cursor = restoring.cursor
110112
if (cursor) dropdown._moveCursor(cursor)
@@ -164,10 +166,9 @@
164166
function onCtrlKeyDown (e) {
165167
if (e.keyCode !== CTRL_KEY_CODE) return
166168
this.ctrlKeyDown = true
167-
var dropdown = this.dropdown
168-
var container = getScrollableResultsContainer(dropdown)
169+
var container = getScrollableResultsContainer(this.dropdown)
169170
var prevScrollTop = container.scrollTop()
170-
dropdown.getCurrentCursor().find('a').focus()
171+
this.dropdown.getCurrentCursor().find('a').focus()
171172
container.scrollTop(prevScrollTop) // calling focus can cause the container to scroll, so restore it
172173
}
173174

@@ -195,10 +196,24 @@
195196
}
196197
}
197198

198-
function clearSearch () {
199-
this.isActivated = true // we can't rely on this state being correct
200-
this.setVal()
201-
delete this.ctrlKeyDown
199+
function paginate (delta, e) {
200+
e.preventDefault()
201+
var dataset = this.dropdown.datasets[0]
202+
dataset.page = (dataset.page || 0) + delta
203+
requery.call(this)
204+
}
205+
206+
function updatePagination () {
207+
var result = this.datasets[0].result
208+
var page = result.page
209+
var menu = this.$menu
210+
menu
211+
.find('.ds-pagination--curr')
212+
.html(result.pages ? 'Page ' + (page + 1) + ' of ' + result.pages : 'No results')
213+
.data('page', page)
214+
menu.find('.ds-pagination--prev').css('visibility', page > 0 ? '' : 'hidden')
215+
menu.find('.ds-pagination--next').css('visibility', result.pages > page + 1 ? '' : 'hidden')
216+
getScrollableResultsContainer(this).scrollTop(0)
202217
}
203218

204219
function requery (query) {
@@ -209,8 +224,34 @@
209224
this.dropdown.open()
210225
}
211226

227+
function clearSearch () {
228+
this.isActivated = true // we can't rely on this state being correct
229+
this.setVal()
230+
delete this.ctrlKeyDown
231+
delete this.dropdown.datasets[0].result
232+
}
233+
234+
function processQuery (controller, query) {
235+
var algoliaOptions = {}
236+
if (this.$facetFilterInput.prop('checked')) {
237+
algoliaOptions.facetFilters = [this.$facetFilterInput.data('facetFilter')]
238+
}
239+
var dataset = this.dropdown.datasets[0]
240+
var activeResult = dataset.result
241+
algoliaOptions.page = !activeResult || query === activeResult.query ? dataset.page || 0 : (dataset.page = 0)
242+
controller.algoliaOptions = Object.keys(algoliaOptions).length
243+
? Object.assign({}, controller.baseAlgoliaOptions, algoliaOptions)
244+
: controller.baseAlgoliaOptions
245+
}
246+
247+
function processQueryData (data) {
248+
var result = data.results[0]
249+
this.dropdown.datasets[0].result = { page: result.page, pages: result.nbPages, query: result.query }
250+
result.hits = preserveHitOrder(result.hits)
251+
}
252+
212253
// preserves the original order of results by qualifying unique occurrences of the same lvl0 and lvl1 values
213-
function protectHitOrder (hits) {
254+
function preserveHitOrder (hits) {
214255
var prevLvl0
215256
var lvl0Qualifiers = {}
216257
var lvl1Qualifiers = {}
@@ -236,7 +277,7 @@
236277
function readSavedSearchState () {
237278
try {
238279
var state = window.localStorage.getItem(SAVED_SEARCH_STATE_KEY)
239-
if (state) return JSON.parse(state)
280+
if (state && (state = JSON.parse(state))._version.toString() === SAVED_SEARCH_STATE_VERSION) return state
240281
} catch (e) {
241282
window.localStorage.removeItem(SAVED_SEARCH_STATE_KEY)
242283
}
@@ -247,6 +288,9 @@
247288
if (!searchState) return
248289
this.dropdown.restoring = searchState
249290
this.$facetFilterInput.prop('checked', searchState.filter) // change event will be ignored
291+
var dataset = this.dropdown.datasets[0]
292+
dataset.page = searchState.page
293+
delete dataset.result
250294
requery.call(this, searchState.query) // cursor is restored by onResultsUpdated =>
251295
}
252296

@@ -255,9 +299,11 @@
255299
window.localStorage.setItem(
256300
SAVED_SEARCH_STATE_KEY,
257301
JSON.stringify({
258-
query: this.getVal(),
259-
filter: this.$facetFilterInput.prop('checked'),
302+
_version: SAVED_SEARCH_STATE_VERSION,
260303
cursor: this.dropdown.getCurrentCursor().index() + 1,
304+
filter: this.$facetFilterInput.prop('checked'),
305+
page: this.dropdown.datasets[0].page,
306+
query: this.getVal(),
261307
})
262308
)
263309
}

0 commit comments

Comments
 (0)