search.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. // Search module for phpDocumentor
  2. //
  3. // This module is a wrapper around fuse.js that will use a given index and attach itself to a
  4. // search form and to a search results pane identified by the following data attributes:
  5. //
  6. // 1. data-search-form
  7. // 2. data-search-results
  8. //
  9. // The data-search-form is expected to have a single input element of type 'search' that will trigger searching for
  10. // a series of results, were the data-search-results pane is expected to have a direct UL child that will be populated
  11. // with rendered results.
  12. //
  13. // The search has various stages, upon loading this stage the data-search-form receives the CSS class
  14. // 'phpdocumentor-search--enabled'; this indicates that JS is allowed and indices are being loaded. It is recommended
  15. // to hide the form by default and show it when it receives this class to achieve progressive enhancement for this
  16. // feature.
  17. //
  18. // After loading this module, it is expected to load a search index asynchronously, for example:
  19. //
  20. // <script defer src="js/searchIndex.js"></script>
  21. //
  22. // In this script the generated index should attach itself to the search module using the `appendIndex` function. By
  23. // doing it like this the page will continue loading, unhindered by the loading of the search.
  24. //
  25. // After the page has fully loaded, and all these deferred indexes loaded, the initialization of the search module will
  26. // be called and the form will receive the class 'phpdocumentor-search--active', indicating search is ready. At this
  27. // point, the input field will also have it's 'disabled' attribute removed.
  28. var Search = (function () {
  29. var fuse;
  30. var index = [];
  31. var options = {
  32. shouldSort: true,
  33. threshold: 0.6,
  34. location: 0,
  35. distance: 100,
  36. maxPatternLength: 32,
  37. minMatchCharLength: 1,
  38. keys: [
  39. "fqsen",
  40. "name",
  41. "summary",
  42. "url"
  43. ]
  44. };
  45. // Credit David Walsh (https://davidwalsh.name/javascript-debounce-function)
  46. // Returns a function, that, as long as it continues to be invoked, will not
  47. // be triggered. The function will be called after it stops being called for
  48. // N milliseconds. If `immediate` is passed, trigger the function on the
  49. // leading edge, instead of the trailing.
  50. function debounce(func, wait, immediate) {
  51. var timeout;
  52. return function executedFunction() {
  53. var context = this;
  54. var args = arguments;
  55. var later = function () {
  56. timeout = null;
  57. if (!immediate) func.apply(context, args);
  58. };
  59. var callNow = immediate && !timeout;
  60. clearTimeout(timeout);
  61. timeout = setTimeout(later, wait);
  62. if (callNow) func.apply(context, args);
  63. };
  64. }
  65. function close() {
  66. // Start scroll prevention: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/
  67. const scrollY = document.body.style.top;
  68. document.body.style.position = '';
  69. document.body.style.top = '';
  70. window.scrollTo(0, parseInt(scrollY || '0') * -1);
  71. // End scroll prevention
  72. var form = document.querySelector('[data-search-form]');
  73. var searchResults = document.querySelector('[data-search-results]');
  74. form.classList.toggle('phpdocumentor-search--has-results', false);
  75. searchResults.classList.add('phpdocumentor-search-results--hidden');
  76. var searchField = document.querySelector('[data-search-form] input[type="search"]');
  77. searchField.blur();
  78. }
  79. function search(event) {
  80. // Start scroll prevention: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/
  81. document.body.style.position = 'fixed';
  82. document.body.style.top = `-${window.scrollY}px`;
  83. // End scroll prevention
  84. // prevent enter's from autosubmitting
  85. event.stopPropagation();
  86. var form = document.querySelector('[data-search-form]');
  87. var searchResults = document.querySelector('[data-search-results]');
  88. var searchResultEntries = document.querySelector('[data-search-results] .phpdocumentor-search-results__entries');
  89. searchResultEntries.innerHTML = '';
  90. if (!event.target.value) {
  91. close();
  92. return;
  93. }
  94. form.classList.toggle('phpdocumentor-search--has-results', true);
  95. searchResults.classList.remove('phpdocumentor-search-results--hidden');
  96. var results = fuse.search(event.target.value, {limit: 25});
  97. results.forEach(function (result) {
  98. var entry = document.createElement("li");
  99. entry.classList.add("phpdocumentor-search-results__entry");
  100. entry.innerHTML += '<h3><a href="' + document.baseURI + result.url + '">' + result.name + "</a></h3>\n";
  101. entry.innerHTML += '<small>' + result.fqsen + "</small>\n";
  102. entry.innerHTML += '<div class="phpdocumentor-summary">' + result.summary + '</div>';
  103. searchResultEntries.appendChild(entry)
  104. });
  105. }
  106. function appendIndex(added) {
  107. index = index.concat(added);
  108. // re-initialize search engine when appending an index after initialisation
  109. if (typeof fuse !== 'undefined') {
  110. fuse = new Fuse(index, options);
  111. }
  112. }
  113. function init() {
  114. fuse = new Fuse(index, options);
  115. var form = document.querySelector('[data-search-form]');
  116. var searchField = document.querySelector('[data-search-form] input[type="search"]');
  117. var closeButton = document.querySelector('.phpdocumentor-search-results__close');
  118. closeButton.addEventListener('click', function() { close() }.bind(this));
  119. var searchResults = document.querySelector('[data-search-results]');
  120. searchResults.addEventListener('click', function() { close() }.bind(this));
  121. form.classList.add('phpdocumentor-search--active');
  122. searchField.setAttribute('placeholder', 'Search (Press "/" to focus)');
  123. searchField.removeAttribute('disabled');
  124. searchField.addEventListener('keyup', debounce(search, 300));
  125. window.addEventListener('keyup', function (event) {
  126. if (event.key === '/') {
  127. searchField.focus();
  128. }
  129. if (event.code === 'Escape') {
  130. close();
  131. }
  132. }.bind(this));
  133. }
  134. return {
  135. appendIndex,
  136. init
  137. }
  138. })();
  139. window.addEventListener('DOMContentLoaded', function () {
  140. var form = document.querySelector('[data-search-form]');
  141. // When JS is supported; show search box. Must be before including the search for it to take effect immediately
  142. form.classList.add('phpdocumentor-search--enabled');
  143. });
  144. window.addEventListener('load', function () {
  145. Search.init();
  146. });