1var Comparator = { 2 // ============================================================ 3 // STATE / CONFIG 4 // ============================================================ 5 items: [], 6 itemsImg: [], 7 backendItems: [], 8 _popstateHandler: null, 9 _observer: null, 10 11 selectors: { 12 checkbox: 'input[type="checkbox"].comparator_checkbox', 13 comparator: '#comparator', 14 onlyOneItemsSelected: '[data-target="onlyOneItemsSelected"]', 15 pluralES: '[data-target="pluralES"]', 16 maxItemsSelected: '[data-target="maxItemsSelected"]', 17 compareButton: '[data-action="comparatItems"]', 18 quantityItemsSelected: '[data-target="quantityItemsSelected"]', 19 selectedImages: '[data-target="selectedImages"]', 20 productList: '[data-target="productList"]', 21 blockProductList: '[data-target="blockProductList"]', 22 sidebarActionsBar: '.skp-sidebar-actions__bar', 23 noArticles: '[data-no-articles]' 24 }, 25 26 // ============================================================ 27 // MAIN INIT / BIND EVENTS 28 // ============================================================ 29 main: function () { 30 Comparator.resetAndRefreshUI(); 31 Comparator.recuperateItems(); 32 33 $(document).on("change", ".comparator_checkbox", Comparator.controlChecks); 34 $(document).on("click", '[data-action="closeComparator"]', Comparator.closeComparator); 35 $(document).on("click", '[data-action="comparatItems"]', Comparator.goCompare); 36 $(document).on("click", '[data-action="removeItemProduct"]', Comparator.removeItemProduct); 37 38 Comparator.bindCheckboxObserver(); 39 }, 40 41 init: function (items) { 42 Comparator.backendItems = items || []; 43 Comparator.bindComparatorBackActions(); 44 Comparator.updateMeasureRows(Comparator.backendItems); 45 }, 46 47 // ============================================================ 48 // HELPERS / UTILS 49 // ============================================================ 50 normalizeId: function (id) { 51 return String(id) 52 .replace(/^comparator_/, "") 53 .replace(/_\d+$/, "") 54 .replace(/\./g, ""); 55 }, 56 57 getAllCheckboxes: function () { 58 return document.querySelectorAll(Comparator.selectors.checkbox); 59 }, 60 61 // ============================================================ 62 // UI REFRESH (CENTRALIZED) 63 // ============================================================ 64 refreshUI: function () { 65 Comparator.updateChatBotPosition(); 66 Comparator.updateComparatorUI(); 67 Comparator.updateMaxItemsClass(); 68 Comparator.getItemsCompare(); 69 }, 70 71 // ============================================================ 72 // BACK NAVIGATION / HISTORY STATE 73 // ============================================================ 74 bindComparatorBackActions: function () { 75 function handleBackAction() { 76 $('#editSelection').attr('loading', true); 77 $.busyLoadFull('show'); 78 79 const uniquePartnumbers = [ 80 ...new Set( 81 [...document.querySelectorAll('[data-partnumber]')] 82 .map(el => el.getAttribute('data-partnumber')) 83 .filter(Boolean) 84 ) 85 ]; 86 87 const filteredItems = Comparator.backendItems.filter(item => 88 uniquePartnumbers.includes(String(item.partnumber)) 89 ); 90 91 localStorage.setItem('itemsComparator', JSON.stringify(filteredItems)); 92 window.history.go(-1); 93 } 94 95 $('#editSelection') 96 .off('click.comparatorBack') 97 .on('click.comparatorBack', function (e) { 98 e.preventDefault(); 99 handleBackAction(); 100 }); 101 102 // BREADCRUMB LAST LINK (KEEP SELECTION) 103 const breadcrumbLinks = document.querySelectorAll('ol.breadcrumb a.breadcrumb-link'); 104 105 if (breadcrumbLinks.length) { 106 const lastLink = breadcrumbLinks[breadcrumbLinks.length - 1]; 107 108 $(lastLink) 109 .off('click.comparatorBreadcrumb') 110 .on('click.comparatorBreadcrumb', function (e) { 111 e.preventDefault(); 112 113 handleBackAction(); 114 }); 115 } 116 117 if (Comparator._popstateHandler) { 118 window.removeEventListener('popstate', Comparator._popstateHandler); 119 } 120 121 Comparator._popstateHandler = function () { 122 handleBackAction(); 123 }; 124 125 window.addEventListener('popstate', Comparator._popstateHandler); 126 127 if (!history.state || !history.state.comparator) { 128 history.pushState({ comparator: true }, '', window.location.href); 129 } 130 }, 131 132 // ============================================================ 133 // RECOVER ITEMS FROM LOCALSTORAGE 134 // ============================================================ 135 recuperateItems: function () { 136 const items = JSON.parse(localStorage.getItem('itemsComparator')) || []; 137 localStorage.removeItem('itemsComparator'); 138 139 if (!items.length) return; 140 141 const ids = items.map(item => Comparator.normalizeId(item.partnumber)); 142 const pending = new Set(ids); 143 144 let attempts = 0; 145 const maxAttempts = 20; 146 147 const interval = setInterval(() => { 148 pending.forEach(id => { 149 const checkbox = document.getElementById('comparator_' + id); 150 151 if (checkbox) { 152 if (!Comparator.items.includes(id)) { 153 checkbox.checked = true; 154 checkbox.dispatchEvent(new Event('change', { bubbles: true })); 155 } 156 pending.delete(id); 157 } 158 }); 159 160 attempts++; 161 162 if (!pending.size || attempts >= maxAttempts) { 163 clearInterval(interval); 164 } 165 }, 300); 166 }, 167 168 // ============================================================ 169 // UI: CLOSE / RESET 170 // ============================================================ 171 closeComparator: function (e) { 172 e.preventDefault(); 173 174 Comparator.items = []; 175 Comparator.itemsImg = []; 176
177 Comparator.getAllCheckboxes().forEach(cb => { 178 cb.checked = false; 179 cb.disabled = false; 180 }); 181 182 Comparator.refreshUI(); 183 }, 184 185 resetAndRefreshUI: function () { 186 setTimeout(() => { 187 Comparator.getAllCheckboxes().forEach(cb => { 188 cb.checked = false; 189 cb.disabled = false; 190 }); 191 192 Comparator.items = []; 193 Comparator.itemsImg = []; 194 195 Comparator.refreshUI(); 196 }, 100); 197 }, 198 199 // ============================================================ 200 // UI: CHATBOT POSITION / SIDEBAR 201 // ============================================================ 202 updateChatBotPosition: function () { 203 const personalWebShopper = document.querySelector('personal-web-shopper'); 204 const chatBot = personalWebShopper?.shadowRoot?.getElementById('app'); 205 const sidebarActionsBar = document.querySelector(Comparator.selectors.sidebarActionsBar); 206 207 const hasItems = Comparator.items.length >= 1; 208 209 if (chatBot) { 210 chatBot.style.bottom = hasItems ? '5rem' : '0'; 211 } 212 213 if (sidebarActionsBar) { 214 sidebarActionsBar.classList.toggle('backToTopComparator', hasItems); 215 } 216 }, 217 218 // ============================================================ 219 // UI: MAIN COMPARATOR BLOCK 220 // ============================================================ 221 updateComparatorUI: function () { 222 const comparator = document.querySelector(Comparator.selectors.comparator); 223 const onlyOneItemsSelected = document.querySelector(Comparator.selectors.onlyOneItemsSelected); 224 const pluralES = document.querySelector(Comparator.selectors.pluralES); 225 const compareButton = document.querySelector(Comparator.selectors.compareButton); 226 227 const count = Comparator.items.length; 228 229 if (comparator) { 230 comparator.toggleAttribute("hidden", count < 1); 231 } 232 233 if (onlyOneItemsSelected) { 234 onlyOneItemsSelected.toggleAttribute("hidden", count !== 1); 235 } 236 237 if (pluralES) { 238 pluralES.toggleAttribute("hidden", count < 2); 239 } 240 241 if (compareButton) { 242 compareButton.toggleAttribute("disabled", count < 2); 243 } 244 }, 245 246 // ============================================================ 247 // UI: SELECTED IMAGES 248 // ============================================================ 249 updateSelectedImages: function (imgItems) { 250 const wrapper = document.querySelector(Comparator.selectors.selectedImages); 251 if (!wrapper) return; 252 253 wrapper.innerHTML = ""; 254 255 if (!imgItems?.length) return; 256 257 wrapper.innerHTML = imgItems 258 .map(img => ` 259 <wc-aspect-ratio ratio="square"> 260 <img 261 src="${img}" 262 alt="" 263 loading="lazy" 264 class="selectedItemImagen" 265 onerror="this.src='/webroot/img/icons/comparator-no-image.png';" 266 > 267 </wc-aspect-ratio> 268 `) 269 .join(""); 270 }, 271 272 // ============================================================ 273 // UI: PRODUCT LIST CLASS (items-X) 274 // ============================================================ 275 updateMaxItemsClass: function () { 276 const productList = document.querySelector(Comparator.selectors.productList); 277 const blockProductList = document.querySelector(Comparator.selectors.blockProductList); 278 279 if (!productList || !blockProductList) return; 280 281 const itemsCount = blockProductList.querySelectorAll('[data-name="article"]').length; 282 283 [...productList.classList] 284 .filter(cls => cls.startsWith("items-")) 285 .forEach(cls => productList.classList.remove(cls)); 286 287 productList.classList.add('items-' + itemsCount); 288 }, 289 290 // ============================================================ 291 // UI: UPDATE ONLY MEASURE ROWS (GROUPED BY MEASURE NAME) 292 // ============================================================ 293 updateMeasureRows: function (items) { 294 if (!items?.length) return; 295 296 const measureRowsContainer = document.querySelector('[data-target="measureRows"]'); 297 const isSpi = measureRowsContainer?.dataset?.id === "1"; 298 if (!measureRowsContainer) return; 299 300 // --- 1. Obtener lista única de nombres de medidas (case insensitive) --- 301 const measureNames = []; // guardamos siempre en lowercase 302
303 items.forEach(item => { 304 (item.measure || []).forEach(measure => { 305 const name = measure?.name?.trim(); 306 if (!name) return; 307 308 const normalizedName = name.toLowerCase(); 309 310 if (!measureNames.includes(normalizedName)) { 311 measureNames.push(normalizedName); 312 } 313 }); 314 }); 315 316 // --- 2. Construir HTML agrupado por nombre --- 317 let html = ''; 318 319 measureNames.forEach(measureName => { 320 const formattedName = measureName.charAt(0).toUpperCase() + measureName.slice(1); 321 322 html += `<tr data-target="rowMeasure">`; 323 324 items.forEach(item => { 325 const measureObj = (item.measure || []).find(measure => { 326 const currentName = measure?.name?.trim(); 327 return currentName && currentName.toLowerCase() === measureName; 328 }); 329 330 if (measureObj) { 331 html += ` 332 <td data-partnumber="${item.partnumber}"> 333 <p class="font-weight-bold">${formattedName}</p> 334 <p>${isSpi ? measureObj.metric : measureObj.imperial}</p> 335 </td> 336 `; 337 } else { 338 html += ` 339 <td data-partnumber="${item.partnumber}"> 340 - 341 </td> 342 `; 343 } 344 }); 345 346 html += `</tr>`; 347 }); 348 349 // --- 3. Reemplazar SOLO las filas de medidas --- 350 measureRowsContainer.innerHTML = html; 351 }, 352 353 // ============================================================ 354 // OBSERVER: KEEP CHECKBOXES SYNC WHEN DOM RERENDERS 355 // ============================================================ 356 bindCheckboxObserver: function () { 357 /** 358 * Este observer existe para cuando el listado de productos se re-renderiza (filtros, búsqueda, paginación...). 359 * En esos casos los checkbox se recrean en el DOM y pierden su estado visual. 360 * 361 * La fuente de verdad NO es el DOM, es Comparator.items. 362 * Por tanto: 363 * - re-marcamos los checkbox que estén en Comparator.items 364 * - re-aplicamos la regla de disabled según Comparator.items.length 365 */ 366 let rafScheduled = false; 367 368 const sync = () => { 369 const checkboxes = Comparator.getAllCheckboxes(); 370 const selected = new Set(Comparator.items); 371 const maxReached = Comparator.items.length >= 5; 372 373 checkboxes.forEach(cb => { 374 const id = Comparator.normalizeId(cb.id); 375 cb.checked = selected.has(id); 376 cb.disabled = maxReached ? !selected.has(id) : false; 377 }); 378 }; 379 380 if (Comparator._observer) { 381 Comparator._observer.disconnect(); 382 } 383 384 Comparator._observer = new MutationObserver(() => { 385 if (rafScheduled) return; 386 387 rafScheduled = true; 388 requestAnimationFrame(() => { 389 rafScheduled = false; 390 sync(); 391 }); 392 }); 393 394 Comparator._observer.observe(document.body, { 395 childList: true, 396 subtree: true 397 }); 398 399 sync(); 400 }, 401 402 // ============================================================ 403 // CHECKBOX CONTROL / MAX 5 ITEMS RULE 404 // ============================================================ 405 controlChecks: function (e) { 406 const itemId = Comparator.normalizeId(this.id); 407 408 if (this.checked) { 409 if (Comparator.items.length < 5 && !Comparator.items.includes(itemId)) { 410 Comparator.items.push(itemId); 411 Comparator.itemsImg.push(Comparator.getCheckedImage(e)); 412 } else { 413 this.checked = false; 414 } 415 } else { 416 const index = Comparator.items.indexOf(itemId); 417 if (index > -1) { 418 Comparator.items.splice(index, 1); 419 Comparator.itemsImg.splice(index, 1); 420 } 421 } 422 423 const maxReached = Comparator.items.length >= 5; 424
425 Comparator.getAllCheckboxes().forEach(cb => { 426 if (!cb.checked) cb.disabled = maxReached; 427 else cb.disabled = false; 428 }); 429 430 Comparator.refreshUI(); 431 }, 432 433 // ============================================================ 434 // REMOVE ENERGY BLOCK IF EMPTY 435 // ============================================================ 436 removeEnergyEfficiencyBlock: function () { 437 const wrappers = document.querySelectorAll('.energyWrappers'); 438 439 const shouldRemove = [...wrappers].every(wrapper => { 440 const energy = wrapper.dataset.energy; 441 return !energy || energy.trim() === ''; 442 }); 443 444 if (shouldRemove) { 445 document.querySelector('.blockEnergy')?.remove(); 446 } 447 }, 448 449 // ============================================================ 450 // ACTION: REMOVE PRODUCT FROM COMPARATOR PAGE 451 // ============================================================ 452 removeItemProduct: function (e) { 453 e.preventDefault(); 454 455 const partnumber = this.getAttribute("data-partnumber"); 456 if (!partnumber) return; 457 458 // --- 1. Eliminar del DOM todos los elementos con ese partnumber --- 459 document.querySelectorAll(`[data-partnumber="${partnumber}"]`).forEach(el => el.remove()); 460 461 // --- 2. Eliminar también del array backendItems --- 462 Comparator.backendItems = Comparator.backendItems.filter(item => 463 String(item.partnumber) !== String(partnumber) 464 ); 465 466 // --- 3. Re-crear SOLO las filas de medidas con los items restantes --- 467 Comparator.updateMeasureRows(Comparator.backendItems); 468 469 // --- 4. Si ya no queda ningún producto, ocultar bloques --- 470 const remaining = document.querySelectorAll('[data-partnumber]'); 471 472 if (!remaining.length) { 473 document.querySelectorAll('[data-articles]').forEach(el => el.remove()); 474 document.querySelector(Comparator.selectors.noArticles)?.removeAttribute("hidden"); 475 } 476 477 Comparator.refreshUI(); 478 Comparator.removeEnergyEfficiencyBlock(); 479 }, 480 481 482 // ============================================================ 483 // GET IMAGE FROM CHECKED ITEM 484 // ============================================================ 485 getCheckedImage: function (event) { 486 const target = event.originalEvent?.target; 487 if (!target) return null; 488 489 const id = Comparator.normalizeId(target.id); 490 const link = document.querySelector(`a[data-item="${id}"]`); 491 if (!link) return null; 492 493 const image = link.querySelector('.default_image') || link.querySelector('.card-img-top'); 494 if (!image) return null; 495 496 return image.dataset.bg || image.dataset.src || null; 497 }, 498 499 // ============================================================ 500 // UI: ITEMS COUNTER + UPDATE SELECTED IMAGES 501 // ============================================================ 502 getItemsCompare: function () { 503 const counter = document.querySelector(Comparator.selectors.quantityItemsSelected); 504 505 if (counter) { 506 counter.textContent = Comparator.items.length + "/5"; 507 } 508 509 Comparator.updateComparatorUI(); 510 Comparator.updateSelectedImages(Comparator.itemsImg); 511 }, 512 513 // ============================================================ 514 // ACTION: GO TO COMPARATOR PAGE 515 // ============================================================ 516 goCompare: function (e) { 517 e.preventDefault(); 518 519 const button = this; 520 521 $(button).attr('loading', true); 522 $.busyLoadFull('show'); 523 524 $.ajax({ 525 async: true, 526 type: "POST", 527 data: { items: Comparator.items }, 528 dataType: "json", 529 url: validateURL.URL() + '/comparator/goCompare', 530 success: function (result) { 531 if (!result.success) return; 532 533 window.location.href = result.url_redirect; 534 }, 535 }); 536 }, 537}; 538 539// ============================================================ 540// DOCUMENT READY 541// ============================================================ 542$(document).ready(function () { 543 Comparator.main(); 544});
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.