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