chunk-004-search.js

I’ll build this as a powerful standalone search chunk for RX Theme: safe DOM handling, debounce, keyboard support, suggestions, recent searches, highlighting, accessibility, and optional WordPress REST API integration.

Yes friend. Put this full advanced code inside:

assets/static-js/static-chunks-js/chunk-004-search.js

This file is written as a safe standalone RX Theme search module. It supports:

Search overlay, instant search, debounce, REST API search, keyboard navigation, recent searches, popular searches, search highlight, accessibility, body scroll lock, escape close, outside click close, no-result message, loading state, mobile friendly behavior, and auto-init.

/*!
 * RX Theme - Advanced Search Chunk
 * File: assets/static-js/static-chunks-js/chunk-004-search.js
 * Theme: RX Theme
 * Author: RxHarun
 *
 * Purpose:
 * Advanced front-end search system for WordPress theme.
 *
 * Supported HTML selectors:
 * .rx-search-toggle
 * .rx-search-close
 * .rx-search-overlay
 * .rx-search-panel
 * .rx-search-input
 * .rx-search-results
 * .rx-search-form
 * .rx-search-submit
 * .rx-search-clear
 * .rx-search-popular
 * .rx-search-recent
 */

(function () {
  "use strict";

  /**
   * Prevent duplicate loading.
   */
  if (window.RXThemeSearchChunkLoaded) {
    return;
  }

  window.RXThemeSearchChunkLoaded = true;

  /**
   * Main namespace.
   */
  window.RXTheme = window.RXTheme || {};

  /**
   * RX Advanced Search Module.
   */
  var RXSearch = {
    version: "1.0.0",

    config: {
      selectors: {
        toggle: ".rx-search-toggle",
        close: ".rx-search-close",
        overlay: ".rx-search-overlay",
        panel: ".rx-search-panel",
        input: ".rx-search-input",
        form: ".rx-search-form",
        results: ".rx-search-results",
        submit: ".rx-search-submit",
        clear: ".rx-search-clear",
        popular: ".rx-search-popular",
        recent: ".rx-search-recent",
        bodyOpenClass: "rx-search-is-open",
        overlayOpenClass: "is-open",
        activeClass: "is-active",
        loadingClass: "is-loading",
        hiddenClass: "is-hidden"
      },

      search: {
        minChars: 2,
        debounceDelay: 350,
        maxResults: 8,
        maxRecent: 8,
        requestTimeout: 10000,
        highlightTerms: true,
        cacheResults: true,
        cacheTTL: 5 * 60 * 1000,
        allowEmptySubmit: false,
        submitToWordPressSearch: true,
        wpSearchPath: "/?s="
      },

      rest: {
        enabled: true,
        postsEndpoint: "/wp-json/wp/v2/search",
        postTypes: ["post", "page"],
        perPage: 8,
        subtype: "any"
      },

      storage: {
        enabled: true,
        key: "rx_theme_recent_searches"
      },

      popularSearches: [
        "back pain",
        "neck pain",
        "knee pain",
        "diabetes",
        "hypertension",
        "heart disease",
        "arthritis",
        "vitamin D"
      ],

      messages: {
        start: "Start typing to search RX medical articles.",
        loading: "Searching...",
        noResults: "No results found. Try another keyword.",
        error: "Search is temporarily unavailable. Please try again.",
        tooShort: "Type at least 2 characters.",
        empty: "Please enter a search term."
      }
    },

    state: {
      initialized: false,
      isOpen: false,
      lastQuery: "",
      activeIndex: -1,
      results: [],
      cache: {},
      abortController: null,
      debounceTimer: null,
      lastFocusedElement: null
    },

    elements: {
      toggles: [],
      closes: [],
      overlay: null,
      panel: null,
      input: null,
      form: null,
      results: null,
      submit: null,
      clear: null,
      popular: null,
      recent: null
    },

    /**
     * Initialize module.
     */
    init: function () {
      if (this.state.initialized) {
        return;
      }

      this.collectElements();

      if (!this.elements.input && !this.elements.overlay) {
        return;
      }

      this.bindEvents();
      this.setupAccessibility();
      this.renderInitialState();
      this.renderPopularSearches();
      this.renderRecentSearches();

      this.state.initialized = true;

      this.dispatch("rxSearchReady", {
        version: this.version
      });
    },

    /**
     * Collect DOM elements.
     */
    collectElements: function () {
      var s = this.config.selectors;

      this.elements.toggles = Array.prototype.slice.call(document.querySelectorAll(s.toggle));
      this.elements.closes = Array.prototype.slice.call(document.querySelectorAll(s.close));
      this.elements.overlay = document.querySelector(s.overlay);
      this.elements.panel = document.querySelector(s.panel);
      this.elements.input = document.querySelector(s.input);
      this.elements.form = document.querySelector(s.form);
      this.elements.results = document.querySelector(s.results);
      this.elements.submit = document.querySelector(s.submit);
      this.elements.clear = document.querySelector(s.clear);
      this.elements.popular = document.querySelector(s.popular);
      this.elements.recent = document.querySelector(s.recent);
    },

    /**
     * Bind all events.
     */
    bindEvents: function () {
      var self = this;

      this.elements.toggles.forEach(function (button) {
        button.addEventListener("click", function (event) {
          event.preventDefault();
          self.open();
        });
      });

      this.elements.closes.forEach(function (button) {
        button.addEventListener("click", function (event) {
          event.preventDefault();
          self.close();
        });
      });

      if (this.elements.overlay) {
        this.elements.overlay.addEventListener("click", function (event) {
          if (event.target === self.elements.overlay) {
            self.close();
          }
        });
      }

      if (this.elements.panel) {
        this.elements.panel.addEventListener("click", function (event) {
          event.stopPropagation();
        });
      }

      if (this.elements.input) {
        this.elements.input.addEventListener("input", function () {
          self.handleInput(this.value);
        });

        this.elements.input.addEventListener("keydown", function (event) {
          self.handleInputKeydown(event);
        });

        this.elements.input.addEventListener("focus", function () {
          self.renderRecentSearches();
        });
      }

      if (this.elements.form) {
        this.elements.form.addEventListener("submit", function (event) {
          self.handleSubmit(event);
        });
      }

      if (this.elements.clear) {
        this.elements.clear.addEventListener("click", function (event) {
          event.preventDefault();
          self.clearSearch();
        });
      }

      document.addEventListener("keydown", function (event) {
        self.handleDocumentKeydown(event);
      });

      document.addEventListener("click", function (event) {
        self.handleDocumentClick(event);
      });

      window.addEventListener("resize", function () {
        self.adjustPanelHeight();
      });

      window.addEventListener("orientationchange", function () {
        self.adjustPanelHeight();
      });
    },

    /**
     * Accessibility setup.
     */
    setupAccessibility: function () {
      var overlay = this.elements.overlay;
      var panel = this.elements.panel;
      var input = this.elements.input;
      var results = this.elements.results;

      if (overlay) {
        overlay.setAttribute("aria-hidden", "true");
      }

      if (panel) {
        panel.setAttribute("role", "dialog");
        panel.setAttribute("aria-modal", "true");
        panel.setAttribute("aria-label", "Search");
      }

      if (input) {
        input.setAttribute("autocomplete", "off");
        input.setAttribute("spellcheck", "false");
        input.setAttribute("aria-label", "Search articles");
        input.setAttribute("aria-autocomplete", "list");
        input.setAttribute("aria-expanded", "false");

        if (!input.id) {
          input.id = "rx-search-input";
        }
      }

      if (results) {
        results.setAttribute("role", "listbox");

        if (!results.id) {
          results.id = "rx-search-results";
        }

        if (input) {
          input.setAttribute("aria-controls", results.id);
        }
      }
    },

    /**
     * Open search overlay.
     */
    open: function () {
      var s = this.config.selectors;

      this.state.lastFocusedElement = document.activeElement;
      this.state.isOpen = true;

      document.body.classList.add(s.bodyOpenClass);

      if (this.elements.overlay) {
        this.elements.overlay.classList.add(s.overlayOpenClass);
        this.elements.overlay.setAttribute("aria-hidden", "false");
      }

      this.adjustPanelHeight();

      if (this.elements.input) {
        setTimeout(function () {
          RXSearch.elements.input.focus();
        }, 40);
      }

      this.dispatch("rxSearchOpen", {});
    },

    /**
     * Close search overlay.
     */
    close: function () {
      var s = this.config.selectors;

      this.state.isOpen = false;
      this.state.activeIndex = -1;

      document.body.classList.remove(s.bodyOpenClass);

      if (this.elements.overlay) {
        this.elements.overlay.classList.remove(s.overlayOpenClass);
        this.elements.overlay.setAttribute("aria-hidden", "true");
      }

      if (this.elements.input) {
        this.elements.input.setAttribute("aria-expanded", "false");
      }

      if (
        this.state.lastFocusedElement &&
        typeof this.state.lastFocusedElement.focus === "function"
      ) {
        this.state.lastFocusedElement.focus();
      }

      this.dispatch("rxSearchClose", {});
    },

    /**
     * Toggle overlay.
     */
    toggle: function () {
      if (this.state.isOpen) {
        this.close();
      } else {
        this.open();
      }
    },

    /**
     * Input handler.
     */
    handleInput: function (rawValue) {
      var self = this;
      var query = this.cleanQuery(rawValue);

      this.state.lastQuery = query;
      this.state.activeIndex = -1;

      this.updateClearButton(query);

      clearTimeout(this.state.debounceTimer);

      if (!query.length) {
        this.renderInitialState();
        return;
      }

      if (query.length < this.config.search.minChars) {
        this.renderMessage(this.config.messages.tooShort);
        return;
      }

      this.renderLoading();

      this.state.debounceTimer = setTimeout(function () {
        self.performSearch(query);
      }, this.config.search.debounceDelay);
    },

    /**
     * Submit handler.
     */
    handleSubmit: function (event) {
      var query = this.elements.input ? this.cleanQuery(this.elements.input.value) : "";

      if (!query && !this.config.search.allowEmptySubmit) {
        event.preventDefault();
        this.renderMessage(this.config.messages.empty);
        return;
      }

      if (query) {
        this.saveRecentSearch(query);
      }

      var activeItem = this.getActiveResultItem();

      if (activeItem) {
        event.preventDefault();
        this.goToResult(activeItem);
        return;
      }

      if (this.config.search.submitToWordPressSearch) {
        event.preventDefault();
        window.location.href = this.config.search.wpSearchPath + encodeURIComponent(query);
      }
    },

    /**
     * Keyboard handling inside input.
     */
    handleInputKeydown: function (event) {
      var key = event.key;

      if (key === "ArrowDown") {
        event.preventDefault();
        this.moveActive(1);
      }

      if (key === "ArrowUp") {
        event.preventDefault();
        this.moveActive(-1);
      }

      if (key === "Enter") {
        var activeItem = this.getActiveResultItem();

        if (activeItem) {
          event.preventDefault();
          this.goToResult(activeItem);
        }
      }

      if (key === "Escape") {
        event.preventDefault();
        this.close();
      }
    },

    /**
     * Global keyboard events.
     */
    handleDocumentKeydown: function (event) {
      if (event.key === "Escape" && this.state.isOpen) {
        this.close();
      }

      /**
       * Optional shortcut:
       * Press "/" to open search when not typing.
       */
      if (event.key === "/" && !this.state.isOpen && !this.isTypingContext(event.target)) {
        event.preventDefault();
        this.open();
      }
    },

    /**
     * Global click handler for dynamic result buttons.
     */
    handleDocumentClick: function (event) {
      var target = event.target;

      var popularButton = this.closest(target, "[data-rx-popular-search]");
      var recentButton = this.closest(target, "[data-rx-recent-search]");
      var removeRecentButton = this.closest(target, "[data-rx-remove-recent]");
      var resultItem = this.closest(target, "[data-rx-search-result]");

      if (popularButton) {
        event.preventDefault();
        this.setInputAndSearch(popularButton.getAttribute("data-rx-popular-search"));
        return;
      }

      if (recentButton) {
        event.preventDefault();
        this.setInputAndSearch(recentButton.getAttribute("data-rx-recent-search"));
        return;
      }

      if (removeRecentButton) {
        event.preventDefault();
        event.stopPropagation();
        this.removeRecentSearch(removeRecentButton.getAttribute("data-rx-remove-recent"));
        return;
      }

      if (resultItem) {
        this.saveRecentSearch(this.state.lastQuery);
      }
    },

    /**
     * Perform search.
     */
    performSearch: function (query) {
      var self = this;

      if (this.config.search.cacheResults) {
        var cached = this.getCachedResult(query);

        if (cached) {
          this.state.results = cached;
          this.renderResults(cached, query);
          return;
        }
      }

      if (!this.config.rest.enabled) {
        this.fallbackSearch(query);
        return;
      }

      this.abortPreviousRequest();

      this.state.abortController = this.createAbortController();

      var endpoint = this.buildRestUrl(query);

      var fetchOptions = {
        method: "GET",
        credentials: "same-origin",
        headers: {
          Accept: "application/json"
        }
      };

      if (this.state.abortController) {
        fetchOptions.signal = this.state.abortController.signal;
      }

      var timeoutId = setTimeout(function () {
        self.abortPreviousRequest();
      }, this.config.search.requestTimeout);

      fetch(endpoint, fetchOptions)
        .then(function (response) {
          clearTimeout(timeoutId);

          if (!response.ok) {
            throw new Error("Search request failed");
          }

          return response.json();
        })
        .then(function (data) {
          var results = self.normalizeResults(data);

          self.state.results = results;

          if (self.config.search.cacheResults) {
            self.setCachedResult(query, results);
          }

          self.renderResults(results, query);
        })
        .catch(function (error) {
          clearTimeout(timeoutId);

          if (error && error.name === "AbortError") {
            return;
          }

          self.renderError();
        });
    },

    /**
     * Build REST URL.
     */
    buildRestUrl: function (query) {
      var rest = this.config.rest;
      var url = rest.postsEndpoint;
      var params = new URLSearchParams();

      params.set("search", query);
      params.set("per_page", String(rest.perPage));
      params.set("subtype", rest.subtype || "any");

      if (rest.postTypes && rest.postTypes.length) {
        params.set("type", rest.postTypes.join(","));
      }

      return url + "?" + params.toString();
    },

    /**
     * Normalize WP REST search result.
     */
    normalizeResults: function (data) {
      if (!Array.isArray(data)) {
        return [];
      }

      return data.slice(0, this.config.search.maxResults).map(function (item) {
        return {
          id: item.id || "",
          title: item.title || item.name || "Untitled",
          url: item.url || item.link || "#",
          type: item.subtype || item.type || "post",
          excerpt: item.excerpt || "",
          date: item.date || "",
          raw: item
        };
      });
    },

    /**
     * Fallback search redirect.
     */
    fallbackSearch: function (query) {
      var url = this.config.search.wpSearchPath + encodeURIComponent(query);

      this.renderResults(
        [
          {
            id: "fallback",
            title: 'Search for "' + query + '"',
            url: url,
            type: "search",
            excerpt: "Open full WordPress search results."
          }
        ],
        query
      );
    },

    /**
     * Render initial empty state.
     */
    renderInitialState: function () {
      this.state.results = [];
      this.state.activeIndex = -1;

      this.renderMessage(this.config.messages.start);

      if (this.elements.input) {
        this.elements.input.setAttribute("aria-expanded", "false");
      }
    },

    /**
     * Render loading.
     */
    renderLoading: function () {
      var results = this.elements.results;

      if (!results) {
        return;
      }

      results.classList.add(this.config.selectors.loadingClass);
      results.innerHTML =
        '<div class="rx-search-status" role="status" aria-live="polite">' +
        '<span class="rx-search-spinner" aria-hidden="true"></span>' +
        '<span>' + this.escapeHTML(this.config.messages.loading) + '</span>' +
        "</div>";
    },

    /**
     * Render error.
     */
    renderError: function () {
      this.renderMessage(this.config.messages.error, "error");
    },

    /**
     * Render message.
     */
    renderMessage: function (message, type) {
      var results = this.elements.results;

      if (!results) {
        return;
      }

      results.classList.remove(this.config.selectors.loadingClass);

      results.innerHTML =
        '<div class="rx-search-message rx-search-message-' +
        this.escapeAttribute(type || "info") +
        '" role="status" aria-live="polite">' +
        this.escapeHTML(message) +
        "</div>";
    },

    /**
     * Render search results.
     */
    renderResults: function (items, query) {
      var results = this.elements.results;

      if (!results) {
        return;
      }

      results.classList.remove(this.config.selectors.loadingClass);

      if (!items || !items.length) {
        this.renderMessage(this.config.messages.noResults);
        return;
      }

      var html = '<div class="rx-search-result-list">';

      for (var i = 0; i < items.length; i++) {
        html += this.resultTemplate(items[i], i, query);
      }

      html += "</div>";

      results.innerHTML = html;

      if (this.elements.input) {
        this.elements.input.setAttribute("aria-expanded", "true");
      }
    },

    /**
     * Result HTML template.
     */
    resultTemplate: function (item, index, query) {
      var title = item.title || "Untitled";
      var excerpt = item.excerpt || "";
      var type = item.type || "post";
      var url = item.url || "#";

      if (this.config.search.highlightTerms && query) {
        title = this.highlight(title, query);
        excerpt = this.highlight(excerpt, query);
      } else {
        title = this.escapeHTML(title);
        excerpt = this.escapeHTML(excerpt);
      }

      return (
        '<a class="rx-search-result-item" ' +
        'href="' + this.escapeAttribute(url) + '" ' +
        'role="option" ' +
        'aria-selected="false" ' +
        'data-rx-search-result ' +
        'data-rx-result-index="' + index + '">' +
        '<span class="rx-search-result-title">' + title + "</span>" +
        '<span class="rx-search-result-meta">' + this.escapeHTML(type) + "</span>" +
        (excerpt
          ? '<span class="rx-search-result-excerpt">' + excerpt + "</span>"
          : "") +
        "</a>"
      );
    },

    /**
     * Render popular searches.
     */
    renderPopularSearches: function () {
      var wrapper = this.elements.popular;

      if (!wrapper || !this.config.popularSearches.length) {
        return;
      }

      var html = '<div class="rx-search-chip-group" aria-label="Popular searches">';

      this.config.popularSearches.forEach(function (term) {
        html +=
          '<button type="button" class="rx-search-chip" data-rx-popular-search="' +
          RXSearch.escapeAttribute(term) +
          '">' +
          RXSearch.escapeHTML(term) +
          "</button>";
      });

      html += "</div>";

      wrapper.innerHTML = html;
    },

    /**
     * Render recent searches.
     */
    renderRecentSearches: function () {
      var wrapper = this.elements.recent;

      if (!wrapper || !this.config.storage.enabled) {
        return;
      }

      var searches = this.getRecentSearches();

      if (!searches.length) {
        wrapper.innerHTML = "";
        return;
      }

      var html = '<div class="rx-search-recent-list" aria-label="Recent searches">';

      searches.forEach(function (term) {
        html +=
          '<div class="rx-search-recent-item">' +
          '<button type="button" class="rx-search-recent-term" data-rx-recent-search="' +
          RXSearch.escapeAttribute(term) +
          '">' +
          RXSearch.escapeHTML(term) +
          "</button>" +
          '<button type="button" class="rx-search-recent-remove" aria-label="Remove ' +
          RXSearch.escapeAttribute(term) +
          '" data-rx-remove-recent="' +
          RXSearch.escapeAttribute(term) +
          '">×</button>' +
          "</div>";
      });

      html += "</div>";

      wrapper.innerHTML = html;
    },

    /**
     * Set input and search.
     */
    setInputAndSearch: function (query) {
      query = this.cleanQuery(query);

      if (!query || !this.elements.input) {
        return;
      }

      this.elements.input.value = query;
      this.elements.input.focus();

      this.handleInput(query);
      this.saveRecentSearch(query);
    },

    /**
     * Clear search.
     */
    clearSearch: function () {
      if (this.elements.input) {
        this.elements.input.value = "";
        this.elements.input.focus();
      }

      this.state.lastQuery = "";
      this.state.activeIndex = -1;
      this.updateClearButton("");
      this.renderInitialState();
    },

    /**
     * Update clear button state.
     */
    updateClearButton: function (query) {
      if (!this.elements.clear) {
        return;
      }

      if (query.length) {
        this.elements.clear.classList.remove(this.config.selectors.hiddenClass);
        this.elements.clear.setAttribute("aria-hidden", "false");
      } else {
        this.elements.clear.classList.add(this.config.selectors.hiddenClass);
        this.elements.clear.setAttribute("aria-hidden", "true");
      }
    },

    /**
     * Move active result by keyboard.
     */
    moveActive: function (direction) {
      var items = this.getResultItems();

      if (!items.length) {
        return;
      }

      this.state.activeIndex += direction;

      if (this.state.activeIndex < 0) {
        this.state.activeIndex = items.length - 1;
      }

      if (this.state.activeIndex >= items.length) {
        this.state.activeIndex = 0;
      }

      this.updateActiveResult(items);
    },

    /**
     * Update active result UI.
     */
    updateActiveResult: function (items) {
      var activeClass = this.config.selectors.activeClass;

      items.forEach(function (item, index) {
        var isActive = index === RXSearch.state.activeIndex;

        item.classList.toggle(activeClass, isActive);
        item.setAttribute("aria-selected", isActive ? "true" : "false");

        if (isActive) {
          item.scrollIntoView({
            block: "nearest"
          });
        }
      });
    },

    /**
     * Get result items.
     */
    getResultItems: function () {
      if (!this.elements.results) {
        return [];
      }

      return Array.prototype.slice.call(
        this.elements.results.querySelectorAll("[data-rx-search-result]")
      );
    },

    /**
     * Get active result item.
     */
    getActiveResultItem: function () {
      var items = this.getResultItems();

      if (this.state.activeIndex < 0 || this.state.activeIndex >= items.length) {
        return null;
      }

      return items[this.state.activeIndex];
    },

    /**
     * Go to selected result.
     */
    goToResult: function (item) {
      if (!item) {
        return;
      }

      var href = item.getAttribute("href");

      if (href) {
        this.saveRecentSearch(this.state.lastQuery);
        window.location.href = href;
      }
    },

    /**
     * Save recent search.
     */
    saveRecentSearch: function (query) {
      if (!this.config.storage.enabled) {
        return;
      }

      query = this.cleanQuery(query);

      if (!query) {
        return;
      }

      var searches = this.getRecentSearches();

      searches = searches.filter(function (item) {
        return item.toLowerCase() !== query.toLowerCase();
      });

      searches.unshift(query);
      searches = searches.slice(0, this.config.search.maxRecent);

      try {
        localStorage.setItem(this.config.storage.key, JSON.stringify(searches));
      } catch (error) {
        return;
      }

      this.renderRecentSearches();
    },

    /**
     * Get recent searches.
     */
    getRecentSearches: function () {
      if (!this.config.storage.enabled) {
        return [];
      }

      try {
        var raw = localStorage.getItem(this.config.storage.key);
        var parsed = raw ? JSON.parse(raw) : [];

        if (!Array.isArray(parsed)) {
          return [];
        }

        return parsed.filter(function (item) {
          return typeof item === "string" && item.trim().length;
        });
      } catch (error) {
        return [];
      }
    },

    /**
     * Remove recent search.
     */
    removeRecentSearch: function (query) {
      var searches = this.getRecentSearches();

      searches = searches.filter(function (item) {
        return item.toLowerCase() !== query.toLowerCase();
      });

      try {
        localStorage.setItem(this.config.storage.key, JSON.stringify(searches));
      } catch (error) {
        return;
      }

      this.renderRecentSearches();
    },

    /**
     * Cache result.
     */
    setCachedResult: function (query, results) {
      this.state.cache[query.toLowerCase()] = {
        time: Date.now(),
        results: results
      };
    },

    /**
     * Get cached result.
     */
    getCachedResult: function (query) {
      var key = query.toLowerCase();
      var cached = this.state.cache[key];

      if (!cached) {
        return null;
      }

      if (Date.now() - cached.time > this.config.search.cacheTTL) {
        delete this.state.cache[key];
        return null;
      }

      return cached.results;
    },

    /**
     * Abort previous fetch request.
     */
    abortPreviousRequest: function () {
      if (
        this.state.abortController &&
        typeof this.state.abortController.abort === "function"
      ) {
        this.state.abortController.abort();
      }

      this.state.abortController = null;
    },

    /**
     * Create abort controller safely.
     */
    createAbortController: function () {
      if ("AbortController" in window) {
        return new AbortController();
      }

      return null;
    },

    /**
     * Clean search query.
     */
    cleanQuery: function (query) {
      if (typeof query !== "string") {
        return "";
      }

      return query
        .replace(/\s+/g, " ")
        .replace(/[<>]/g, "")
        .trim();
    },

    /**
     * Highlight query terms.
     */
    highlight: function (text, query) {
      text = this.escapeHTML(String(text || ""));
      query = this.cleanQuery(query);

      if (!query) {
        return text;
      }

      var words = query
        .split(" ")
        .filter(function (word) {
          return word.length > 1;
        })
        .map(this.escapeRegExp);

      if (!words.length) {
        return text;
      }

      var regex = new RegExp("(" + words.join("|") + ")", "gi");

      return text.replace(regex, '<mark class="rx-search-highlight">$1</mark>');
    },

    /**
     * Escape HTML.
     */
    escapeHTML: function (value) {
      return String(value || "")
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    },

    /**
     * Escape HTML attribute.
     */
    escapeAttribute: function (value) {
      return this.escapeHTML(value).replace(/`/g, "&#096;");
    },

    /**
     * Escape regex.
     */
    escapeRegExp: function (value) {
      return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    },

    /**
     * Closest helper.
     */
    closest: function (element, selector) {
      if (!element || element === document) {
        return null;
      }

      if (element.closest) {
        return element.closest(selector);
      }

      while (element && element.nodeType === 1) {
        if (this.matches(element, selector)) {
          return element;
        }

        element = element.parentElement;
      }

      return null;
    },

    /**
     * Matches helper.
     */
    matches: function (element, selector) {
      var proto = Element.prototype;
      var fn =
        proto.matches ||
        proto.msMatchesSelector ||
        proto.webkitMatchesSelector;

      if (!fn) {
        return false;
      }

      return fn.call(element, selector);
    },

    /**
     * Check typing context.
     */
    isTypingContext: function (element) {
      if (!element) {
        return false;
      }

      var tag = element.tagName ? element.tagName.toLowerCase() : "";

      return (
        tag === "input" ||
        tag === "textarea" ||
        tag === "select" ||
        element.isContentEditable
      );
    },

    /**
     * Adjust panel height for mobile browsers.
     */
    adjustPanelHeight: function () {
      if (!this.elements.overlay) {
        return;
      }

      var height = window.innerHeight || document.documentElement.clientHeight;

      this.elements.overlay.style.setProperty("--rx-search-vh", height + "px");
    },

    /**
     * Dispatch custom event.
     */
    dispatch: function (name, detail) {
      var event;

      try {
        event = new CustomEvent(name, {
          detail: detail || {}
        });
      } catch (error) {
        event = document.createEvent("CustomEvent");
        event.initCustomEvent(name, true, true, detail || {});
      }

      document.dispatchEvent(event);
    }
  };

  /**
   * Expose globally.
   */
  window.RXTheme.search = RXSearch;

  /**
   * Auto init.
   */
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      RXSearch.init();
    });
  } else {
    RXSearch.init();
  }
})();

Use this minimum HTML in your theme header/search template:

<button class="rx-search-toggle" type="button" aria-label="Open search">
  Search
</button>

<div class="rx-search-overlay">
  <div class="rx-search-panel">
    <button class="rx-search-close" type="button" aria-label="Close search">×</button>

    <form class="rx-search-form" role="search">
      <input
        class="rx-search-input"
        type="search"
        name="s"
        placeholder="Search medical articles..."
      />

      <button class="rx-search-submit" type="submit">
        Search
      </button>

      <button class="rx-search-clear is-hidden" type="button" aria-label="Clear search">
        Clear
      </button>
    </form>

    <div class="rx-search-popular"></div>
    <div class="rx-search-recent"></div>
    <div class="rx-search-results"></div>
  </div>
</div>

Add this CSS later in your search CSS file:

.rx-search-overlay {
  position: fixed;
  inset: 0;
  z-index: 99999;
  display: none;
  min-height: var(--rx-search-vh, 100vh);
  background: rgba(0, 0, 0, 0.58);
}

.rx-search-overlay.is-open {
  display: block;
}

.rx-search-is-open {
  overflow: hidden;
}

.rx-search-panel {
  width: min(760px, calc(100% - 32px));
  margin: 48px auto;
  padding: 24px;
  border-radius: 18px;
  background: #fff;
}

.rx-search-form {
  display: flex;
  gap: 10px;
}

.rx-search-input {
  flex: 1;
  width: 100%;
  min-height: 46px;
  padding: 0 14px;
}

.rx-search-result-list {
  margin-top: 18px;
}

.rx-search-result-item {
  display: block;
  padding: 14px;
  border-bottom: 1px solid #eee;
  text-decoration: none;
}

.rx-search-result-item.is-active,
.rx-search-result-item:hover {
  background: #f5f7fb;
}

.rx-search-result-title {
  display: block;
  font-weight: 700;
}

.rx-search-result-meta {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  opacity: 0.7;
}

.rx-search-result-excerpt {
  display: block;
  margin-top: 6px;
  font-size: 14px;
}

.rx-search-highlight {
  padding: 0 2px;
}

.rx-search-chip-group,
.rx-search-recent-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 16px;
}

.rx-search-chip,
.rx-search-recent-term,
.rx-search-recent-remove {
  cursor: pointer;
}

.rx-search-message,
.rx-search-status {
  padding: 18px 0;
}

.is-hidden {
  display: none !important;
}

Then enqueue it in your RX Theme:

wp_enqueue_script(
    'rx-theme-search-chunk',
    get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-004-search.js',
    array(),
    filemtime(get_template_directory() . '/assets/static-js/static-chunks-js/chunk-004-search.js'),
    true
);

For performance, this file is best loaded with defer or in footer. Since wp_enqueue_script(..., true) loads it near footer, it is already good for RX Theme.

Leave a Reply

Your email address will not be published. Required fields are marked *