I’ll build this as a standalone advanced AJAX search chunk for RX Theme, with debounce, caching, keyboard navigation, accessibility, loading states, error handling, and WordPress REST/AJAX compatibility.
Below is an advanced full code for:
assets/static-js/static-chunks-js/chunk-045-ajax-search.js
It is written as a theme chunk file, safe for RX Theme, and includes: debounce, abort controller, local cache, keyboard navigation, ARIA accessibility, recent search memory, popular search support, REST API/AJAX fallback, mobile overlay support, close on Escape/outside click, and extensible custom events.
/*!
* RX Theme - Advanced AJAX Search Chunk
* File: assets/static-js/static-chunks-js/chunk-045-ajax-search.js
* Version: 1.0.0
*
* Expected HTML example:
*
* <form class="rx-ajax-search" role="search">
* <label class="screen-reader-text" for="rx-search-input">Search</label>
* <input
* id="rx-search-input"
* class="rx-ajax-search__input"
* type="search"
* name="s"
* placeholder="Search diseases, symptoms, treatments..."
* autocomplete="off"
* data-rx-search-input
* >
* <button type="submit">Search</button>
* <div class="rx-ajax-search__results" data-rx-search-results></div>
* </form>
*
* Optional global config from WordPress:
*
* wp_localize_script(
* 'rx-chunk-045-ajax-search',
* 'RX_AJAX_SEARCH',
* [
* 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
* 'restUrl' => esc_url_raw( rest_url( 'wp/v2/search' ) ),
* 'homeUrl' => home_url( '/' ),
* 'searchUrl' => home_url( '/' ),
* 'nonce' => wp_create_nonce( 'rx_ajax_search_nonce' ),
* 'restNonce' => wp_create_nonce( 'wp_rest' ),
* 'action' => 'rx_ajax_search',
* 'minChars' => 2,
* 'limit' => 8,
* 'debounce' => 280,
* 'cacheTTL' => 300000,
* 'enableCache' => true,
* 'enableRecent' => true,
* ]
* );
*/
(function RXAjaxSearchChunk(window, document) {
'use strict';
if (!window || !document) {
return;
}
var ROOT = document.documentElement;
var DEFAULTS = {
selectors: {
form: '.rx-ajax-search',
input: '[data-rx-search-input], .rx-ajax-search__input',
results: '[data-rx-search-results], .rx-ajax-search__results',
submit: '[data-rx-search-submit], .rx-ajax-search__submit',
clear: '[data-rx-search-clear], .rx-ajax-search__clear',
overlay: '[data-rx-search-overlay]',
openButton: '[data-rx-search-open]',
closeButton: '[data-rx-search-close]'
},
classes: {
active: 'is-active',
loading: 'is-loading',
open: 'is-open',
empty: 'is-empty',
error: 'has-error',
selected: 'is-selected',
hidden: 'is-hidden',
bodyLocked: 'rx-search-open'
},
attributes: {
expanded: 'aria-expanded',
controls: 'aria-controls',
selected: 'aria-selected',
activedescendant: 'aria-activedescendant',
busy: 'aria-busy',
live: 'aria-live'
},
minChars: 2,
limit: 8,
debounce: 280,
cacheTTL: 5 * 60 * 1000,
enableCache: true,
enableRecent: true,
maxRecent: 8,
enablePopular: true,
requestMode: 'auto', // auto | rest | ajax
highlight: true,
submitOnEnterWithoutSelection: true,
closeOnOutsideClick: true,
closeOnEscape: true,
focusFirstResultOnArrowDown: true,
messages: {
start: 'Start typing to search.',
loading: 'Searching...',
empty: 'No results found.',
error: 'Search is not available right now.',
tooShort: 'Please type at least {min} characters.',
recentTitle: 'Recent searches',
popularTitle: 'Popular searches',
clearRecent: 'Clear recent searches',
viewAll: 'View all results',
resultSingular: 'result found',
resultPlural: 'results found'
},
popular: [
'Back pain',
'Diabetes',
'High blood pressure',
'Anemia',
'Neutropenia',
'Disc displacement'
]
};
var WP_CONFIG = window.RX_AJAX_SEARCH || {};
var CONFIG = deepMerge(DEFAULTS, normalizeConfig(WP_CONFIG));
var memoryCache = new Map();
var activeController = null;
var instanceCounter = 0;
function normalizeConfig(config) {
return {
ajaxUrl: config.ajaxUrl || config.ajax_url || '',
restUrl: config.restUrl || config.rest_url || '',
homeUrl: config.homeUrl || config.home_url || '/',
searchUrl: config.searchUrl || config.search_url || '/',
nonce: config.nonce || '',
restNonce: config.restNonce || config.rest_nonce || '',
action: config.action || 'rx_ajax_search',
minChars: toInt(config.minChars, DEFAULTS.minChars),
limit: toInt(config.limit, DEFAULTS.limit),
debounce: toInt(config.debounce, DEFAULTS.debounce),
cacheTTL: toInt(config.cacheTTL, DEFAULTS.cacheTTL),
enableCache: toBool(config.enableCache, DEFAULTS.enableCache),
enableRecent: toBool(config.enableRecent, DEFAULTS.enableRecent),
enablePopular: toBool(config.enablePopular, DEFAULTS.enablePopular),
requestMode: config.requestMode || DEFAULTS.requestMode,
popular: Array.isArray(config.popular) ? config.popular : DEFAULTS.popular
};
}
function deepMerge(target, source) {
var output = Object.assign({}, target);
Object.keys(source || {}).forEach(function mergeKey(key) {
if (
source[key] &&
typeof source[key] === 'object' &&
!Array.isArray(source[key]) &&
target[key] &&
typeof target[key] === 'object'
) {
output[key] = deepMerge(target[key], source[key]);
} else {
output[key] = source[key];
}
});
return output;
}
function toInt(value, fallback) {
var parsed = parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : fallback;
}
function toBool(value, fallback) {
if (typeof value === 'boolean') {
return value;
}
if (value === 'true' || value === '1' || value === 1) {
return true;
}
if (value === 'false' || value === '0' || value === 0) {
return false;
}
return fallback;
}
function debounce(fn, delay) {
var timer = null;
return function debounced() {
var context = this;
var args = arguments;
window.clearTimeout(timer);
timer = window.setTimeout(function runDebounced() {
fn.apply(context, args);
}, delay);
};
}
function escapeHTML(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function normalizeQuery(value) {
return String(value || '')
.trim()
.replace(/\s+/g, ' ')
.slice(0, 120);
}
function buildSearchUrl(query) {
var base = CONFIG.searchUrl || CONFIG.homeUrl || '/';
var url;
try {
url = new URL(base, window.location.origin);
url.searchParams.set('s', query);
return url.toString();
} catch (error) {
return '/?s=' + encodeURIComponent(query);
}
}
function emit(name, detail) {
document.dispatchEvent(
new CustomEvent('rx:ajax-search:' + name, {
bubbles: true,
detail: detail || {}
})
);
}
function getCacheKey(query) {
return normalizeQuery(query).toLowerCase();
}
function getFromCache(query) {
if (!CONFIG.enableCache) {
return null;
}
var key = getCacheKey(query);
var item = memoryCache.get(key);
if (!item) {
return null;
}
if (Date.now() - item.time > CONFIG.cacheTTL) {
memoryCache.delete(key);
return null;
}
return item.data;
}
function setToCache(query, data) {
if (!CONFIG.enableCache) {
return;
}
memoryCache.set(getCacheKey(query), {
time: Date.now(),
data: data
});
if (memoryCache.size > 60) {
var firstKey = memoryCache.keys().next().value;
memoryCache.delete(firstKey);
}
}
function storageAvailable() {
try {
var key = '__rx_storage_test__';
window.localStorage.setItem(key, key);
window.localStorage.removeItem(key);
return true;
} catch (error) {
return false;
}
}
var canUseStorage = storageAvailable();
var RECENT_KEY = 'rx_theme_recent_searches';
function getRecentSearches() {
if (!CONFIG.enableRecent || !canUseStorage) {
return [];
}
try {
var raw = window.localStorage.getItem(RECENT_KEY);
var parsed = JSON.parse(raw || '[]');
if (!Array.isArray(parsed)) {
return [];
}
return parsed
.filter(Boolean)
.map(normalizeQuery)
.filter(Boolean)
.slice(0, CONFIG.maxRecent);
} catch (error) {
return [];
}
}
function saveRecentSearch(query) {
if (!CONFIG.enableRecent || !canUseStorage) {
return;
}
var clean = normalizeQuery(query);
if (!clean || clean.length < CONFIG.minChars) {
return;
}
var existing = getRecentSearches().filter(function removeDuplicate(item) {
return item.toLowerCase() !== clean.toLowerCase();
});
existing.unshift(clean);
try {
window.localStorage.setItem(
RECENT_KEY,
JSON.stringify(existing.slice(0, CONFIG.maxRecent))
);
} catch (error) {
// Ignore localStorage write errors.
}
}
function clearRecentSearches() {
if (!canUseStorage) {
return;
}
try {
window.localStorage.removeItem(RECENT_KEY);
} catch (error) {
// Ignore localStorage errors.
}
}
function highlightText(text, query) {
var safeText = escapeHTML(text);
if (!CONFIG.highlight || !query) {
return safeText;
}
var cleanQuery = normalizeQuery(query);
if (!cleanQuery) {
return safeText;
}
var terms = cleanQuery
.split(' ')
.filter(function validTerm(term) {
return term.length > 1;
})
.map(function escapeRegExp(term) {
return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
});
if (!terms.length) {
return safeText;
}
try {
var regex = new RegExp('(' + terms.join('|') + ')', 'gi');
return safeText.replace(regex, '<mark>$1</mark>');
} catch (error) {
return safeText;
}
}
function normalizeResult(item) {
var title =
item.title ||
item.post_title ||
item.name ||
item.label ||
'';
if (typeof title === 'object' && title.rendered) {
title = title.rendered;
}
var url =
item.url ||
item.link ||
item.permalink ||
item.guid ||
'#';
var excerpt =
item.excerpt ||
item.post_excerpt ||
item.description ||
item.summary ||
'';
if (typeof excerpt === 'object' && excerpt.rendered) {
excerpt = excerpt.rendered;
}
var type =
item.type ||
item.subtype ||
item.post_type ||
'post';
var image =
item.image ||
item.thumbnail ||
item.featured_image ||
item.featuredImage ||
'';
return {
id: item.id || item.ID || url || title,
title: stripTags(title),
url: url,
excerpt: stripTags(excerpt),
type: type,
image: image
};
}
function stripTags(value) {
var div = document.createElement('div');
div.innerHTML = String(value || '');
return div.textContent || div.innerText || '';
}
async function fetchResults(query) {
var cached = getFromCache(query);
if (cached) {
emit('cache-hit', {
query: query,
results: cached
});
return cached;
}
if (activeController) {
activeController.abort();
}
activeController = new AbortController();
var mode = CONFIG.requestMode;
if (mode === 'rest') {
return fetchRestResults(query, activeController.signal);
}
if (mode === 'ajax') {
return fetchAjaxResults(query, activeController.signal);
}
try {
if (CONFIG.restUrl) {
return await fetchRestResults(query, activeController.signal);
}
return await fetchAjaxResults(query, activeController.signal);
} catch (error) {
if (error && error.name === 'AbortError') {
throw error;
}
if (CONFIG.ajaxUrl && CONFIG.restUrl) {
return fetchAjaxResults(query, activeController.signal);
}
throw error;
}
}
async function fetchRestResults(query, signal) {
if (!CONFIG.restUrl) {
throw new Error('REST URL is missing.');
}
var url = new URL(CONFIG.restUrl, window.location.origin);
url.searchParams.set('search', query);
url.searchParams.set('per_page', CONFIG.limit);
var headers = {
Accept: 'application/json'
};
if (CONFIG.restNonce) {
headers['X-WP-Nonce'] = CONFIG.restNonce;
}
emit('request-start', {
query: query,
mode: 'rest'
});
var response = await window.fetch(url.toString(), {
method: 'GET',
credentials: 'same-origin',
headers: headers,
signal: signal
});
if (!response.ok) {
throw new Error('REST search failed with status ' + response.status);
}
var json = await response.json();
var results = Array.isArray(json) ? json.map(normalizeResult) : [];
setToCache(query, results);
emit('request-success', {
query: query,
mode: 'rest',
results: results
});
return results;
}
async function fetchAjaxResults(query, signal) {
if (!CONFIG.ajaxUrl) {
throw new Error('AJAX URL is missing.');
}
var formData = new FormData();
formData.append('action', CONFIG.action);
formData.append('nonce', CONFIG.nonce);
formData.append('search', query);
formData.append('s', query);
formData.append('limit', CONFIG.limit);
emit('request-start', {
query: query,
mode: 'ajax'
});
var response = await window.fetch(CONFIG.ajaxUrl, {
method: 'POST',
credentials: 'same-origin',
body: formData,
signal: signal
});
if (!response.ok) {
throw new Error('AJAX search failed with status ' + response.status);
}
var json = await response.json();
var rawResults = [];
if (Array.isArray(json)) {
rawResults = json;
} else if (json && Array.isArray(json.data)) {
rawResults = json.data;
} else if (json && json.data && Array.isArray(json.data.results)) {
rawResults = json.data.results;
} else if (json && Array.isArray(json.results)) {
rawResults = json.results;
}
var results = rawResults.map(normalizeResult);
setToCache(query, results);
emit('request-success', {
query: query,
mode: 'ajax',
results: results
});
return results;
}
function RXAjaxSearch(form) {
this.form = form;
this.id = 'rx-ajax-search-' + ++instanceCounter;
this.input = form.querySelector(CONFIG.selectors.input);
this.results = form.querySelector(CONFIG.selectors.results);
this.submit = form.querySelector(CONFIG.selectors.submit);
this.clear = form.querySelector(CONFIG.selectors.clear);
this.selectedIndex = -1;
this.currentResults = [];
this.isOpen = false;
this.lastQuery = '';
if (!this.input) {
return;
}
if (!this.results) {
this.results = document.createElement('div');
this.results.className = 'rx-ajax-search__results';
this.results.setAttribute('data-rx-search-results', '');
this.form.appendChild(this.results);
}
this.init();
}
RXAjaxSearch.prototype.init = function init() {
var self = this;
this.setupAccessibility();
this.handleInput = debounce(function handleDebouncedInput() {
self.onInput();
}, CONFIG.debounce);
this.input.addEventListener('input', this.handleInput);
this.input.addEventListener('focus', function onFocus() {
self.onFocus();
});
this.input.addEventListener('keydown', function onKeydown(event) {
self.onKeydown(event);
});
this.form.addEventListener('submit', function onSubmit(event) {
self.onSubmit(event);
});
if (this.clear) {
this.clear.addEventListener('click', function onClear(event) {
event.preventDefault();
self.clearSearch();
});
}
this.results.addEventListener('mousedown', function preventBlur(event) {
event.preventDefault();
});
this.results.addEventListener('click', function onResultClick(event) {
self.onResultsClick(event);
});
if (CONFIG.closeOnOutsideClick) {
document.addEventListener('click', function onDocumentClick(event) {
if (!self.form.contains(event.target)) {
self.close();
}
});
}
this.renderInitial();
emit('init', {
id: this.id,
form: this.form
});
};
RXAjaxSearch.prototype.setupAccessibility = function setupAccessibility() {
this.results.id = this.results.id || this.id + '-results';
this.input.setAttribute('role', 'combobox');
this.input.setAttribute('autocomplete', 'off');
this.input.setAttribute('aria-autocomplete', 'list');
this.input.setAttribute(CONFIG.attributes.expanded, 'false');
this.input.setAttribute(CONFIG.attributes.controls, this.results.id);
this.results.setAttribute('role', 'listbox');
this.results.setAttribute(CONFIG.attributes.live, 'polite');
this.results.setAttribute(CONFIG.attributes.busy, 'false');
};
RXAjaxSearch.prototype.setLoading = function setLoading(isLoading) {
this.form.classList.toggle(CONFIG.classes.loading, !!isLoading);
this.results.setAttribute(CONFIG.attributes.busy, isLoading ? 'true' : 'false');
};
RXAjaxSearch.prototype.open = function open() {
this.isOpen = true;
this.form.classList.add(CONFIG.classes.active);
this.results.classList.add(CONFIG.classes.open);
this.input.setAttribute(CONFIG.attributes.expanded, 'true');
};
RXAjaxSearch.prototype.close = function close() {
this.isOpen = false;
this.selectedIndex = -1;
this.form.classList.remove(CONFIG.classes.active);
this.results.classList.remove(CONFIG.classes.open);
this.input.setAttribute(CONFIG.attributes.expanded, 'false');
this.input.removeAttribute(CONFIG.attributes.activedescendant);
this.updateSelection();
};
RXAjaxSearch.prototype.onFocus = function onFocus() {
var query = normalizeQuery(this.input.value);
if (query.length >= CONFIG.minChars && this.currentResults.length) {
this.open();
return;
}
this.renderInitial();
this.open();
};
RXAjaxSearch.prototype.onInput = async function onInput() {
var query = normalizeQuery(this.input.value);
this.lastQuery = query;
this.selectedIndex = -1;
if (!query) {
this.renderInitial();
this.open();
return;
}
if (query.length < CONFIG.minChars) {
this.renderMessage(
CONFIG.messages.tooShort.replace('{min}', CONFIG.minChars),
'too-short'
);
this.open();
return;
}
this.setLoading(true);
this.renderMessage(CONFIG.messages.loading, 'loading');
this.open();
try {
var results = await fetchResults(query);
if (this.lastQuery !== query) {
return;
}
this.currentResults = results;
if (!results.length) {
this.renderMessage(CONFIG.messages.empty, 'empty');
} else {
this.renderResults(results, query);
}
emit('render', {
query: query,
results: results
});
} catch (error) {
if (error && error.name === 'AbortError') {
return;
}
this.currentResults = [];
this.renderMessage(CONFIG.messages.error, 'error');
emit('error', {
query: query,
error: error
});
} finally {
this.setLoading(false);
}
};
RXAjaxSearch.prototype.onKeydown = function onKeydown(event) {
var key = event.key;
if (key === 'Escape' && CONFIG.closeOnEscape) {
this.close();
return;
}
if (!this.isOpen && (key === 'ArrowDown' || key === 'ArrowUp')) {
this.open();
}
var items = this.getSelectableItems();
if (!items.length) {
return;
}
if (key === 'ArrowDown') {
event.preventDefault();
if (
this.selectedIndex === -1 &&
CONFIG.focusFirstResultOnArrowDown
) {
this.selectedIndex = 0;
} else {
this.selectedIndex = Math.min(this.selectedIndex + 1, items.length - 1);
}
this.updateSelection();
}
if (key === 'ArrowUp') {
event.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
this.updateSelection();
}
if (key === 'Enter') {
if (this.selectedIndex >= 0 && items[this.selectedIndex]) {
event.preventDefault();
this.followItem(items[this.selectedIndex]);
}
}
if (key === 'Tab') {
this.close();
}
};
RXAjaxSearch.prototype.onSubmit = function onSubmit(event) {
var query = normalizeQuery(this.input.value);
var items = this.getSelectableItems();
if (this.selectedIndex >= 0 && items[this.selectedIndex]) {
event.preventDefault();
this.followItem(items[this.selectedIndex]);
return;
}
if (!query || query.length < CONFIG.minChars) {
event.preventDefault();
this.renderMessage(
CONFIG.messages.tooShort.replace('{min}', CONFIG.minChars),
'too-short'
);
this.open();
return;
}
saveRecentSearch(query);
if (CONFIG.submitOnEnterWithoutSelection) {
event.preventDefault();
window.location.href = buildSearchUrl(query);
}
};
RXAjaxSearch.prototype.onResultsClick = function onResultsClick(event) {
var recentButton = event.target.closest('[data-rx-recent-query]');
var clearRecentButton = event.target.closest('[data-rx-clear-recent]');
var popularButton = event.target.closest('[data-rx-popular-query]');
var viewAll = event.target.closest('[data-rx-view-all]');
if (clearRecentButton) {
event.preventDefault();
clearRecentSearches();
this.renderInitial();
return;
}
if (recentButton) {
event.preventDefault();
this.input.value = recentButton.getAttribute('data-rx-recent-query') || '';
this.input.focus();
this.onInput();
return;
}
if (popularButton) {
event.preventDefault();
this.input.value = popularButton.getAttribute('data-rx-popular-query') || '';
this.input.focus();
this.onInput();
return;
}
if (viewAll) {
var query = normalizeQuery(this.input.value);
if (query) {
saveRecentSearch(query);
}
}
};
RXAjaxSearch.prototype.followItem = function followItem(item) {
var link = item.querySelector('a[href]');
if (!link) {
return;
}
var query = normalizeQuery(this.input.value);
if (query) {
saveRecentSearch(query);
}
window.location.href = link.href;
};
RXAjaxSearch.prototype.getSelectableItems = function getSelectableItems() {
return Array.prototype.slice.call(
this.results.querySelectorAll('[data-rx-search-item]')
);
};
RXAjaxSearch.prototype.updateSelection = function updateSelection() {
var self = this;
var items = this.getSelectableItems();
items.forEach(function updateItem(item, index) {
var isSelected = index === self.selectedIndex;
item.classList.toggle(CONFIG.classes.selected, isSelected);
item.setAttribute(CONFIG.attributes.selected, isSelected ? 'true' : 'false');
if (isSelected) {
self.input.setAttribute(CONFIG.attributes.activedescendant, item.id);
item.scrollIntoView({
block: 'nearest'
});
}
});
if (this.selectedIndex < 0) {
this.input.removeAttribute(CONFIG.attributes.activedescendant);
}
};
RXAjaxSearch.prototype.renderInitial = function renderInitial() {
var html = '';
var recent = getRecentSearches();
this.currentResults = [];
if (recent.length) {
html += '<div class="rx-search-panel rx-search-panel--recent">';
html += '<div class="rx-search-panel__header">';
html += '<strong>' + escapeHTML(CONFIG.messages.recentTitle) + '</strong>';
html += '<button type="button" class="rx-search-panel__clear" data-rx-clear-recent>';
html += escapeHTML(CONFIG.messages.clearRecent);
html += '</button>';
html += '</div>';
html += '<div class="rx-search-suggestions">';
recent.forEach(function renderRecent(query) {
html += '<button type="button" class="rx-search-suggestion" data-rx-recent-query="' + escapeHTML(query) + '">';
html += escapeHTML(query);
html += '</button>';
});
html += '</div>';
html += '</div>';
}
if (CONFIG.enablePopular && CONFIG.popular && CONFIG.popular.length) {
html += '<div class="rx-search-panel rx-search-panel--popular">';
html += '<div class="rx-search-panel__header">';
html += '<strong>' + escapeHTML(CONFIG.messages.popularTitle) + '</strong>';
html += '</div>';
html += '<div class="rx-search-suggestions">';
CONFIG.popular.slice(0, 10).forEach(function renderPopular(query) {
html += '<button type="button" class="rx-search-suggestion" data-rx-popular-query="' + escapeHTML(query) + '">';
html += escapeHTML(query);
html += '</button>';
});
html += '</div>';
html += '</div>';
}
if (!html) {
html = this.getMessageHTML(CONFIG.messages.start, 'start');
}
this.results.innerHTML = html;
};
RXAjaxSearch.prototype.renderMessage = function renderMessage(message, type) {
this.currentResults = [];
this.results.innerHTML = this.getMessageHTML(message, type);
this.form.classList.toggle(CONFIG.classes.empty, type === 'empty');
this.form.classList.toggle(CONFIG.classes.error, type === 'error');
};
RXAjaxSearch.prototype.getMessageHTML = function getMessageHTML(message, type) {
return (
'<div class="rx-search-message rx-search-message--' + escapeHTML(type || 'info') + '">' +
escapeHTML(message) +
'</div>'
);
};
RXAjaxSearch.prototype.renderResults = function renderResults(results, query) {
var self = this;
var totalText =
results.length === 1
? CONFIG.messages.resultSingular
: CONFIG.messages.resultPlural;
var html = '';
html += '<div class="rx-search-results-wrap">';
html += '<div class="rx-search-results-count">';
html += escapeHTML(results.length + ' ' + totalText);
html += '</div>';
html += '<ul class="rx-search-results-list" role="presentation">';
results.forEach(function renderResult(result, index) {
var itemId = self.id + '-item-' + index;
var typeLabel = result.type ? escapeHTML(result.type) : '';
html += '<li';
html += ' id="' + escapeHTML(itemId) + '"';
html += ' class="rx-search-result"';
html += ' role="option"';
html += ' aria-selected="false"';
html += ' data-rx-search-item';
html += '>';
html += '<a class="rx-search-result__link" href="' + escapeHTML(result.url) + '">';
if (result.image) {
html += '<span class="rx-search-result__media">';
html += '<img src="' + escapeHTML(result.image) + '" alt="" loading="lazy" decoding="async">';
html += '</span>';
}
html += '<span class="rx-search-result__content">';
html += '<span class="rx-search-result__title">';
html += highlightText(result.title, query);
html += '</span>';
if (result.excerpt) {
html += '<span class="rx-search-result__excerpt">';
html += highlightText(result.excerpt.slice(0, 150), query);
html += '</span>';
}
if (typeLabel) {
html += '<span class="rx-search-result__type">';
html += typeLabel;
html += '</span>';
}
html += '</span>';
html += '</a>';
html += '</li>';
});
html += '</ul>';
html += '<a class="rx-search-view-all" href="' + escapeHTML(buildSearchUrl(query)) + '" data-rx-view-all>';
html += escapeHTML(CONFIG.messages.viewAll);
html += '</a>';
html += '</div>';
this.results.innerHTML = html;
this.form.classList.remove(CONFIG.classes.empty, CONFIG.classes.error);
};
RXAjaxSearch.prototype.clearSearch = function clearSearch() {
this.input.value = '';
this.selectedIndex = -1;
this.currentResults = [];
this.renderInitial();
this.input.focus();
this.open();
emit('clear', {
id: this.id
});
};
function initForms(context) {
var scope = context || document;
var forms = scope.querySelectorAll(CONFIG.selectors.form);
Array.prototype.forEach.call(forms, function setupForm(form) {
if (form.__rxAjaxSearchReady) {
return;
}
form.__rxAjaxSearchReady = true;
form.__rxAjaxSearchInstance = new RXAjaxSearch(form);
});
}
function initOverlayControls() {
var openButtons = document.querySelectorAll(CONFIG.selectors.openButton);
var closeButtons = document.querySelectorAll(CONFIG.selectors.closeButton);
var overlay = document.querySelector(CONFIG.selectors.overlay);
if (!overlay) {
return;
}
function openOverlay() {
overlay.classList.add(CONFIG.classes.open);
ROOT.classList.add(CONFIG.classes.bodyLocked);
var input = overlay.querySelector(CONFIG.selectors.input);
window.setTimeout(function focusInput() {
if (input) {
input.focus();
}
}, 30);
emit('overlay-open', {
overlay: overlay
});
}
function closeOverlay() {
overlay.classList.remove(CONFIG.classes.open);
ROOT.classList.remove(CONFIG.classes.bodyLocked);
emit('overlay-close', {
overlay: overlay
});
}
Array.prototype.forEach.call(openButtons, function bindOpen(button) {
button.addEventListener('click', function onOpenClick(event) {
event.preventDefault();
openOverlay();
});
});
Array.prototype.forEach.call(closeButtons, function bindClose(button) {
button.addEventListener('click', function onCloseClick(event) {
event.preventDefault();
closeOverlay();
});
});
if (CONFIG.closeOnEscape) {
document.addEventListener('keydown', function closeOnEsc(event) {
if (event.key === 'Escape' && overlay.classList.contains(CONFIG.classes.open)) {
closeOverlay();
}
});
}
overlay.addEventListener('click', function closeOnBackdrop(event) {
if (event.target === overlay) {
closeOverlay();
}
});
}
function initMutationObserver() {
if (!('MutationObserver' in window)) {
return;
}
var observer = new MutationObserver(function onMutation(mutations) {
mutations.forEach(function eachMutation(mutation) {
Array.prototype.forEach.call(mutation.addedNodes, function eachNode(node) {
if (!node || node.nodeType !== 1) {
return;
}
if (node.matches && node.matches(CONFIG.selectors.form)) {
initForms(node.parentNode || document);
} else if (node.querySelector && node.querySelector(CONFIG.selectors.form)) {
initForms(node);
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
function ready(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback);
} else {
callback();
}
}
ready(function onReady() {
initForms(document);
initOverlayControls();
initMutationObserver();
emit('ready', {
config: CONFIG
});
});
window.RXAjaxSearch = {
init: initForms,
clearCache: function clearCache() {
memoryCache.clear();
},
getRecent: getRecentSearches,
clearRecent: clearRecentSearches,
config: CONFIG
};
})(window, document);
For WordPress backend support, you need an AJAX handler like this in your theme PHP file, preferably inside:
inc/ajax/ajax-search.php
<?php
/**
* RX Theme AJAX Search Handler
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'wp_ajax_rx_ajax_search', 'rx_theme_ajax_search' );
add_action( 'wp_ajax_nopriv_rx_ajax_search', 'rx_theme_ajax_search' );
function rx_theme_ajax_search() {
if ( isset( $_POST['nonce'] ) ) {
check_ajax_referer( 'rx_ajax_search_nonce', 'nonce' );
}
$search = isset( $_POST['search'] )
? sanitize_text_field( wp_unslash( $_POST['search'] ) )
: '';
$limit = isset( $_POST['limit'] )
? absint( $_POST['limit'] )
: 8;
if ( strlen( $search ) < 2 ) {
wp_send_json_success( array() );
}
$query = new WP_Query(
array(
's' => $search,
'post_type' => array( 'post', 'page' ),
'post_status' => 'publish',
'posts_per_page' => min( $limit, 20 ),
'ignore_sticky_posts' => true,
'no_found_rows' => true,
)
);
$results = array();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$results[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'url' => get_permalink(),
'excerpt' => wp_trim_words( get_the_excerpt(), 22 ),
'type' => get_post_type(),
'thumbnail' => get_the_post_thumbnail_url( get_the_ID(), 'thumbnail' ),
);
}
wp_reset_postdata();
}
wp_send_json_success( $results );
}
And enqueue/localize it like this:
wp_enqueue_script(
'rx-chunk-045-ajax-search',
get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-045-ajax-search.js',
array(),
wp_get_theme()->get( 'Version' ),
true
);
wp_localize_script(
'rx-chunk-045-ajax-search',
'RX_AJAX_SEARCH',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'restUrl' => esc_url_raw( rest_url( 'wp/v2/search' ) ),
'homeUrl' => home_url( '/' ),
'searchUrl' => home_url( '/' ),
'nonce' => wp_create_nonce( 'rx_ajax_search_nonce' ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
'action' => 'rx_ajax_search',
'minChars' => 2,
'limit' => 8,
'debounce' => 280,
'cacheTTL' => 300000,
'enableCache' => true,
'enableRecent' => true,
'enablePopular'=> true,
'popular' => array(
'Back pain',
'Diabetes',
'High blood pressure',
'Anemia',
'Neutropenia',
'Disc displacement',
),
)
);
Best RX Theme folder placement:
rx-theme/
├── assets/
│ └── static-js/
│ └── static-chunks-js/
│ └── chunk-045-ajax-search.js
│
├── inc/
│ └── ajax/
│ └── ajax-search.php
│
└── functions.php
In functions.php, include the AJAX PHP file:
require_once get_template_directory() . '/inc/ajax/ajax-search.php';
This file is now suitable for an advanced RX Theme search system.