|
| 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 | + |
| 46 | + // Credit David Walsh (https://davidwalsh.name/javascript-debounce-function) |
| 47 | + // Returns a function, that, as long as it continues to be invoked, will not |
| 48 | + // be triggered. The function will be called after it stops being called for |
| 49 | + // N milliseconds. If `immediate` is passed, trigger the function on the |
| 50 | + // leading edge, instead of the trailing. |
| 51 | + function debounce(func, wait, immediate) { |
| 52 | + var timeout; |
| 53 | + |
| 54 | + return function executedFunction() { |
| 55 | + var context = this; |
| 56 | + var args = arguments; |
| 57 | + |
| 58 | + var later = function () { |
| 59 | + timeout = null; |
| 60 | + if (!immediate) func.apply(context, args); |
| 61 | + }; |
| 62 | + |
| 63 | + var callNow = immediate && !timeout; |
| 64 | + clearTimeout(timeout); |
| 65 | + timeout = setTimeout(later, wait); |
| 66 | + if (callNow) func.apply(context, args); |
| 67 | + }; |
| 68 | + } |
| 69 | + |
| 70 | + function close() { |
| 71 | + // Start scroll prevention: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/ |
| 72 | + const scrollY = document.body.style.top; |
| 73 | + document.body.style.position = ''; |
| 74 | + document.body.style.top = ''; |
| 75 | + window.scrollTo(0, parseInt(scrollY || '0') * -1); |
| 76 | + // End scroll prevention |
| 77 | + |
| 78 | + var form = document.querySelector('[data-search-form]'); |
| 79 | + var searchResults = document.querySelector('[data-search-results]'); |
| 80 | + |
| 81 | + form.classList.toggle('phpdocumentor-search--has-results', false); |
| 82 | + searchResults.classList.add('phpdocumentor-search-results--hidden'); |
| 83 | + var searchField = document.querySelector('[data-search-form] input[type="search"]'); |
| 84 | + searchField.blur(); |
| 85 | + } |
| 86 | + |
| 87 | + function search(event) { |
| 88 | + // Start scroll prevention: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/ |
| 89 | + document.body.style.position = 'fixed'; |
| 90 | + document.body.style.top = `-${window.scrollY}px`; |
| 91 | + // End scroll prevention |
| 92 | + |
| 93 | + // prevent enter's from autosubmitting |
| 94 | + event.stopPropagation(); |
| 95 | + |
| 96 | + var form = document.querySelector('[data-search-form]'); |
| 97 | + var searchResults = document.querySelector('[data-search-results]'); |
| 98 | + var searchResultEntries = document.querySelector('[data-search-results] .phpdocumentor-search-results__entries'); |
| 99 | + |
| 100 | + searchResultEntries.innerHTML = ''; |
| 101 | + |
| 102 | + if (!event.target.value) { |
| 103 | + close(); |
| 104 | + return; |
| 105 | + } |
| 106 | + |
| 107 | + form.classList.toggle('phpdocumentor-search--has-results', true); |
| 108 | + searchResults.classList.remove('phpdocumentor-search-results--hidden'); |
| 109 | + var results = fuse.search(event.target.value, {limit: 25}); |
| 110 | + |
| 111 | + results.forEach(function (result) { |
| 112 | + var entry = document.createElement("li"); |
| 113 | + entry.classList.add("phpdocumentor-search-results__entry"); |
| 114 | + entry.innerHTML += '<h3><a href="' + document.baseURI + result.url + '">' + result.name + "</a></h3>\n"; |
| 115 | + entry.innerHTML += '<small>' + result.fqsen + "</small>\n"; |
| 116 | + entry.innerHTML += '<div class="phpdocumentor-summary">' + result.summary + '</div>'; |
| 117 | + searchResultEntries.appendChild(entry) |
| 118 | + }); |
| 119 | + } |
| 120 | + |
| 121 | + function appendIndex(added) { |
| 122 | + index = index.concat(added); |
| 123 | + |
| 124 | + // re-initialize search engine when appending an index after initialisation |
| 125 | + if (typeof fuse !== 'undefined') { |
| 126 | + fuse = new Fuse(index, options); |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + function init() { |
| 131 | + fuse = new Fuse(index, options); |
| 132 | + |
| 133 | + var form = document.querySelector('[data-search-form]'); |
| 134 | + var searchField = document.querySelector('[data-search-form] input[type="search"]'); |
| 135 | + |
| 136 | + var closeButton = document.querySelector('.phpdocumentor-search-results__close'); |
| 137 | + closeButton.addEventListener('click', function() { close() }.bind(this)); |
| 138 | + |
| 139 | + var searchResults = document.querySelector('[data-search-results]'); |
| 140 | + searchResults.addEventListener('click', function() { close() }.bind(this)); |
| 141 | + |
| 142 | + form.classList.add('phpdocumentor-search--active'); |
| 143 | + |
| 144 | + searchField.setAttribute('placeholder', 'Search (Press "/" to focus)'); |
| 145 | + searchField.removeAttribute('disabled'); |
| 146 | + searchField.addEventListener('keyup', debounce(search, 300)); |
| 147 | + |
| 148 | + window.addEventListener('keyup', function (event) { |
| 149 | + if (event.key === '/') { |
| 150 | + searchField.focus(); |
| 151 | + } |
| 152 | + if (event.code === 'Escape') { |
| 153 | + close(); |
| 154 | + } |
| 155 | + }.bind(this)); |
| 156 | + } |
| 157 | + |
| 158 | + return { |
| 159 | + appendIndex, |
| 160 | + init |
| 161 | + } |
| 162 | +})(); |
| 163 | + |
| 164 | +window.addEventListener('DOMContentLoaded', function () { |
| 165 | + var form = document.querySelector('[data-search-form]'); |
| 166 | + |
| 167 | + // When JS is supported; show search box. Must be before including the search for it to take effect immediately |
| 168 | + form.classList.add('phpdocumentor-search--enabled'); |
| 169 | +}); |
| 170 | + |
| 171 | +window.addEventListener('load', function () { |
| 172 | + Search.init(); |
| 173 | +}); |
0 commit comments