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.