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 });