Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 108 additions & 85 deletions hlx_statics/blocks/header/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,25 @@ async function initSearch() {

const { connectAutocomplete } = instantsearch.connectors;

const searchClient = window.algoliasearch.algoliasearch(ALGOLIA_CONFIG.APP_KEY, ALGOLIA_CONFIG.API_KEY);
const algoliaClient = window.algoliasearch.algoliasearch(ALGOLIA_CONFIG.APP_KEY, ALGOLIA_CONFIG.API_KEY);
const SUGGESTION_MAX_RESULTS = 50;
const SEARCH_MAX_RESULTS = 100;
const SEARCH_MIN_QUERY_LENGTH = 3;
const isSearchableQuery = (q) => q.trim().length >= SEARCH_MIN_QUERY_LENGTH;
const isSearchableQuery = (q) => q.trim() !== '';
const searchClient = { // "To prevent the initial empty query, you must wrap a custom search client around..." source: https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/js
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !isSearchableQuery(params.query ?? ''))) {
return Promise.resolve({
results: requests.map(() => ({
hits: [], nbHits: 0, nbPages: 0, page: 0,
processingTimeMS: 0, hitsPerPage: 0,
exhaustiveNbHits: false, query: '', params: '',
})),
});
}
return algoliaClient.search(requests);
},
};

