PageSourceSearch

https://www.badshop.de/static/version1790862157/frontend/Badshop/d…/js/wishlist-redirect-fix.js

js badshop.de collected 2026-10-04 06:52:44 UTC 44,042 bytes, 1,102 lines download raw bytes

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.