195 lines
5.6 KiB
JavaScript
195 lines
5.6 KiB
JavaScript
import * as params from '@params';
|
|
|
|
const resList = document.getElementById('searchResults');
|
|
const sInput = document.getElementById('searchInput');
|
|
const searchBox = document.getElementById('searchbox');
|
|
|
|
let fuse;
|
|
let currentElement = null;
|
|
let firstResult = null;
|
|
let lastResult = null;
|
|
|
|
const defaultFuseOptions = {
|
|
distance: 100,
|
|
threshold: 0.4,
|
|
ignoreLocation: true,
|
|
keys: ['title', 'permalink', 'summary', 'content']
|
|
};
|
|
|
|
const buildFuseOptions = () => {
|
|
if (!params.fuseOpts) {
|
|
return defaultFuseOptions;
|
|
}
|
|
|
|
return {
|
|
isCaseSensitive: params.fuseOpts.iscasesensitive ?? false,
|
|
includeScore: params.fuseOpts.includescore ?? false,
|
|
includeMatches: params.fuseOpts.includematches ?? false,
|
|
minMatchCharLength: params.fuseOpts.minmatchcharlength ?? 1,
|
|
shouldSort: params.fuseOpts.shouldsort ?? true,
|
|
findAllMatches: params.fuseOpts.findallmatches ?? false,
|
|
keys: params.fuseOpts.keys ?? defaultFuseOptions.keys,
|
|
location: params.fuseOpts.location ?? 0,
|
|
threshold: params.fuseOpts.threshold ?? defaultFuseOptions.threshold,
|
|
distance: params.fuseOpts.distance ?? defaultFuseOptions.distance,
|
|
ignoreLocation: params.fuseOpts.ignorelocation ?? defaultFuseOptions.ignoreLocation
|
|
};
|
|
};
|
|
|
|
const debounce = (fn, delay) => {
|
|
let timeout;
|
|
return (...args) => {
|
|
clearTimeout(timeout);
|
|
timeout = window.setTimeout(() => fn(...args), delay);
|
|
};
|
|
};
|
|
|
|
const reset = () => {
|
|
currentElement = null;
|
|
firstResult = null;
|
|
lastResult = null;
|
|
resList.innerHTML = '';
|
|
sInput.value = '';
|
|
sInput.focus();
|
|
};
|
|
|
|
const setActiveResult = (element) => {
|
|
document.querySelectorAll('.focus').forEach((item) => item.classList.remove('focus'));
|
|
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
element.focus();
|
|
element.parentElement?.classList.add('focus');
|
|
currentElement = element;
|
|
};
|
|
|
|
const renderResults = (results) => {
|
|
if (!Array.isArray(results) || results.length === 0) {
|
|
resList.innerHTML = '';
|
|
firstResult = lastResult = currentElement = null;
|
|
return;
|
|
}
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
for (const result of results) {
|
|
const li = document.createElement('li');
|
|
const titleText = document.createTextNode(result.item.title);
|
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('width', '24');
|
|
svg.setAttribute('height', '24');
|
|
svg.setAttribute('viewBox', '0 0 24 24');
|
|
svg.setAttribute('fill', 'none');
|
|
svg.setAttribute('stroke', 'currentColor');
|
|
svg.setAttribute('stroke-width', '2');
|
|
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);
|
|
}
|
|
|
|
resList.innerHTML = '';
|
|
resList.appendChild(fragment);
|
|
firstResult = resList.firstElementChild;
|
|
lastResult = resList.lastElementChild;
|
|
};
|
|
|
|
const performSearch = () => {
|
|
if (!fuse) {
|
|
return;
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
});
|