const indices = window.adp_search.indices
const indexToProduct = window.adp_search.index_to_product;
Expand Down Expand Up @@ -125,9 +139,17 @@ async function initSearch() {
let results = new Map();

search.start();

let currentDynamicWidgets = []; // Widgets that change on each call
let staticWidgetsAdded = false; // One-time widgets

// Function to initialize or update the search
function updateSearch() {
// Remove widgets from the previous call before adding new ones
if (currentDynamicWidgets.length) {
search.removeWidgets(currentDynamicWidgets);
currentDynamicWidgets = [];
}
// Get indices corresponding to selected products
const selectedIndices = indices.filter((indexName) => {
const product = indexToProduct[indexName];
Expand All @@ -143,14 +165,14 @@ async function initSearch() {
// Calculate hits dynamically based number of selected indices
const hits = Math.min(15, Math.max(4, Math.floor(SUGGESTION_MAX_RESULTS / selectedIndices.length)));

// Add common widgets like hits per index and how long results are (content)
search.addWidgets([
instantsearch.widgets.configure({
hitsPerPage: hits,
attributesToHighlight: ['title', 'content'],
attributesToSnippet: ['content:50'],
}),
]);
// Add common widgets like hits per index and how long results are (content) - and save reference so it can be removed on the next call
const configureWidget = instantsearch.widgets.configure({
hitsPerPage: hits,
attributesToHighlight: ['title', 'content'],
attributesToSnippet: ['content:50'],
});
currentDynamicWidgets.push(configureWidget);
search.addWidgets([configureWidget]);

// Custom InstantSearch search box to deal with suggestions and full results which depends on user input
function customSearchBox() { return { init({ helper }) {
Expand Down Expand Up @@ -230,8 +252,12 @@ async function initSearch() {
if (event.key === 'Enter') {
searchCleared = false; // Reset cleared flag when user presses Enter
const trimmed = searchInput.value.trim();
if (trimmed !== '' && !isSearchableQuery(trimmed)) {
event.preventDefault();
if (trimmed === '') { // If users presses enter with an empty query while in a full search, clear results and show suggestions again
searchResults.classList.remove('has-results');
searchResults.style.visibility = 'hidden';
outerSearchSuggestions.style.display = 'flex';
suggestionsFlag = true;
searchExecuted = false;
return;
}
helper.setQuery(searchInput.value).search();
Expand Down Expand Up @@ -277,7 +303,8 @@ async function initSearch() {
// Process each hit
//
results.set(instantsearch.highlight({ hit, attribute: "title" }), {
url: hit.url,
// Add anchor link if it exists to URL
url: hit.fragment ? `${hit.url}${hit.fragment}` : hit.url,
product: hit.product,
content: instantsearch.snippet({ hit, attribute: 'content' }),
});
Expand All @@ -298,21 +325,25 @@ async function initSearch() {
}

const customMergedHits = connectAutocomplete(mergedHits);
search.addWidgets([
customSearchBox(),
customMergedHits({
container: document.querySelector(searchBoxContainer)
}),
]);

// Loop through rest of indices
selectedIndices.slice(1).forEach((indexName) => {
// Only add the search box and hits renderer once — fixes event listeners duplicates
if (!staticWidgetsAdded) {
search.addWidgets([
instantsearch.widgets.index({
indexName: indexName,
customSearchBox(),
customMergedHits({
container: document.querySelector(searchBoxContainer)
}),
]);
});
staticWidgetsAdded = true;
}

// Instead of looping through other indices - add a child index widget for rest of indices (the main index is always searched, so it doesn't need a widget)
const indexWidgets = selectedIndices
.filter((indexName) => indexName !== initialIndex)
.map((indexName) => instantsearch.widgets.index({ indexName }));
if (indexWidgets.length) {
currentDynamicWidgets.push(...indexWidgets);
search.addWidgets(indexWidgets);
}

search.refresh();
}
Expand Down Expand Up @@ -405,24 +436,7 @@ async function initSearch() {
});
}

// Function that is called after each search render to hide/show product checkboxes
function updateCheckboxVisibility(productsWithResults) {
const allSelected = selectedProducts.length === allProducts.length;

// loop over each product‐wrapper
document.querySelectorAll('.search-checkbox-div[data-product]').forEach((div) => {
const product = div.dataset.product;
if (allSelected) {
// only show those with results
div.style.display = productsWithResults.has(product) ? '' : 'none';
} else {
// specific‐product mode: show all product checkboxes
div.style.display = '';
}
});
}

// Function that attaches event listeners to each checkbox
// Function that attaches event listeners to each checkbox
function attachCheckboxEventListeners() {
const allProductsCheckbox = document.getElementById('checkbox-all-products');
const productCheckboxes = document.querySelectorAll('.filters input[type="checkbox"]:not(#checkbox-all-products)');
Expand All @@ -447,6 +461,9 @@ async function initSearch() {
selectedProducts = Array.from(productCheckboxes)
.filter((cb) => cb.checked) // Get checked product checkboxes
.map((cb) => cb.value);
if (checkbox.checked) { // Add new selected product to the beginning of the list to prioritize it in results
selectedProducts = [checkbox.value, ...selectedProducts.filter((p) => p !== checkbox.value)];
}

if (selectedProducts.length === 0) {
// If no products selected, revert to "All Products"
Expand Down Expand Up @@ -476,18 +493,20 @@ async function initSearch() {
// figure out which products have at least one hit
const productsWithResults = new Set(productGroupedResults.keys());

// hide/show checkboxes based on current results + mode
updateCheckboxVisibility(productsWithResults);

// determine display order
const allProductsCheckbox = document.getElementById('checkbox-all-products');
const productsToShow = allProductsCheckbox.checked ? allProducts : selectedProducts;

const productsWith = [], productsWithout = [];
productsToShow.forEach((p) =>
productsWithResults.has(p) ? productsWith.push(p) : productsWithout.push(p)
);
const sorted = [...productsWith, ...productsWithout];
let sorted;
if (allProductsCheckbox.checked) {
const productsWith = [], productsWithout = [];
productsToShow.forEach((p) =>
productsWithResults.has(p) ? productsWith.push(p) : productsWithout.push(p)
);
sorted = [...productsWith, ...productsWithout];
} else {
sorted = productsToShow; // preserve selected order regardless of results
}

// render each group
sorted.forEach((product) => {
Expand Down Expand Up @@ -536,18 +555,22 @@ async function initSearch() {

// compute who has any suggestions
const productsWithResults = new Set(productGroupedResults.keys());
updateCheckboxVisibility(productsWithResults);

const allProductsCheckbox = document.getElementById('checkbox-all-products');
const productsToShow = allProductsCheckbox.checked
? allProducts
: selectedProducts;

const withHits = [], withoutHits = [];
productsToShow.forEach((p) =>
productsWithResults.has(p) ? withHits.push(p) : withoutHits.push(p)
);
const sorted = [...withHits, ...withoutHits];
let sorted;
if (allProductsCheckbox.checked) {
const withHits = [], withoutHits = [];
productsToShow.forEach((p) =>
productsWithResults.has(p) ? withHits.push(p) : withoutHits.push(p)
);
sorted = [...withHits, ...withoutHits];
} else {
sorted = productsToShow; // preserve selected order regardless of results
}

// render each section
sorted.forEach((product) => {
Expand Down Expand Up @@ -1118,33 +1141,33 @@ export default async function decorate(block) {

// check if documentation template then retrieve from config otherwise default back to google drive path
let navPath;
if (IS_DEV_DOCS) {
const topNavHtml = await fetchTopNavHtml();
if (topNavHtml) {
navigationLinks.innerHTML += topNavHtml;
}
} else {
navPath = cfg.nav || getClosestFranklinSubfolder(window.location.origin,'nav');
let fragment = await loadFragment(navPath);
if (fragment == null) {
// load the default nav in franklin_assets folder nav
fragment = await loadFragment(getClosestFranklinSubfolder(window.location.origin, 'nav', true));
}
const ul = fragment.querySelector("ul");
ul.classList.add("menu");
ul.setAttribute("id", "navigation-links");
fragment.querySelectorAll("li").forEach((li, index) => {
if (index == 0) {
if (isTopLevelNav(window.location.pathname)) {
const homeLink = ul.querySelector('li:nth-child(1)');
homeLink.className = 'navigation-home';
} else {
li.classList.add("navigation-products");
}
}
});
navigationLinks = ul;
}
// if (IS_DEV_DOCS) {
// const topNavHtml = await fetchTopNavHtml();
// if (topNavHtml) {
// navigationLinks.innerHTML += topNavHtml;
// }
// } else {
// navPath = cfg.nav || getClosestFranklinSubfolder(window.location.origin,'nav');
// let fragment = await loadFragment(navPath);
// if (fragment == null) {
// // load the default nav in franklin_assets folder nav
// fragment = await loadFragment(getClosestFranklinSubfolder(window.location.origin, 'nav', true));
// }
// const ul = fragment.querySelector("ul");
// ul.classList.add("menu");
// ul.setAttribute("id", "navigation-links");
// fragment.querySelectorAll("li").forEach((li, index) => {
// if (index == 0) {
// if (isTopLevelNav(window.location.pathname)) {
// const homeLink = ul.querySelector('li:nth-child(1)');
// homeLink.className = 'navigation-home';
// } else {
// li.classList.add("navigation-products");
// }
// }
// });
// navigationLinks = ul;
// }

navigationLinks.querySelectorAll('li > ul').forEach((dropDownList, index) => {
let dropdownLinkDropdownHTML = '';
Expand Down
Loading