autocomplete.js
3 months ago
submit-ticket.js
3 months ago
support.css
3 months ago
support.js
3 months ago
autocomplete.js
179 lines
| 1 | // Debounce function to limit the rate of API calls |
| 2 | const debounce = ( func, delay ) => { |
| 3 | let timeoutId; |
| 4 | return ( ...args ) => { |
| 5 | if ( timeoutId ) { |
| 6 | clearTimeout( timeoutId ); |
| 7 | } |
| 8 | timeoutId = setTimeout( () => { |
| 9 | func( ...args ); |
| 10 | }, delay ); |
| 11 | }; |
| 12 | }; |
| 13 | |
| 14 | export function searchCombobox( |
| 15 | inputId, |
| 16 | suggestionsListId, |
| 17 | endpoint, |
| 18 | formatSuggestion, |
| 19 | onSelect |
| 20 | ) { |
| 21 | const input = document.getElementById( inputId ); |
| 22 | const suggestionsList = document.getElementById( suggestionsListId ); |
| 23 | let selectedSuggestionIndex = -1; |
| 24 | |
| 25 | // fetch users from API |
| 26 | const fetchData = async ( search ) => { |
| 27 | try { |
| 28 | const response = await fetch( |
| 29 | endpoint.replace( '{{search}}', search ) |
| 30 | ); |
| 31 | return await response.json(); |
| 32 | } catch ( error ) { |
| 33 | console.error( 'Error fetching:', error ); |
| 34 | return []; |
| 35 | } |
| 36 | }; |
| 37 | |
| 38 | // Render suggestions in the dropdown |
| 39 | const renderSuggestions = ( suggestions ) => { |
| 40 | // clear previous suggestions |
| 41 | clearSuggestions(); |
| 42 | // set aria-expanded attribute |
| 43 | input.setAttribute( 'aria-expanded', 'true' ); |
| 44 | suggestionsList.style.display = 'block'; |
| 45 | |
| 46 | // Show "No results found" message if no suggestions |
| 47 | if ( suggestions.length === 0 ) { |
| 48 | const li = document.createElement( 'li' ); |
| 49 | li.textContent = 'No results found'; |
| 50 | li.classList.add( 'no-results' ); |
| 51 | suggestionsList.appendChild( li ); |
| 52 | return; |
| 53 | } |
| 54 | |
| 55 | // iterate over suggestions and create list items |
| 56 | // for each suggestion, create a list item and append it to the suggestions list |
| 57 | suggestions.forEach( ( suggestion, index ) => { |
| 58 | const li = document.createElement( 'li' ); |
| 59 | li.id = `suggestion-${ index }`; |
| 60 | li.setAttribute( 'role', 'option' ); |
| 61 | li.innerHTML = formatSuggestion( suggestion ); |
| 62 | li.dataset.index = index; |
| 63 | |
| 64 | // add event listener for suggestion click |
| 65 | // when a suggestion is clicked, set the input value to the suggestion name |
| 66 | li.addEventListener( 'click', () => { |
| 67 | onSelect( suggestion, input ); |
| 68 | clearSuggestions(); |
| 69 | } ); |
| 70 | |
| 71 | // append the list item to the suggestions list |
| 72 | suggestionsList.appendChild( li ); |
| 73 | } ); |
| 74 | }; |
| 75 | |
| 76 | // clear suggestions |
| 77 | const clearSuggestions = () => { |
| 78 | suggestionsList.innerHTML = ''; |
| 79 | selectedSuggestionIndex = -1; |
| 80 | // set aria-expanded attribute |
| 81 | input.setAttribute( 'aria-expanded', 'false' ); |
| 82 | suggestionsList.style.display = ''; |
| 83 | // remove aria-activedescendant attribute |
| 84 | input.removeAttribute( 'aria-activedescendant' ); |
| 85 | }; |
| 86 | |
| 87 | // Update the selected suggestion |
| 88 | // Highlight the selected suggestion |
| 89 | const updateSelectedSuggestion = () => { |
| 90 | const suggestions = suggestionsList.querySelectorAll( |
| 91 | 'li:not(.no-results)' |
| 92 | ); |
| 93 | // find the currently selected suggestion and highlight it |
| 94 | suggestions.forEach( ( li, index ) => { |
| 95 | if ( index === selectedSuggestionIndex ) { |
| 96 | // add selected class |
| 97 | li.classList.add( 'selected' ); |
| 98 | // scroll the selected suggestion into view |
| 99 | li.scrollIntoView( { block: 'nearest' } ); |
| 100 | // set aria-activedescendant attribute |
| 101 | input.setAttribute( 'aria-activedescendant', li.id ); |
| 102 | } else { |
| 103 | // remove selected class |
| 104 | li.classList.remove( 'selected' ); |
| 105 | } |
| 106 | } ); |
| 107 | }; |
| 108 | |
| 109 | // Handle input event |
| 110 | const handleInput = async ( e ) => { |
| 111 | // Get the input value |
| 112 | const query = e.target.value.toLowerCase(); |
| 113 | |
| 114 | // Clear suggestions if input is empty |
| 115 | if ( query.length < 1 ) { |
| 116 | clearSuggestions(); |
| 117 | return; |
| 118 | } |
| 119 | |
| 120 | const data = await fetchData( query ); |
| 121 | renderSuggestions( data ); |
| 122 | }; |
| 123 | |
| 124 | // add debounce to input event |
| 125 | input.addEventListener( 'input', debounce( handleInput, 300 ) ); |
| 126 | // Handle keyboard navigation |
| 127 | input.addEventListener( 'keydown', ( e ) => { |
| 128 | // get the current suggestions |
| 129 | const suggestions = suggestionsList.querySelectorAll( |
| 130 | 'li:not(.no-results)' |
| 131 | ); |
| 132 | // check if there are any suggestions and if the suggestions list is open |
| 133 | if ( |
| 134 | suggestions.length === 0 || |
| 135 | input.getAttribute( 'aria-expanded' ) === 'false' |
| 136 | ) { |
| 137 | return; |
| 138 | } |
| 139 | |
| 140 | // Handle arrow key down navigation and move down the suggestions |
| 141 | if ( e.key === 'ArrowDown' ) { |
| 142 | e.preventDefault(); |
| 143 | selectedSuggestionIndex = |
| 144 | ( selectedSuggestionIndex + 1 ) % suggestions.length; |
| 145 | updateSelectedSuggestion(); |
| 146 | |
| 147 | // Handle arrow key up navigation and move up the suggestions |
| 148 | } else if ( e.key === 'ArrowUp' ) { |
| 149 | e.preventDefault(); |
| 150 | selectedSuggestionIndex = |
| 151 | ( selectedSuggestionIndex - 1 + suggestions.length ) % |
| 152 | suggestions.length; |
| 153 | updateSelectedSuggestion(); |
| 154 | |
| 155 | // Handle enter key and select the suggestion |
| 156 | } else if ( e.key === 'Enter' ) { |
| 157 | e.preventDefault(); |
| 158 | if ( selectedSuggestionIndex > -1 ) { |
| 159 | suggestions[ selectedSuggestionIndex ].click(); |
| 160 | } |
| 161 | // Handle escape key and close the suggestions |
| 162 | } else if ( e.key === 'Escape' ) { |
| 163 | input.value = ''; |
| 164 | clearSuggestions(); |
| 165 | } |
| 166 | } ); |
| 167 | |
| 168 | // Close suggestions when Escape is pressed anywhere in the document |
| 169 | document.addEventListener( 'keydown', ( e ) => { |
| 170 | if ( |
| 171 | e.key === 'Escape' && |
| 172 | input.getAttribute( 'aria-expanded' ) === 'true' |
| 173 | ) { |
| 174 | input.value = ''; |
| 175 | clearSuggestions(); |
| 176 | } |
| 177 | } ); |
| 178 | } |
| 179 |