mirror of
https://github.com/adityatelange/hugo-PaperMod.git
synced 2026-05-21 11:05:49 +00:00
refactor(fastsearch): improve search initialization and result rendering
- refactored search index loading to use async/await for better readability - improved result rendering logic to handle empty results and focus management - added debounce to search input for performance optimization - defined default options for Fuse.js to streamline configuration
This commit is contained in:
+168
-130
@@ -1,156 +1,194 @@
|
|||||||
import * as params from '@params';
|
import * as params from '@params';
|
||||||
|
|
||||||
let fuse; // holds our search engine
|
const resList = document.getElementById('searchResults');
|
||||||
let resList = document.getElementById('searchResults');
|
const sInput = document.getElementById('searchInput');
|
||||||
let sInput = document.getElementById('searchInput');
|
const searchBox = document.getElementById('searchbox');
|
||||||
let first, last, current_elem = null
|
|
||||||
let resultsAvailable = false;
|
|
||||||
|
|
||||||
// load our search index
|
let fuse;
|
||||||
window.onload = function () {
|
let currentElement = null;
|
||||||
let xhr = new XMLHttpRequest();
|
let firstResult = null;
|
||||||
xhr.onreadystatechange = function () {
|
let lastResult = null;
|
||||||
if (xhr.readyState === 4) {
|
|
||||||
if (xhr.status === 200) {
|
const defaultFuseOptions = {
|
||||||
let data = JSON.parse(xhr.responseText);
|
|
||||||
if (data) {
|
|
||||||
// fuse.js options; check fuse.js website for details
|
|
||||||
let options = {
|
|
||||||
distance: 100,
|
distance: 100,
|
||||||
threshold: 0.4,
|
threshold: 0.4,
|
||||||
ignoreLocation: true,
|
ignoreLocation: true,
|
||||||
keys: [
|
keys: ['title', 'permalink', 'summary', 'content']
|
||||||
'title',
|
};
|
||||||
'permalink',
|
|
||||||
'summary',
|
const buildFuseOptions = () => {
|
||||||
'content'
|
if (!params.fuseOpts) {
|
||||||
]
|
return defaultFuseOptions;
|
||||||
};
|
}
|
||||||
if (params.fuseOpts) {
|
|
||||||
options = {
|
return {
|
||||||
isCaseSensitive: params.fuseOpts.iscasesensitive ?? false,
|
isCaseSensitive: params.fuseOpts.iscasesensitive ?? false,
|
||||||
includeScore: params.fuseOpts.includescore ?? false,
|
includeScore: params.fuseOpts.includescore ?? false,
|
||||||
includeMatches: params.fuseOpts.includematches ?? false,
|
includeMatches: params.fuseOpts.includematches ?? false,
|
||||||
minMatchCharLength: params.fuseOpts.minmatchcharlength ?? 1,
|
minMatchCharLength: params.fuseOpts.minmatchcharlength ?? 1,
|
||||||
shouldSort: params.fuseOpts.shouldsort ?? true,
|
shouldSort: params.fuseOpts.shouldsort ?? true,
|
||||||
findAllMatches: params.fuseOpts.findallmatches ?? false,
|
findAllMatches: params.fuseOpts.findallmatches ?? false,
|
||||||
keys: params.fuseOpts.keys ?? ['title', 'permalink', 'summary', 'content'],
|
keys: params.fuseOpts.keys ?? defaultFuseOptions.keys,
|
||||||
location: params.fuseOpts.location ?? 0,
|
location: params.fuseOpts.location ?? 0,
|
||||||
threshold: params.fuseOpts.threshold ?? 0.4,
|
threshold: params.fuseOpts.threshold ?? defaultFuseOptions.threshold,
|
||||||
distance: params.fuseOpts.distance ?? 100,
|
distance: params.fuseOpts.distance ?? defaultFuseOptions.distance,
|
||||||
ignoreLocation: params.fuseOpts.ignorelocation ?? true
|
ignoreLocation: params.fuseOpts.ignorelocation ?? defaultFuseOptions.ignoreLocation
|
||||||
}
|
|
||||||
}
|
|
||||||
fuse = new Fuse(data, options); // build the index from the json file
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(xhr.responseText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
xhr.open('GET', "../index.json");
|
};
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
function activeToggle(ae) {
|
const debounce = (fn, delay) => {
|
||||||
document.querySelectorAll('.focus').forEach(function (element) {
|
let timeout;
|
||||||
// rm focus class
|
return (...args) => {
|
||||||
element.classList.remove("focus")
|
clearTimeout(timeout);
|
||||||
});
|
timeout = window.setTimeout(() => fn(...args), delay);
|
||||||
if (ae) {
|
};
|
||||||
ae.focus()
|
};
|
||||||
document.activeElement = current_elem = ae;
|
|
||||||
ae.parentElement.classList.add("focus")
|
|
||||||
} else {
|
|
||||||
document.activeElement.parentElement.classList.add("focus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
const reset = () => {
|
||||||
resultsAvailable = false;
|
currentElement = null;
|
||||||
resList.innerHTML = sInput.value = ''; // clear inputbox and searchResults
|
firstResult = null;
|
||||||
sInput.focus(); // shift focus to input box
|
lastResult = null;
|
||||||
}
|
|
||||||
|
|
||||||
// execute search as each character is typed
|
|
||||||
sInput.onkeyup = function (e) {
|
|
||||||
// run a search query (for "term") every time a letter is typed
|
|
||||||
// in the search box
|
|
||||||
if (fuse) {
|
|
||||||
let results;
|
|
||||||
if (params.fuseOpts) {
|
|
||||||
results = fuse.search(this.value.trim(), { limit: params.fuseOpts.limit }); // the actual query being run using fuse.js along with options
|
|
||||||
} else {
|
|
||||||
results = fuse.search(this.value.trim()); // the actual query being run using fuse.js
|
|
||||||
}
|
|
||||||
if (results.length !== 0) {
|
|
||||||
// build our html if result exists
|
|
||||||
let resultSet = ''; // our results bucket
|
|
||||||
|
|
||||||
for (let item in results) {
|
|
||||||
resultSet +=
|
|
||||||
`<li>` +
|
|
||||||
`${results[item].item.title}` +
|
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-right"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg>` +
|
|
||||||
`<a class="entry-link" href="${results[item].item.permalink}" aria-label="${results[item].item.title}"></a>` +
|
|
||||||
`</li>`
|
|
||||||
}
|
|
||||||
|
|
||||||
resList.innerHTML = resultSet;
|
|
||||||
resultsAvailable = true;
|
|
||||||
first = resList.firstChild;
|
|
||||||
last = resList.lastChild;
|
|
||||||
} else {
|
|
||||||
resultsAvailable = false;
|
|
||||||
resList.innerHTML = '';
|
resList.innerHTML = '';
|
||||||
}
|
sInput.value = '';
|
||||||
}
|
sInput.focus();
|
||||||
}
|
};
|
||||||
|
|
||||||
sInput.addEventListener('search', function (e) {
|
const setActiveResult = (element) => {
|
||||||
// clicked on x
|
document.querySelectorAll('.focus').forEach((item) => item.classList.remove('focus'));
|
||||||
if (!this.value) reset()
|
|
||||||
})
|
|
||||||
|
|
||||||
// kb bindings
|
if (!element) {
|
||||||
document.onkeydown = function (e) {
|
return;
|
||||||
let key = e.key;
|
}
|
||||||
let ae = document.activeElement;
|
|
||||||
|
|
||||||
let inbox = document.getElementById("searchbox").contains(ae)
|
element.focus();
|
||||||
|
element.parentElement?.classList.add('focus');
|
||||||
|
currentElement = element;
|
||||||
|
};
|
||||||
|
|
||||||
if (ae === sInput) {
|
const renderResults = (results) => {
|
||||||
let elements = document.getElementsByClassName('focus');
|
if (!Array.isArray(results) || results.length === 0) {
|
||||||
while (elements.length > 0) {
|
resList.innerHTML = '';
|
||||||
elements[0].classList.remove('focus');
|
firstResult = lastResult = currentElement = null;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (current_elem) ae = current_elem;
|
|
||||||
|
|
||||||
if (key === "Escape") {
|
const fragment = document.createDocumentFragment();
|
||||||
reset()
|
|
||||||
} else if (!resultsAvailable || !inbox) {
|
for (const result of results) {
|
||||||
return
|
const li = document.createElement('li');
|
||||||
} else if (key === "ArrowDown") {
|
const titleText = document.createTextNode(result.item.title);
|
||||||
e.preventDefault();
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
if (ae == sInput) {
|
svg.setAttribute('width', '24');
|
||||||
// if the currently focused element is the search input, focus the <a> of first <li>
|
svg.setAttribute('height', '24');
|
||||||
activeToggle(resList.firstChild.lastChild);
|
svg.setAttribute('viewBox', '0 0 24 24');
|
||||||
} else if (ae.parentElement != last) {
|
svg.setAttribute('fill', 'none');
|
||||||
// if the currently focused element's parent is last, do nothing
|
svg.setAttribute('stroke', 'currentColor');
|
||||||
// otherwise select the next search result
|
svg.setAttribute('stroke-width', '2');
|
||||||
activeToggle(ae.parentElement.nextSibling.lastChild);
|
svg.setAttribute('stroke-linecap', 'round');
|
||||||
|
svg.setAttribute('stroke-linejoin', 'round');
|
||||||
|
svg.classList.add('feather', 'feather-chevrons-right');
|
||||||
|
|
||||||
|
svg.innerHTML = '<polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline>';
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.className = 'entry-link';
|
||||||
|
link.href = result.item.permalink;
|
||||||
|
link.setAttribute('aria-label', result.item.title);
|
||||||
|
|
||||||
|
li.appendChild(titleText);
|
||||||
|
li.appendChild(svg);
|
||||||
|
li.appendChild(link);
|
||||||
|
fragment.appendChild(li);
|
||||||
}
|
}
|
||||||
} else if (key === "ArrowUp") {
|
|
||||||
e.preventDefault();
|
resList.innerHTML = '';
|
||||||
if (ae.parentElement == first) {
|
resList.appendChild(fragment);
|
||||||
// if the currently focused element is first item, go to input box
|
firstResult = resList.firstElementChild;
|
||||||
activeToggle(sInput);
|
lastResult = resList.lastElementChild;
|
||||||
} else if (ae != sInput) {
|
};
|
||||||
// if the currently focused element is input box, do nothing
|
|
||||||
// otherwise select the previous search result
|
const performSearch = () => {
|
||||||
activeToggle(ae.parentElement.previousSibling.lastChild);
|
if (!fuse) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (key === "ArrowRight") {
|
|
||||||
ae.click(); // click on active link
|
const query = sInput.value.trim();
|
||||||
|
if (!query) {
|
||||||
|
renderResults([]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const searchOptions = params.fuseOpts?.limit ? { limit: params.fuseOpts.limit } : undefined;
|
||||||
|
const results = searchOptions ? fuse.search(query, searchOptions) : fuse.search(query);
|
||||||
|
renderResults(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initSearch = async () => {
|
||||||
|
if (!sInput || !resList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sInput.disabled = false;
|
||||||
|
sInput.focus();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('../index.json');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Search index load failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data) {
|
||||||
|
fuse = new Fuse(data, buildFuseOptions());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('load', initSearch);
|
||||||
|
|
||||||
|
sInput?.addEventListener('input', debounce(performSearch, 150));
|
||||||
|
|
||||||
|
sInput?.addEventListener('search', () => {
|
||||||
|
if (!sInput.value) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
const { key } = event;
|
||||||
|
const active = document.activeElement;
|
||||||
|
const isInSearchBox = searchBox?.contains(active);
|
||||||
|
|
||||||
|
if (key === 'Escape') {
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstResult || !isInSearchBox) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'ArrowDown') {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (active === sInput) {
|
||||||
|
setActiveResult(firstResult.querySelector('.entry-link'));
|
||||||
|
} else if (active?.parentElement !== lastResult) {
|
||||||
|
setActiveResult(active?.parentElement?.nextElementSibling?.querySelector('.entry-link'));
|
||||||
|
}
|
||||||
|
} else if (key === 'ArrowUp') {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (active?.parentElement === firstResult) {
|
||||||
|
setActiveResult(sInput);
|
||||||
|
} else if (active !== sInput) {
|
||||||
|
setActiveResult(active?.parentElement?.previousElementSibling?.querySelector('.entry-link'));
|
||||||
|
}
|
||||||
|
} else if (key === 'ArrowRight') {
|
||||||
|
if (active?.matches?.('.entry-link')) {
|
||||||
|
active.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="searchbox" class="searchbox">
|
<div id="searchbox" class="searchbox">
|
||||||
<input id="searchInput" autofocus placeholder="{{ .Params.placeholder | default (printf "%s ↵" .Title) }}"
|
<input id="searchInput" disabled placeholder="{{ .Params.placeholder | default (printf "%s ↵" .Title) }}"
|
||||||
aria-label="search" type="search" autocomplete="off" maxlength="64">
|
aria-label="search" type="search" autocomplete="off" maxlength="64">
|
||||||
<ul id="searchResults" class="searchResults" aria-label="search results"></ul>
|
<ul id="searchResults" class="searchResults" aria-label="search results"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user