github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/docs/themes/hugo-theme-relearn/static/js/search.js (about) 1 window.relearn = window.relearn || {}; 2 3 window.relearn.runInitialSearch = function(){ 4 if( window.relearn.isSearchInit && window.relearn.isLunrInit ){ 5 var input = document.querySelector('#R-search-by-detail'); 6 if( !input ){ 7 return; 8 } 9 var value = input.value; 10 searchDetail( value ); 11 } 12 } 13 14 var lunrIndex, pagesIndex; 15 16 function initLunrIndex( index ){ 17 pagesIndex = index; 18 // Set up Lunr by declaring the fields we use 19 // Also provide their boost level for the ranking 20 lunrIndex = lunr(function() { 21 this.use(lunr.multiLanguage.apply(null, contentLangs)); 22 this.ref('index'); 23 this.field('title', { 24 boost: 15 25 }); 26 this.field('tags', { 27 boost: 10 28 }); 29 this.field('content', { 30 boost: 5 31 }); 32 33 this.pipeline.remove(lunr.stemmer); 34 this.searchPipeline.remove(lunr.stemmer); 35 36 // Feed Lunr with each file and let LUnr actually index them 37 pagesIndex.forEach(function(page, idx) { 38 page.index = idx; 39 this.add(page); 40 }, this); 41 }); 42 43 window.relearn.isLunrInit = true; 44 window.relearn.runInitialSearch(); 45 } 46 47 function triggerSearch(){ 48 var input = document.querySelector('#R-search-by-detail'); 49 if( !input ){ 50 return; 51 } 52 var value = input.value; 53 searchDetail( value ); 54 55 // add a new entry to the history after the user 56 // changed the term; this does not reload the page 57 // but will add to the history and update the address bar URL 58 var url = new URL( window.location ); 59 var oldValue = url.searchParams.get( 'search-by' ); 60 if( value != oldValue ){ 61 var state = window.history.state || {}; 62 state = Object.assign( {}, ( typeof state === 'object' ) ? state : {} ); 63 url.searchParams.set( 'search-by', value ); 64 state.search = url.toString(); 65 // with normal pages, this is handled by the 'pagehide' event, but this 66 // doesn't fire in case of pushState, so we have to do the same thing 67 // here, too 68 state.contentScrollTop = +elc.scrollTop; 69 window.history.pushState( state, '', url ); 70 } 71 } 72 73 window.addEventListener( 'popstate', function ( event ){ 74 // restart search if browsed thru history 75 if( event.state ){ 76 var state = window.history.state || {}; 77 state = Object.assign( {}, ( typeof state === 'object' ) ? state : {} ); 78 if( state.search ) { 79 var url = new URL( state.search ); 80 if( url.searchParams.has('search-by') ){ 81 var search = url.searchParams.get( 'search-by' ); 82 83 // we have to insert the old search term into the inputs 84 var inputs = document.querySelectorAll( 'input.search-by' ); 85 inputs.forEach( function( e ){ 86 e.value = search; 87 var event = document.createEvent( 'Event' ); 88 event.initEvent( 'input', false, false ); 89 e.dispatchEvent( event ); 90 }); 91 92 // recreate the last search results and eventually 93 // restore the previous scrolling position 94 searchDetail( search ); 95 } 96 } 97 } 98 }); 99 100 var input = document.querySelector('#R-search-by-detail'); 101 if( input ){ 102 input.addEventListener( 'keydown', function(event) { 103 // if we are pressing ESC in the searchdetail our focus will 104 // be stolen by the other event handlers, so we have to refocus 105 // here after a short while 106 if (event.key == "Escape") { 107 setTimeout( function(){ input.focus(); }, 0 ); 108 } 109 }); 110 } 111 112 function initLunrJson() { 113 // old way to load the search index via XHR; 114 // this does not work if pages are served via 115 // file:// protocol; this is only left for 116 // backward compatiblity if the user did not 117 // define the `search` output format for the homepage 118 if( window.index_json_url && !window.index_js_url ){ 119 xhr = new XMLHttpRequest; 120 xhr.onreadystatechange = function(){ 121 if( xhr.readyState == 4 ){ 122 if( xhr.status == 200 ){ 123 initLunrIndex( JSON.parse( xhr.responseText ) ); 124 } 125 else{ 126 var err = xhr.status; 127 console.error( 'Error getting Hugo index file: ', err ); 128 } 129 } 130 } 131 xhr.open( 'GET', index_json_url ); 132 xhr.send(); 133 } 134 } 135 136 function initLunrJs() { 137 // new way to load our search index 138 if( window.index_js_url ){ 139 var js = document.createElement("script"); 140 js.src = index_js_url; 141 js.setAttribute("async", ""); 142 js.onload = function(){ 143 initLunrIndex(relearn_search_index); 144 }; 145 js.onerror = function(e){ 146 console.error('Error getting Hugo index file'); 147 }; 148 document.head.appendChild(js); 149 } 150 } 151 152 /** 153 * Trigger a search in Lunr and transform the result 154 * 155 * @param {String} term 156 * @return {Array} results 157 */ 158 function search(term) { 159 // Find the item in our index corresponding to the Lunr one to have more info 160 // Remove Lunr special search characters: https://lunrjs.com/guides/searching.html 161 term = term.replace( /[*:^~+-]/g, ' ' ); 162 var searchTerm = lunr.tokenizer( term ).reduce( function(a,token){return a.concat(searchPatterns(token.str))}, []).join(' '); 163 return !searchTerm || !lunrIndex ? [] : lunrIndex.search(searchTerm).map(function(result) { 164 return { index: result.ref, matches: Object.keys(result.matchData.metadata) } 165 }); 166 } 167 168 function searchPatterns(word) { 169 // for short words high amounts of typos doesn't make sense 170 // for long words we allow less typos because this largly increases search time 171 var typos = [ 172 { len: -1, typos: 1 }, 173 { len: 60, typos: 2 }, 174 { len: 40, typos: 3 }, 175 { len: 20, typos: 4 }, 176 { len: 16, typos: 3 }, 177 { len: 12, typos: 2 }, 178 { len: 8, typos: 1 }, 179 { len: 4, typos: 0 }, 180 ]; 181 return [ 182 word + '^100', 183 word + '*^10', 184 '*' + word + '^10', 185 word + '~' + typos.reduce( function( a, c, i ){ return word.length < c.len ? c : a; } ).typos + '^1' 186 ]; 187 } 188 189 190 function resolvePlaceholders( s, args ) { 191 var args = args || []; 192 // use replace to iterate over the string 193 // select the match and check if the related argument is present 194 // if yes, replace the match with the argument 195 return s.replace(/{([0-9]+)}/g, function (match, index) { 196 // check if the argument is present 197 return typeof args[index] == 'undefined' ? match : args[index]; 198 }); 199 }; 200 201 function searchDetail( value ) { 202 var results = document.querySelector('#R-searchresults'); 203 var hint = document.querySelector('.searchhint'); 204 hint.innerText = ''; 205 results.textContent = ''; 206 var a = search( value ); 207 if( a.length ){ 208 hint.innerText = resolvePlaceholders( window.T_N_results_found, [ value, a.length ] ); 209 a.forEach( function(item){ 210 var page = pagesIndex[item.index]; 211 var numContextWords = 10; 212 var contextPattern = '(?:\\S+ +){0,' + numContextWords + '}\\S*\\b(?:' + 213 item.matches.map( function(match){return match.replace(/\W/g, '\\$&')} ).join('|') + 214 ')\\b\\S*(?: +\\S+){0,' + numContextWords + '}'; 215 var context = page.content.match(new RegExp(contextPattern, 'i')); 216 var divsuggestion = document.createElement('a'); 217 divsuggestion.className = 'autocomplete-suggestion'; 218 divsuggestion.setAttribute('data-term', value); 219 divsuggestion.setAttribute('data-title', page.title); 220 divsuggestion.setAttribute('href', window.relearn.relBaseUri + page.uri); 221 divsuggestion.setAttribute('data-context', context); 222 var divtitle = document.createElement('div'); 223 divtitle.className = 'title'; 224 divtitle.innerText = '» ' + page.title; 225 divsuggestion.appendChild( divtitle ); 226 var divbreadcrumb = document.createElement('div'); 227 divbreadcrumb.className = 'breadcrumbs'; 228 divbreadcrumb.innerText = (page.breadcrumb || ''); 229 divsuggestion.appendChild( divbreadcrumb ); 230 if( context ){ 231 var divcontext = document.createElement('div'); 232 divcontext.className = 'context'; 233 divcontext.innerText = (context || ''); 234 divsuggestion.appendChild( divcontext ); 235 } 236 results.appendChild( divsuggestion ); 237 }); 238 window.relearn.markSearch(); 239 } 240 else if( value.length ) { 241 hint.innerText = resolvePlaceholders( window.T_No_results_found, [ value ] ); 242 } 243 input.focus(); 244 setTimeout( adjustContentWidth, 0 ); 245 246 // if we are initiating search because of a browser history 247 // operation, we have to restore the scrolling postion the 248 // user previously has used; if this search isn't initiated 249 // by a browser history operation, it simply does nothing 250 var state = window.history.state || {}; 251 state = Object.assign( {}, ( typeof state === 'object' ) ? state : {} ); 252 if( state.hasOwnProperty( 'contentScrollTop' ) ){ 253 window.setTimeout( function(){ 254 elc.scrollTop = +state.contentScrollTop; 255 }, 10 ); 256 return; 257 } 258 } 259 260 initLunrJson(); 261 initLunrJs(); 262 263 function startSearch(){ 264 var input = document.querySelector('#R-search-by-detail'); 265 if( input ){ 266 var state = window.history.state || {}; 267 state = Object.assign( {}, ( typeof state === 'object' ) ? state : {} ); 268 state.search = window.location.toString(); 269 window.history.replaceState( state, '', window.location ); 270 } 271 272 var searchList = new autoComplete({ 273 /* selector for the search box element */ 274 selectorToInsert: 'search:has(.searchbox)', 275 selector: '#R-search-by', 276 /* source is the callback to perform the search */ 277 source: function(term, response) { 278 response(search(term)); 279 }, 280 /* renderItem displays individual search results */ 281 renderItem: function(item, term) { 282 var page = pagesIndex[item.index]; 283 var numContextWords = 2; 284 var contextPattern = '(?:\\S+ +){0,' + numContextWords + '}\\S*\\b(?:' + 285 item.matches.map( function(match){return match.replace(/\W/g, '\\$&')} ).join('|') + 286 ')\\b\\S*(?: +\\S+){0,' + numContextWords + '}'; 287 var context = page.content.match(new RegExp(contextPattern, 'i')); 288 var divsuggestion = document.createElement('div'); 289 divsuggestion.className = 'autocomplete-suggestion'; 290 divsuggestion.setAttribute('data-term', term); 291 divsuggestion.setAttribute('data-title', page.title); 292 divsuggestion.setAttribute('data-uri', window.relearn.relBaseUri + page.uri); 293 divsuggestion.setAttribute('data-context', context); 294 var divtitle = document.createElement('div'); 295 divtitle.className = 'title'; 296 divtitle.innerText = '» ' + page.title; 297 divsuggestion.appendChild( divtitle ); 298 if( context ){ 299 var divcontext = document.createElement('div'); 300 divcontext.className = 'context'; 301 divcontext.innerText = (context || ''); 302 divsuggestion.appendChild( divcontext ); 303 } 304 return divsuggestion.outerHTML; 305 }, 306 /* onSelect callback fires when a search suggestion is chosen */ 307 onSelect: function(e, term, item) { 308 location.href = item.getAttribute('data-uri'); 309 e.preventDefault(); 310 } 311 }); 312 }; 313 314 ready( startSearch );