Write a script that highlights users' search key in Sitecore SXA's OOTB search components.
Pre-Requisite
- Basic understanding of OOTB (Out Of The Box) search components, specifically the search results and search box component.
- Understanding of SXA Search's Search Signature (In short, it's a string that you define in SXA in order to restrict interaction between search components. For example, we can configure a search box to only target one of the search components, but not the others.)
- Cash JS or JQuery
Overview
As of SXA version 10.0.0, there is no out-of-the-box way of highlighting search keys in the search results. So we must write the logic ourselves. This almost seems like a trivial exercise where we wrap strong tag around the search key in paragraphs. However, we need some additional steps to ensure we don't break SXA's OOTB components
Code Structure
The skeleton of the code will look like this. XA.component.search.xxx refers to the js modules that ship with SXA. We MUST use this because we need to listen to the events that SXA search components fire. In this case, we need to listen to 'results-loaded' event and handle the highlighting of search key in onResultsLoaded method that I will define in a bit. This structure is being used by pretty much every single js module that ship with SXA, so please take a look at them if you re confused.
import $ from "cash-dom";
XA.component.search.resultsUtil = (function ($, document) {
var api = {};
[OMITTED CODE]
// Listen to Events
XA.component.search.vent.on("results-loaded", onResultsLoaded);
api.init = function () {};
return api;
}(jQuery, document));
XA.register('searchResultsUtil', XA.component.search.resultsUtil);
Rest Of The Code
Don't be alarmed by the length. This code is identical to the above except that we just filled in the OMITTED CODE with a bunch of functions. Here is what's basically going on:
- Locating all search keys from the url hash (q= or [signature-name]_q=). For example, if signature name is abcd, we look for abcd_q. if no signature, look for q
- Locating all search results
- Match the search signatures between the search results components and search keys
- In each search reuslts component, if search key exists, locate them in the html and wrap the target strings in bold (ensure case agonostic)
- Everytime highlightQueryInSearchResults is called again, clean all the strong tags first before highlighting the new search keys.
import $ from "cash-dom";
XA.component.search.resultsUtil = (function ($, document) {
var api = {};
function getQueryMap(url) {
// returns a map from query key to value in sxa's context. if no custom signature, we just need to look for q=
// with custom signature, need to look for _q=
// example 1 (with custom signature): #sig1_e=0&sig1_q=summary -? {sig1_q: 'summary'}
// example 2 (no custom signature): #q=summary -> {q: 'summary'}
if (!url.includes('#')) return; // sxa search uses hash. no hash, no param
const hash = url.split('#')[1];
if (hash == '') return;
const validQueryList = hash.split('&').filter(hsh => {
return hsh.includes('q=');
});
const queryMap = {};
validQueryList.forEach(queryString => {
const queryStringSplit = queryString.split('=');
if (queryStringSplit[1] != null && queryStringSplit[1] !== '') {
queryMap[queryStringSplit[0]] = queryStringSplit[1].replaceAll('%20', ' ');
}
});
return queryMap;
}
function findAllWordsInString(str, word) {
// returns locations (indices) of the word in str
const strLower = str.toLowerCase();
const wordLower = word.toLowerCase();
const indices = [];
let index = 0;
let limiter = 0; // prevent infinite loop
while (index < strlower.length="" &&="" index="" !="=" -1="" &&="" limiter="" />< 10000)="" {="" index="strLower.indexOf(wordLower," index);="" if="" (index="" !="=" -1)="" {="" prevents="" infinite="" loop="" caused="" by="" indexof="" returning="" -1,="" therefore="" making="" index="" 0="" indices.push(index)="" index="index" +="" 1;="" }="" limiter++;="" }="" return="" indices;="" }="" function="" boldifystringinhtml(str,="" target)="" {="" const="" indices="findAllWordsInString(str," target)="" for="" (let="" i="indices.length" -="" 1;="" i="" /> -1; i--) {
const currentStr = str.substring(0, indices[i])
+ ''
+ str.substring(indices[i], indices[i] + target.length)
+ ''
+ str.substring(indices[i] + target.length);
str = currentStr;
}
return str;
}
function cleanStrongElementsInHTML(str) {
// removes all strong tags in a string
return str.split('').join('').split('').join('');
}
function highlightQueryInSearchResults() {
const queryMap = getQueryMap(window.location.href);
if (queryMap == null) return;
const searchResultsList = $('.component.search-results');
for (let i = 0; i < searchresultslist.length;="" i++)="" {="" for="" each="" search="" results="" component="" let="" signature="JSON.parse($(searchResultsList[i]).attr('data-properties')).sig;" signature="signature" signature="" :="" '';="" const="" searchresultbody="$(searchResultsList[i]).find('.search-result" .search-result__body');="" for="" (let="" j="0;" j="" />< searchresultbody.length;="" j++)="" {="" const="" searchresultbodyhtml="$(searchResultBody[j]).html();" if="" (searchresultbodyhtml="==" ''="" ||="" searchresultbodyhtml="=" null)="" continue;="" const="" cleansearchresultbodyhtml="cleanStrongElementsInHTML(searchResultBodyHTML);" if="" (signature="==" '')="" {="" no="" custom="" signature="" if="" ('q'="" in="" querymap)="" {="" $(searchresultbody[j]).html(boldifystringinhtml(cleansearchresultbodyhtml,="" querymap['q']));="" }="" }="" else="" {="" if="" (signature="" +="" '_q'="" in="" querymap)="" {="" with="" custom="" signature,="" need="" to="" identify="" />_q
$(searchResultBody[j]).html(BoldifyStringInHTML(cleanSearchResultBodyHTML, queryMap[signature + '_q']));
}
}
}
}
}
function onResultsLoaded() {
setTimeout(() => { //wait for the DOM to load
highlightQueryInSearchResults();
})
}
// Listen to Events
XA.component.search.vent.on("results-loaded", onResultsLoaded);
api.init = function () {};
return api;
}(jQuery, document));
XA.register('searchResultsUtil', XA.component.search.resultsUtil);
Final Results
Room For Improvement?
One word: REGEX. It would've simplified the process of locating search key in html. At the time of writing, I was not super comfortable with it.
Thank You