1/** 2 * EazyDocs â Search Banner (AJAX live search) 3 * 4 * Single source of truth for every search-banner context: 5 * - Built-in banner on the doc single page (templates/search-banner.php) 6 * - Gutenberg search-banner block (includes/block-templates/search-banner.php) 7 * - Elementor search widget (includes/Elementor/Search/ezd-search.php) 8 * 9 * Behaviour is driven entirely by markup / data-attributes so the same file 10 * serves all three. Optional elements (post-type filter, result tabs) simply 11 * no-op when they are not present. 12 * 13 * #ezd_searchInput search input (optional data-post-type) 14 * #ezd-search-results results container (data-noresult*, data-show-tabs) 15 * .header_search_keyword a popular keyword chips 16 * .ezd-type-filter* post-type filter (Elementor widget only) 17 * 18 * Requires: jQuery and the localized `eazydocs_local_object` 19 * (ajaxurl, nonce, ezd_search_submit, i18n). 20 */ 21( function ( $ ) { 22 'use strict'; 23 24 var MIN_CHARS = 2; // Minimum keyword length before querying. 25 var DEBOUNCE_MS = 400; // Live-search debounce. 26 var activeRequest = null; // In-flight jqXHR so stale responses can be aborted. 27 28 function $results() { 29 return $( '#ezd-search-results' ); 30 } 31 32 function selectedType() { 33 return $( '#ezd_searchInput' ).data( 'post-type' ) || 'all'; 34 } 35 36 /** 37 * Announce a message to assistive technology via a polite live region. 38 * The region is created once and visually hidden (no CSS dependency). 39 */ 40 function announce( message ) { 41 var $status = $( '#ezd-search-status' ); 42 if ( ! $status.length ) { 43 $status = $( '<span>', { 44 id: 'ezd-search-status', 45 'aria-live': 'polite', 46 'aria-atomic': 'true' 47 } ).css( { 48 position: 'absolute', 49 width: '1px', 50 height: '1px', 51 margin: '-1px', 52 padding: '0', 53 border: '0', 54 overflow: 'hidden', 55 'white-space': 'nowrap', 56 'clip-path': 'inset(50%)' 57 } ).appendTo( 'body' ); 58 } 59 $status.text( message ); 60 } 61 62 // Reset the search UI: close results and drop the focus state. 63 function resetSearch() { 64 $results().removeClass( 'ajax-search' ).html( '' ); 65 $( 'body' ).removeClass( 'ezd-search-focused' ); 66 $( 'form.ezd_search_form' ).css( 'z-index', '' ); 67 } 68 69 // Friendly "no results" state (optional image + title + subtitle). 70 function buildNoResult() { 71 var $r = $results(); 72 var img = $r.data( 'noresult-img' ); 73 var title = $r.data( 'noresult-title' ) || $r.attr( 'data-noresult' ) || 'No Results Found'; 74 var sub = $r.data( 'noresult-sub' ); 75 var imgHtml = img ? '<div class="ezd-no-results-img"><img src="' + img + '" alt=""></div>' : ''; 76 var subHtml = sub ? '<p class="ezd-no-results-sub">' + sub + '</p>' : ''; 77 $r.addClass( 'ajax-search' ).html( 78 '<div class="ezd-no-results">' + imgHtml + 79 '<h4 class="ezd-no-results-title">' + title + '</h4>' + 80 subHtml + '</div>' 81 ); 82 announce( title ); 83 } 84 85 // Honour the widget's "show result tabs" setting. 86 function applyTabVisibility() { 87 if ( $results().data( 'show-tabs' ) == '0' ) { 88 $results().find( '.ezd-result-tabs' ).hide(); 89 } 90 } 91 92 // When a fixed post type is configured, open its tab by default. 93 function activateConfiguredTab() { 94 var type = selectedType(); 95 if ( type !== 'all' ) { 96 $results().find( '.ezd-tab[data-tab="' + type + '"]' ).trigger( 'click' ); 97 } 98 } 99 100 function renderResults( data ) { 101 if ( $.trim( data ).length > 0 ) { 102 $results().addClass( 'ajax-search' ).html( data ); 103 applyTabVisibility(); 104 activateConfiguredTab(); 105 106 var count = $results().find( '.search-result-item' ).length; 107 var tpl = ( eazydocs_local_object.i18n && eazydocs_local_object.i18n.results_found ) || '%d results found'; 108 announce( tpl.replace( '%d', count ) ); 109 } else { 110 buildNoResult(); 111 } 112 } 113 114 // Run the AJAX live search. 115 function runSearch() { 116 var keyword = $( '#ezd_searchInput' ).val() || ''; 117 118 if ( $.trim( keyword ).length < MIN_CHARS ) { 119 if ( activeRequest ) { 120 activeRequest.abort(); 121 activeRequest = null; 122 } 123 $results().removeClass( 'ajax-search' ).html( '' ); 124 return; 125 } 126 127 // Abort any earlier request so a slow response can't overwrite a newer one. 128 if ( activeRequest ) { 129 activeRequest.abort(); 130 } 131 132 activeRequest = $.ajax( { 133 url: eazydocs_local_object.ajaxurl, 134 type: 'post', 135 data: { 136 action: 'eazydocs_search_results', 137 keyword: keyword, 138 post_type: selectedType(), 139 security: eazydocs_local_object.nonce 140 }, 141 beforeSend: function () { 142 $( '.spinner-border' ).show(); 143 } 144 } ).done( function ( data ) { 145 renderResults( data ); 146 } ).always( function ( jqXHR, status ) { 147 if ( status !== 'abort' ) { 148 $( '.spinner-border' ).hide(); 149 activeRequest = null; 150 } 151 } ); 152 } 153 154 // Expose for any inline/legacy callers. 155 window.ezSearchResults = runSearch; 156 157 var debouncedSearch = ( function () { 158 var timer = 0; 159 return function () { 160 clearTimeout( timer ); 161 timer = setTimeout( runSearch, DEBOUNCE_MS ); 162 }; 163 } )(); 164 165 // --- Keyboard navigation across result items ------------------------------- 166 function resultLinks() { 167 return $results().find( '.search-result-item a.title' ); 168 } 169 170 function moveSelection( dir ) { 171 var $items = resultLinks(); 172 if ( ! $items.length ) { 173 return; 174 } 175 var idx = $items.index( document.activeElement ); 176 var next = idx + dir; 177 if ( next < 0 ) { 178 $( '#ezd_searchInput' ).trigger( 'focus' ); 179 return; 180 } 181 if ( next >= $items.length ) { 182 next = $items.length - 1; 183 } 184 $items.eq( next ).trigger( 'focus' ); 185 } 186 187 $( function () { 188 189 // Focus state. 190 $( document ).on( 'focus', '#ezd_searchInput', function () { 191 $( 'body' ).addClass( 'ezd-search-focused' ); 192 $( this ).closest( 'form.ezd_search_form' ).css( 'z-index', '999' ); 193 } ); 194
195 $( document ).on( 'click', '.focus_overlay', resetSearch ); 196 197 // Result type tabs. 198 $( document ).on( 'click', '.ezd-tab', function () { 199 var tab = $( this ).data( 'tab' ); 200 $( '.ezd-tab' ).removeClass( 'active' ); 201 $( this ).addClass( 'active' ); 202 if ( tab === 'all' ) { 203 $( '#ezd-search-results .ezd-result-group' ).show(); 204 } else { 205 $( '#ezd-search-results .ezd-result-group' ).hide(); 206 $( '#ezd-search-results .ezd-result-group[data-type="' + tab + '"]' ).show(); 207 } 208 } ); 209 210 // Post-type filter dropdown (Elementor widget). 211 $( document ).on( 'click', '.ezd-type-filter-btn', function ( e ) { 212 e.stopPropagation(); 213 $( this ).next( '.ezd-type-filter-dropdown' ).toggleClass( 'open' ); 214 $( this ).toggleClass( 'open' ); 215 } ); 216 217 $( document ).on( 'click', '.ezd-type-option', function ( e ) { 218 e.preventDefault(); 219 e.stopPropagation(); 220 var newType = $( this ).data( 'type' ); 221 var newLabel = $( this ).text().trim(); 222 $( '#ezd_searchInput' ).attr( 'data-post-type', newType ).data( 'post-type', newType ); 223 $( '.ezd-filter-label' ).text( newLabel ); 224 $( '.ezd-type-filter-dropdown' ).removeClass( 'open' ); 225 $( '.ezd-type-filter-btn' ).removeClass( 'open' ); 226 if ( $.trim( $( '#ezd_searchInput' ).val() ).length ) { 227 runSearch(); 228 } 229 } ); 230 231 // Dropdown title click: docs â load child docs inline; others â navigate. 232 $( document ).on( 'click', '.ezd-title-dropdown li a:not(.ezd-type-option)', function ( e ) { 233 e.preventDefault(); 234 var postId = $( this ).data( 'id' ); 235 var postTitle = $( this ).text().trim(); 236 var fallbackUrl = $( this ).attr( 'href' ); 237 238 $( '.ezd-type-filter-dropdown' ).removeClass( 'open' ); 239 $( '.ezd-type-filter-btn' ).removeClass( 'open' ); 240 241 if ( selectedType() !== 'docs' ) { 242 window.location.href = fallbackUrl; 243 return; 244 } 245 246 $( '#ezd_searchInput' ).val( '' ); 247 $results().addClass( 'ajax-search' ).html( 248 '<div class="ezd-panel-loading"><span class="spinner-border spinner-border-sm" role="status"></span></div>' 249 ); 250 251 $.ajax( { 252 url: eazydocs_local_object.ajaxurl, 253 type: 'post', 254 data: { 255 action: 'eazydocs_child_docs', 256 parent_id: postId, 257 security: eazydocs_local_object.nonce 258 } 259 } ).done( function ( response ) { 260 if ( response.success && $.trim( response.data.html ).length > 0 ) { 261 $results().addClass( 'ajax-search' ).html( 262 '<div class="ezd-result-group-label">' + postTitle + '</div>' + response.data.html 263 ); 264 } else { 265 buildNoResult(); 266 } 267 } ).fail( buildNoResult ); 268 } ); 269 270 // Close the type dropdown when clicking outside it. 271 $( document ).on( 'click', function ( e ) { 272 if ( ! $( e.target ).closest( '.ezd-type-filter' ).length ) { 273 $( '.ezd-type-filter-dropdown' ).removeClass( 'open' ); 274 $( '.ezd-type-filter-btn' ).removeClass( 'open' ); 275 } 276 } ); 277 278 // Close results when clicking outside the search form. 279 $( document ).on( 'mousedown', function ( e ) { 280 var $t = $( e.target ); 281 if ( 282 ! $t.closest( '#ezd-search-results' ).length && 283 ! $t.closest( '.header_search_form_info' ).length && 284 ! $t.closest( '.header_search_keyword' ).length && 285 ! $t.closest( '.ezd_search_keywords' ).length && 286 ! $t.closest( '.focus_overlay' ).length 287 ) { 288 resetSearch(); 289 } 290 } ); 291 292 // Popular keyword chips. Two markups exist: the Elementor widget renders 293 // `.header_search_keyword`, the built-in banner/block renders `.ezd_search_keywords`. 294 $( document ).on( 'click', '.header_search_keyword ul li a, .ezd_search_keywords ul li a', function ( e ) { 295 e.preventDefault(); 296 $( '#ezd_searchInput' ).val( $.trim( $( this ).text() ) ).trigger( 'focus' ); 297 runSearch(); 298 } ); 299 300 // Live search (debounced); ignore navigation keys. 301 $( document ).on( 'keyup', '#ezd_searchInput', function ( e ) { 302 if ( e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' ) { 303 return; 304 } 305 debouncedSearch(); 306 } ); 307 308 // ArrowDown from the input moves into the result list. 309 $( document ).on( 'keydown', '#ezd_searchInput', function ( e ) { 310 if ( e.key === 'ArrowDown' && resultLinks().length ) { 311 e.preventDefault(); 312 resultLinks().first().trigger( 'focus' ); 313 } 314 } ); 315 316 // Arrow navigation between result items. 317 $( document ).on( 'keydown', '#ezd-search-results .search-result-item a.title', function ( e ) { 318 if ( e.key === 'ArrowDown' ) { 319 e.preventDefault(); 320 moveSelection( 1 ); 321 } else if ( e.key === 'ArrowUp' ) { 322 e.preventDefault(); 323 moveSelection( -1 ); 324 } 325 } ); 326 327 // Escape closes results from anywhere. 328 $( document ).on( 'keyup', function ( e ) { 329 if ( e.key === 'Escape' ) { 330 resetSearch(); 331 } 332 } ); 333 334 // Honour the "Enter key search" option. 335 $( document ).on( 'keypress', '#ezd_searchInput', function ( e ) { 336 if ( eazydocs_local_object.ezd_search_submit != 1 && e.key === 'Enter' ) { 337 e.preventDefault(); 338 } 339 } ); 340
341 // Submit button (widget): submit when allowed, otherwise block. 342 $( document ).on( 'click', 'button.search_submit_btn', function ( e ) { 343 if ( eazydocs_local_object.ezd_search_submit != 1 ) { 344 e.preventDefault(); 345 return false; 346 } 347 $( this ).closest( '.ezd_search_form' ).trigger( 'submit' ); 348 } ); 349 350 // Icon label (built-in banner): submit only when allowed; otherwise let it focus the input. 351 $( document ).on( 'click', 'label[for="ezd_searchInput"]', function () { 352 if ( eazydocs_local_object.ezd_search_submit == 1 ) { 353 $( this ).closest( '.ezd_search_form' ).trigger( 'submit' ); 354 } 355 } ); 356 357 // Block empty submits. 358 $( document ).on( 'submit', '.ezd_search_form', function ( e ) { 359 if ( $.trim( $( this ).find( '#ezd_searchInput' ).val() ) === '' ) { 360 e.preventDefault(); 361 return false; 362 } 363 } ); 364 365 // Native search-field clear (the "x") empties results. Bound directly 366 // because the `search` event does not reliably bubble for delegation. 367 var inputEl = document.getElementById( 'ezd_searchInput' ); 368 if ( inputEl ) { 369 inputEl.addEventListener( 'search', function () { 370 $results().empty().removeClass( 'ajax-search' ); 371 } ); 372 } 373 374 // Seamless inputâresults join: toggle a class while results are shown. 375 var resultsEl = document.getElementById( 'ezd-search-results' ); 376 if ( resultsEl && window.MutationObserver ) { 377 var $form = $( resultsEl ).closest( 'form.ezd_search_form' ); 378 new MutationObserver( function ( mutations ) { 379 mutations.forEach( function ( m ) { 380 if ( m.attributeName === 'class' ) { 381 $form.toggleClass( 'ezd-results-open', resultsEl.classList.contains( 'ajax-search' ) ); 382 } 383 } ); 384 } ).observe( resultsEl, { attributes: true } ); 385 } 386 } ); 387 388} )( jQuery );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.