111 строки
3.1 KiB
JavaScript
111 строки
3.1 KiB
JavaScript
const keycode = require('keycode')
|
|
const assert = require('assert')
|
|
|
|
module.exports = function searchWithYourKeyboard (inputSelector, hitsSelector) {
|
|
assert(typeof inputSelector === 'string', 'inputSelector should be a string')
|
|
assert(typeof hitsSelector === 'string', 'hitsSelector should be a string')
|
|
|
|
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
|
|
input.addEventListener('keydown', event => {
|
|
if (!targetEventCodes.includes(event.code)) {
|
|
activeIndex = 0
|
|
deactivateHits()
|
|
}
|
|
})
|
|
|
|
document.addEventListener('keydown', event => {
|
|
// bail early if key code is not one that we're explicity expecting
|
|
if (!event || !event.code || !targetEventCodes.includes(keycode(event))) return
|
|
|
|
const hits = getCurrentHits()
|
|
const queryExists = Boolean(input && input.value && input.value.length > 0)
|
|
|
|
switch (keycode(event)) {
|
|
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()
|
|
event.preventDefault() // prevent slash from being typed into input
|
|
}
|
|
break
|
|
|
|
case 'up':
|
|
if (!queryExists) return
|
|
|
|
// search input is the zero index (don't go beyond it)
|
|
if (activeIndex > 0) {
|
|
activeIndex--
|
|
event.preventDefault() // prevent window scrolling
|
|
}
|
|
updateActiveHit()
|
|
break
|
|
|
|
case 'down':
|
|
if (!queryExists) return
|
|
|
|
// last hit is the last index (don't go beyond it)
|
|
if (activeIndex < hits.length) {
|
|
activeIndex++
|
|
event.preventDefault() // prevent window scrolling
|
|
}
|
|
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
|
|
window.location = href
|
|
}
|
|
break
|
|
}
|
|
})
|
|
|
|
function deactivateHits () {
|
|
Array.from(document.querySelectorAll(hitsSelector)).forEach(hit => {
|
|
hit.classList.remove('active')
|
|
})
|
|
}
|
|
|
|
function getCurrentHits () {
|
|
return Array.from(document.querySelectorAll(hitsSelector)).filter(el => {
|
|
return el.style.display !== 'none' && el.offsetParent !== null // element is visible
|
|
})
|
|
}
|
|
|
|
function updateActiveHit () {
|
|
deactivateHits()
|
|
|
|
if (activeIndex === 0) {
|
|
input.focus()
|
|
input.select()
|
|
} else {
|
|
const hits = getCurrentHits()
|
|
hits[activeIndex - 1].classList.add('active')
|
|
input.blur()
|
|
}
|
|
}
|
|
}
|