blog

Log | Files | Refs

fastsearch.js (5711B)


      1 import * as params from '@params';
      2 
      3 const resList = document.getElementById('searchResults');
      4 const sInput = document.getElementById('searchInput');
      5 const searchBox = document.getElementById('searchbox');
      6 
      7 let fuse;
      8 let currentElement = null;
      9 let firstResult = null;
     10 let lastResult = null;
     11 
     12 const defaultFuseOptions = {
     13     distance: 100,
     14     threshold: 0.4,
     15     ignoreLocation: true,
     16     keys: ['title', 'permalink', 'summary', 'content']
     17 };
     18 
     19 const buildFuseOptions = () => {
     20     if (!params.fuseOpts) {
     21         return defaultFuseOptions;
     22     }
     23 
     24     return {
     25         isCaseSensitive: params.fuseOpts.iscasesensitive ?? false,
     26         includeScore: params.fuseOpts.includescore ?? false,
     27         includeMatches: params.fuseOpts.includematches ?? false,
     28         minMatchCharLength: params.fuseOpts.minmatchcharlength ?? 1,
     29         shouldSort: params.fuseOpts.shouldsort ?? true,
     30         findAllMatches: params.fuseOpts.findallmatches ?? false,
     31         keys: params.fuseOpts.keys ?? defaultFuseOptions.keys,
     32         location: params.fuseOpts.location ?? 0,
     33         threshold: params.fuseOpts.threshold ?? defaultFuseOptions.threshold,
     34         distance: params.fuseOpts.distance ?? defaultFuseOptions.distance,
     35         ignoreLocation: params.fuseOpts.ignorelocation ?? defaultFuseOptions.ignoreLocation
     36     };
     37 };
     38 
     39 const debounce = (fn, delay) => {
     40     let timeout;
     41     return (...args) => {
     42         clearTimeout(timeout);
     43         timeout = window.setTimeout(() => fn(...args), delay);
     44     };
     45 };
     46 
     47 const reset = () => {
     48     currentElement = null;
     49     firstResult = null;
     50     lastResult = null;
     51     resList.innerHTML = '';
     52     sInput.value = '';
     53     sInput.focus();
     54 };
     55 
     56 const setActiveResult = (element) => {
     57     document.querySelectorAll('.focus').forEach((item) => item.classList.remove('focus'));
     58 
     59     if (!element) {
     60         return;
     61     }
     62 
     63     element.focus();
     64     element.parentElement?.classList.add('focus');
     65     currentElement = element;
     66 };
     67 
     68 const renderResults = (results) => {
     69     if (!Array.isArray(results) || results.length === 0) {
     70         resList.innerHTML = '';
     71         firstResult = lastResult = currentElement = null;
     72         return;
     73     }
     74 
     75     const fragment = document.createDocumentFragment();
     76 
     77     for (const result of results) {
     78         const li = document.createElement('li');
     79         const titleText = document.createTextNode(result.item.title);
     80         const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
     81         svg.setAttribute('width', '24');
     82         svg.setAttribute('height', '24');
     83         svg.setAttribute('viewBox', '0 0 24 24');
     84         svg.setAttribute('fill', 'none');
     85         svg.setAttribute('stroke', 'currentColor');
     86         svg.setAttribute('stroke-width', '2');
     87         svg.setAttribute('stroke-linecap', 'round');
     88         svg.setAttribute('stroke-linejoin', 'round');
     89         svg.classList.add('feather', 'feather-chevrons-right');
     90 
     91         svg.innerHTML = '<polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline>';
     92 
     93         const link = document.createElement('a');
     94         link.className = 'entry-link';
     95         link.href = result.item.permalink;
     96         link.setAttribute('aria-label', result.item.title);
     97 
     98         li.appendChild(titleText);
     99         li.appendChild(svg);
    100         li.appendChild(link);
    101         fragment.appendChild(li);
    102     }
    103 
    104     resList.innerHTML = '';
    105     resList.appendChild(fragment);
    106     firstResult = resList.firstElementChild;
    107     lastResult = resList.lastElementChild;
    108 };
    109 
    110 const performSearch = () => {
    111     if (!fuse) {
    112         return;
    113     }
    114 
    115     const query = sInput.value.trim();
    116     if (!query) {
    117         renderResults([]);
    118         return;
    119     }
    120 
    121     const searchOptions = params.fuseOpts?.limit ? { limit: params.fuseOpts.limit } : undefined;
    122     const results = searchOptions ? fuse.search(query, searchOptions) : fuse.search(query);
    123     renderResults(results);
    124 };
    125 
    126 const initSearch = async () => {
    127     if (!sInput || !resList) {
    128         return;
    129     }
    130 
    131     sInput.disabled = false;
    132     sInput.focus();
    133 
    134     try {
    135         const response = await fetch('../index.json');
    136         if (!response.ok) {
    137             throw new Error(`Search index load failed: ${response.status}`);
    138         }
    139 
    140         const data = await response.json();
    141         if (data) {
    142             fuse = new Fuse(data, buildFuseOptions());
    143         }
    144     } catch (error) {
    145         console.error(error);
    146     }
    147 };
    148 
    149 window.addEventListener('load', initSearch);
    150 
    151 sInput?.addEventListener('input', debounce(performSearch, 150));
    152 
    153 sInput?.addEventListener('search', () => {
    154     if (!sInput.value) {
    155         reset();
    156     }
    157 });
    158 
    159 document.addEventListener('keydown', (event) => {
    160     const { key } = event;
    161     const active = document.activeElement;
    162     const isInSearchBox = searchBox?.contains(active);
    163 
    164     if (key === 'Escape') {
    165         reset();
    166         return;
    167     }
    168 
    169     if (!firstResult || !isInSearchBox) {
    170         return;
    171     }
    172 
    173     if (key === 'ArrowDown') {
    174         event.preventDefault();
    175 
    176         if (active === sInput) {
    177             setActiveResult(firstResult.querySelector('.entry-link'));
    178         } else if (active?.parentElement !== lastResult) {
    179             setActiveResult(active?.parentElement?.nextElementSibling?.querySelector('.entry-link'));
    180         }
    181     } else if (key === 'ArrowUp') {
    182         event.preventDefault();
    183 
    184         if (active?.parentElement === firstResult) {
    185             setActiveResult(sInput);
    186         } else if (active !== sInput) {
    187             setActiveResult(active?.parentElement?.previousElementSibling?.querySelector('.entry-link'));
    188         }
    189     } else if (key === 'ArrowRight') {
    190         if (active?.matches?.('.entry-link')) {
    191             active.click();
    192         }
    193     }
    194 });