hugo-book/assets/search.js
Gon Pombo d94e72e18d
feat search box (#1)
* feat search box

* replace flexshearch with lunrjs cause fuzzy + highlight

* add svg search. Add infinity scroll to avoid creating a new page. Add some comments
2022-10-06 22:11:49 -03:00

202 lines
5.5 KiB
JavaScript

'use strict';
{{ $searchDataFile := printf "%s.search-data.json" .Language.Lang }}
{{ $searchData := resources.Get "search-data.json" | resources.ExecuteAsTemplate $searchDataFile . | resources.Minify | resources.Fingerprint }}
(function () {
const searchDataURL = '{{ $searchData.RelPermalink }}';
const input = document.querySelector('#book-search-input input');
const searchOverlay = document.querySelector('#search-overlay')
const resultsContainer = document.querySelector('#book-search-hits');
const results = document.querySelector('#book-search-results ul');
const LIMIT_RESULTS = Infinity
const MIN_INPUT_SIZE = 2 // SDK ✅
const documents = new Map()
if (!input) {
return
}
// Listeners
input.addEventListener('focus', init);
input.addEventListener('keyup', search);
document.addEventListener('keypress', focusSearchFieldOnKeyPress);
searchOverlay.addEventListener('click', () => {
hideSearchBox()
})
/**
* @param {Event} event
*/
function focusSearchFieldOnKeyPress(event) {
if (event.target.value !== undefined) {
return;
}
if (input === document.activeElement) {
return;
}
const characterPressed = String.fromCharCode(event.charCode);
if (!isHotkey(characterPressed)) {
return;
}
input.focus();
event.preventDefault();
}
/**
* @param {String} character
* @returns {Boolean}
*/
function isHotkey(character) {
const dataHotkeys = input.getAttribute('data-hotkeys') || '';
return dataHotkeys.indexOf(character) >= 0;
}
function init() {
input.removeEventListener('focus', init); // init once
input.required = true;
fetch(searchDataURL)
.then(pages => pages.json())
.then(pages => {
window.lunrIdx = lunr(function() {
this.ref('id')
this.field('id')
this.field('content')
this.field('href')
this.metadataWhitelist = ['position']
for (const page of pages) {
documents.set(page.id, page)
this.add(page);
}
})
})
.then(() => input.required = false)
.then(search);
}
function search() {
const value = input.value?.trim()
if (input.required) {
return
}
while (results.firstChild) {
results.removeChild(results.firstChild);
}
if (!value || value.length <= MIN_INPUT_SIZE) {
hideSearchBox()
return;
}
function searchValue(fuzzy) {
// Operators:
// +: means AND. i.e +sdk +metaverse will found words that contains sdk & metaverse
// ~n: looks for N fuzzy words. i.e. metaverse~1 => metavese ✅
return value.split(' ').map(val => {
// Avoid blankspaces
if (!val) return
// if its a short word or fuzzy option is turned off, then return only the value with the +operator
if (val.length <= 4 || !fuzzy) return `+${val}`
return `+${val}~1`
}).filter(a => !!a).join(' ')
}
function getSearchHits() {
// First search for the words without fuzzy, so we can have a more accurate result.
const hits = window.lunrIdx.search(searchValue()).slice(0, LIMIT_RESULTS);
if (hits.length) return hits
return window.lunrIdx.search(searchValue(true)).slice(0, LIMIT_RESULTS);
}
const searchHits = getSearchHits()
showSearchBox()
if (!searchHits.length) {
resultCard(`Not Found`, `Sorry, we couldn't find any matches. Try searching for a different keyword`)
return
}
searchHits.forEach((hit) => {
const document = documents.get(Number(hit.ref))
if (!document) return
const highlightedContent = highlightContent(document.content, hit)
resultCard(document.title, highlightedContent, document.href)
});
}
function resultCard(title, content, href) {
const li = element('<li><a href><h4></h4><span></span></a></li>');
if (href) li.querySelector('a').href = href;
li.querySelector('h4').textContent = title;
li.querySelector('span').innerHTML = content
results.appendChild(li);
}
function highlightContent(content, hit) {
const amountLetters = 60
const { metadata } = hit.matchData
let from = 0
let to = 100
const keys = Object.keys(metadata).sort()
for (const key of keys) {
const positions = metadata[key]?.content?.position
if (!positions) {
continue
}
for (const position of positions) {
const positionStart = position[0]
from = Math.max(0, content.length - positionStart <= amountLetters
? positionStart - amountLetters * 2
: positionStart - amountLetters)
to = positionStart + position[1] + amountLetters
}
break
}
let value = content.slice(from, to)
if (from !== 0) {
value = `...${value}`
}
for (const key of keys) {
value = value.replace(new RegExp(key, 'gi'), '<strong>$&</strong>')
}
return value + '...'
}
// HELPERS
/**
* @param {String} content
* @returns {Node}
*/
function element(content) {
const div = document.createElement('div');
div.innerHTML = content;
return div.firstChild;
}
function hide(element) {
element.classList.add('hidden')
}
function show(element) {
element.classList.remove('hidden')
}
function showSearchBox() {
if (!resultsContainer.classList.contains('hidden')) {
return
}
resultsContainer.scrollTop = 0
show(searchOverlay)
show(resultsContainer)
}
function hideSearchBox() {
hide(searchOverlay)
hide(resultsContainer)
}
})();