offline-search.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. // Adapted from code by Matt Walters https://www.mattwalters.net/posts/2018-03-28-hugo-and-lunr/
  2. (function ($) {
  3. 'use strict';
  4. $(document).ready(function () {
  5. const $searchInput = $('.td-search input');
  6. //
  7. // Register handler
  8. //
  9. $searchInput.on('change', (event) => {
  10. render($(event.target));
  11. // Hide keyboard on mobile browser
  12. $searchInput.blur();
  13. });
  14. // Prevent reloading page by enter key on sidebar search.
  15. $searchInput.closest('form').on('submit', () => {
  16. return false;
  17. });
  18. //
  19. // Lunr
  20. //
  21. let idx = null; // Lunr index
  22. const resultDetails = new Map(); // Will hold the data for the search results (titles and summaries)
  23. // Set up for an Ajax call to request the JSON data file that is created by Hugo's build process
  24. $.ajax($searchInput.data('offline-search-index-json-src')).then((data) => {
  25. idx = lunr(function () {
  26. this.ref('ref');
  27. // If you added more searchable fields to the search index, list them here.
  28. // Here you can specify searchable fields to the search index - e.g. individual toxonomies for you project
  29. // With "boost" you can add weighting for specific (default weighting without boost: 1)
  30. this.field('title', { boost: 5 });
  31. this.field('categories', { boost: 3 });
  32. this.field('tags', { boost: 3 });
  33. // this.field('projects', { boost: 3 }); // example for an individual toxonomy called projects
  34. this.field('description', { boost: 2 });
  35. this.field('body');
  36. data.forEach((doc) => {
  37. this.add(doc);
  38. resultDetails.set(doc.ref, {
  39. title: doc.title,
  40. excerpt: doc.excerpt,
  41. });
  42. });
  43. });
  44. $searchInput.trigger('change');
  45. });
  46. const render = ($targetSearchInput) => {
  47. //
  48. // Dispose existing popover
  49. //
  50. {
  51. let popover = bootstrap.Popover.getInstance($targetSearchInput[0]);
  52. if (popover !== null) {
  53. popover.dispose();
  54. }
  55. }
  56. //
  57. // Search
  58. //
  59. if (idx === null) {
  60. return;
  61. }
  62. const searchQuery = $targetSearchInput.val();
  63. if (searchQuery === '') {
  64. return;
  65. }
  66. const results = idx
  67. .query((q) => {
  68. const tokens = lunr.tokenizer(searchQuery.toLowerCase());
  69. tokens.forEach((token) => {
  70. const queryString = token.toString();
  71. q.term(queryString, {
  72. boost: 100,
  73. });
  74. q.term(queryString, {
  75. wildcard:
  76. lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING,
  77. boost: 10,
  78. });
  79. q.term(queryString, {
  80. editDistance: 2,
  81. });
  82. });
  83. })
  84. .slice(0, $targetSearchInput.data('offline-search-max-results'));
  85. //
  86. // Make result html
  87. //
  88. const $html = $('<div>');
  89. $html.append(
  90. $('<div>')
  91. .css({
  92. display: 'flex',
  93. justifyContent: 'space-between',
  94. marginBottom: '1em',
  95. })
  96. .append(
  97. $('<span>').text('Search results').css({ fontWeight: 'bold' })
  98. )
  99. .append(
  100. $('<span>').addClass('td-offline-search-results__close-button')
  101. )
  102. );
  103. const $searchResultBody = $('<div>').css({
  104. maxHeight: `calc(100vh - ${
  105. $targetSearchInput.offset().top - $(window).scrollTop() + 180
  106. }px)`,
  107. overflowY: 'auto',
  108. });
  109. $html.append($searchResultBody);
  110. if (results.length === 0) {
  111. $searchResultBody.append(
  112. $('<p>').text(`No results found for query "${searchQuery}"`)
  113. );
  114. } else {
  115. results.forEach((r) => {
  116. const doc = resultDetails.get(r.ref);
  117. const href =
  118. $searchInput.data('offline-search-base-href') +
  119. r.ref.replace(/^\//, '');
  120. const $entry = $('<div>').addClass('mt-4');
  121. $entry.append(
  122. $('<small>').addClass('d-block text-body-secondary').text(r.ref)
  123. );
  124. $entry.append(
  125. $('<a>')
  126. .addClass('d-block')
  127. .css({
  128. fontSize: '1.2rem',
  129. })
  130. .attr('href', href)
  131. .text(doc.title)
  132. );
  133. $entry.append($('<p>').text(doc.excerpt));
  134. $searchResultBody.append($entry);
  135. });
  136. }
  137. $targetSearchInput.one('shown.bs.popover', () => {
  138. $('.td-offline-search-results__close-button').on('click', () => {
  139. $targetSearchInput.val('');
  140. $targetSearchInput.trigger('change');
  141. });
  142. });
  143. const popover = new bootstrap.Popover($targetSearchInput, {
  144. content: $html[0],
  145. html: true,
  146. customClass: 'td-offline-search-results',
  147. placement: 'bottom',
  148. });
  149. popover.show();
  150. };
  151. });
  152. })(jQuery);