|
| 1 | +import autoComplete from '@tarekraafat/autocomplete.js'; |
1 | 2 | const Mousetrap = require('mousetrap');
|
2 | 3 |
|
3 | 4 | (() => {
|
| 5 | + const autoCompleteInput = document.querySelector('#autoComplete'); |
| 6 | + |
| 7 | + if (autoCompleteInput) { |
| 8 | + autoCompleteInput.addEventListener('init', () => { |
| 9 | + // Hiding the input until it is ready to avoid showing it unstyled |
| 10 | + document.querySelector('.search-box').style.opacity = 1; |
| 11 | + }); |
| 12 | + |
| 13 | + const autoCompleteJS = new autoComplete({ // eslint-disable-line new-cap |
| 14 | + name: 'autoComplete', |
| 15 | + placeHolder: 'Search packages or get suggestions...', |
| 16 | + submit: false, |
| 17 | + debounce: 300, |
| 18 | + data: { |
| 19 | + src: async query => { |
| 20 | + try { |
| 21 | + // Fetch Data from external Source |
| 22 | + const source = await fetch(`${window.location.protocol}//${window.location.host}/json/JsonApi?invoke&action=GetSuggestions&SearchTerm=${query}`); |
| 23 | + |
| 24 | + // Data should be an array of `Objects` or `Strings` |
| 25 | + const data = await source.json(); |
| 26 | + |
| 27 | + return data; |
| 28 | + } catch (error) { |
| 29 | + return error; |
| 30 | + } |
| 31 | + }, |
| 32 | + keys: ['PackageId'] // Data source 'Object' key to be searched |
| 33 | + }, |
| 34 | + resultsList: { |
| 35 | + element: (list, data) => { |
| 36 | + const templateHeader = document.createElement('div'); |
| 37 | + const templateNoSuggestions = document.createElement('li'); |
| 38 | + let templateHelp = ''; |
| 39 | + const prefixed = ['tag:', 'author:', 'id:']; |
| 40 | + |
| 41 | + templateHeader.classList.add('autocomplete-header'); |
| 42 | + templateNoSuggestions.classList.add('autocomplete-no-suggestions', 'mt-4'); |
| 43 | + |
| 44 | + // Check if any of the keys in 'prefixed' array are present in the user input |
| 45 | + const containsPrefixedKey = prefixed.some(key => data.query.includes(key)); |
| 46 | + |
| 47 | + if (containsPrefixedKey) { |
| 48 | + templateNoSuggestions.innerHTML = '<p class="mb-0">Searching with a prefix of <strong>id:</strong>, <strong>tag:</strong>, or <strong>author:</strong> will not display suggestions. Press <kbd>Enter</kbd> to do a full search.</p>'; |
| 49 | + } else if (data.results.length === 0) { |
| 50 | + templateNoSuggestions.innerHTML = `<p class="mb-0">No suggestions for package id "<strong>${data.query}</strong>". Press <kbd>Enter</kbd> to do a full search.</p>`; |
| 51 | + } |
| 52 | + |
| 53 | + if (data.results.length !== 0) { |
| 54 | + templateHelp = ` |
| 55 | +<div class="d-flex align-items-center justify-content-sm-center text-bg-theme-elevation-1 px-3 py-2 small border-bottom"> |
| 56 | + <p class="mb-0">Press <kbd>Enter</kbd> to do a full search or select a suggestion below.</p> |
| 57 | +</div> |
| 58 | +<p class="text-primary ps-4 pe-3 mt-4 mb-2"><strong>Suggestions</strong></p>`; |
| 59 | + } |
| 60 | + |
| 61 | + templateHeader.innerHTML = ` |
| 62 | +<div class="d-flex justify-content-between align-items-center text-center text-bg-theme-neutral p-3 small border-bottom"> |
| 63 | + <p class="mb-0"><strong>id:searchValue</strong><br />search by id</p> |
| 64 | + <p class="mb-0 mx-3"><strong>tag:searchValue</strong><br />search by tag</p> |
| 65 | + <p class="mb-0"><strong>author:searchValue</strong><br />search by author</p> |
| 66 | +</div> |
| 67 | +${templateHelp}`; |
| 68 | + |
| 69 | + if (data.results.length === 0) { |
| 70 | + list.insertBefore(templateNoSuggestions, templateHeader.nextSibling); |
| 71 | + } |
| 72 | + |
| 73 | + list.prepend(templateHeader); |
| 74 | + }, |
| 75 | + noResults: true, |
| 76 | + maxResults: 5, |
| 77 | + tabSelect: true |
| 78 | + }, |
| 79 | + resultItem: { |
| 80 | + element: (item, data) => { |
| 81 | + item.innerHTML = ` |
| 82 | +<p class="mb-0">${data.match}</p> |
| 83 | +<p class="mb-0 text-end autocomplete-downloads"><small>${data.value.DownloadCount} downloads</small></p>`; |
| 84 | + }, |
| 85 | + highlight: true |
| 86 | + }, |
| 87 | + events: { |
| 88 | + input: { |
| 89 | + selection: () => { |
| 90 | + window.location.href = `${window.location.protocol}//${window.location.host}/packages/${selection}`; |
| 91 | + }, |
| 92 | + focus: event => { |
| 93 | + if (event.target.value) { |
| 94 | + autoCompleteJS.start(event.target.value); |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | + } |
| 99 | + }); |
| 100 | + } |
| 101 | + |
| 102 | + // Insert search keys in input field |
4 | 103 | const isMac = navigator.userAgent.indexOf('Mac OS X') != -1;
|
5 | 104 | const searchInput = document.querySelector('.search-box .search-input');
|
6 | 105 | const topNav = document.querySelector('#topNav');
|
@@ -56,4 +155,92 @@ const Mousetrap = require('mousetrap');
|
56 | 155 | return false;
|
57 | 156 | });
|
58 | 157 | }
|
| 158 | + |
| 159 | + // Package filtering |
| 160 | + // Set tag links on list page |
| 161 | + const packageTags = document.querySelectorAll('.package-tag'); |
| 162 | + packageTags.forEach(el => { |
| 163 | + const tag = el.getAttribute('data-package-tag'); |
| 164 | + let query; |
| 165 | + |
| 166 | + if (window.location.search) { |
| 167 | + // Only search in approved packages |
| 168 | + if (window.location.search.includes('moderatorQueue=true')) { |
| 169 | + query = window.location.search.replace('moderatorQueue=true', 'moderatorQueue=false'); |
| 170 | + } else { |
| 171 | + query = window.location.search; |
| 172 | + } |
| 173 | + } else { |
| 174 | + query = '?'; |
| 175 | + } |
| 176 | + |
| 177 | + // Only append tag to query if it doesn't already exist |
| 178 | + if (query.includes(`tags=${tag}&`)) { |
| 179 | + el.href = `/packages${query}`; |
| 180 | + } else if (query.endsWith(`tags=${tag}`)) { |
| 181 | + el.href = `/packages${query}`; |
| 182 | + } else { |
| 183 | + el.href = `/packages${query}&tags=${tag}`; |
| 184 | + } |
| 185 | + }); |
| 186 | + |
| 187 | + const sortOrderOptions = { |
| 188 | + relevance: 'relevance', |
| 189 | + popularity: 'package-download-count', |
| 190 | + alphabetical: 'package-title', |
| 191 | + recent: 'package-created' |
| 192 | + }; |
| 193 | + |
| 194 | + // Trigger search and selection |
| 195 | + const searchTriggers = document.querySelectorAll('.trigger-search'); |
| 196 | + searchTriggers.forEach(trigger => { |
| 197 | + trigger.addEventListener('change', e => { |
| 198 | + const currentForm = e.target.closest('form'); |
| 199 | + const hiddenSortOrder = currentForm.querySelector('[name="sortOrder"]'); |
| 200 | + |
| 201 | + // If no search term, and the "relevance" sort order is selected |
| 202 | + if (e.target.classList.contains('selected-search-term-query') && hiddenSortOrder.value === sortOrderOptions.relevance) { |
| 203 | + // Change the attribute name so that it doesn't submit with the form and the value defaults to what is set by the db |
| 204 | + hiddenSortOrder.setAttribute('name', ''); |
| 205 | + } |
| 206 | + |
| 207 | + currentForm.submit(); |
| 208 | + }); |
| 209 | + }); |
| 210 | + |
| 211 | + // Trigger search on enter in search box |
| 212 | + const searchBox = document.querySelector('.search-box'); |
| 213 | + searchBox.addEventListener('keydown', e => { |
| 214 | + if (e.key !== 'Enter') { |
| 215 | + return; |
| 216 | + } |
| 217 | + |
| 218 | + const currentQuery = window.location.search || ''; |
| 219 | + const currentQueryParams = new URLSearchParams(currentQuery); |
| 220 | + const existingQuery = currentQueryParams.get('q') || ''; |
| 221 | + const existingSortOrder = currentQueryParams.get('sortOrder') || ''; |
| 222 | + const inputValue = e.target.value.trim(); |
| 223 | + |
| 224 | + if (existingQuery !== inputValue) { |
| 225 | + currentQueryParams.set('q', inputValue); |
| 226 | + |
| 227 | + // If no search term, and the "relevance" sort order is selected |
| 228 | + if (inputValue === '' && existingSortOrder === sortOrderOptions.relevance) { |
| 229 | + // Delete this so the value defaults to what is set by the db |
| 230 | + currentQueryParams.delete('sortOrder'); |
| 231 | + } |
| 232 | + } |
| 233 | + |
| 234 | + // Manually construct the query string to ensure 'q=' comes first |
| 235 | + let newQuery = `q=${currentQueryParams.get('q')}`; |
| 236 | + currentQueryParams.delete('q'); |
| 237 | + |
| 238 | + if (currentQueryParams.toString() !== '') { |
| 239 | + newQuery += `&${currentQueryParams.toString()}`; |
| 240 | + } |
| 241 | + |
| 242 | + const newUrl = `${window.location.protocol}//${window.location.host}/packages?${newQuery}`; |
| 243 | + |
| 244 | + window.location.href = newUrl; |
| 245 | + }); |
59 | 246 | })();
|
0 commit comments