PageSourceSearch

https://ccpdirect.com/Files/Templates/Designs/Swift/Assets/js/ccp-meili-search.js

js ccpdirect.com collected 2026-10-09 21:36:13 UTC 21,536 bytes, 584 lines download raw bytes

1(function (window) {
2  'use strict';
3
4  var DEFAULTS = {
5    hitsPerPage: 10,
6    minQueryLength: 3,
7    debounceMs: 350,
8    hitTitleField: 'ProductTitleWebCCP'
9  };
10
11  var Ccp = window.CcpMeiliSearch = window.CcpMeiliSearch || {};
12  Ccp.instances = Ccp.instances || {};
13
14  function ensureTnzMeiliSearch() {
15    var T = window.TnzMeiliSearch = window.TnzMeiliSearch || {};
16    if (!T._ready) {
17      T.url = '/Admin/Public/MeiliSearchProxy.ashx';
18      T.toHex = function (str) {
19        var bytes = new TextEncoder().encode(str);
20        var hex = '';
21        for (var i = 0; i < bytes.length; i++) {
22          var part = bytes[i].toString(16);
23          hex += (part.length < 2 ? '0' : '') + part;
24        }
25        return hex;
26      };
27      T.fetchJson = function (payload) {
28        var body = 's=' + encodeURIComponent(T.toHex(JSON.stringify(payload)));
29        return fetch(T.url, {
30          method: 'POST',
31          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
32          body: body
33        }).then(function (response) {
34          if (!response.ok) {
35            return response.text().then(function (bodyText) {
36              var parsed = {};
37              try { parsed = JSON.parse(bodyText); } catch (e) { /* ignore */ }
38              throw new Error((parsed && parsed.error) || 'Search failed');
39            });
40          }
41          return response.json();
42        });
43      };
44      T.rawSearch = function (indexUid, searchBody) {
45        var payload = Object.assign({ a: 2, x: indexUid }, searchBody);
46        return T.fetchJson(payload);
47      };
48      T._ready = true;
49    }
50
51    T.searchClient = {
52      search: function (requests) {
53        var req = requests[0] || {};
54        var params = req.params || {};
55        if (typeof params === 'string') {
56          var parsedParams = {};
57          new URLSearchParams(params).forEach(function (value, key) {
58            parsedParams[key] = value;
59          });
60          params = parsedParams;
61        }
62        var payload = {
63          a: 1,
64          x: req.indexName || '',
65          q: params.query || '',
66          h: parseInt(params.hitsPerPage, 10) || DEFAULTS.hitsPerPage,
67          pg: parseInt(params.page, 10) || 0
68        };
69        if (params.filters) {
70          payload.f = params.filters;
71        } else if (params.filter) {
72          payload.f = params.filter;
73        }
74        return T.fetchJson(payload);
75      }
76    };
77
78    return T;
79  }
80
81  function loadInstantSearch() {
82    if (window.instantsearch) {
83      return Promise.resolve(window.instantsearch);
84    }
85    if (Ccp._instantSearchPromise) {
86      return Ccp._instantSearchPromise;
87    }
88    Ccp._instantSearchPromise = new Promise(function (resolve, reject) {
89      var script = document.createElement('script');
90      script.src = 'https://cdn.jsdelivr.net/npm/instantsearch.js@4/dist/instantsearch.production.min.js';
91      script.async = true;
92      script.onload = function () { resolve(window.instantsearch); };
93      script.onerror = function () { reject(new Error('Failed to load InstantSearch.js')); };
94      document.head.appendChild(script);
95    });
96    return Ccp._instantSearchPromise;
97  }
98
99  function checkContentResults(contentSearchPageId, query) {
100    return fetch('/Default.aspx?ID=' + encodeURIComponent(contentSearchPageId) +
101      '&q=' + encodeURIComponent(query) +
102      '&checkOnly=true&format=json')
103      .then(function (response) {
104        if (response.ok) {
105          return response.json().then(function (data) {
106            return data && data.hasResults !== false;
107          });
108        }
109        return true;
110      })
111      .catch(function (error) {
112        console.warn('Could not check content results:', error);
113        return true;
114      });
115  }
116
117  function emptyInstantSearchResponse(query, hitsPerPage) {
118    return {
119      results: [{
120        hits: [],
121        nbHits: 0,
122        page: 0,
123        nbPages: 0,
124        hitsPerPage: hitsPerPage || DEFAULTS.hitsPerPage,
125        processingTimeMS: 0,
126        query: query || '',
127        params: ''
128      }]
129    };
130  }
131
132  function createSearchClient(instance) {
133    return {
134      search: function (requests) {
135        var req = requests[0] || {};
136        var params = req.params || {};
137        if (typeof params === 'string') {
138          var parsedParams = {};
139          new URLSearchParams(params).forEach(function (value, key) {
140            parsedParams[key] = value;
141          });
142          params = parsedParams;
143        }
144        var query = (params.query || '').trim();
145        if (query.length < instance.minQueryLength) {
146          return Promise.resolve(emptyInstantSearchResponse(query, instance.hitsPerPage));
147        }
148        return window.TnzMeiliSearch.searchClient.search(requests);
149      }
150    };
151  }
152
153  function buildHitItemTemplate(hitTitleField) {
154    return function (hit) {
155      var rawProductId = hit.ProductId || hit.ProductID || '';
156      var sanitizedProductId = rawProductId.replace(/\//g, '_');
157      var title = hit[hitTitleField] || hit.ProductName || 'No Name';
158
159      return '' +
160        '<div data-product-id="' + rawProductId + '"' +
161        ' class="hit-item"' +
162        ' style="display: flex; align-items: center; gap: 8px;">' +
163        '<img' +
164        ' src="/Admin/Public/GetImage.ashx?width=48&image=/Files/Images/Products/' + sanitizedProductId + '.jpg&format=webp"' +
165        ' alt="Product Image"' +
166        ' loading="lazy"' +
167        ' style="width: 48px; height: 48px; object-fit: cover; border-radius: 4px;"' +
168        ' onerror="this.onerror=null;this.src=\'/Admin/Public/GetImage.ashx?width=48&image=/Files/Images/missing_image.jpg\';"' +
169        ' />' +
170        '<strong>' + title +
171        '<span style="color: #666; font-weight: normal;">' +
172        ' (' + (rawProductId || 'N/A') + ')' +
173        '</span>' +
174        '</strong>' +
175        '</div>';
176    };
177  }
178
179  function attachInstanceMethods(instance) {
180    instance.syncDropdownVisibility = function (query) {
181      var hitsContainer = instance.container.querySelector('#' + instance.hitsId + ' .ais-Hits');
182      var searchInput = instance.container.querySelector('#' + instance.searchboxId + ' .ais-SearchBox-input');
183      if (!hitsContainer) {
184        return;
185      }
186      if (query === undefined) {
187        query = searchInput ? searchInput.value.trim() : '';
188      }
189      if (query.length >= instance.minQueryLength) {
190        hitsContainer.classList.add('has-query');
191        instance.currentSelectedIndex = -1;
192        instance.updateKeyboardSelection();
193      } else {
194        hitsContainer.classList.remove('has-query');
195        instance.currentSelectedIndex = -1;
196      }
197    };
198
199    instance.handleViewAllRedirect = function (query, productIds) {
200      if (!productIds.length) {
201        return;
202      }
203      var perPage = 50;
204      var firstPageIds = productIds.slice(0, perPage);
205      var url = new URL('/Default.aspx', window.location.origin);
206      url.searchParams.set('ID', instance.productDetailsPageId);
207      url.searchParams.set('redirect', 'false');
208      url.searchParams.set('SearchLayout', 'field');
209      url.searchParams.set('MainProductID', firstPageIds.join(','));
210      url.searchParams.set('query', query || '');
211      url.searchParams.set('page', '1');
212      url.searchParams.set('perPage', String(perPage));
213      window.location.href = url.toString();
214    };
215
216    instance.handleViewAllContentRedirect = function (query) {
217      var url = new URL('/Default.aspx', window.location.origin);
218      url.searchParams.set('ID', instance.contentSearchPageId);
219      url.searchParams.set('q', query || '');
220      url.searchParams.set('redirect', 'false');
221      url.searchParams.set('SearchLayout', 'field');
222      window.location.href = url.toString();
223    };
224
225    instance.handleViewAllContentClick = function (query, contentLink) {
226      var wrapper = contentLink ? contentLink.closest('.view-all-link-wrapper') : null;
227      if (contentLink) {
228        contentLink.classList.add('loading');
229      }
230      checkContentResults(instance.contentSearchPageId, query).then(function (hasContent) {
231        if (hasContent) {
232          instance.handleViewAllContentRedirect(query);
233          return;
234        }
235        if (wrapper) {
236          wrapper.outerHTML = '<div class="view-all-link-wrapper"><span class="no-content-message">We have no content results for "' +
237            query + '"</span></div>';
238        }
239      });
240    };
241
242    instance.updateKeyboardSelection = function () {
243      var hitsRoot = instance.container.querySelector('#' + instance.hitsId);
244      if (!hitsRoot) {
245        return;
246      }
247      hitsRoot.querySelectorAll('.keyboard-selected').forEach(function (el) {
248        el.classList.remove('keyboard-selected');
249      });
250
251      var hitItems = hitsRoot.querySelectorAll('.hit-item');
252      var viewAllLinkWrappers = hitsRoot.querySelectorAll('.view-all-link-wrapper');
253
254      if (instance.currentSelectedIndex >= 0) {
255        if (instance.currentSelectedIndex < hitItems.length) {
256          hitItems[instance.currentSelectedIndex].classList.add('keyboard-selected');
257          var parentHitItem = hitItems[instance.currentSelectedIndex].closest('.ais-Hits-item');
258          if (parentHitItem) {
259            parentHitItem.classList.add('keyboard-selected');
260          }
261        } else {
262          var footerIndex = instance.currentSelectedIndex - hitItems.length;
263          if (footerIndex < viewAllLinkWrappers.length) {
264            viewAllLinkWrappers[footerIndex].classList.add('keyboard-selected');
265          }
266        }
267      }
268    };
269
270    instance.scrollToSelected = function () {
271      var hitsRoot = instance.container.querySelector('#' + instance.hitsId);
272      var selected = hitsRoot ? hitsRoot.querySelector('.keyboard-selected') : null;
273      if (selected) {
274        selected.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
275      }
276    };
277
278    instance.handleEnterKey = function (query) {
279      var hitsRoot = instance.container.querySelector('#' + instance.hitsId);
280      if (!hitsRoot) {
281        return;
282      }
283
284      var hitItems = hitsRoot.querySelectorAll('.hit-item');
285      var viewAllLinkWrappers = hitsRoot.querySelectorAll('.view-all-link-wrapper');
286
287      if (instance.currentSelectedIndex >= 0) {
288        if (instance.currentSelectedIndex < hitItems.length) {
289          var selectedItem = hitItems[instance.currentSelectedIndex];
290          var productId = selectedItem.getAttribute('data-product-id');
291          if (productId) {
292            window.location.href = '/Default.aspx?ID=' + encodeURIComponent(instance.productDetailsPageId) +
293              '&ProductId=' + encodeURIComponent(productId);
294          }
295        } else {
296          var footerIndex = instance.currentSelectedIndex - hitItems.length;
297          if (footerIndex < viewAllLinkWrappers.length) {
298            var selectedWrapper = viewAllLinkWrappers[footerIndex];
299            var productsLink = selectedWrapper.querySelector('.js-view-all-products');
300            var contentLink = selectedWrapper.querySelector('.js-view-all-content');
301            if (productsLink) {
302              var productIds = instance.searchResults
303                .map(function (h) { return h.ProductId || h.ProductID; })
304                .filter(Boolean);
305              instance.handleViewAllRedirect(query, productIds);
306            } else if (contentLink) {
307              instance.handleViewAllContentClick(query, contentLink);
308            }
309          }
310        }
311      } else if (query.length >= instance.minQueryLength) {
312        var ids = instance.searchResults
313          .map(function (h) { return h.ProductId || h.ProductID; })
314          .filter(Boolean);
315        instance.handleViewAllRedirect(query, ids);
316      }
317    };
318
319    instance.bindEvents = function () {
320      instance.container.addEventListener('click', function (e) {
321        var hitItem = e.target.closest('.hit-item');
322        if (!hitItem || !instance.container.contains(hitItem)) {
323          return;
324        }
325        var productId = hitItem.getAttribute('data-product-id');
326        if (productId) {
327          window.location.href = '/Default.aspx?ID=' + encodeURIComponent(instance.productDetailsPageId) +
328            '&ProductId=' + encodeURIComponent(productId);
329        }
330      });
331
332      document.addEventListener('click', function (e) {
333        if (instance.container.contains(e.target)) {
334          return;
335        }
336        var hitsContainer = instance.container.querySelector('#' + instance.hitsId + ' .ais-Hits');
337        if (hitsContainer) {
338          hitsContainer.classList.remove('has-query');
339          instance.currentSelectedIndex = -1;
340          instance.updateKeyboardSelection();
341        }
342      });
343
344      document.addEventListener('keydown', function (e) {
345        var searchInput = instance.container.querySelector('#' + instance.searchboxId + ' .ais-SearchBox-input');
346        var hitsContainer = instance.container.querySelector('#' + instance.hitsId + ' .ais-Hits');
347        if (!hitsContainer || (document.activeElement !== searchInput && !hitsContainer.classList.contains('has-query'))) {
348          return;
349        }
350
351        var hitItems = instance.container.querySelectorAll('#' + instance.hitsId + ' .hit-item');
352        var viewAllLinkWrappers = instance.container.querySelectorAll('#' + instance.hitsId + ' .view-all-link-wrapper');
353        var totalItems = hitItems.length + viewAllLinkWrappers.length;
354
355        switch (e.key) {
356          case 'ArrowDown':
357            e.preventDefault();
358            instance.currentSelectedIndex = Math.min(instance.currentSelectedIndex + 1, totalItems - 1);
359            instance.updateKeyboardSelection();
360            instance.scrollToSelected();
361            break;
362          case 'ArrowUp':
363            e.preventDefault();
364            instance.currentSelectedIndex = Math.max(instance.currentSelectedIndex - 1, -1);
365            instance.updateKeyboardSelection();
366            instance.scrollToSelected();
367            break;
368          case 'Enter':
369            e.preventDefault();
370            instance.handleEnterKey(searchInput.value.trim());
371            break;
372          case 'Escape':
373            hitsContainer.classList.remove('has-query');
374            instance.currentSelectedIndex = -1;
375            instance.updateKeyboardSelection();
376            searchInput.blur();
377            break;
378          default:
379            break;
380        }
381      });
382    };
383  }
384
385  function createViewAllWidget(instance) {
386    return {
387      render: function (_ref) {
388        var results = _ref.results;
389        var state = _ref.state;
390        var hitsContainer = instance.container.querySelector('#' + instance.hitsId + ' .ais-Hits');
391        if (!hitsContainer) {
392          return;
393        }
394
395        var footer = hitsContainer.querySelector('.view-all-footer');
396        if (footer) {
397          footer.remove();
398        }
399
400        var query = state.query;
401        var nbHits = results && results.nbHits ? results.nbHits : 0;
402        instance.searchResults = results && results.hits ? results.hits : [];
403
404        if (!query || query.trim().length < instance.minQueryLength) {
405          return;
406        }
407
408        footer = document.createElement('div');
409        footer.className = 'view-all-footer text-center';
410
411        var productIds = results.hits
412          .map(function (h) { return h.ProductId || h.ProductID; })
413          .filter(Boolean);
414
415        var footerContent = '';
416        if (nbHits > 0) {
417          footerContent += '<div class="view-all-link-wrapper"><a href="#" class="view-all-link js-view-all-products">View all ' +
418            nbHits + ' products for "' + query + '"</a></div>';
419        }
420        footerContent += '<div class="view-all-link-wrapper"><a href="#" class="view-all-link js-view-all-content">View all content for "' +
421          query + '"</a></div>';
422
423        footer.innerHTML = '<div class="d-flex flex-wrap justify-content-center gap-2 p-2">' + footerContent + '</div>';
424
425        var productsLink = footer.querySelector('.js-view-all-products');
426        var contentLink = footer.querySelector('.js-view-all-content');
427
428        if (productsLink) {
429          productsLink.addEventListener('click', function (e) {
430            e.preventDefault();
431            instance.handleViewAllRedirect(query, productIds);
432          });
433        }
434
435        if (contentLink) {
436          contentLink.addEventListener('click', function (e) {
437            e.preventDefault();
438            instance.handleViewAllContentClick(query, contentLink);
439          });
440        }
441
442        hitsContainer.appendChild(footer);
443      }
444    };
445  }
446
447  function initInstance(config) {
448    if (Ccp.instances[config.paragraphId]) {
449      return Ccp.instances[config.paragraphId].promise;
450    }
451
452    ensureTnzMeiliSearch();
453
454    var pending = loadInstantSearch().then(function (instantsearch) {
455      var instance = {
456        paragraphId: config.paragraphId,
457        searchboxId: config.searchboxId,
458        hitsId: config.hitsId,
459        productDetailsPageId: String(config.productDetailsPageId),
460        contentSearchPageId: String(config.contentSearchPageId),
461        indexName: config.indexName,
462        filters: config.filters,
463        hitTitleField: config.hitTitleField || DEFAULTS.hitTitleField,
464        hitsPerPage: config.hitsPerPage || DEFAULTS.hitsPerPage,
465        minQueryLength: config.minQueryLength || DEFAULTS.minQueryLength,
466        debounceMs: config.debounceMs || DEFAULTS.debounceMs,
467        container: document.querySelector('[data-paragraph-id="' + config.paragraphId + '"]'),
468        currentSelectedIndex: -1,
469        searchResults: [],
470        debounceTimer: null,
471        search: null
472      };
473
474      if (!instance.container) {
475        console.warn('CCP search container not found for paragraph', config.paragraphId);
476        return null;
477      }
478
479      attachInstanceMethods(instance);
480
481      instance.search = instantsearch({
482        indexName: instance.indexName,
483        searchClient: createSearchClient(instance)
484      });
485
486      instance.search.addWidgets([
487        instantsearch.widgets.configure({
488          hitsPerPage: instance.hitsPerPage,
489          filters: instance.filters
490        }),
491        instantsearch.widgets.searchBox({
492          container: '#' + instance.searchboxId,
493          placeholder: 'Search here',
494          showReset: true,
495          showSubmit: true,
496          showLoadingIndicator: true,
497          queryHook: function (query, refine) {
498            clearTimeout(instance.debounceTimer);
499            var trimmed = (query || '').trim();
500            instance.syncDropdownVisibility(trimmed);
501            if (trimmed.length > 0 && trimmed.length < instance.minQueryLength) {
502              return;
503            }
504            instance.debounceTimer = setTimeout(function () {
505              refine(trimmed);
506            }, instance.debounceMs);
507          }
508        }),
509        instantsearch.widgets.hits({
510          container: '#' + instance.hitsId,
511          templates: {
512            item: buildHitItemTemplate(instance.hitTitleField),
513            empty: 'No products found.'
514          }
515        })
516      ]);
517
518      instance.search.addWidgets([createViewAllWidget(instance)]);
519
520      instance.search.on('render', function () {
521        instance.syncDropdownVisibility();
522      });
523
524      instance.search.start();
525      instance.bindEvents();
526
527      instance.container.addEventListener('input', function (e) {
528        if (e.target.closest('#' + instance.searchboxId + ' .ais-SearchBox-input')) {
529          instance.syncDropdownVisibility(e.target.value.trim());
530        }
531      });
532      return instance;
533    }).catch(function (error) {
534      console.error('Error initializing CCP search:', error);
535      delete Ccp.instances[config.paragraphId];
536      return null;
537    });
538
539    Ccp.instances[config.paragraphId] = { promise: pending };
540    return pending;
541  }
542
543  Ccp.register = function (config) {
544    if (!config || !config.paragraphId || Ccp.instances[config.paragraphId]) {
545      return;
546    }
547
548    var container = document.querySelector('[data-paragraph-id="' + config.paragraphId + '"]');
549    if (!container) {
550      return;
551    }
552
553    if (config.lazyInit === false) {
554      initInstance(config);
555      return;
556    }
557
558    function activate(shouldFocus) {
559      initInstance(config).then(function (instance) {
560        if (!shouldFocus || !instance) {
561          return;
562        }
563        var input = instance.container.querySelector('#' + instance.searchboxId + ' .ais-SearchBox-input');
564        if (input) {
565          input.focus();
566        }
567      });
568    }
569
570    container.addEventListener('focusin', function handleFocusIn() {
571      container.removeEventListener('focusin', handleFocusIn);
572      activate(false);
573    });
574
575    container.addEventListener('click', function handleClick() {
576      container.removeEventListener('click', handleClick);
577      if (!Ccp.instances[config.paragraphId]) {
578        activate(true);
579      }
580    }, { once: true });
581  };
582
583  Ccp.init = initInstance;
584})(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.