1class GcaSearchBase { 2 constructor() { 3 this.query; 4 this.initialPageSize; 5 this.pageSize; 6 this.orderBy; 7 this.orderDir; 8 this.position; 9 this.tab; 10 this.lastShowMore; 11 this.paginationEnabled; 12 this.hasMore; 13 this.dispatcherType; 14 this.dispatcherId; 15 this.elementsInRow; 16 this.elementsInRowMobile; 17 this.context; 18 this.extraParams; 19 this._urlMapper = this._populateUrlParamsMapper(); 20 } 21 22 setup(query, dispatcherType, dispatcherId, context, size, orderBy, orderDir, tab, position, entityId, elementsInRow, elementsInRowMobile) { 23 return new Promise((resolve, reject) => { 24 this.query = query; 25 this.dispatcherType = dispatcherType; 26 this.dispatcherId = dispatcherId; 27 this.entityId = entityId; 28 this.elementsInRow = elementsInRow; 29 this.elementsInRowMobile = elementsInRowMobile 30 this.context = context; 31 this.initialPageSize = size; 32 this.pageSize = size; 33 this.orderBy = orderBy; 34 this.orderDir = orderDir; 35 this.tab = tab; 36 this.position = position; 37 this.lastShowMore = new Date().getTime(); 38 this.paginationEnabled = false; 39 this.hasMore = true; 40 this.extraParams = null; 41 42 this.setupFilterEvents(); 43 this.setupTabEvent(); 44 this.setupShowMoreEvent(); 45 this.setupScrollEvent(); 46 resolve(); 47 }); 48 } 49 50 _populateUrlParamsMapper() { 51 let urlParamsMapper = {}; 52 let urlParamsToMap = ["select", "delete", "having", "union"]; 53 urlParamsToMap.forEach(function (paramValue) { 54 let val1 = paramValue; 55 let val2 = paramValue.toUpperCase(); 56 let val3 = paramValue.charAt(0).toUpperCase() + paramValue.slice(1); 57 58 urlParamsMapper[val1] = "__" + val1 59 urlParamsMapper[val2] = "__" + val2 60 urlParamsMapper[val3] = "__" + val3 61 }); 62 // Map specific symbols: 63 urlParamsMapper["&"] = "__and__"; 64 65 return urlParamsMapper; 66 } 67 68 _mapParamToUrl(val) { 69 let newVal = val; 70 if(typeof val === "string") { 71 Object.keys(this._urlMapper).forEach(key => { 72 if(val.includes(key)) { 73 newVal = val.replace(key, this._urlMapper[key]); 74 } 75 }) 76 } 77 return newVal; 78 } 79 80 _mapParamFromUrl(val) { 81 let newVal = val; 82 if(this._isMappedUrlParameter(val)) { 83 Object.keys(this._urlMapper).forEach(key => { 84 let mappedVal = this._urlMapper[key]; 85 if(val.includes(mappedVal)) { 86 newVal = val.replace(mappedVal, key); 87 } 88 }) 89 } 90 return newVal; 91 } 92 93 _containsParamValue(groupValues, value) { 94 let included = false; 95 if(groupValues !== undefined && groupValues.length > 0) { 96 groupValues.forEach(v => { 97 let groupValue = this._mapParamFromUrl(v); 98 if(groupValue == value) { 99 included = true; 100 return included; 101 } 102 }) 103 } 104 return included; 105 } 106 107 _isMappedUrlParameter(val) { 108 let mapped = false; 109 if(val && val.includes("__")) { 110 Object.values(this._urlMapper).forEach(mappedValue => { 111 if(val.includes(mappedValue)) { 112 mapped = true; 113 } 114 }) 115 } 116 return mapped; 117 } 118 119 120 pushSearchToGoogle(query, type) { 121 if (query && query.trim().length > 0) { 122 jQuery.ajax({ 123 dataType: "json", 124 url: "/ajax?action=allowTrack&recipient=google&service=analytics&event=search", 125 cache: false, 126 type: "get" 127 }).done(function (allowed) { 128 if (allowed === true) { 129 dataLayer.push({ 130 'event': 'search', 131 'search_term': query, 132 'search_type': type 133 }); 134 } 135 }); 136 } 137 } 138 139 doSearch(facetGroups) { 140 gca.showPageLoader(); 141 142 let self = this; 143 let body = {}; 144 body.query = self.query;
145 body.dispatcherType = self.dispatcherType; 146 body.dispatcherId = self.dispatcherId; 147 body.context = self.context; 148 body.facetGroups = facetGroups; 149 body.filters = self.getFilterParams(); 150 body.size = self.pageSize; 151 body.orderBy = self.orderBy; 152 body.orderDir = self.orderDir; 153 body.extraParams = self.getExtraParams(); 154 155 // Remove any old filtering params.. 156 if (body.facetGroups !== undefined) { 157 body.facetGroups.forEach(function(group) { 158 gca.removeURLParameterWithoutReload(group.name); 159 }); 160 } 161 162 let hasFilters = false; 163 for (let i in body.filters) { 164 let filter = body.filters[i]; 165 if (filter.values.length > 0 || filter.ranges.length > 0) { 166 hasFilters = true; 167 break; 168 } 169 } 170 171 let hasUrlFilterParams = false; 172 let urlFilters = {}; 173 if (!hasFilters) { 174 const queryString = window.location.search; 175 const urlParams = new URLSearchParams(queryString); 176 const urlParamsSize = new Set(urlParams.keys()).size; 177 if (urlParamsSize > 1 && urlParams.has("filtered") && urlParams.get("filtered") === "true") { 178 urlParams.entries().forEach(function (entry) { 179 let key = entry[0]; 180 if(key.startsWith("attribute.") || key === "vendor") { // TODO: Handle all fixed facet options. Available in Tool/facade. As this is a JS file, we should populate a list of fixed system facet groups by Ajax request once during init/setup. 181 let value = entry[1]; 182 let type = "terms"; 183 if(value.split(":").length === 3 && value.split(":")[2] === "__range") { 184 type = "range"; 185 } 186 if (urlFilters[key] === undefined) { 187 urlFilters[key] = { 188 key: key, 189 type: type, 190 values: [], 191 ranges: [] 192 }; 193 } 194 195 if(type === "range") { 196 const gteVal = value.split(":")[0]; 197 const lteVal = value.split(":")[1]; 198 let range = {}; 199 range["gte"] = gteVal; 200 range["lte"] = lteVal; 201 urlFilters[key].ranges.push(range); 202 } 203 else { 204 urlFilters[key].values.push( self._mapParamFromUrl(value) ); 205 } 206 gca.removeURLParameterWithoutReload(key); 207 hasUrlFilterParams = true; 208 } 209 }) 210 body.filters = Object.values(urlFilters); 211 } 212 } 213 214 fetch ("/ajax?action=search&tab=" + self.tab,{ 215 method: 'POST', 216 headers: { 217 'Content-Type': 'application/json', 218 }, 219 body: JSON.stringify(body) 220 }) 221 .then(response => response.text()) 222 .then(data => { 223 self.pushSearchToGoogle(self.query, "default") 224 gca.hidePageLoader(); 225 self.showResults(data); 226 227 if(body.filters && body.filters.length > 0) { 228 gca.updateURLParameterWithoutReload("filtered", "true"); 229 body.filters.forEach(function(entry) { 230 if (!entry.key.startsWith("_")) { 231 if (entry.type === "range") { 232 entry.ranges.forEach(function(range) { 233 gca.addURLParameterWithoutReload(entry.key, range.gte + ":" + range.lte + ":__range"); 234 }); 235 } 236 else {
237 entry.values.forEach(function(value) { 238 gca.addURLParameterWithoutReload(entry.key, self._mapParamToUrl(value)); 239 }); 240 } 241 } 242 }) 243 } 244 else { 245 gca.removeURLParameterWithoutReload("filtered"); 246 } 247 if(hasUrlFilterParams) { 248 body.filters.forEach(function (entry) { 249 const group = entry.key 250 251 const facetGroupSelector = ".search-facet-group [data-facet-group='" + group + "']"; 252 gca.waitForElement(facetGroupSelector).then((elm) => { 253 const allButtons = document.querySelectorAll(facetGroupSelector); 254 allButtons.forEach(function (el) { 255 const type = el.getAttribute("data-facet-group-type"); 256 257 if (type === "range") { 258 const rangeValues = entry.ranges 259 if(rangeValues && rangeValues[0] !== undefined) { 260 const rangeInput = document.querySelector(facetGroupSelector); 261 const gteInput = rangeInput.querySelector("input[data-facet-group-range-type='gte']"); 262 const lteInput = rangeInput.querySelector("input[data-facet-group-range-type='lte']"); 263 264 rangeInput.setAttribute("data-facet-checked", "true"); 265 gteInput.value = rangeValues[0]["gte"]; 266 lteInput.value = rangeValues[0]["lte"]; 267 } 268 } 269 else { 270 let value = null; 271 try { 272 value = el.getAttribute("data-facet-value") 273 } 274 catch (e) { 275 console.log(e); 276 } 277 const groupValues = entry.values 278 if (groupValues !== undefined && groupValues.includes(value)) { 279 el.setAttribute("data-facet-checked", "true"); 280 } 281 } 282 }) 283 }); 284 }) 285 } 286 }) 287 .catch((error) => { 288 console.error('Error:', error); 289 }); 290 } 291 292 doIncrementalSearch(query, target) { 293 let self = this; 294 let body = {}; 295 body.query = query; 296 body.dispatcherType = self.dispatcherType; 297 body.dispatcherId = self.dispatcherId; 298 body.context = "incremental"; 299 body.filters = []; 300 body.size = 5; 301 body.extraParams = self.getExtraParams(); 302 303 fetch ("/ajax?action=searchIncremental",{ 304 method: 'POST', 305 headers: { 306 'Content-Type': 'application/json', 307 }, 308 body: JSON.stringify(body) 309 }) 310 .then(response => response.text()) 311 .then((data) => { 312 this.renderIncrementalResults(data, target) 313 self.pushSearchToGoogle(query, "incremental") 314 }) 315 .catch((error) => { 316 console.error('Error:', error); 317 }); 318 } 319 320 doFilter(facetGroups, offCanvas) { 321 gca.showPageLoader(); 322 323 let self = this; 324 let body = {}; 325 body.query = self.query; 326 body.dispatcherType = self.dispatcherType; 327 body.dispatcherId = self.dispatcherId; 328 body.context = self.context; 329 body.facetGroups = facetGroups; 330 if (offCanvas) { 331 body.filters = self.getOffCanvasFilterParams(); 332 } 333 else { 334 body.filters = self.getFilterParams(); 335 } 336 body.size = self.pageSize; 337 body.orderBy = self.orderBy; 338 body.orderDir = self.orderDir; 339 body.extraParams = self.getExtraParams(); 340 341 // Remove any old filtering params.. 342 if (body.facetGroups !== undefined) { 343 body.facetGroups.forEach(function(group) { 344 gca.removeURLParameterWithoutReload(group.name.toLowerCase()); 345 }); 346 } 347 348 let hasFilters = false;
349 for (let i in body.filters) { 350 let filter = body.filters[i]; 351 if (filter.values.length > 0 || filter.ranges.length > 0) { 352 hasFilters = true; 353 break; 354 } 355 } 356 357 if (!hasFilters) { 358 gca.removeURLParameterWithoutReload("filtered"); 359 location.reload(); 360 return; 361 } 362 363 let filter = {}; 364 filter.key = "__page"; 365 filter.values = []; 366 filter.values.push(self.dispatcherId); 367 body.filters.push(filter); 368 369 fetch ("/ajax?action=filter&id=" + this.entityId + "&sort=" + self.orderBy + "&dir=" + this.orderDir + "&view=" + gca.getListView(this.entityId) + "&row=" + this.elementsInRow + "&rowMobile=" + this.elementsInRowMobile, { 370 method: 'POST', 371 headers: { 372 'Content-Type': 'application/json', 373 }, 374 body: JSON.stringify(body) 375 }) 376 .then(response => response.text()) 377 .then(data => { 378 gca.hidePageLoader(); 379 self.showResults(data); 380 381 gca.updateURLParameterWithoutReload("filtered", "true"); 382 body.filters.forEach(function(entry) { 383 if (!entry.key.startsWith("_")) { 384 if (entry.type === "range") { 385 entry.ranges.forEach(function(range) { 386 gca.addURLParameterWithoutReload(entry.key.toLowerCase(), range.gte + ":" + range.lte + ":__range"); 387 }); 388 } 389 else { 390 entry.values.forEach(function(value) { 391 if (typeof value === "string") { 392 gca.addURLParameterWithoutReload(entry.key.toLowerCase(), self._mapParamToUrl(value.toLowerCase())); 393 } 394 else { 395 gca.addURLParameterWithoutReload(entry.key.toLowerCase(), self._mapParamToUrl(value)); 396 } 397 }); 398 } 399 } 400 }) 401 }) 402 .catch((error) => { 403 console.error('Error:', error); 404 }); 405 } 406 407 doSort() { 408 const sortFieldSelect = $("[data-target='sortFields'] :selected"); 409 gca.updateURLParameterWithoutReload("sort", sortFieldSelect.data("field")); 410 gca.updateURLParameterWithoutReload("dir", sortFieldSelect.data("direction")); 411 location.reload(); 412 } 413 414 updateOffCanvasFilter(facetGroups) { 415 let self = this; 416 let body = {}; 417 body.query = self.query; 418 body.dispatcherType = self.dispatcherType; 419 body.dispatcherId = self.dispatcherId; 420 body.context = self.context; 421 body.facetGroups = facetGroups; 422 body.filters = self.getOffCanvasFilterParams(); 423 body.size = self.pageSize; 424 body.orderBy = self.orderBy; 425 body.orderDir = self.orderDir; 426 body.extraParams = self.getExtraParams(); 427 428 let filter = {}; 429 filter.key = "__page"; 430 filter.values = []; 431 filter.values.push(self.dispatcherId); 432 body.filters.push(filter); 433 434 fetch ("/ajax?action=updateOffCanvasFilter",{ 435 method: 'POST', 436 headers: { 437 'Content-Type': 'application/json', 438 }, 439 body: JSON.stringify(body) 440 }) 441 .then(response => response.text()) 442 .then(data => { 443 let htmlContent = jQuery.parseHTML(data); 444 const offCanvasBody = $(htmlContent).find("div#offcanvas-body").html(); 445 $("#offcanvas-search-facets-body").html(offCanvasBody); 446 }) 447 .catch((error) => { 448 console.error('Error:', error); 449 }); 450 } 451 452 getFilterFacets() { 453 let self = this; 454 let body = {}; 455 body.query = self.query;
456 body.dispatcherType = self.dispatcherType; 457 body.dispatcherId = self.dispatcherId; 458 body.context = self.context; 459 body.filters = self.getFilterParams(); 460 body.size = self.pageSize; 461 body.orderBy = self.orderBy; 462 body.orderDir = self.orderDir; 463 body.extraParams = self.getExtraParams(); 464 465 let filter = {}; 466 filter.key = "__page"; 467 filter.values = []; 468 filter.values.push(self.dispatcherId); 469 470 body.filters.push(filter); 471 472 fetch ("/ajax?action=getFilterFacets",{ 473 method: 'POST', 474 headers: { 475 'Content-Type': 'application/json', 476 }, 477 body: JSON.stringify(body) 478 }) 479 .then(response => response.text()) 480 .then(data => { 481 gca.hidePageLoader(); 482 self.showFacets(data); 483 }) 484 .catch((error) => { 485 console.error('Error:', error); 486 }); 487 } 488 489 getFilterParams() { 490 let filters = {}; 491 let values = {}; 492 let ranges = {}; 493 494 jQuery.each($("[data-facet-group]"), function(i, el) { 495 el = $(el); 496 497 const name = encodeURIComponent(el.data('facet-group')); 498 const type = el.data("facet-group-type"); 499 const value = el.data('facet-value'); 500 const checked = el.attr('data-facet-checked'); 501 502 if (values[name] === undefined) { 503 values[name] = []; 504 } 505 if (ranges[name] === undefined) { 506 ranges[name] = []; 507 } 508 509 if ((type === "terms" || type === 'range') && checked === 'true') { 510 // Create filter if not already done... 511 if (filters[name] === undefined) { 512 filters[name] = { 513 key: name, 514 type: type, 515 values: [], 516 ranges: [] 517 }; 518 } 519 520 // Add value or range... 521 if (type === "terms" && checked === 'true') { 522 filters[name].values.push(value); 523 } 524 else if (type === 'range' && checked === 'true') { 525 const gte = el.find("[data-facet-group-range-type='gte']").val(); 526 const lte = el.find("[data-facet-group-range-type='lte']").val(); 527 528 if (gte !== undefined && gte.length > 0 && lte !== undefined && lte.length > 0) { 529 let range = {}; 530 range["gte"] = gte; 531 range["lte"] = lte; 532 filters[name].ranges.push(range); 533 } 534 } 535 } 536 }); 537 538 return Object.values(filters); 539 } 540 541 getOffCanvasFilterParams() { 542 let filters = {}; 543 let values = {}; 544 let ranges = {}; 545 546 jQuery.each($("[data-offcanvas-facet-group]"), function(i, el) { 547 el = $(el); 548 549 const name = encodeURIComponent(el.data('offcanvas-facet-group')); 550 const type = el.data("offcanvas-facet-group-type"); 551 const value = el.data('offcanvas-facet-value'); 552 const checked = el.attr('data-offcanvas-facet-checked'); 553 554 if (values[name] === undefined) { 555 values[name] = []; 556 } 557 if (ranges[name] === undefined) { 558 ranges[name] = []; 559 } 560 561 if ((type === "terms" && checked === 'true') || (type === 'range')) { 562 // Create filter if not already done... 563 if (filters[name] === undefined) { 564 filters[name] = { 565 key: name, 566 type: type, 567 values: [], 568 ranges: [] 569 }; 570 } 571 572 // Add value or range... 573 if (type === "terms" && checked === 'true') { 574 filters[name].values.push(value); 575 } 576 else if (type === 'range' && checked === 'true') { 577 const gte = el.find("[data-offcanvas-facet-group-range-type='gte']").val(); 578 const lte = el.find("[data-offcanvas-facet-group-range-type='lte']").val(); 579 580 if (gte !== undefined && gte.length > 0 && lte !== undefined && lte.length > 0) { 581 let range = {}; 582 range["gte"] = gte; 583 range["lte"] = lte; 584 filters[name].ranges.push(range); 585 } 586 } 587 } 588 }); 589 590 return Object.values(filters); 591 } 592 593 getFacetGroups() { 594 let facetGroups = {}; 595 let values = {}; 596 let ranges = {}; 597 598 let facetGroupElements = document.querySelectorAll(".search-facet-group")
599 facetGroupElements.forEach(function (el) { 600 let facetGroupBtn = el.querySelector("button[id^='facet_']") 601 let facetGroupName = facetGroupBtn.id.replace("facet_", "") 602 facetGroups[facetGroupName] = { 603 name: facetGroupName, 604 description: facetGroupBtn.getAttribute("title"), 605 type: el.getAttribute("data-facet-group-type"), 606 facets: [], 607 ranges: [], 608 extraParams: { 609 hidden: el.getAttribute("data-facet-group-hidden") 610 } 611 } 612 }); 613 614 jQuery.each($("[data-facet-group]"), function(i, el) { 615 el = $(el); 616 617 const name = encodeURIComponent(el.data('facet-group')); 618 const type = el.data("facet-group-type"); 619 const value = el.data('facet-value'); 620 const descr = el.text().trim(); 621 const checked = el.attr('data-facet-checked'); 622 const hidden = el.data('facet-group-hidden'); 623 624 if (values[name] === undefined) { 625 values[name] = []; 626 } 627 if (ranges[name] === undefined) { 628 ranges[name] = []; 629 } 630 631 if (type === "terms" || type === 'range') { 632 // Create filter if not already done... 633 if (facetGroups[name] === undefined) { 634 facetGroups[name] = { 635 name: name, 636 description: document.getElementById('facet_' + name).getAttribute('title'), 637 type: type, 638 facets: [], 639 ranges: [], 640 extraParams: { 641 hidden: hidden 642 } 643 }; 644 } 645 646 // Add value or range... 647 if (type === "terms") { 648 let facet = {}; 649 facet["value"] = value; 650 facet["description"] = descr; 651 facet["checked"] = checked; 652 facetGroups[name].facets.push(facet); 653 } 654 else if (type === 'range') { 655 const gte = el.find("[data-facet-group-range-type='gte']").val(); 656 const lte = el.find("[data-facet-group-range-type='lte']").val(); 657 658 if (gte !== undefined && gte.length > 0 && lte !== undefined && lte.length > 0) { 659 let range = {}; 660 range["max"] = gte; 661 range["min"] = lte; 662 range["checked"] = checked; 663 facetGroups[name].ranges.push(range); 664 } 665 } 666 } 667 }); 668 669 return Object.values(facetGroups); 670 } 671 672 getOffcanvasFacetGroups() { 673 let facetGroups = {}; 674 let values = {}; 675 let ranges = {}; 676 677 let facetGroupElements = document.querySelectorAll(".accordion-search-facets") 678 facetGroupElements.forEach(function (el) { 679 let facetGroupBtn = el.querySelector("div[id^='offcanvas-facet_']") 680 let facetGroupName = facetGroupBtn.id.replace("offcanvas-facet_", "") 681 facetGroups[facetGroupName] = { 682 name: facetGroupName, 683 description: facetGroupBtn.getAttribute("title"), 684 type: el.getAttribute("data-offcanvas-facet-group-type"), 685 facets: [], 686 ranges: [] 687 } 688 }); 689 690 jQuery.each($("[data-offcanvas-facet-group]"), function(i, el) { 691 el = $(el); 692 693 const name = encodeURIComponent(el.data('offcanvas-facet-group')); 694 const type = el.data("offcanvas-facet-group-type"); 695 const value = el.data('offcanvas-facet-value'); 696 const descr = el.text().trim(); 697 const checked = el.attr('data-offcanvas-facet-checked'); 698 699 if (values[name] === undefined) { 700 values[name] = []; 701 } 702 if (ranges[name] === undefined) { 703 ranges[name] = []; 704 } 705 706 if (type === "terms" || type === 'range') { 707 // Create filter if not already done... 708 if (facetGroups[name] === undefined) { 709 facetGroups[name] = { 710 name: name, 711 description: document.getElementById('offcanvas-facet_' + name.replaceAll(".", "-")).getAttribute('title'), 712 type: type, 713 facets: [], 714 ranges: [] 715 }; 716 } 717 718 // Add value or range... 719 if (type === "terms") { 720 let facet = {}; 721 facet["value"] = value;
722 facet["description"] = descr; 723 facet["checked"] = checked; 724 facetGroups[name].facets.push(facet); 725 } 726 else if (type === 'range') { 727 const gte = el.find("[data-offcanvas-facet-group-range-type='gte']").val(); 728 const lte = el.find("[data-offcanvas-facet-group-range-type='lte']").val(); 729 730 if (gte !== undefined && gte.length > 0 && lte !== undefined && lte.length > 0) { 731 let range = {}; 732 range["max"] = gte; 733 range["min"] = lte; 734 range["checked"] = checked; 735 facetGroups[name].ranges.push(range); 736 } 737 } 738 } 739 }); 740 741 return Object.values(facetGroups); 742 } 743 744 getExtraParams() { 745 return this.extraParams; 746 } 747 748 loadExtraParams() { 749 const self = this; 750 return fetch("/ajax?action=getExtraSearchRequestParams") 751 .then(function (response) { 752 return response.json(); 753 }) 754 .then(function (data) { 755 self.extraParams = data; 756 return self.extraParams; 757 }); 758 } 759 760 loadShowMoreProductsButton() { 761 fetch("/ajax?" + new URLSearchParams({ 762 action: "getShowMoreProductsButton", 763 context: "search", 764 })).then(function (response) { 765 return response.text(); 766 }).then(function (data) { 767 $("[data-target='showMoreProductsButton']").html(data.trim()) 768 }).catch(function (err) { 769 console.warn('Something went wrong.', err); 770 }); 771 } 772 773 showResults(data) { 774 const content = jQuery.parseHTML(data); 775 const results = $(content).find("div#results").html(); 776 const facets = $(content).find("div#facets").html(); 777 const hasMore = $(content).find("div#hasMore").html(); 778 779 $("[data-target='search-results']").html(results); 780 this.showFacets(facets); 781 782 const querySuggestionDiv = $(content).find('div#querySuggestion'); 783 if (querySuggestionDiv.length > 0) { 784 $("[data-ajax_target='querySuggestion']").html(querySuggestionDiv.html()); 785 } 786 787 this.hasMore = hasMore !== undefined && hasMore.trim() === "true"; 788 789 if (this.hasMore) { 790 this.loadShowMoreProductsButton(); 791 } 792 793 gca.loadProductData(); 794 gca.loadFavoritesStuff(); 795 gca.executeCustomFunctions(); 796 gca.pushProductListsToGoogle(); 797 } 798 799 showFacets(data) { 800 $("[data-target='search-facets']").html(data); 801 this.initRangeSlider(); 802 } 803 804 showMoreProducts() { 805 let self = this; 806 let now = new Date().getTime(); 807 if (now - self.lastShowMore > 500 && self.hasMore) { 808 self.pageSize = self.pageSize + self.initialPageSize; 809 let facetGroups = self.getFacetGroups(); 810 if (self.dispatcherType === "search") { 811 self.doSearch(facetGroups); 812 } 813 else { 814 self.doFilter(facetGroups); 815 } 816 self.lastShowMore = now; 817 } 818 819 gca.updateURLParameterWithoutReload("size", self.pageSize); 820 } 821 822 setupKeyHandlerForIncrementalResults(input) { 823 let self = this; 824 let target = input.getAttribute("data-content-target"); 825 let targetEl = this.getIncrementalResultsContainer(target); 826 827 document.addEventListener("keydown", handleKeys); 828 829 function handleKeys(e) { 830 if (input.isEqualNode(e.target)) { 831 const $selectedEl = $(targetEl).find(".selected"); 832 833 if (e.code === "ArrowDown") { 834 if ($selectedEl !== undefined && $selectedEl.length > 0) { 835 $selectedEl.removeClass("selected"); 836 $selectedEl.next().addClass("selected"); 837 } 838 else { 839 const $selectEl = $(targetEl).find(".incremental-product-container").first(); 840 $selectEl.addClass("selected"); 841 } 842 } 843 else if (e.code === "ArrowUp") { 844 if ($selectedEl !== undefined && $selectedEl.length > 0) { 845 $selectedEl.removeClass("selected"); 846 $selectedEl.prev().addClass("selected"); 847 } 848 else { 849 const $selectEl = $(targetEl).find(".incremental-product-container").last(); 850 $selectEl.addClass("selected"); 851 } 852 } 853 else if (e.code === "Enter") { 854 e.preventDefault(); 855 if ($selectedEl !== undefined && $selectedEl.length > 0) { 856 location.href = $selectedEl.find("a").attr("href"); 857 } 858 } 859 else if (e.code === "Escape") { 860 self.hideIncrementalResults(target); 861 } 862 } 863 } 864 } 865 866 renderIncrementalResults(data, target) { 867 const el = this.getIncrementalResultsContainer(target); 868 el.innerHTML = data; 869 this.showIncrementalResults(target); 870 } 871 872 showIncrementalResults(target) { 873 const el = this.getIncrementalResultsContainer(target); 874 el.style.display = "block"; 875 } 876 877 hideIncrementalResults(target) { 878 const el = this.getIncrementalResultsContainer(target); 879 if(el) el.style.display = "none"; 880 } 881 882 getIncrementalResultsContainer(target) { 883 return document.querySelectorAll("[data-content='" + target + "']")[0]; 884 } 885 886 isIncrementalResultsVisible(target) { 887 const el = this.getIncrementalResultsContainer(target); 888 return el.style.display !== "none"; 889 } 890 891 setupIncrementalSearchEvent() { 892 let self = this; 893 const inputs = document.querySelectorAll("[data-action='incremental-search']"); 894 895 if (inputs !== undefined && inputs.length > 0) { 896 for (const input of inputs) { 897 let target = input.getAttribute("data-content-target"); 898 899 const process = (e) => { 900 if (e.code === "Enter") { 901 input.parentElement.parentElement.submit(); 902 } 903 else if (e.code !== "Escape" && e.code.indexOf("Arrow") < 0) { 904 self.doIncrementalSearch(input.value, target); 905 } 906 }; 907 908 const handler = gca.debounced(500, process); 909 const targetEl = this.getIncrementalResultsContainer(target); 910 911 input.addEventListener("keyup", handler); 912 input.onclick = function() { 913 self.showIncrementalResults(target); 914 }; 915 this.setupKeyHandlerForIncrementalResults(input); 916 917 document.addEventListener("click", (event) => { 918 if (self.isIncrementalResultsVisible(target)) { 919 const withinBoundaries = event.composedPath().includes(targetEl) || event.composedPath().includes(input); 920 if (!withinBoundaries) { 921 self.hideIncrementalResults(target); 922 } 923 } 924 }); 925 } 926 } 927 } 928 929 setupFilterEvents() { 930 let self = this; 931 let container = $("[data-target='search-facets']"); 932 933 const queryString = window.location.search;
934 const urlParams = new URLSearchParams(queryString); 935 const urlParamsSize = new Set(urlParams.keys()).size; 936 if (urlParamsSize > 1 && urlParams.has("filtered") && urlParams.get("filtered") === "true" && self.dispatcherType !== "search") { 937 gca.showPageLoader(); 938 939 gca.waitForElement("[data-action='search-filter']").then((elm) => { 940 let foundFilterParams = false; 941 const allButtons = document.querySelectorAll("[data-facet-group]"); 942 allButtons.forEach(function (el) { 943 const group = el.getAttribute("data-facet-group").toLowerCase(); 944 const type = el.getAttribute("data-facet-group-type"); 945 let value = null; 946 947 try { 948 value = el.getAttribute("data-facet-value").toLowerCase() 949 } 950 catch (e) { 951 console.log(e); 952 } 953 954 if (urlParams.has(group)) { 955 const groupValues = urlParams.getAll(group); 956 if (type === "range") { 957 const rawVal = groupValues[0]; 958 const gteVal = rawVal.split(":")[0]; 959 const lteVal = rawVal.split(":")[1]; 960 // const postfix = rawVal.split(":")[2]; Not in use here, but indicator for range type range (is in use for search filter url params to interpret range type) 961 962 const rangeInput = document.querySelector("[data-facet-group='" + group + "']"); 963 const gteInput = rangeInput.querySelector("input[data-facet-group-range-type='gte']"); 964 const lteInput = rangeInput.querySelector("input[data-facet-group-range-type='lte']"); 965 rangeInput.setAttribute("data-facet-checked", "true"); 966 967 gteInput.value = gteVal; 968 lteInput.value = lteVal; 969 foundFilterParams = true; 970 } 971 else { 972 if (self._containsParamValue(groupValues, value)) { 973 el.setAttribute("data-facet-checked", "true"); 974 foundFilterParams = true; 975 } 976 } 977 } 978 }) 979 980 if (foundFilterParams) { 981 let facetGroups = self.getFacetGroups(); 982 self.doFilter(facetGroups); 983 } 984 }); 985 } 986 987 container.on("click", "[data-action='search-filter']", function() { 988 let facetGroups = self.getFacetGroups() 989 if ($(this).data('facet-checked')) { 990 $(this).attr('data-facet-checked', false); 991 } 992 else { 993 $(this).attr('data-facet-checked', true); 994 } 995 if (self.dispatcherType === "search") { 996 self.doSearch(facetGroups); 997 } 998 else { 999 self.doFilter(facetGroups); 1000 } 1001 }); 1002 1003 container.on("click", "[data-action='do-filter-range']", function(e) { 1004 $(this).closest("[data-facet-group]").attr('data-facet-checked', true); 1005 if (self.dispatcherType === "search") { 1006 self.doSearch(self.getFacetGroups()); 1007 } 1008 else { 1009 self.doFilter(self.getFacetGroups()); 1010 } 1011 }); 1012 1013 container.on("click", "[data-action='do-filter']", function(e) { 1014 let facetGroups = self.getFacetGroups(); 1015 self.doFilter(facetGroups, true) 1016 }); 1017 1018 container.on("click", "[data-action='uncheck-filter']", function() { 1019 let group = $(this).attr('data-facet-group'); 1020 let value = $(this).attr('data-facet-value'); 1021 container.find("[data-action='search-filter'][data-facet-group='" + group + "'][data-facet-value='" + value + "']").click(); 1022 1023 if (value === "") { 1024 const rangeGroup = container.find("[data-facet-group='" + group + "'][data-facet-group-type='range']"); 1025 if (rangeGroup !== undefined) { 1026 rangeGroup.attr('data-facet-checked', false); 1027 if (self.dispatcherType === "search") { 1028 self.doSearch(self.getFacetGroups()); 1029 } 1030 else { 1031 self.doFilter(self.getFacetGroups()); 1032 } 1033 } 1034 } 1035 }); 1036 1037 container.on("click", "[data-action='offcanvas-search-filter']", function() { 1038 const el = $(this); 1039 if (el.attr('data-offcanvas-facet-checked') === "true") { 1040 el.find("i").addClass("fa-square").removeClass("fa-check-square"); 1041 el.attr('data-offcanvas-facet-checked', false); 1042 } 1043 else { 1044 el.attr('data-offcanvas-facet-checked', true); 1045 el.find("i").addClass("fa-check-square").removeClass("fa-square"); 1046 } 1047 let facetGroups = self.getOffcanvasFacetGroups(); 1048 self.updateOffCanvasFilter(facetGroups); 1049 }); 1050 1051 container.on("click", "[data-action='offcanvas-do-filter-range']", function(e) { 1052 $(this).closest("[data-offcanvas-facet-group]").attr('data-offcanvas-facet-checked', true); 1053 let facetGroups = self.getOffcanvasFacetGroups(); 1054 self.doFilter(facetGroups, true) 1055 }); 1056 1057 container.on("click", "[data-action='offcanvas-clear-filter']", function() { 1058 jQuery.each($("[data-offcanvas-facet-group]"), function(i, el) { 1059 el = $(el); 1060 el.find("i").addClass("fa-square").removeClass("fa-check-square"); 1061 el.attr('data-offcanvas-facet-checked', false); 1062 }); 1063 let facetGroups = self.getOffcanvasFacetGroups(); 1064 self.doFilter(facetGroups, true) 1065 }); 1066 1067 container.on("click", "[data-facet-group-range-type='gte'], [data-facet-group-range-type='lte'], [data-offcanvas-facet-group-range-type='gte'], [data-offcanvas-facet-group-range-type='lte'] ", function() { 1068 $(this).select(); 1069 }); 1070 1071 container.on("click", "[data-action='clear-all-filters']", function() { 1072 const { origin, pathname } = location; 1073 location.href = origin + pathname; 1074 }); 1075 } 1076 1077 setupTabEvent() { 1078 let self = this; 1079 $(document).on("click", "a[role=tab]", function() { 1080 self.tab = $(this).attr("href").replace("#tab-",""); 1081 gca.updateURLParameterWithoutReload("tab", self.tab); 1082 }); 1083 } 1084 1085 setupShowMoreEvent() { 1086 const self = this; 1087 $("[data-target='search-results']").on("click", "[data-action='showMoreProducts'][data-context='search']", function() { 1088 self.paginationEnabled = true; 1089 self.showMoreProducts(); 1090 $(this).hide(); 1091 }) 1092 } 1093 1094 setupScrollEvent() { 1095 let self = this; 1096 $(window).scroll(function() { 1097 let currentPosition = $(window).scrollTop(); 1098 self.position = currentPosition; 1099 1100 if (self.paginationEnabled && (currentPosition + $(window).height() >
1100 $(document).height() - $('section#footer').height())) { 1101 let productTab = $("#tab-products"); 1102 if (productTab.length === 0 || productTab.hasClass("active")) { 1103 self.showMoreProducts(); 1104 } 1105 } 1106 }); 1107 } 1108 1109 toggleBreadcrumbs(event) { 1110 let self = $(event); 1111 let icon = self.children(".toggle-breadcrumb"); 1112 1113 if (icon.hasClass('fa-angle-down')) { 1114 icon.removeClass('fa-angle-down').addClass('fa-angle-up') 1115 } else { 1116 icon.removeClass('fa-angle-up').addClass('fa-angle-down') 1117 } 1118 1119 self.closest(".category-paths").find('.hidden-breadcrumb').slideToggle(); 1120 } 1121 1122 filterFacetSearch(self) { 1123 const facets = $(self).closest("ul.dropdown-menu").children("li.facet-group-type-terms"); 1124 1125 facets.each(function () { 1126 let facet = $(this); 1127 facet.text().toLowerCase().includes(self.value.toLowerCase()) ? facet.show() : facet.hide(); 1128 }); 1129 } 1130 1131 filterFacetShowMore(event) { 1132 let self = $(event); 1133 let label = self.children("span"); 1134 let icon = self.children("i"); 1135 const facets = $(self).closest("ul.dropdown-menu").children("li.facet-group-type-terms"); 1136 1137 if (icon.hasClass('fa-angle-down')) { 1138 icon.removeClass('fa-angle-down').addClass('fa-angle-up') 1139 label.text(self.data("title-see-less")) 1140 } else { 1141 icon.removeClass('fa-angle-up').addClass('fa-angle-down') 1142 label.text(self.data("title-see-more")) 1143 } 1144 1145 facets.toggleClass("d-flex"); 1146 } 1147 1148 initRangeSlider() { 1149 gca.waitForElement("[data-facet-group-type='range']").then(() => gcaRangeSlider.init()); 1150 } 1151 1152} 1153 1154 1155class GcaRangeSlider { 1156 _observer; 1157 _ranges; 1158 constructor() { 1159 this._observer = null; 1160 this._ranges = new Map(); 1161 } 1162 1163 init() { 1164 // init any facets currently in the DOM 1165 this.initAll(); 1166 1167 // Watch the search-facet template for swaps (AJAX re-renders) 1168 this.observeFacetsRoot(); 1169 1170 // Hydrate again on BFCache/back/forward restores 1171 // window.addEventListener('pageshow', () => this.initAll()); 1172 } 1173 1174 observeFacetsRoot() { 1175 // Container/element that gets updated from filtering 1176 const root = document.querySelector('.search-facet-groups'); 1177 if (!root) return; 1178 1179 // Avoid duplicate observers 1180 if (this._observer) this._observer.disconnect(); 1181 1182 this._observer = new MutationObserver(muts => { 1183 for (const m of muts) { 1184 m.addedNodes.forEach(n => { 1185 if (n.nodeType !== 1) return; // elements only 1186 if (n.matches(".collapse-search-facets")) { 1187 this.initAll(n); // <- scans n for facets and calls initOne() 1188 } 1189 }); 1190 } 1191 }); 1192 1193 this._observer.observe(root, { childList: true, subtree: true }); 1194 } 1195 1196 // Find range-facet under a given DOM root - either whole page or under certain div 1197 initAll(ctx = document) { 1198 ctx.querySelectorAll("li[data-facet-group][data-facet-group-type='range']") 1199 .forEach(li => this.initOne(li)); 1200 } 1201 1202 initOne(li) { 1203 if (li.dataset.rangeInitialized === "1") return; 1204 li.dataset.rangeInitialized = "1"; 1205 1206 const minNum = li.querySelector('input[data-facet-group-range-type="gte"]'); 1207 const maxNum = li.querySelector('input[data-facet-group-range-type="lte"]'); 1208 const applyBtn = li.querySelector('[data-action="do-filter-range"]'); 1209 const root = li.querySelector('.vl-facet-range'); 1210 if (!minNum || !maxNum || !root) return; 1211 1212 const minR = root.querySelector('.vl-range-min'); 1213 const maxR = root.querySelector('.vl-range-max'); 1214 const progress = root.querySelector('.vl-progress'); 1215 1216 // ----- keep a stable absolute domain across re-renders ----- 1217 const facetKey = li.getAttribute('data-facet-group'); 1218 1219 // Set constant MIN and MAX price range for dots to be placed correctly in relation to this values 1220 let ABS_MIN = parseFloat(li.getAttribute('data-abs-min')); 1221 let ABS_MAX = parseFloat(li.getAttribute('data-abs-max')); 1222 let range = this._ranges.get(facetKey); 1223 if(range === undefined) { 1224 this._ranges.set(facetKey, new GcaRange(ABS_MIN, ABS_MAX)); 1225 } 1226 else { 1227 ABS_MIN = range.min; 1228 ABS_MAX = range.max; 1229 } 1230 1231 // FORCE the inputs to the absolute domain so thumbs paint correctly 1232 minR.min = maxR.min = ABS_MIN; 1233 minR.max = maxR.max = ABS_MAX; 1234 1235 // Maths used on multiple functions 1236 // In case of STEPS being implemented - default is 1 1237 // GAP to avoid overlapping dot overlapping can be changed here 1238 const MIN = ABS_MIN; 1239 const MAX = ABS_MAX; 1240 const STEP = parseFloat(minR.step || maxR.step || 1); 1241 const GAP = STEP; 1242 1243 const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi); 1244 const pct = v => ((v - MIN) / (MAX - MIN)) * 100; 1245 const roundToStep = v => Math.round(v / STEP) * STEP; 1246 1247 // Paint colored part of the slider 1248 const paint = () => { 1249 const l = pct(+minR.value); 1250 const r = pct(+maxR.value); 1251 const w = Math.max(0, r - l); 1252 progress.style.left = l + '%'; 1253 progress.style.width = w + '%'; // CSS must be left+width (no right) 1254 }; 1255 1256 const syncFromRanges = (e) => { 1257 const fromMin = e && e.target === minR; 1258 let lo = +minR.value, hi = +maxR.value; 1259 1260 lo = roundToStep(lo); 1261 hi = roundToStep(hi); 1262 1263 // Dot gap 1264 if (hi - lo < GAP) { 1265 if (fromMin) { 1266 lo = hi - GAP; 1267 } else { 1268 hi = lo + GAP; 1269 } 1270 } 1271 1272 // Avoid overlapping 1273 lo = clamp(lo, MIN, MAX - GAP); 1274 hi = clamp(hi, MIN + GAP, MAX); 1275 1276 minR.value = lo; maxR.value = hi; 1277 minNum.value = lo; maxNum.value = hi;
1278 paint(); 1279 }; 1280 1281 const syncFromNumbers = () => { 1282 let lo = parseFloat(minNum.value), hi = parseFloat(maxNum.value); 1283 if (isNaN(lo)) lo = MIN; if (isNaN(hi)) hi = MAX; 1284 1285 lo = roundToStep(lo); hi = roundToStep(hi); 1286 if (hi - lo < GAP) lo = hi - GAP; 1287 1288 lo = clamp(lo, MIN, MAX - GAP); 1289 hi = clamp(hi, MIN + GAP, MAX); 1290 1291 minR.value = lo; maxR.value = hi; 1292 minNum.value = lo; maxNum.value = hi; 1293 paint(); 1294 }; 1295 1296 const hydrateFromQuery = () => { 1297 const params = new URLSearchParams(location.search); 1298 const raw = params.get(facetKey); 1299 1300 if (!raw) { 1301 syncFromNumbers(); 1302 return; 1303 } 1304 1305 const [a,b] = String(raw).split(':'); 1306 const lo = parseFloat(a), hi = parseFloat(b); 1307 1308 if (Number.isFinite(lo) && Number.isFinite(hi)) { 1309 minNum.value = lo; maxNum.value = hi; 1310 } 1311 syncFromNumbers(); // normalize & paint 1312 }; 1313 1314 // events 1315 minR.addEventListener('input', e => syncFromRanges(e)); 1316 maxR.addEventListener('input', e => syncFromRanges(e)); 1317 minNum.addEventListener('change', syncFromNumbers); 1318 maxNum.addEventListener('change', syncFromNumbers); 1319 1320 if (applyBtn) { 1321 // before AJAX filter runs, push last thumb values into number inputs 1322 applyBtn.addEventListener('click', () => { 1323 minNum.value = minR.value; 1324 maxNum.value = maxR.value; 1325 }); 1326 } 1327 1328 // first paint 1329 hydrateFromQuery(); 1330 paint(); 1331 requestAnimationFrame(paint); // covers "inserted while collapsed" cases 1332 } 1333} 1334const gcaRangeSlider = new GcaRangeSlider(); 1335
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.