PageSourceSearch

https://help.tudo.lk/wp-content/plugins/eazydocs/assets/js/frontend/search-banner.js?ver=2.13.1

js tudo.lk collected 2026-10-01 16:33:05 UTC 12,464 bytes, 388 lines download raw bytes

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.