1/** 2 * Global PDP Wishlist Redirect Fix 3 * -------------------------------- 4 * Faengt Klicks auf Magento-Wishlist-Buttons ab, BEVOR mage/dataPost greift, 5 * und sendet stattdessen einen AJAX-POST. Bei Erfolg wird der Merklisten-Drawer 6 * per Custom-Event 'bs:wishlist:open' geoeffnet. 7 * 8 * Strategie: data-post wird vom Button entfernt und nach data-bs-wishlist-post 9 * umbenannt â mage/dataPost ist blind und kann keinen 302-Redirect mehr 10 * ausloesen. Mein Handler liest stattdessen das umbenannte Attribut. 11 * 12 * Wird global ueber default.xml geladen â laeuft auf jeder Seite, nicht 13 * abhaengig von Template-Bedingungen wie $exclusion oder bestimmten Bloecken. 14 */ 15(function () { 16 if (window.__bsWishlistPdpInit) { return; } 17 window.__bsWishlistPdpInit = true; 18 19 var TAG = '[bs-wishlist-fix]'; 20 21 var ajaxInFlight = false; 22 var disarmedTotal = 0; 23 var RESTORE_KEY_PREFIX = 'bs_pending_wishlist_restore_'; 24 var RESTORE_TTL_MS = 30 * 60 * 1000; 25 26 function hideGuestToast(el) { 27 if (!el) { 28 el = document.getElementById('bs-wishlist-guest-toast'); 29 } 30 if (!el) { return; } 31 if (el.__bsHideTimer) { clearTimeout(el.__bsHideTimer); el.__bsHideTimer = null; } 32 if (el.__bsRemoveTimer) { clearTimeout(el.__bsRemoveTimer); el.__bsRemoveTimer = null; } 33 el.classList.remove('bs-wishlist-guest-toast--show'); 34 el.__bsRemoveTimer = setTimeout(function () { 35 if (el.parentNode) { el.parentNode.removeChild(el); } 36 }, 350); 37 } 38 39 /* 40 * showGuestToast(msg, loginHandler) 41 * --------------------------------- 42 * Inline-Toast (rote Pill oben mittig) fuer Gaeste. Optional mit Button 43 * âJetzt anmelden", der PrepareLogin + Login-Redirect ausloest, ohne 44 * die Konfiguration auf der PDP zu verlieren. 45 */ 46 function showGuestToast(msg, loginHandler) { 47 var TEXT = msg || 'Bitte melden Sie sich an, um Artikel zur Merkliste hinzuzuf\u00fcgen.'; 48 var el = document.getElementById('bs-wishlist-guest-toast'); 49 if (el) { 50 if (el.__bsHideTimer) { clearTimeout(el.__bsHideTimer); el.__bsHideTimer = null; } 51 if (el.__bsRemoveTimer) { clearTimeout(el.__bsRemoveTimer); el.__bsRemoveTimer = null; } 52 } else { 53 el = document.createElement('div'); 54 el.id = 'bs-wishlist-guest-toast'; 55 el.className = 'bs-wishlist-guest-toast'; 56 el.setAttribute('role', 'status'); 57 el.setAttribute('aria-live', 'polite'); 58 document.body.appendChild(el); 59 } 60 61 el.innerHTML = ''; 62 var textEl = document.createElement('span'); 63 textEl.className = 'bs-wishlist-guest-toast__text'; 64 textEl.textContent = TEXT; 65 el.appendChild(textEl); 66 67 if (typeof loginHandler === 'function') { 68 var btn = document.createElement('button'); 69 btn.type = 'button'; 70 /* bs-kb-focus-invert: weisser Tastatur-Fokusring statt des 71 * shopweiten blauen Rings (customdesign.css Z. 489-501) â 72 * blau wirkt auf dem roten Toast-Hintergrund unpassend. */ 73 btn.className = 'bs-wishlist-guest-toast__login bs-kb-focus-invert'; 74 btn.textContent = 'Jetzt anmelden'; 75 btn.addEventListener('click', function (ev) { 76 ev.preventDefault(); 77 ev.stopPropagation(); 78 hideGuestToast(el); 79 loginHandler(); 80 }); 81 el.appendChild(btn); 82 } 83 84 var closeBtn = document.createElement('button'); 85 closeBtn.type = 'button'; 86 closeBtn.className = 'bs-wishlist-guest-toast__close bs-kb-focus-invert'; 87 closeBtn.setAttribute('aria-label', 'Schlie\u00dfen'); 88 /* SVG statt Unicode-×-Glyphe: Font-Baseline-Metriken variieren 89 * je Schrift/Browser und lassen ein Textzeichen im Kreis nie exakt 90 * mittig wirken. Ein Vektor-Pfad mit viewBox 0..24 ist dagegen per 91 * Konstruktion symmetrisch â gleiches Muster wie die uebrigen Icons 92 * in diesem Modul (Herz, Stift, Muelleimer, s. wishlist-sidebar.js). */ 93 var closeIcon = document.createElement('span'); 94 closeIcon.className = 'bs-wishlist-guest-toast__close-icon'; 95 closeIcon.setAttribute('aria-hidden', 'true'); 96 closeIcon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">' + 97 '<path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>' + 98 '</svg>'; 99 closeBtn.appendChild(closeIcon); 100 closeBtn.addEventListener('click', function (ev) { 101 ev.preventDefault(); 102 ev.stopPropagation(); 103 hideGuestToast(el); 104 }); 105 el.appendChild(closeBtn); 106 107 requestAnimationFrame(function () { 108 el.classList.add('bs-wishlist-guest-toast--show'); 109 }); 110 el.__bsHideTimer = setTimeout(function () { 111 hideGuestToast(el); 112 }, typeof loginHandler === 'function' ? 20000 : 8000); 113 } 114 115 function getProductIdFromPost(post) { 116 if (!post || !post.data) { return null; } 117 return post.data.product || post.data.product_id || null; 118 } 119 120 function serializeAkfSelections() { 121 try { 122 if (!window.akfState || !window.akfState.selections) { return null; } 123 var out = {};
124 Object.keys(window.akfState.selections).forEach(function (code) { 125 var bucket = window.akfState.selections[code]; 126 if (!bucket) { return; } 127 out[code] = { 128 type: bucket.type, 129 ids: Array.from(bucket.ids || []), 130 labels: Array.from(bucket.labels || []) 131 }; 132 }); 133 return Object.keys(out).length ? out : null; 134 } catch (e) { 135 return null; 136 } 137 } 138 139 function savePendingConfigToSessionStorage($, post) { 140 if (!window.sessionStorage) { return; } 141 var productId = getProductIdFromPost(post); 142 if (!productId) { return; } 143 var payload = { 144 referer: window.location.href, 145 formData: collectProductFormData($), 146 akfSelections: serializeAkfSelections(), 147 savedAt: Date.now() 148 }; 149 try { 150 sessionStorage.setItem(RESTORE_KEY_PREFIX + productId, JSON.stringify(payload)); 151 } catch (e) {} 152 } 153 154 function redirectGuestToLogin($, orig, post) { 155 post = enrichWishlistPostData($, orig, post); 156 if (!post) { return; } 157 savePendingConfigToSessionStorage($, post); 158 159 var payload = $.extend({}, post.data || {}); 160 var prepareUrl = (window.BASE_URL || '/') + 'bswishlist/wishlist/prepareLogin'; 161 162 $.ajax({ 163 url: prepareUrl, 164 method: 'POST', 165 data: payload, 166 dataType: 'json', 167 xhrFields: { withCredentials: true } 168 }).done(function (res) { 169 if (res && res.ok && res.loginUrl) { 170 window.location.href = res.loginUrl; 171 return; 172 } 173 window.location.href = (window.BASE_URL || '/') + 'customer/account/login'; 174 }).fail(function () { 175 window.location.href = (window.BASE_URL || '/') + 'customer/account/login'; 176 }); 177 } 178 179 function showGuestToastWithLogin($, orig, post) { 180 showGuestToast(null, function () { 181 redirectGuestToLogin($, orig, post); 182 }); 183 } 184 185 /* fireDrawerEvent: kleine Helper-Funktion um Custom-Events robust zu
186 * dispatchen (CustomEvent in IE11/Edge-Legacy nicht verfuegbar, daher 187 * Fallback ueber document.createEvent). */ 188 function fireDrawerEvent(name, detail) { 189 try { 190 document.dispatchEvent(new CustomEvent(name, { detail: detail || {} })); 191 return; 192 } catch (evtErr) { 193 try { 194 var evt = document.createEvent('Event'); 195 evt.initEvent(name, true, true); 196 if (detail) { evt.detail = detail; } 197 document.dispatchEvent(evt); 198 } catch (e) {} 199 } 200 } 201 202 /** @param {string} raw data-post JSON */ 203 function parseWishlistPost(raw) { 204 if (!raw) { return null; } 205 try { return JSON.parse(raw); } catch (e) { return null; } 206 } 207 208 /** Merkliste konfigurieren: updateItemOptions â Standard-Magento-POST (Redirect + Erfolgsmeldung). */ 209 function isWishlistUpdatePostRaw(raw) { 210 var post = parseWishlistPost(raw); 211 return !!(post && post.action && post.action.indexOf('updateItemOptions') !== -1); 212 } 213 214 function isWishlistUpdateElement(el) { 215 if (!el) { return false; } 216 return isWishlistUpdatePostRaw(el.getAttribute('data-bs-wishlist-post') || el.getAttribute('data-post')); 217 } 218 219 /* 220 * âIn die Merkliste verschieben" aus dem Warenkorb (wishlist/index/fromcart). 221 * --------------------------------------------------------------------------- 222 * Dieser AJAX-Interceptor ist fuer das HINZUFUEGEN von der PDP/PLP gebaut und 223 * erwartet eine JSON-Antwort (Magento\Wishlist\Controller\Index\Add isAjax-Branch). 224 * Der Warenkorb-Button (Magento_Wishlist .../cart/item/renderer/actions/move_to_wishlist.phtml) 225 * postet aber an wishlist/index/fromcart â dieser Controller liefert KEIN JSON, 226 * sondern verschiebt das Item serverseitig und redirected zurueck zum Warenkorb. 227 * Dadurch lief der Erfolgsfall ins Leere und der Code zeigte faelschlich eine rote 228 * Fehler-/âbitte einloggen"-Meldung, obwohl der Artikel laengst verschoben war. 229 * Loesung: fromcart wie âMerkliste aktualisieren" vom Intercept ausnehmen â natives 230 * Magento mage/dataPost uebernimmt (Verschieben + korrekte Erfolgsmeldung + Reload). 231 */ 232 function isFromCartPostRaw(raw) { 233 var post = parseWishlistPost(raw); 234 return !!(post && post.action && post.action.indexOf('fromcart') !== -1); 235 } 236 237 function isFromCartElement(el) { 238 if (!el) { return false; } 239 return isFromCartPostRaw(el.getAttribute('data-bs-wishlist-post') || el.getAttribute('data-post')); 240 } 241 242 function normSectionLoggedIn(raw) { 243 if (raw === true || raw === 1 || raw === '1' || raw === 'true') { return true; } 244 if (raw === false || raw === 0 || raw === '0' || raw === 'false') { return false; } 245 return null; 246 } 247 248 function readHeaderAuthHint() { 249 var el = document.getElementById('konto-wrapper'); 250 if (!el) { return null; } 251 var raw = el.getAttribute('data-badshop-header-auth'); 252 if (raw === '1') { return true; } 253 if (raw === '0') { return false; } 254 return null; 255 } 256 257 /** Bereits entwaffnete âAktualisierenâ-Buttons wieder fuer mage/dataPost freigeben. */ 258 function restoreWishlistUpdateButtons() { 259 var btns = document.querySelectorAll('.action.towishlist.updated, button.towishlist.updated, a.towishlist.updated'); 260 for (var i = 0; i < btns.length; i++) { 261 var b = btns[i]; 262 var raw = b.getAttribute('data-bs-wishlist-post'); 263 if (!raw || !isWishlistUpdatePostRaw(raw)) { continue; } 264 b.setAttribute('data-post', raw); 265 b.removeAttribute('data-bs-wishlist-post'); 266 if (window.jQuery) { try { window.jQuery(b).removeData('post'); } catch (e3) {} } 267 } 268 } 269 270 function disarmStandardButtons() { 271 /* Auch bereits entwaffnete Buttons nochmal anfassen (type="button" sicherstellen), 272 * weil Magento beim Re-Render z. B. den Button neu schreibt aber data-bs-wishlist-post 273 * stehen laesst. */ 274 var btns = document.querySelectorAll('.action.towishlist, [data-action="add-to-wishlist"]'); 275 var n = 0; 276 for (var i = 0; i < btns.length; i++) { 277 var b = btns[i]; 278 279 /* (1) Wenn das DOM-Element ein <button> ist und kein type-Attribute hat â type="button" setzen. 280 * Default ist submit, was in <form id="product_addtocart_form"> ein Top-Level-Form-POST 281 * an /wishlist/index/add/ ausloesen kann (Magento Server antwortet 302 â /wishlist/index/). */ 282 if (b.tagName === 'BUTTON') { 283 var t = b.getAttribute('type'); 284 if (!t || t.toLowerCase() === 'submit') { 285 b.setAttribute('type', 'button'); 286 } 287 } 288 289 /* (2) data-post abklemmen, damit mage/dataPost den Button nicht mehr sieht.
290 * Ausnahme: âAktualisieren" (updateItemOptions) UND âaus Warenkorb verschieben" 291 * (fromcart) bleiben armed â natives Magento dataPost erledigt diese korrekt. */ 292 var raw = b.getAttribute('data-post'); 293 if (raw && raw.indexOf('wishlist') !== -1 && !isWishlistUpdatePostRaw(raw) && !isFromCartPostRaw(raw)) { 294 b.setAttribute('data-bs-wishlist-post', raw); 295 b.removeAttribute('data-post'); 296 if (window.jQuery) { try { window.jQuery(b).removeData('post'); } catch (e3) {} } 297 n++; 298 } 299 } 300 if (n > 0) { 301 disarmedTotal += n; 302 } 303 return n; 304 } 305 306 /* Self-Heal fuer corruptes LocalStorage: 307 * jQuery Storage API (lib/web/jquery/jquery.storageapi.min.js) wirft beim 308 * .remove(section) eine TypeError âCannot convert undefined or null to object", 309 * wenn das parent-JSON (mage-cache-storage) einen NULL-Eintrag enthaelt oder 310 * das Storage selbst NULL ist. Magento's customer-data Code (Z. 116 + Z. 181) 311 * ruft das mehrfach pro Page auf â wir muessen das proaktiv aufraeumen. */ 312 try { 313 var keysToCheck = [ 314 'mage-cache-storage', 315 'mage-cache-storage-section-invalidation', 316 'mage-cache-sessid', 317 'product_data_storage', 318 'recently_viewed_product', 319 'recently_viewed_product_previous', 320 'recently_compared_product', 321 'recently_compared_product_previous' 322 ]; 323 /* Alle diese Keys sind JSON-Object-Namespace-Parents (Ausnahme: der reine 324 * String-Wert 'mage-cache-sessid'). Ein KORRUPTER Parent darf nicht per 325 * removeItem geloescht werden â sonst wirft der naechste 326 * $.initNamespaceStorage(...).localStorage.remove() erneut den 327 * JSON.parse(null)-Crash. Stattdessen als leeres Object re-initialisieren. */ 328 var objectNamespaceKeys = { 329 'mage-cache-storage': true, 330 'mage-cache-storage-section-invalidation': true, 331 'product_data_storage': true, 332 'recently_viewed_product': true, 333 'recently_viewed_product_previous': true, 334 'recently_compared_product': true, 335 'recently_compared_product_previous': true 336 }; 337 for (var k = 0; k < keysToCheck.length; k++) { 338 var key = keysToCheck[k]; 339 var v = window.localStorage && window.localStorage.getItem(key); 340 if (v === 'null' || v === 'undefined' || v === '') { 341 if (objectNamespaceKeys[key]) { 342 window.localStorage.setItem(key, '{}'); 343 } else { 344 window.localStorage.removeItem(key); 345 } 346 continue; 347 } 348 /* Pruefe ob der Storage-Wert ein parseable JSON-Object ist und ob 349 * irgendein innerer Section-Eintrag null ist (= das ist die Ursache 350 * des Object._remove-Errors). Wenn ja, korruptes Item entfernen. */ 351 if (v && (v.charAt(0) === '{' || v.charAt(0) === '[')) { 352 try { 353 var parsed = JSON.parse(v); 354 if (parsed && typeof parsed === 'object') { 355 var dirty = false; 356 for (var sec in parsed) { 357 if (parsed.hasOwnProperty(sec) && (parsed[sec] === null || parsed[sec] === undefined)) { 358 delete parsed[sec]; 359 dirty = true; 360 } 361 } 362 if (dirty) { 363 window.localStorage.setItem(key, JSON.stringify(parsed)); 364 } 365 } 366 } catch (parseErr) { 367 /* Unparseable JSON â Namespace-Parents als leeres Object 368 * re-initialisieren (nicht loeschen), sonst droht der 369 * JSON.parse(null)-Crash beim naechsten remove(). */ 370 if (objectNamespaceKeys[key]) { 371 window.localStorage.setItem(key, '{}'); 372 } else { 373 window.localStorage.removeItem(key); 374 } 375 } 376 } 377 } 378 } catch (storageErr) { 379 } 380 381 /* Monkey-Patch fuer jquery.storage's _remove: ist die Wurzel des âCannot convert 382 * undefined or null to object"-Fehlers in js.storage.js:201: 383 * delete tmp[a[i]]; // tmp ist null wenn JSON.parse('null') == null 384 * Wir wrappen jQuery's $.localStorage und $.sessionStorage so, dass die remove()- 385 * Methode null-Storage tolerant ist. Magento's customer-data.js Z. 116 und Z. 181 386 * loesen den Crash sonst alle paar Sekunden aus â 56+ Errors. */ 387 function patchJqueryStorageRemove($) { 388 try { 389 if (!$ || $.__bsStoragePatched) { return; } 390 $.__bsStoragePatched = true; 391 ['localStorage', 'sessionStorage', 'cookieStorage'].forEach(function (storageName) { 392 var storage = $[storageName]; 393 if (!storage || typeof storage.remove !== 'function') { return; } 394 var origRemove = storage.remove; 395 storage.remove = function () { 396 try { 397 return origRemove.apply(this, arguments); 398 } catch (storageRemoveErr) { 399 /* Bei dem TypeError â Parent-Namespace-Key als leeres JSON-Object 400 * RE-INITIALISIEREN (nicht loeschen!). removeItem() wuerde den 401 * Key entfernen und so beim naechsten remove() erneut den 402 * JSON.parse(null)-Crash ausloesen. */ 403 try { 404 var win = window; 405 if (storageName === 'localStorage' && win.localStorage) { 406 win.localStorage.setItem('mage-cache-storage', '{}'); 407 win.localStorage.setItem('mage-cache-storage-section-invalidation', '{}'); 408 } else if (storageName === 'sessionStorage' && win.sessionStorage) { 409 win.sessionStorage.setItem('mage-cache-storage', '{}'); 410 } 411 } catch (resetErr) {} 412 return false;
413 } 414 }; 415 }); 416 } catch (patchErr) { 417 } 418 } 419 /* requireJS-Pfad bevorzugen â kommt nach dem Storage-Plugin Boot */ 420 if (typeof require === 'function') { 421 try { 422 require(['jquery'], function ($) { patchJqueryStorageRemove($); }); 423 } catch (reqErr) {} 424 } 425 /* Fallback: window.jQuery falls bereits global verfuegbar */ 426 if (window.jQuery) { patchJqueryStorageRemove(window.jQuery); } 427 428 restoreWishlistUpdateButtons(); 429 disarmStandardButtons(); 430 document.addEventListener('DOMContentLoaded', function () { 431 restoreWishlistUpdateButtons(); 432 disarmStandardButtons(); 433 }); 434 435 try { 436 if (typeof MutationObserver === 'function') { 437 var mo = new MutationObserver(function (mutations) { 438 for (var i = 0; i < mutations.length; i++) { 439 if (mutations[i].addedNodes && mutations[i].addedNodes.length) { 440 disarmStandardButtons(); 441 return; 442 } 443 } 444 }); 445 var startObserve = function () { 446 if (document.body) { 447 mo.observe(document.body, { childList: true, subtree: true }); 448 } else { 449 setTimeout(startObserve, 50); 450 } 451 }; 452 startObserve(); 453 } 454 } catch (e2) {} 455 456 /** 457 * Liest Custom Options, Menge und weitere Konfiguration aus #product_addtocart_form. 458 * Ohne diese Felder speichert Magento in der Merkliste nur den Basispreis. 459 */ 460 function collectProductFormData($) { 461 var form = document.getElementById('product_addtocart_form'); 462 if (!form || !$) { return {}; } 463 464 var data = {}; 465 var fields = $(form).serializeArray(); 466 467 for (var i = 0; i < fields.length; i++) { 468 var name = fields[i].name; 469 var value = fields[i].value; 470 471 if (!name || name === 'form_key') { continue; } 472 473 if (Object.prototype.hasOwnProperty.call(data, name)) { 474 if (!Array.isArray(data[name])) { 475 data[name] = [data[name]]; 476 } 477 data[name].push(value); 478 } else { 479 data[name] = value; 480 } 481 } 482 483 return data; 484 } 485 486 /** 487 * Reichert post.data mit live Wishlist-Widget-Daten und Formularwerten an. 488 */ 489 function enrichWishlistPostData($, orig, post) { 490 if (!post) { return post; } 491 post.data = post.data || {}; 492 493 if ($ && orig) { 494 try { 495 var livePost = $(orig).data('post'); 496 if (livePost && livePost.data) { 497 post.data = $.extend({}, post.data, livePost.data); 498 } 499 } catch (liveErr) { 500 } 501 } 502 503 var formData = collectProductFormData($); 504 if (formData && Object.keys(formData).length) { 505 post.data = $.extend({}, post.data, formData); 506 } 507 508 return post; 509 } 510 511 function runAjax(orig) { 512 var raw = orig.getAttribute('data-bs-wishlist-post') || orig.getAttribute('data-post'); 513 if (!raw) { return; } 514 var post; 515 try { post = JSON.parse(raw); } catch (err) { return; } 516 if (!post || !post.action) { return; } 517 if (typeof require !== 'function') { return; } 518 519 ajaxInFlight = true; 520 521 require(['jquery', 'Magento_Customer/js/customer-data'], function ($, customerData) { 522 post = enrichWishlistPostData($, orig, post); 523 if (!post || !post.action) { 524 ajaxInFlight = false; 525 return; 526 } 527 /* 528 * Gast-Pre-Check (3-stufig, ohne Server-Roundtrip). 529 * -------------------------------------------------- 530 * Ziel: bei Gaesten den Toast SOFORT zeigen, ohne den Drawer 531 * optimistisch zu oeffnen und ohne die langsame Magento-Login- 532 * Redirect-Mechanik (typ. 5-10 s, weil Login-Page cold gerendert 533 * wird) abwarten zu muessen. 534 * 535 * Hintergrund: User-Feedback 21.05.2026 17:32 mit Screenshot: 536 * âwenn ich in einem privaten fenster ohne Kundenlogin einen 537 * Artikel zur Merkliste hinzufuegen moechte, dann oeffnet sich 538 * der Merklisten-Einschub und laedt erst 10 Sekunden oder so 539 * bevor er es rafft und die Meldung mit dem Einloggen kommt. 540 * die sollte aber sofort kommen, ohne dass sich der Einschub 541 * oeffnet" 542 * 543 * Ursache des alten Pre-Checks (vor diesem Patch): die Bedingung 544 * `Object.keys(custData).length > 0` blockierte den Skip-Pfad im 545 * privaten Fenster, weil dort beim ersten Klick die customer- 546 * Section noch nicht aus localStorage geladen war â custData = {} 547 * â length === 0 â fall-through zum AJAX â Drawer optimistisch 548 * geoeffnet â 5-10 s Wartezeit bis Server-Login-Redirect â 549 * erst dann Toast. 550 * 551 * Neue Logik (3 Indikatoren, OR-verknuepft fuer âGast"): 552 * (1) Knockout-Observable `customer` ohne `firstname` 553 * (auch bei leerem Object `{}`, weil Magento die Section auf 554 * Gast-Pages bewusst leer initialisiert). 555 * (2) localStorage[`mage-cache-storage`].customer ohne `firstname` 556 * (direkter Lese-Pfad, falls KO observable noch nicht 557 * subscribed wurde). 558 * (3) Cookie `X-Magento-Vary` fehlt 559 * (FPC-Vary-Cookie wird nur fuer Sessions mit Customer- 560 * spezifischem Cache-Vary gesetzt; Standard-Gaeste haben es 561 * nicht). Schwaecherer Indikator als (1)+(2) â daher nur als 562 * Tiebreaker, wenn (1) und (2) kein klares âeingeloggt" liefern. 563 * 564 * Bei Unsicherheit: AJAX laeuft + Drawer oeffnet sich wie bisher; 565 * der bestehende Response-Handler-Fallback faengt Login-Redirects 566 * (= xhr.responseURL enthaelt `/customer/account/login`) ab und 567 * zeigt dann den Toast â als zweite Sicherung. 568 */ 569 var isDefinitelyLoggedIn = false;
570 var isLikelyGuest = false; 571 572 try { 573 var custObj = customerData.get('customer'); 574 var custData = custObj ? custObj() : null; 575 if (custData && typeof custData === 'object') { 576 if (custData.firstname) { 577 isDefinitelyLoggedIn = true; 578 } 579 if (normSectionLoggedIn(custData.isLoggedIn) === true) { 580 isDefinitelyLoggedIn = true; 581 } 582 if (normSectionLoggedIn(custData.isLoggedIn) === false) { 583 isLikelyGuest = true; 584 } 585 } 586 } catch (koErr) { 587 } 588 589 if (!isDefinitelyLoggedIn) { 590 var headerAuth = readHeaderAuthHint(); 591 if (headerAuth === true) { 592 isDefinitelyLoggedIn = true; 593 } else if (headerAuth === false) { 594 isLikelyGuest = true; 595 } 596 } 597 598 if (!isDefinitelyLoggedIn && !isLikelyGuest) { 599 try { 600 var rawStorage = window.localStorage && window.localStorage.getItem('mage-cache-storage'); 601 if (rawStorage) { 602 var parsedStorage = JSON.parse(rawStorage); 603 if (parsedStorage && parsedStorage.customer && typeof parsedStorage.customer === 'object') { 604 if (parsedStorage.customer.firstname) { 605 isDefinitelyLoggedIn = true; 606 } 607 if (normSectionLoggedIn(parsedStorage.customer.isLoggedIn) === true) { 608 isDefinitelyLoggedIn = true; 609 } 610 if (normSectionLoggedIn(parsedStorage.customer.isLoggedIn) === false) { 611 isLikelyGuest = true; 612 } 613 } 614 } 615 } catch (lsErr) { 616 } 617 } 618 619 /* Merkliste konfigurieren ist nur fuer eingeloggte Kunden erreichbar. */ 620 if (!isDefinitelyLoggedIn && !isLikelyGuest && /\/wishlist\/index\/configure\b/i.test(window.location.pathname)) { 621 isDefinitelyLoggedIn = true; 622 } 623 624 if (!isDefinitelyLoggedIn && !isLikelyGuest) { 625 try { 626 var hasVaryCookie = document.cookie.split(';').some(function (c) { 627 return c.replace(/^\s+/, '').indexOf('X-Magento-Vary=') === 0; 628 }); 629 if (!hasVaryCookie) { 630 isLikelyGuest = true; 631 } 632 } catch (cookieErr) { 633 } 634 } 635 636 if (isLikelyGuest && !isDefinitelyLoggedIn) { 637 ajaxInFlight = false; 638 showGuestToastWithLogin($, orig, post); 639 return; 640 } 641 642 /* 643 * deferDrawerOpen: alle drei Pre-Check-Indikatoren waren 644 * unschluessig (weder definitiv eingeloggt noch definitiv Gast 645 * erkannt â typischerweise ein sehr frueher Klick, bevor die 646 * customer-data-Section vollstaendig synchronisiert ist). 647 * 648 * Root Cause des User-Feedbacks 07.07.2026 13:56 (Screenshot): 649 * "jetzt oeffnet er erst den merkliste einschub, stellt dann fest, 650 * dass man nicht eingeloggt ist und dann kommt die info" â in 651 * genau diesem unschluessigen Fall wurde der Drawer bisher SOFORT 652 * optimistisch geoeffnet (siehe Kommentar unten), und erst der 653 * AJAX-Response-Handler hat im Nachhinein per Login-Redirect- 654 * Erkennung den Gast-Fall aufgedeckt â Drawer flackert kurz auf, 655 * bevor der Toast kommt. 656 * 657 * Fix: im unschluessigen Fall den Drawer NICHT vorab oeffnen und 658 * KEIN optimistisches Item pushen. Stattdessen erst im 659 * AJAX-.done()-Handler entscheiden: Login-Redirect erkannt â nur 660 * Toast (Drawer bleibt geschlossen, kein Flackern); echter Erfolg 661 * â Drawer JETZT oeffnen (siehe dort). Fuer definitiv eingeloggte 662 * User (isDefinitelyLoggedIn) bleibt das schnelle optimistische 663 * Oeffnen unten unveraendert (dort ist das Risiko eines falschen 664 * Gast-Flackerns praktisch ausgeschlossen). 665 */ 666 var deferDrawerOpen = !isDefinitelyLoggedIn && !isLikelyGuest; 667 if (deferDrawerOpen) { 668 } 669 670 /* 671 * Drawer SOFORT oeffnen (pending) â parallel zum POST. 672 * Verhalten analog Warenkorb-Sidebar bei ajax:addToCart:sending: 673 * Drawer schiebt sich rein, zeigt zuerst âWird hinzugefuegt â¦"- 674 * Banner; der Success-Handler tauscht den Banner unten auf gruen 675 * und triggert den finalen customer-data-reload. 676 * 677 * Nur fuer den definitiv-eingeloggt-Fall â im unschluessigen Fall 678 * (deferDrawerOpen) siehe Kommentar oben: Drawer-Open verschiebt 679 * sich auf den AJAX-Response-Handler. 680 */ 681 if (!deferDrawerOpen) { 682 fireDrawerEvent('bs:wishlist:open', { pending: true, message: 'Wird hinzugef\u00fcgt \u2026' }); 683 } 684 685 /* 686 * Optimistisches Wishlist-Item sofort in die KO-Section pushen. 687 * Nur im schnellen (nicht-deferred) Pfad relevant â im
688 * unschluessigen Fall gibt es noch keinen offenen Drawer, der 689 * davon profitieren wuerde, und der echte Reload nach Erfolg 690 * liefert ohnehin die korrekten Daten. 691 * -------------------------------------------------------------- 692 * Hintergrund (User-Feedback 21.05.2026 17:41): 693 * âwarum dauert es sooo lange einen Artikel zur Merkliste 694 * hinzuzufuegen, die laed ja ewig. 10 Sekunden oder so." 695 * 696 * Bottleneck-Analyse: Magento's `Magento\Wishlist\CustomerData\ 697 * Wishlist::getItems()` (vendor/magento/module-wishlist/CustomerData 698 * /Wishlist.php Z. 124-137) ruft pro Section-Reload ein volles 699 * `$this->view->loadLayout()` auf und rendert fuer jedes Item das 700 * Pricing-HTML via `$this->block->getProductPriceHtml(..., 701 * 'wishlist_configured_price', ...)`. Bei 5+ Items + Cold-Cache 702 * (z. B. erster Drawer-Open in der Session): mehrere Sekunden 703 * Server-Render + RTT â der Drawer steht in der Zwischenzeit 704 * leer mit Spinner. 705 * 706 * Optimistic-Update-Strategie: noch BEVOR der AJAX-POST startet, 707 * bauen wir aus dem DOM der aktuellen PDP/PLP ein vorlaeufiges 708 * Item-Objekt und pushen es per `customerData.set('wishlist', ...)` 709 * direkt in die KO-Observable. wishlist-sidebar.js subscribed auf 710 * diese Observable und rendert die Liste sofort neu â d. h. das 711 * Item ist VOR dem Server-Response sichtbar. Der nachfolgende 712 * `customer-data.reload(['wishlist'], true)` im Success-Handler 713 * laeuft im Hintergrund und ersetzt das optimistic Item durch die 714 * echten Server-Daten (mit korrekter item_id, configure_url, 715 * Pricing-HTML etc.). 716 * 717 * Datenquelle aus DOM: 718 * - product_name: <h1 itemprop="name">-Textcontent (PDP) oder 719 * `.product-item-link`-Text (PLP-Card); SKU-Suffix `( PE-1260-... )` 720 * wird abgeschnitten, weil head_name.phtml den im <span> rendert 721 * - image: `.gallery-placeholder img` (PDP-Hauptbild) oder 722 * `.product-item img.product-image-photo` (PLP-Kachelbild) 723 * - product_url: PDP: location.href; PLP: closest `a.product-item-link` 724 * - product_price: outerHTML der `.pricebox`-Hauptpreis-Span 725 * - product_id: aus `post.data.product` oder `post.data.product_id` 726 * 727 * Robustheit: jeder Lookup in try/catch; wenn product_name oder 728 * product_id fehlen, wird KEIN optimistic-item gepusht (Sidebar 729 * zeigt dann wie bisher den Spinner bis zum Reload). 730 */ 731 function buildOptimisticWishlistItem(button, postData) { 732 try { 733 var productId = postData && (postData.product || postData.product_id); 734 if (!productId) { return null; } 735 736 var productName = ''; 737 var productUrl = window.location.href; 738 var imageSrc = ''; 739 var imageAlt = ''; 740 var priceHtml = ''; 741 742 var isPdp = !!document.querySelector('h1[itemprop="name"]'); 743 744 if (isPdp) { 745 var h1 = document.querySelector('h1[itemprop="name"]'); 746 if (h1) { 747 /* Klon, damit wir die SKU-<span> entfernen koennen 748 * ohne das echte DOM zu modifizieren. */ 749 var h1Clone = h1.cloneNode(true); 750 var skuSpan = h1Clone.querySelector('.pdp-sku'); 751 if (skuSpan) { skuSpan.parentNode.removeChild(skuSpan); } 752 productName = h1Clone.textContent.replace(/\s+/g, ' ').trim(); 753 } 754 var pdpImg = document.querySelector('.gallery-placeholder img, .fotorama__stage__frame img, .product.media img'); 755 if (pdpImg) { 756 imageSrc = pdpImg.getAttribute('src') || pdpImg.getAttribute('data-src') || ''; 757 imageAlt = pdpImg.getAttribute('alt') || productName; 758 } 759 var pdpPrice = document.querySelector('#options-pricebox .price.total_price'); 760 if (!pdpPrice) { 761 pdpPrice = document.querySelector('.pricebox [data-price-type="finalPrice"], .pricebox .price-final_price, .pricebox .price-box'); 762 } 763 if (pdpPrice) { 764 priceHtml = '<div class="price-box price-configured_price"><span class="price">' + 765 pdpPrice.innerHTML + '</span></div>'; 766 } 767 } else { 768 /* PLP/Search-List: das geklickte Element steckt in einer 769 * `.product-item`-Kachel. Wir holen Name/Image/Preis aus 770 * der gleichen Kachel statt aus der globalen Seite. */ 771 var card = button && button.closest ? button.closest('.product-item') : null; 772 if (card) { 773 var nameEl = card.querySelector('.product-item-link, .product-item-name, h2.product-name a'); 774 if (nameEl) { 775 productName = nameEl.textContent.replace(/\s+/g, ' ').trim(); 776 productUrl = nameEl.getAttribute('href') || productUrl; 777 } 778 var cardImg = card.querySelector('img.product-image-photo, .product-image-wrapper img, .product-image-container img'); 779 if (cardImg) { 780 imageSrc = cardImg.getAttribute('src') || cardImg.getAttribute('data-src') || ''; 781 imageAlt = cardImg.getAttribute('alt') || productName; 782 } 783 var cardPrice = card.querySelector('[data-price-type="finalPrice"], .price-box .price'); 784 if (cardPrice) { 785 priceHtml = cardPrice.outerHTML; 786 } 787 } 788 } 789 790 if (!productName) { return null; } 791 792 return { 793 /* Magento\Wishlist\CustomerData\Wishlist::getItemData() Format 794 * (siehe vendor/magento/module-wishlist/CustomerData/Wishlist.php Z. 145-165). 795 * `__optimistic: true` markiert das Item als vorlaeufig â bei 796 * spaeterem KO-Update aus dem reload wird es einfach ersetzt. */ 797 image: { 798 template: 'Magento_Catalog/product/image_with_borders', 799 src: imageSrc, 800 width: 80, 801 height: 80, 802 alt: imageAlt 803 }, 804 product_sku: '', 805 product_id: productId, 806 product_url: productUrl, 807 product_name: productName, 808 product_price: priceHtml, 809 product_is_saleable_and_visible: true, 810 product_has_required_options: false, 811 add_to_cart_params: '', 812 delete_item_params: '', 813 item_id: '__pending_' + productId, 814 configure_url: '', 815 __optimistic: true 816 }; 817 } catch (buildErr) { 818 return null; 819 } 820 } 821 822 if (!deferDrawerOpen) { 823 try { 824 var optimisticItem = buildOptimisticWishlistItem(orig, post.data); 825 if (optimisticItem) { 826 var wlObs = customerData.get('wishlist'); 827 var wlCurrent = wlObs ? wlObs() : null; 828 var wlData = (wlCurrent && typeof wlCurrent === 'object') ? $.extend(true, {}, wlCurrent) : { items: [], counter: '' }; 829 var wlItems = (wlData.items && Array.isArray(wlData.items)) ? wlData.items.slice() : []; 830 /* Newest-first: prepend, damit das frische Item oben in der 831 * Sidebar erscheint (gleiche Reihenfolge wie der spaetere 832 * Server-Reload via `setOrder('added_at')` DESC). */ 833 wlItems.unshift(optimisticItem); 834 wlData.items = wlItems; 835 /* counter ist im Magento-Standard ein __('%1 items')-String 836 * (z. B. â6 items"). Wir setzen ihn konservativ als reine 837 * Zahl â der Reload kommt eh und korrigiert den String, 838 * und der Header-Counter im Theme rendert robust beides. */ 839 var oldCounter = parseInt((wlData.counter || '').toString().replace(/\D/g, ''), 10); 840 wlData.counter = (isNaN(oldCounter) ? 1 : oldCounter + 1); 841 customerData.set('wishlist', wlData); 842 } else { 843 } 844 } catch (optimisticErr) { 845 } 846 } 847 848 /* form_key aus dem COOKIE lesen â das ist die einzige zuverlaessige Quelle. 849 * Magento\PageCache cached das HTML, das DOM-Input enthaelt dann einen veralteten 850 * form_key (vom ersten User, der die Seite gerendert hat). Magento\PageCache's 851 * form-key-provider.js liest beim Page-Load das Cookie und ueberschreibt das DOM â 852 * aber wenn der User klickt, bevor das passiert ist, ist DOM-Form-Key stale. 853 * Cookie-Wert match IMMER mit Server-Session-Form-Key. */ 854 function getFormKeyFromCookie() { 855 var cookies = document.cookie.split(';'); 856 for (var i = 0; i < cookies.length; i++) { 857 var c = cookies[i].replace(/^\s+/, ''); 858 if (c.indexOf('form_key=') === 0) { 859 return c.substring(9); // 'form_key='.length === 9 860 } 861 } 862 return ''; 863 } 864 var formKey = getFormKeyFromCookie(); 865 if (!formKey) { 866 /* Fallback: DOM-Input. Sollte fast nie noetig sein. */ 867 var fkInput = document.querySelector('input[name="form_key"]'); 868 if (fkInput && fkInput.value) { formKey = fkInput.value; } 869 else if (window.FORM_KEY) { formKey = window.FORM_KEY; } 870 } 871 872 /* isAjax + ajax als POST-data senden â Magento\Framework\App\Request\Http::isAjax() 873 * checkt zusaetzlich zu X-Requested-With auch getParam('ajax') und getParam('isAjax'). 874 * Dreifach-Sicherung gegen Server-302-Redirect-Pfad. */
875 var payload = $.extend({ form_key: formKey, ajax: 1, isAjax: 1 }, post.data || {}); 876 var actionUrl = post.action; 877 actionUrl += (actionUrl.indexOf('?') === -1 ? '?' : '&') + 'ajax=1'; 878 879 $.ajax({ 880 url: actionUrl, 881 method: 'POST', 882 data: payload, 883 headers: { 884 'X-Requested-With': 'XMLHttpRequest', 885 'Accept': 'application/json, text/javascript, */*; q=0.01' 886 }, 887 /* dataType: 'text' damit jQuery nicht bei HTML-Response (XHR-302-Follow) 888 * mit âparsererror" failed. Wir parsen die JSON-Antwort manuell und 889 * koennen so HTML-Antworten korrekt als Fehler erkennen. */ 890 dataType: 'text' 891 }).done(function (resText, textStatus, xhr) { 892 ajaxInFlight = false; 893 var ct = (xhr.getResponseHeader('content-type') || '').toLowerCase(); 894 var status = xhr.status; 895 /* xhr.responseURL ist die FINALE URL nach automatischem 302-Follow durch 896 * XHR â entscheidend, um zwischen Login-Redirect, Wishlist-Redirect und 897 * echter JSON-Response zu unterscheiden. */ 898 var finalUrl = xhr.responseURL || ''; 899 900 /* (A) Guest-Detection: zwei voneinander unabhaengige Indizien. 901 * 902 * (A1) xhr.responseURL: die FINALE URL nach automatischem 302-Follow 903 * durch XHR. Wenn diese auf /customer/account/login zeigt, ist klar: 904 * Magento\Wishlist\Controller\Index\Plugin Z. 110 hat den Request 905 * abgefangen und zur Login-Page redirected (customerSession-> 906 * authenticate() returnt false fuer Guests). 907 * 908 * (A2) HTML-Inhalts-Indicators: einige Browser/Setups setzen 909 * responseURL nicht zuverlaessig (z. B. Same-Origin-Redirects mit 910 * CORS, Tracking-Protection-Erweiterungen, oder Firefox/Safari- 911 * Edge-Cases). Daher pruefen wir zusaetzlich, ob das zurueck- 912 * gegebene HTML klar als Login-Page erkennbar ist â durch ein 913 * eindeutiges DOM-Fragment in der Magento-Standard-Login-Page. */ 914 function isLoginPageHtml(html) { 915 if (!html || typeof html !== 'string') { return false; } 916 /* Robuste Marker fuer Magento\Customer\Block\Form\Login. 917 * Mind. 1 von 3 muss matchen â Login-Page. */ 918 return ( 919 html.indexOf('id="login-form"') !== -1 || 920 html.indexOf('customer/account/loginPost') !== -1 || 921 /<meta[^>]+content=["']Kundenlogin["']/i.test(html) || 922 /<meta[^>]+content=["']Customer Login["']/i.test(html) 923 ); 924 } 925 var isLoginByUrl = finalUrl && finalUrl.indexOf('/customer/account/login') !== -1; 926 var isLoginByHtml = isLoginPageHtml(resText); 927 928 if (isLoginByUrl || isLoginByHtml) { 929 /* 930 * Fallback-Pfad: Pre-Check oben hat den Klick nicht abgefangen 931 * (typisch: erste Klick-Aktion direkt nach Page-Load, bevor 932 * customer-data im Storage liegt). Wir wissen aber JETZT 933 * sicher, dass der User ein Gast ist (Server hat zur Login- 934 * Page geantwortet). Statt window.location.href = loginUrl 935 * (= Page-Wechsel) zeigen wir den Inline-Toast. 936 * 937 * Drawer ggf. wieder schliessen: war optimistisch geoeffnet, 938 * macht jetzt keinen Sinn mehr. bs:wishlist:failed mit 939 * silent: true unterdrueckt die rote Fehler-Variante, 940 * closeDrawer: true triggert das tatsaechliche Schliessen. 941 */ 942 showGuestToastWithLogin($, orig, post); 943 fireDrawerEvent('bs:wishlist:failed', { silent: true, closeDrawer: true }); 944 return; 945 } 946 947 /* (B) JSON-Response â echter Erfolgs-Pfad. Magento\Wishlist\Controller 948 * \Index\Add Z. 179-190 (isAjax-Branch) liefert hier 949 * `{"backUrl": "..."}` zurueck. */ 950 var res = null; 951 if (ct.indexOf('json') !== -1 || (resText && resText.trim().charAt(0) === '{')) { 952 try { res = JSON.parse(resText); } catch (parseErr) { 953 } 954 } 955 956 if (res === null) { 957 /* 958 * (C) HTML-Response ohne Login-URL â form_key invalid, product
959 * invalid, oder anderer fruehzeitiger Return im Controller. Item 960 * wurde NICHT hinzugefuegt. Drawer-Banner auf Fehler tauschen 961 * + Drawer nach 3 s schliessen. Statt confirm() / window.reload() 962 * lassen wir den User selbst entscheiden, was er als naechstes 963 * versucht. 964 * 965 * deferDrawerOpen-Fall: Drawer wurde noch NIE geoeffnet 966 * (build() lief nie) â ohne vorheriges bs:wishlist:open 967 * wuerde showOk() in wishlist-sidebar.js stillschweigend 968 * nichts anzeigen (`if (!built || !$sb) return;`). Daher 969 * hier zuerst oeffnen, damit der Fehler-Banner tatsaechlich 970 * sichtbar wird. 971 */ 972 if (deferDrawerOpen) { 973 fireDrawerEvent('bs:wishlist:open', { pending: true, message: 'Wird hinzugef\u00fcgt \u2026' }); 974 } 975 fireDrawerEvent('bs:wishlist:failed', { message: 'Fehler beim Speichern, bitte Seite neu laden und erneut versuchen', closeDrawer: true }); 976 return; 977 } 978 979 980 if (res.backUrl && /\/customer\/account\/login/.test(res.backUrl)) { 981 window.location.href = res.backUrl; 982 return; 983 } 984 985 /* 986 * Echter Erfolg bestaetigt (Server hat NICHT zur Login-Seite 987 * umgeleitet). Im deferDrawerOpen-Fall (Pre-Check war 988 * unschluessig) wissen wir JETZT sicher, dass es kein Gast 989 * war â Drawer wird erst hier geoeffnet, kein verfrühtes 990 * Aufblitzen mehr fuer den eigentlichen Gast-Fall (der landet 991 * ja im Login-Redirect-Zweig oben und kommt nie hier an). 992 * Im schnellen (nicht-deferred) Pfad ist der Drawer seit dem 993 * Klick bereits OFFEN (pending) â dieser Aufruf ist dann ein 994 * no-op-artiges erneutes Oeffnen (openSb setzt lediglich 995 * open=true erneut, siehe wishlist-sidebar.js). 996 * 997 * Danach wie gewohnt: 998 * (1) customer-data invalidieren + EINMAL reloaden â das Update 999 * pusht via knockout-subscribe an wishlist-sidebar.js::render(), 1000 * der die neue Item-Liste rendert. 1001 * (2) bs:wishlist:added dispatchen â der Sidebar tauscht den 1002 * âWird hinzugefuegt â¦"-Banner gegen den gruenen Erfolgs- 1003 * Banner mit Auto-Hide 5500 ms. 1004 */ 1005 if (deferDrawerOpen) { 1006 fireDrawerEvent('bs:wishlist:open', { pending: true, message: 'Wird hinzugef\u00fcgt \u2026' }); 1007 } 1008 try { customerData.invalidate(['wishlist']); } catch (invErr) { 1009 } 1010 try { customerData.reload(['wishlist'], true); } catch (relErr) { 1011 } 1012 fireDrawerEvent('bs:wishlist:added', { message: 'Zur Merkliste hinzugef\u00fcgt' }); 1013 }).fail(function (xhr, textStatus, errorThrown) { 1014 ajaxInFlight = false; 1015 /* 1016 * Echter Hard-Fail: Netzwerkfehler, 5xx, etc. Im nicht- 1017 * deferred Fall wurde der Drawer optimistisch geoeffnet; 1018 * im deferDrawerOpen-Fall (s. o.) noch nie â hier zuerst 1019 * oeffnen, damit der Fehler-Banner sichtbar wird (sonst 1020 * no-op in wishlist-sidebar.js::showOk()). Danach 1021 * Fehler-Variante des Banners + automatisch Drawer-Close 1022 * nach 3 s (in wishlist-sidebar.js). 1023 */ 1024 if (deferDrawerOpen) { 1025 fireDrawerEvent('bs:wishlist:open', { pending: true, message: 'Wird hinzugef\u00fcgt \u2026' }); 1026 } 1027 fireDrawerEvent('bs:wishlist:failed', { message: 'Fehler beim Speichern, bitte erneut versuchen', closeDrawer: true }); 1028 }); 1029 }); 1030 } 1031 1032 /* Zusaetzlicher Sicherheits-Listener auf 'submit' (Capture-Phase auf document): 1033 * Wenn aus irgendeinem Grund das product_addtocart_form trotzdem submittet (z. B. 1034 * weil ein Wishlist-Button als type="submit" innerhalb des Forms angeklickt wurde), 1035 * dann fangen wir das Submit-Event ab und verhindern den Top-Level-POST. */ 1036 document.addEventListener('submit', function (e) { 1037 var form = e.target; 1038 if (!form || !form.tagName || form.tagName !== 'FORM') { return; } 1039 var action = form.getAttribute('action') || ''; 1040 /* Form-Submit an /wishlist/index/add/ â STOPPEN. 1041 * Form-Submit an /checkout/cart/add/ (Warenkorb) â durchlassen. 1042 * Andere Forms (Suche, Login etc.) â durchlassen. */ 1043 if (action.indexOf('/wishlist/index/add') === -1 && action.indexOf('/wishlist/index/configure') === -1) { 1044 return; 1045 } 1046 e.preventDefault(); 1047 e.stopImmediatePropagation(); 1048 if (e.stopPropagation) { e.stopPropagation(); } 1049 }, true); 1050 1051 document.addEventListener('click', function (e) { 1052 if (!e.target || !e.target.closest) { return; } 1053 1054 var orig = e.target.closest('[data-bs-wishlist-post]'); 1055 1056 if (!orig) { 1057 var cand = e.target.closest('button.action.towishlist, a.action.towishlist, [data-action="add-to-wis
1057hlist"]'); 1058 if (cand) { 1059 var dp = cand.getAttribute('data-post'); 1060 if (dp && dp.indexOf('wishlist') !== -1) { 1061 orig = cand; 1062 } 1063 } 1064 } 1065 1066 if (!orig && e.target.closest('#bs-pricebox-wishlist-btn')) { 1067 var updateBtn = document.querySelector( 1068 'button.action.towishlist.updated[data-post], a.action.towishlist.updated[data-post]' 1069 ); 1070 if (updateBtn && isWishlistUpdateElement(updateBtn)) { 1071 e.preventDefault(); 1072 e.stopImmediatePropagation(); 1073 if (e.stopPropagation) { e.stopPropagation(); } 1074 updateBtn.click(); 1075 return; 1076 } 1077 orig = document.querySelector('[data-bs-wishlist-post]') || 1078 document.querySelector('button.action.towishlist[data-post], a.action.towishlist[data-post], [data-action="add-to-wishlist"][data-post]'); 1079 if (!orig) { return; } 1080 } 1081 1082 if (!orig) { return; } 1083 1084 /* âMerkliste aktualisierenâ auf wishlist/index/configure: natives Magento dataPost. */ 1085 if (isWishlistUpdateElement(orig)) { 1086 return; 1087 } 1088 1089 /* âIn die Merkliste verschiebenâ aus dem Warenkorb (wishlist/index/fromcart): 1090 * natives Magento dataPost (kein JSON-Endpoint) â kein AJAX-Intercept. */ 1091 if (isFromCartElement(orig)) { 1092 return; 1093 } 1094 1095 e.preventDefault(); 1096 e.stopImmediatePropagation(); 1097 if (e.stopPropagation) { e.stopPropagation(); } 1098 1099 if (ajaxInFlight) { return; } 1100 runAjax(orig); 1101 }, true); 1102})();
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.