PageSourceSearch

https://megadynegroup.com/wp-content/themes/ammweb-instance/js/product-nav/search.js?ver=1791294285

js megadynegroup.com collected 2026-10-08 01:42:54 UTC 18,668 bytes, 574 lines download raw bytes

1(function (window) {
2  'use strict';
3
4  const { decodeHtml, escapeHtml, isMobile } = window.ammwebProductNavUtils;
5
6  function create(api) {
7    const {
8      navData,
9      searchIndex,
10      searchConfig,
11      state,
12      dom,
13      isActiveNavUrl,
14    } = api;
15
16    const {
17      desktopColumns,
18      mobileMenu,
19      mobileSearchResults,
20      desktopSearchFooter,
21      searchFields,
22      searchClearButtons,
23      mobileSearchContainer,
24      desktopSearchContainer,
25    } = dom;
26
27    const searchMinChars = Math.max(1, Number(searchConfig.minChars) || 2);
28    const searchMaxResults = Math.max(1, Number(searchConfig.maxResults) || 40);
29    const searchDebounceMs = Math.max(0, Number(searchConfig.debounceMs) || 300);
30    const searchPlaceholder = typeof searchConfig.placeholder === 'string' ? searchConfig.placeholder : 'Search products…';
31    const searchEmptyLabel = typeof searchConfig.emptyLabel === 'string' ? searchConfig.emptyLabel : 'No products found';
32    const searchLoadMoreLabel = typeof searchConfig.loadMoreLabel === 'string' ? searchConfig.loadMoreLabel : 'Load more';
33
34    const searchEnabled = searchConfig.enabled !== false;
35
36    let searchDebounceTimer = null;
37
38    const isSearchMode = () => {
39      if (!searchEnabled) return false;
40
41      return state.searchQuery.trim().length >= searchMinChars;
42    };
43
44    const normalizeSearchText = (value) => decodeHtml(String(value ?? '')).toLowerCase().trim();
45
46    const compareNavNames = (left, right) => normalizeSearchText(left).localeCompare(
47      normalizeSearchText(right),
48      undefined,
49      { sensitivity: 'base' }
50    );
51
52    const scoreSearchItem = (item, query) => {
53      const name = normalizeSearchText(item.name);
54      if (!query) {
55        return 0;
56      }
57
58      if (name) {
59        if (name === query) {
60          return 1000;
61        }
62
63        if (name.startsWith(query)) {
64          return 800;
65        }
66
67        if (name.includes(query)) {
68          return 600;
69        }
70      }
71
72      const breadcrumb = normalizeSearchText(item.breadcrumb);
73      if (breadcrumb && breadcrumb.includes(query)) {
74        return 500;
75      }
76
77      return 0;
78    };
79
80    const filterSearchIndex = (query) => {
81      const normalizedQuery = normalizeSearchText(query);
82      if (!normalizedQuery) {
83        return [];
84      }
85
86      return searchIndex
87        .map((item) => ({
88          item,
89          score: scoreSearchItem(item, normalizedQuery),
90        }))
91        .filter((entry) => entry.score > 0)
92        .sort((left, right) => {
93          const scoreDiff = right.score - left.score;
94          if (scoreDiff !== 0) {
95            return scoreDiff;
96          }
97
98          return compareNavNames(left.item.name, right.item.name);
99        })
100        .map((entry) => entry.item);
101    };
102
103    const buildSearchResultRowHtml = (item) => {
104      const breadcrumb = typeof item.breadcrumb === 'string' ? item.breadcrumb.trim() : '';
105      const rowClass = item.type === 'category' || item.type === 'archive'
106        ? 'products-nav-row products-nav-row--search-result products-nav-row--search-category'
107        : 'products-nav-row products-nav-row--search-result products-nav-row--search-product';
108      const activeClass = isActiveNavUrl(item.url) ? ' is-active' : '';
109
110      return `<a class="${rowClass} ax-track-link ax-header-product-menu-search-result-click${activeClass}" data-ax-track="header-product-menu-search-result-click" href="${escapeHtml(item.url)}">
111      <span class="products-nav-row__label">${escapeHtml(item.name)}</span>
112      ${breadcrumb ? `<span class="products-nav-row__path">${escapeHtml(breadcrumb)}</span>` : ''}
113    </a>`;
114    };
115
116    const getSearchColumnCount = () => Math.max(1, Number(navData.columnCount) || 1);
117
118    const buildSearchResultEmptyCellHtml = () => (
119      '<span class="products-nav-search__cell products-nav-search__cell--empty" aria-hidden="true"></span>'
120    );
121
122    const buildSearchResultRowWrapperHtml = (rowItems) => {
123      const columnCount = getSearchColumnCount();
124      const cells = rowItems.map((item) => buildSearchResultRowHtml(item)).join('');
125      const emptyCells = Array.from(
126        { length: Math.max(0, columnCount - rowItems.length) },
127        () => buildSearchResultEmptyCellHtml()
128      ).join('');
129
130      return `<div class="products-nav-search__row">${cells}${emptyCells}</div>`;
131    };
132
133    const buildDesktopSearchResultsRowsHtml = (matches) => {
134      const columnCount = getSearchColumnCount();
135      const rows = [];
136
137      for (let index = 0; index < matches.length; index += columnCount) {
138        rows.push(buildSearchResultRowWrapperHtml(matches.slice(index, index + columnCount)));
139      }
140
141      return rows.join('');
142    };
143
144    const appendDesktopSearchResultBatch = (batch) => {
145      const columnCount = getSearchColumnCount();
146      const items = batch.slice();
147      const lastRow = desktopColumns.querySelector('.products-nav-search__row:last-child');
148
149      if (lastRow) {
150        lastRow.querySelectorAll('.products-nav-search__cell--empty').forEach((cell) => {
151          cell.remove();
152        });
153
154        const currentCount = lastRow.querySelectorAll('.products-nav-row--search-result').length;
155        const openSlots = currentCount % columnCount === 0 ? 0 : columnCount - (currentCount % columnCount);
156
157        if (openSlots > 0 && items.length > 0) {
158          const fillItems = items.splice(0, openSlots);
159          fillItems.forEach((item) => {
160            lastRow.insertAdjacentHTML('beforeend', buildSearchResultRowHtml(item));
161          });
162
163          const updatedCount = lastRow.querySelectorAll('.products-nav-row--search-result').length;
164          const remainingSlots = updatedCount % columnCount === 0 ? 0 : columnCount - (updatedCount % columnCount);
165          for (let index = 0; index < remainingSlots; index += 1) {
166            lastRow.insertAdjacentHTML('beforeend', buildSearchResultEmptyCellHtml());
167          }
168        }
169      }
170
171      if (items.length > 0) {
172        desktopColumns.insertAdjacentHTML('beforeend', buildDesktopSearchResultsRowsHtml(items));
173      }
174    };
175
176    const buildSearchFooterHtml = (query, totalMatches, displayedCount) => {
177      if (!totalMatches) {
178        return `<p class="products-nav-search__empty">${escapeHtml(searchEmptyLabel)}</p>`;
179      }
180
181      if (totalMatches > displayedCount) {
182        const remaining = totalMatches - displayedCount;
183        const nextCount = Math.min(searchMaxResults, remaining);
184        return `<button class="products-nav-search__load-more ax-track-btn ax-header-product-menu-search-load-more-click" type="button" data-products-nav-search-load-more data-ax-track="header-product-menu-search-load-more-click">${escapeHtml(searchLoadMoreLabel)} <span class="products-nav-search__load-more-count">(${escapeHtml(String(nextCount))})</span></button>`;
185      }
186
187      return '';
188    };
189
190    const getFilteredSearchMatches = () => filterSearchIndex(state.searchQuery.trim());
191
192    const updateSearchFooters = (totalMatches, displayedCount) => {
193      const query = state.searchQuery.trim();
194      const footerHtml = buildSearchFooterHtml(query, totalMatches, displayedCount);
195
196      if (desktopSearchFooter) {
197        desktopSearchFooter.innerHTML = footerHtml;
198        desktopSearchFooter.hidden = '' === footerHtml;
199      }
200
201      if (!mobileSearchResults) {
202        return;
203      }
204
205      const inner = mobileSearchResults.querySelector('.products-nav-mobile__search-results-inner');
206      if (!inner) {
207        return;
208      }
209
210      let footer = inner.querySelector('.products-nav-search__footer--mobile');
211      if (!footerHtml) {
212        footer?.remove();
213        return;
214      }
215
216      if (!footer) {
217        footer = document.createElement('div');
218        footer.className = 'products-nav-search__footer products-nav-search__footer--mobile';
219        inner.appendChild(footer);
220      }
221
222      footer.innerHTML = footerHtml;
223    };
224
225    const resetSearchDisplayedCount = () => {
226      state.searchDisplayedCount = 0;
227    };
228
229    const loadMoreSearchResults = () => {
230      const matches = getFilteredSearchMatches();
231      const start = state.searchDisplayedCount;
232      const end = Math.min(start + searchMaxResults, matches.length);
233      const batch = matches.slice(start, end);
234
235      if (!batch.length) {
236        updateSearchFooters(matches.length, start);
237        return;
238      }
239
240      state.searchDisplayedCount = end;
241
242      if (!isMobile()) {
243        appendDesktopSearchResultBatch(batch);
244      }
245
246      const mobileBatchHtml = batch.map((item) => buildSearchResultRowHtml(item)).join('');
247
248      if (!mobileMenu.hidden && mobileSearchResults) {
249        const inner = mobileSearchResults.querySelector('.products-nav-mobile__search-results-inner');
250        const footer = inner?.querySelector('.products-nav-search__footer--mobile');
251        if (inner) {
252          if (footer) {
253            footer.insertAdjacentHTML('beforebegin', mobileBatchHtml);
254          } else {
255            inner.insertAdjacentHTML('beforeend', mobileBatchHtml);
256          }
257        }
258      }
259
260      window.requestAnimationFrame(() => {
261        updateSearchFooters(matches.length, end);
262      });
263    };
264
265    const syncSearchFieldValues = (value) => {
266      const activeField = searchFields.find((field) => field === document.activeElement) || null;
267      const selectionStart = typeof activeField?.selectionStart === 'number' ? activeField.selectionStart : null;
268      const selectionEnd = typeof activeField?.selectionEnd === 'number' ? activeField.selectionEnd : null;
269
270      searchFields.forEach((field) => {
271        if (field.value !== value) {
272          field.value = value;
273        }
274      });
275
276      if (!activeField || !isMobile()) {
277        return;
278      }
279
280      if (document.activeElement !== activeField) {
281        activeField.focus({ preventScroll: true });
282      }
283
284      if (selectionStart !== null && selectionEnd !== null) {
285        try {
286          activeField.setSelectionRange(selectionStart, selectionEnd);
287        } catch (error) {
288          // Some input types do not support selection ranges.
289        }
290      }
291    };
292
293    const updateSearchClearButtons = () => {
294      const hasValue = state.searchQuery.trim().length > 0;
295      searchClearButtons.forEach((button) => {
296        button.hidden = false;
297        button.classList.toggle('is-visible', hasValue);
298        button.setAttribute('aria-hidden', hasValue ? 'false' : 'true');
299      });
300    };
301
302    const setDesktopSearchVisible = (visible) => {
303      if (!searchEnabled || !desktopSearchContainer) {
304        return;
305      }
306
307      desktopSearchContainer.hidden = !visible;
308    };
309
310    const setMobileSearchVisible = (visible) => {
311      if (!searchEnabled || !mobileSearchContainer) {
312        return;
313      }
314
315      mobileSearchContainer.hidden = !visible;
316    };
317
318    const syncMobileSearchLayout = () => {
319      if (!searchEnabled || !isMobile() || mobileMenu.hidden) {
320        return;
321      }
322
323      const panel = api.mobile.getActiveMobilePanel();
324      if (!panel || !mobileSearchContainer) {
325        return;
326      }
327
328      const slot = panel.querySelector('.products-nav-mobile__search-slot');
329      const content = panel.querySelector('.products-nav-mobile__content');
330      if (!slot) {
331        return;
332      }
333
334      if (mobileSearchContainer.parentElement !== slot) {
335        slot.appendChild(mobileSearchContainer);
336      }
337      setMobileSearchVisible(true);
338
339      if (isSearchMode() && mobileSearchResults) {
340        if (mobileSearchResults.parentElement !== panel) {
341          slot.insertAdjacentElement('afterend', mobileSearchResults);
342        }
343        mobileSearchResults.hidden = false;
344        if (content) {
345          content.hidden = true;
346        }
347        mobileMenu.classList.add('is-search-active');
348        return;
349      }
350
351      if (mobileSearchResults) {
352        mobileSearchResults.hidden = true;
353        if (mobileSearchResults.parentElement === panel) {
354          mobileMenu.insertBefore(mobileSearchResults, dom.mobilePanels);
355        }
356      }
357
358      if (content) {
359        content.hidden = false;
360      }
361      mobileMenu.classList.remove('is-search-active');
362    };
363
364    const renderMobileSearchResultsContent = () => {
365      if (!mobileSearchResults) {
366        return;
367      }
368
369      const query = state.searchQuery.trim();
370      const matches = getFilteredSearchMatches();
371      const totalMatches = matches.length;
372      const displayedMatches = matches.slice(0, searchMaxResults);
373      state.searchDisplayedCount = displayedMatches.length;
374      const previousScrollTop = mobileSearchResults.scrollTop;
375      const rowsHtml = displayedMatches.map((item) => buildSearchResultRowHtml(item)).join('');
376      const footerHtml = buildSearchFooterHtml(query, totalMatches, displayedMatches.length);
377
378      mobileSearchResults.innerHTML = `
379      <div class="products-nav-mobile__search-results-inner">
380        ${rowsHtml}
381        ${footerHtml ? `<div class="products-nav-search__footer products-nav-search__footer--mobile">${footerHtml}</div>` : ''}
382      </div>
383    `;
384      mobileSearchResults.scrollTop = previousScrollTop;
385    };
386
387    const renderSearchResults = () => {
388      api.browse.clearDesktopHoverTimer();
389      const query = state.searchQuery.trim();
390      const matches = getFilteredSearchMatches();
391      const totalMatches = matches.length;
392      const displayedMatches = matches.slice(0, searchMaxResults);
393      state.searchDisplayedCount = displayedMatches.length;
394      const columnCount = getSearchColumnCount();
395      const resultsHtml = buildDesktopSearchResultsRowsHtml(displayedMatches);
396
397      desktopColumns.classList.add('is-search-mode');
398      desktopColumns.style.setProperty('--products-nav-column-count', Str
398ing(columnCount));
399      desktopColumns.innerHTML = resultsHtml;
400
401      if (desktopSearchFooter) {
402        const footerHtml = buildSearchFooterHtml(query, totalMatches, displayedMatches.length);
403        desktopSearchFooter.innerHTML = footerHtml;
404        desktopSearchFooter.hidden = '' === footerHtml;
405      }
406
407      api.browse.applyDesktopMenuLayout();
408    };
409
410    const renderMobileSearchResults = () => {
411      if (!mobileSearchResults) {
412        return;
413      }
414
415      const activePanel = api.mobile.getActiveMobilePanel();
416      const slot = activePanel?.querySelector('.products-nav-mobile__search-slot');
417      const isLayoutReady = Boolean(
418        slot
419        && mobileSearchContainer
420        && mobileSearchContainer.parentElement === slot
421        && mobileMenu.classList.contains('is-search-active')
422        && mobileSearchResults.parentElement === activePanel
423      );
424
425      if (!isLayoutReady) {
426        syncMobileSearchLayout();
427      }
428
429      renderMobileSearchResultsContent();
430    };
431
432    const renderBrowseMode = () => {
433      desktopColumns.classList.remove('is-search-mode');
434      if (desktopSearchFooter) {
435        desktopSearchFooter.hidden = true;
436        desktopSearchFooter.innerHTML = '';
437      }
438      if (mobileSearchResults) {
439        mobileSearchResults.hidden = true;
440        mobileSearchResults.innerHTML = '';
441      }
442      if (!isMobile()) {
443        api.browse.renderDesktopColumns();
444      }
445      syncMobileSearchLayout();
446    };
447
448    const syncMenuRender = () => {
449      if (!searchEnabled) return;
450
451      updateSearchClearButtons();
452      if (isSearchMode()) {
453        if (!isMobile()) {
454          renderSearchResults();
455        }
456        if (!mobileMenu.hidden) {
457          renderMobileSearchResults();
458        }
459        return;
460      }
461
462      renderBrowseMode();
463    };
464
465    const resetSearch = () => {
466      if (!searchEnabled) return;
467
468      if (searchDebounceTimer) {
469        window.clearTimeout(searchDebounceTimer);
470        searchDebounceTimer = null;
471      }
472
473      state.searchQuery = '';
474      resetSearchDisplayedCount();
475      syncSearchFieldValues('');
476      updateSearchClearButtons();
477      state.desktopHighlightPath = api.browse.getInitialDesktopPath();
478      state.desktopPath = state.desktopHighlightPath.slice();
479      state.desktopPendingTapKey = '';
480      syncMenuRender();
481    };
482
483    const setSearchQuery = (value, immediate = false) => {
484      if (!searchEnabled) return;
485
486      const nextQuery = String(value ?? '');
487      if (nextQuery.trim() !== state.searchQuery.trim()) {
488        resetSearchDisplayedCount();
489      }
490      state.searchQuery = nextQuery;
491      syncSearchFieldValues(nextQuery);
492      updateSearchClearButtons();
493
494      const run = () => {
495        if (!isSearchMode()) {
496          state.desktopHighlightPath = api.browse.getInitialDesktopPath();
497          state.desktopPath = state.desktopHighlightPath.slice();
498        }
499        syncMenuRender();
500      };
501
502      if (immediate || searchDebounceMs === 0) {
503        if (searchDebounceTimer) {
504          window.clearTimeout(searchDebounceTimer);
505          searchDebounceTimer = null;
506        }
507        run();
508        return;
509      }
510
511      if (searchDebounceTimer) {
512        window.clearTimeout(searchDebounceTimer);
513      }
514      searchDebounceTimer = window.setTimeout(() => {
515        searchDebounceTimer = null;
516        run();
517      }, searchDebounceMs);
518    };
519
520    const bindEvents = () => {
521      if (!searchEnabled) return;
522
523      const { shell } = dom;
524
525      searchFields.forEach((field) => {
526        field.setAttribute('placeholder', searchPlaceholder);
527        field.addEventListener('input', (event) => {
528          setSearchQuery(event.target.value);
529        });
530        field.addEventListener('keydown', (event) => {
531          if (event.key === 'Escape') {
532            event.preventDefault();
533            resetSearch();
534          }
535        });
536      });
537
538      searchClearButtons.forEach((button) => {
539        button.addEventListener('click', (event) => {
540          event.preventDefault();
541          resetSearch();
542          const activeField = searchFields.find((field) => {
543            const container = field.closest('[data-products-nav-search]');
544            return container && !container.hidden;
545          }) || searchFields[0];
546          activeField?.focus();
547        });
548      });
549
550      shell.addEventListener('click', (event) => {
551        const loadMoreButton = event.target.closest('[data-products-nav-search-load-more]');
552        if (loadMoreButton) {
553          event.preventDefault();
554          loadMoreSearchResults();
555        }
556
557        event.stopPropagation();
558      });
559    };
560
561    return {
562      isSearchEnabled: () => searchEnabled,
563      isSearchMode,
564      setDesktopSearchVisible,
565      setMobileSearchVisible,
566      syncMobileSearchLayout,
567      resetSearch,
568      syncMenuRender,
569      bindEvents,
570    };
571  }
572
573  window.ammwebProductNavSearch = { create };
574}(window));

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.