2018-06-01 22:22:39 +03:00
|
|
|
const keycode = require('keycode')
|
|
|
|
|
2018-06-03 22:44:48 +03:00
|
|
|
module.exports = function searchWithYourKeyboard (inputSelector, hitsSelector) {
|
2021-01-13 19:09:18 +03:00
|
|
|
if (typeof inputSelector !== 'string') {
|
|
|
|
throw new TypeError('inputSelector should be a string')
|
|
|
|
}
|
|
|
|
if (typeof hitsSelector !== 'string') {
|
|
|
|
throw new TypeError('hitsSelector should be a string')
|
|
|
|
}
|
2018-06-01 22:22:39 +03:00
|
|
|
|
|
|
|
let activeIndex = 0
|
|
|
|
const targetEventCodes = ['up', 'down', 'enter', '/', 'esc']
|
|
|
|
const input = document.querySelector(inputSelector)
|
|
|
|
|
|
|
|
// deactivate any active hit when search input is focused by a mouse click
|
|
|
|
input.addEventListener('focus', () => {
|
|
|
|
activeIndex = 0
|
|
|
|
deactivateHits()
|
|
|
|
})
|
|
|
|
|
|
|
|
// deactivate any active hit when typing in search box
|
2018-06-18 23:11:33 +03:00
|
|
|
input.addEventListener('keydown', event => {
|
2018-06-01 22:22:39 +03:00
|
|
|
if (!targetEventCodes.includes(event.code)) {
|
|
|
|
activeIndex = 0
|
|
|
|
deactivateHits()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2018-06-18 23:11:33 +03:00
|
|
|
document.addEventListener('keydown', event => {
|
2018-06-01 22:22:39 +03:00
|
|
|
// bail early if key code is not one that we're explicity expecting
|
|
|
|
if (!event || !event.code || !targetEventCodes.includes(keycode(event))) return
|
|
|
|
|
2018-06-21 01:36:59 +03:00
|
|
|
const hits = getCurrentHits()
|
2018-06-21 00:29:49 +03:00
|
|
|
const queryExists = Boolean(input && input.value && input.value.length > 0)
|
2018-06-01 22:22:39 +03:00
|
|
|
|
2019-02-19 13:00:19 +03:00
|
|
|
// If we on `input` element we skip all events, if not
|
|
|
|
// we accept all events described bottom.
|
2020-10-14 20:31:33 +03:00
|
|
|
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName)) return false
|
2019-02-19 13:00:19 +03:00
|
|
|
|
2018-06-18 23:11:33 +03:00
|
|
|
switch (keycode(event)) {
|
2018-06-01 22:22:39 +03:00
|
|
|
case 'esc':
|
|
|
|
input.focus()
|
|
|
|
input.select()
|
|
|
|
input.value = ''
|
|
|
|
return
|
|
|
|
|
|
|
|
case '/':
|
|
|
|
// when the input is focused, `/` should have no special behavior
|
|
|
|
if (event.target !== input) {
|
|
|
|
input.focus()
|
|
|
|
input.select()
|
2018-06-20 23:48:12 +03:00
|
|
|
event.preventDefault() // prevent slash from being typed into input
|
2018-06-01 22:22:39 +03:00
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case 'up':
|
2018-06-21 00:29:49 +03:00
|
|
|
if (!queryExists) return
|
|
|
|
|
2018-06-01 22:22:39 +03:00
|
|
|
// search input is the zero index (don't go beyond it)
|
2018-06-18 23:11:33 +03:00
|
|
|
if (activeIndex > 0) {
|
|
|
|
activeIndex--
|
|
|
|
event.preventDefault() // prevent window scrolling
|
|
|
|
}
|
2018-06-01 22:22:39 +03:00
|
|
|
updateActiveHit()
|
|
|
|
break
|
|
|
|
|
|
|
|
case 'down':
|
2018-06-21 00:29:49 +03:00
|
|
|
if (!queryExists) return
|
|
|
|
|
2018-06-01 22:22:39 +03:00
|
|
|
// last hit is the last index (don't go beyond it)
|
2018-06-18 23:11:33 +03:00
|
|
|
if (activeIndex < hits.length) {
|
|
|
|
activeIndex++
|
|
|
|
event.preventDefault() // prevent window scrolling
|
|
|
|
}
|
2018-06-01 22:22:39 +03:00
|
|
|
updateActiveHit()
|
|
|
|
break
|
|
|
|
|
|
|
|
case 'enter':
|
|
|
|
// look for a link in the given hit, then visit it
|
|
|
|
if (activeIndex > 0) {
|
|
|
|
const hit = hits[activeIndex - 1]
|
|
|
|
if (!hit) return
|
|
|
|
const link = hit.querySelector('a')
|
|
|
|
if (!link) return
|
|
|
|
const href = link.getAttribute('href')
|
|
|
|
if (!href) return
|
2018-07-24 18:48:57 +03:00
|
|
|
|
|
|
|
// If `ctrlKey` is pressed, opens page in new window.
|
|
|
|
// In all other cases, we open the page in the same window.
|
|
|
|
if (event.ctrlKey) {
|
|
|
|
window.open(href, '_blank')
|
|
|
|
// NOTE: The `window.focus()` method not work correctly
|
|
|
|
// on some browser or OS. It don't focus window in
|
|
|
|
// Chrome browser.
|
|
|
|
window.focus()
|
|
|
|
} else {
|
|
|
|
window.location = href
|
|
|
|
}
|
2018-06-01 22:22:39 +03:00
|
|
|
}
|
|
|
|
break
|
2018-06-18 23:11:33 +03:00
|
|
|
}
|
2018-06-01 22:22:39 +03:00
|
|
|
})
|
|
|
|
|
|
|
|
function deactivateHits () {
|
|
|
|
Array.from(document.querySelectorAll(hitsSelector)).forEach(hit => {
|
|
|
|
hit.classList.remove('active')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-06-21 01:36:59 +03:00
|
|
|
function getCurrentHits () {
|
|
|
|
return Array.from(document.querySelectorAll(hitsSelector)).filter(el => {
|
|
|
|
return el.style.display !== 'none' && el.offsetParent !== null // element is visible
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-06-18 23:11:33 +03:00
|
|
|
function updateActiveHit () {
|
2018-06-01 22:22:39 +03:00
|
|
|
deactivateHits()
|
|
|
|
|
|
|
|
if (activeIndex === 0) {
|
|
|
|
input.focus()
|
|
|
|
input.select()
|
|
|
|
} else {
|
2018-06-21 01:36:59 +03:00
|
|
|
const hits = getCurrentHits()
|
2018-06-01 22:22:39 +03:00
|
|
|
hits[activeIndex - 1].classList.add('active')
|
|
|
|
input.blur()
|
|
|
|
}
|
|
|
|
}
|
2018-06-18 23:11:33 +03:00
|
|
|
}
|