Skip to content

Commit 99a85ff

Browse files
authored
Push query string when searching (#17417)
* Push query string when searching * Update search.js * Fix browser test, remove querystring dependency (new shiny!) * Remove language and version from visible URL * Avoid casting event interface * Update search.js * Update browser.js
1 parent 7dd6c93 commit 99a85ff

4 files changed

Lines changed: 76 additions & 51 deletions

File tree

javascripts/search.js

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ let $searchResultsContainer
1414
let $searchOverlay
1515
let $searchInput
1616

17+
// This is our default placeholder, but it can be localized with a <meta> tag
1718
let placeholder = 'Search topics, products...'
1819
let version
1920
let language
2021

2122
export default function search () {
23+
// First, only initialize search if the elements are on the page
2224
$searchInputContainer = document.getElementById('search-input-container')
2325
$searchResultsContainer = document.getElementById('search-results-container')
24-
2526
if (!$searchInputContainer || !$searchResultsContainer) return
2627

28+
// This overlay exists so if you click off the search, it closes
2729
$searchOverlay = document.querySelector('.search-overlay-desktop')
2830

2931
// There's an index for every version/language combination
@@ -36,15 +38,25 @@ export default function search () {
3638
placeholder = $placeholderMeta.content
3739
}
3840

41+
// Write the search form into its container
3942
$searchInputContainer.append(tmplSearchInput())
4043
$searchInput = $searchInputContainer.querySelector('input')
4144

42-
searchWithYourKeyboard('#search-input-container input', '.ais-Hits-item')
43-
toggleSearchDisplay()
44-
45+
// Prevent 'enter' from refreshing the page
4546
$searchInputContainer.querySelector('form')
4647
.addEventListener('submit', evt => evt.preventDefault())
48+
49+
// Search when the user finished typing
4750
$searchInput.addEventListener('keyup', debounce(onSearch))
51+
52+
// Adds ability to navigate search results with keyboard (up, down, enter, esc)
53+
searchWithYourKeyboard('#search-input-container input', '.ais-Hits-item')
54+
55+
// If the user already has a query in the URL, parse it and search away
56+
parseExistingSearch()
57+
58+
// If not on home page, decide if search panel should be open
59+
toggleSearchDisplay() // must come after parseExistingSearch
4860
}
4961

5062
// The home page and 404 pages have a standalone search
@@ -64,43 +76,37 @@ function toggleSearchDisplay () {
6476
// If not on homepage...
6577
if (hasStandaloneSearch()) return
6678

67-
const $input = $searchInput
68-
69-
// Open modal if input is clicked
70-
$input.addEventListener('focus', () => {
71-
openSearch()
72-
})
79+
// Open panel if input is clicked
80+
$searchInput.addEventListener('focus', openSearch)
7381

74-
// Close modal if overlay is clicked
82+
// Close panel if overlay is clicked
7583
if ($searchOverlay) {
76-
$searchOverlay.addEventListener('click', () => {
77-
closeSearch()
78-
})
84+
$searchOverlay.addEventListener('click', closeSearch)
7985
}
8086

81-
// Open modal if page loads with query in the params/input
82-
if ($input.value) {
87+
// Open panel if page loads with query in the params/input
88+
if ($searchInput.value) {
8389
openSearch()
8490
}
8591
}
8692

93+
// On most pages, opens the search panel
8794
function openSearch () {
8895
$searchInput.classList.add('js-open')
8996
$searchResultsContainer.classList.add('js-open')
9097
$searchOverlay.classList.add('js-open')
9198
}
9299

100+
// Close panel if not on homepage
93101
function closeSearch () {
94-
// Close modal if not on homepage
95102
if (!hasStandaloneSearch()) {
96103
$searchInput.classList.remove('js-open')
97104
$searchResultsContainer.classList.remove('js-open')
98105
$searchOverlay.classList.remove('js-open')
99106
}
100107

101-
const $hits = $searchResultsContainer.querySelector('.ais-Hits')
102-
if ($hits) $hits.style.display = 'none'
103108
$searchInput.value = ''
109+
onSearch()
104110
}
105111

106112
function deriveLanguageCodeFromPath () {
@@ -122,6 +128,7 @@ function deriveVersionFromPath () {
122128
: versionObject.miscBaseName
123129
}
124130

131+
// Wait for the event to stop triggering for X milliseconds before responding
125132
function debounce (fn, delay = 300) {
126133
let timer
127134
return (...args) => {
@@ -130,34 +137,47 @@ function debounce (fn, delay = 300) {
130137
}
131138
}
132139

133-
async function onSearch (evt) {
134-
const query = evt.target.value
135-
136-
const url = new URL(location.origin)
137-
url.pathname = '/search'
138-
url.search = new URLSearchParams({ query, version, language }).toString()
140+
// When the user finishes typing, update the results
141+
async function onSearch () {
142+
const query = $searchInput.value
139143

140-
const response = await fetch(url, {
141-
method: 'GET',
142-
headers: {
143-
'Content-Type': 'application/json'
144-
}
145-
})
146-
const results = response.ok ? await response.json() : []
144+
// Update the URL with the search parameters in the query string
145+
const pushUrl = new URL(location)
146+
pushUrl.search = query ? new URLSearchParams({ query }) : ''
147+
history.pushState({}, '', pushUrl)
148+
149+
// If there's a query, call the endpoint
150+
// Otherwise, there's no results by default
151+
let results = []
152+
if (query.trim()) {
153+
const endpointUrl = new URL(location.origin)
154+
endpointUrl.pathname = '/search'
155+
endpointUrl.search = new URLSearchParams({ language, version, query })
156+
157+
const response = await fetch(endpointUrl, {
158+
method: 'GET',
159+
headers: {
160+
'Content-Type': 'application/json'
161+
}
162+
})
163+
results = response.ok ? await response.json() : []
164+
}
147165

166+
// Either way, update the display
148167
$searchResultsContainer.querySelectorAll('*').forEach(el => el.remove())
149168
$searchResultsContainer.append(
150169
tmplSearchResults(results)
151170
)
152-
153171
toggleStandaloneSearch()
154172

155173
// Analytics tracking
156-
sendEvent({
157-
type: 'search',
158-
search_query: query
159-
// search_context
160-
})
174+
if (query.trim()) {
175+
sendEvent({
176+
type: 'search',
177+
search_query: query
178+
// search_context
179+
})
180+
}
161181
}
162182

163183
// If on homepage, toggle results container if query is present
@@ -189,6 +209,14 @@ function toggleStandaloneSearch () {
189209
if (queryPresent && $results) $results.style.display = 'block'
190210
}
191211

212+
// If the user shows up with a query in the URL, go ahead and search for it
213+
function parseExistingSearch () {
214+
const params = new URLSearchParams(location.search)
215+
if (!params.has('query')) return
216+
$searchInput.value = params.get('query')
217+
onSearch()
218+
}
219+
192220
/** * Template functions ***/
193221

194222
function tmplSearchInput () {

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@
7272
"parse5": "^6.0.1",
7373
"platform-utils": "^1.2.0",
7474
"port-used": "^2.0.8",
75-
"querystring": "^0.2.0",
7675
"rate-limit-redis": "^2.0.0",
7776
"react": "^17.0.1",
7877
"react-dom": "^17.0.1",

script/check-deps.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ const main = async () => {
3232
'@babel/*',
3333
'babel-preset-env',
3434
'@primer/*',
35-
'querystring',
3635
'pa11y-ci',
3736
'sass',
3837
'babel-loader',

tests/browser/browser.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* global page, browser */
22
const sleep = require('await-sleep')
3-
const querystring = require('querystring')
43
const { latest } = require('../../lib/enterprise-server-releases')
54

65
describe('homepage', () => {
@@ -12,7 +11,7 @@ describe('homepage', () => {
1211
})
1312
})
1413

15-
describe('algolia browser search', () => {
14+
describe('browser search', () => {
1615
jest.setTimeout(60 * 1000)
1716

1817
it('works on the homepage', async () => {
@@ -42,18 +41,18 @@ describe('algolia browser search', () => {
4241
expect(hits.length).toBeGreaterThan(5)
4342
})
4443

45-
it('sends the correct data to algolia for Enterprise Server', async () => {
44+
it('sends the correct data to search for Enterprise Server', async () => {
4645
expect.assertions(2)
4746

4847
const newPage = await browser.newPage()
49-
await newPage.goto('http://localhost:4001/ja/enterprise/2.22/admin/installation')
48+
await newPage.goto('http://localhost:4001/ja/enterprise-server@2.22/admin/installation')
5049

5150
await newPage.setRequestInterception(true)
5251
newPage.on('request', interceptedRequest => {
5352
if (interceptedRequest.method() === 'GET' && /search/i.test(interceptedRequest.url())) {
54-
const { version, language } = querystring.parse(interceptedRequest.url())
55-
expect(version).toBe('2.22')
56-
expect(language).toBe('ja')
53+
const { searchParams } = new URL(interceptedRequest.url())
54+
expect(searchParams.get('version')).toBe('2.22')
55+
expect(searchParams.get('language')).toBe('ja')
5756
}
5857
interceptedRequest.continue()
5958
})
@@ -63,7 +62,7 @@ describe('algolia browser search', () => {
6362
await newPage.waitForSelector('.search-result')
6463
})
6564

66-
it('sends the correct data to algolia for GHAE', async () => {
65+
it('sends the correct data to search for GHAE', async () => {
6766
expect.assertions(2)
6867

6968
const newPage = await browser.newPage()
@@ -72,9 +71,9 @@ describe('algolia browser search', () => {
7271
await newPage.setRequestInterception(true)
7372
newPage.on('request', interceptedRequest => {
7473
if (interceptedRequest.method() === 'GET' && /search/i.test(interceptedRequest.url())) {
75-
const { version, language } = querystring.parse(interceptedRequest.url())
76-
expect(version).toBe('ghae')
77-
expect(language).toBe('en')
74+
const { searchParams } = new URL(interceptedRequest.url())
75+
expect(searchParams.get('version')).toBe('ghae')
76+
expect(searchParams.get('language')).toBe('en')
7877
}
7978
interceptedRequest.continue()
8079
})

0 commit comments

Comments
 (0)