PageSourceSearch

https://www.coolwick.com/wp-content/plugins/bmart-wishlist/assets/js/frontend.js?ver=1.1.4

js coolwick.com collected 2026-10-02 18:55:18 UTC 14,955 bytes, 469 lines download raw bytes

1/**
2 * Bmart Wishlist frontend.
3 *
4 * Cache-safety contract: server HTML is identical for all visitors; every
5 * per-user mark (button state, header count, list contents) is applied here
6 * after a REST GET. Guests are identified by a cookie key that only this
7 * script writes — page responses never set cookies.
8 */
9( function () {
10	'use strict';
11
12	var cfg = window.bmartWishlist || null;
13	// canUse arrives as "1"/"" (wp_localize_script stringifies booleans) and is
14	// falsy only on a stale cached page from a mode transition — the server
15	// never localizes this config for users who can't use the feature. A
16	// pre-1.1.0 cached page lacks the key entirely; treat that as usable.
17	if ( ! cfg || ( 'canUse' in cfg && ! cfg.canUse ) ) {
18		return;
19	}
20
21	var STORAGE_KEY = 'bmartWishlist:v1';
22
23	var state = {
24		items: [],
25		count: 0,
26		loaded: false,
27	};
28
29	// ── Cookie helpers ──────────────────────────────────────────────
30
31	function getCookie( name ) {
32		var match = document.cookie.match( new RegExp( '(?:^|; )' + name + '=([a-f0-9]{32})' ) );
33		return match ? match[ 1 ] : '';
34	}
35
36	function setCookie( name, value ) {
37		document.cookie = name + '=' + value + '; path=/; max-age=' + 60 * 60 * 24 * 365 + '; SameSite=Lax' +
38			( 'https:' === window.location.protocol ? '; Secure' : '' );
39	}
40
41	function clearCookie( name ) {
42		document.cookie = name + '=; path=/; max-age=0; SameSite=Lax';
43	}
44
45	function guestKey() {
46		// Sent even when logged in: the server merges a stray pre-login guest
47		// list during GET /list (registration/stale-tab paths), which is what
48		// makes clearing the cookie afterwards safe.
49		return getCookie( cfg.cookieName );
50	}
51
52	// ── REST helper ─────────────────────────────────────────────────
53
54	function api( path, options ) {
55		options = options || {};
56		var headers = { 'Content-Type': 'application/json' };
57
58		if ( cfg.loggedIn && cfg.nonce ) {
59			headers[ 'X-WP-Nonce' ] = cfg.nonce;
60		}
61		var key = guestKey();
62		if ( key ) {
63			headers[ 'X-Bmart-Wishlist-Key' ] = key;
64		}
65
66		return window.fetch( cfg.restUrl + path, {
67			method: options.method || 'GET',
68			credentials: 'same-origin',
69			headers: headers,
70			body: options.body ? JSON.stringify( options.body ) : undefined,
71		} ).then( function ( response ) {
72			return response.json().catch( function () {
73				// Non-JSON body (edge error page, challenge) — friendly error,
74				// never a raw SyntaxError in an alert.
75				throw new Error( cfg.i18n.error );
76			} ).then( function ( data ) {
77				if ( ! response.ok ) {
78					var error = new Error( ( data && data.message ) || cfg.i18n.error );
79					error.code = data && data.code;
80					throw error;
81				}
82				return data;
83			} );
84		} );
85	}
86
87	// ── State / storage ─────────────────────────────────────────────
88
89	function hasIdentity() {
90		return cfg.loggedIn || !! guestKey();
91	}
92
93	function setState( items, count ) {
94		state.items = items || [];
95		state.count = 'number' === typeof count ? count : state.items.length;
96		state.loaded = true;
97
98		try {
99			window.sessionStorage.setItem( STORAGE_KEY, JSON.stringify( { items: state.items, count: state.count } ) );
100		} catch ( e ) {
101			// Storage unavailable (private mode etc.) — purely a paint optimization, ignore.
102		}
103
104		updateButtons();
105		updateCounts();
106		renderPage();
107	}
108
109	function paintFromStorage() {
110		try {
111			var cached = JSON.parse( window.sessionStorage.getItem( STORAGE_KEY ) );
112			if ( cached && cached.items ) {
113				state.items = cached.items;
114				state.count = cached.count || cached.items.length;
115				updateButtons();
116				updateCounts();
117			}
118		} catch ( e ) {
119			// Ignore bad cache.
120		}
121	}
122
123	function findItem( productId, variationId ) {
124		for ( var i = 0; i < state.items.length; i++ ) {
125			if ( Number( state.items[ i ].product_id ) === Number( productId ) &&
126				Number( state.items[ i ].variation_id ) === Number( variationId || 0 ) ) {
127				return state.items[ i ];
128			}
129		}
130		return null;
131	}
132
133	function findAnyByProduct( productId ) {
134		for ( var i = 0; i < state.items.length; i++ ) {
135			if ( Number( state.items[ i ].product_id ) === Number( productId ) ) {
136				return state.items[ i ];
137			}
138		}
139		return null;
140	}
141
142	function loadList() {
143		if ( ! hasIdentity() ) {
144			// Logged-in merge already consumed the guest cookie server-side.
145			setState( [], 0 );
146			return;
147		}
148
149		api( 'list' ).then( function ( data ) {
150			setState( data.items, data.count );
151			if ( cfg.loggedIn && getCookie( cfg.cookieName ) ) {
152				// The key rode along on this very GET and the server merged the
153				// guest list into the account before responding — safe to retire.
154				clearCookie( cfg.cookieName );
155			}
156		} ).catch( function () {
157			state.loaded = true;
158			renderPage();
159		} );
160	}
161
162	// ── Buttons ─────────────────────────────────────────────────────
163
164	function buttonContext( button ) {
165		var productId = Number( button.getAttribute( 'data-product-id' ) );
166		var isVariable = '1' === button.getAttribute( 'data-variable' );
167		var permalink = button.getAttribute( 'data-permalink' ) || '';
168		var variationId = 0;
169		var attributes = {};
170		var complete = true;
171
172		if ( isVariable ) {
173			var form = button.closest( 'form.variations_form' );
174			if ( form ) {
175				var input = form.querySelector( 'input[name="variation_id"]' );
176				variationId = input ? Number( input.value ) || 0 : 0;
177				var selects = form.querySelectorAll( 'select[name^="attribute_"]' );
178				for ( var i = 0; i < selects.length; i++ ) {
179					if ( ! selects[ i ].value ) {
180						complete = false;
181					}
182					attributes[ selects[ i ].name ] = selects[ i ].value;
183				}
184				if ( ! variationId ) {
185					complete = false;
186				}
187			} else {
188				complete = false;
189			}
190		}
191
192		return { productId: productId, isVariable: isVariable, isLoop: !! permalink, permalink: permalink, variationId: variationId, attributes: attributes, complete: complete };
193	}
194
195	function updateButtons() {
196		var buttons = document.querySelectorAll( '[data-bmart-wishlist-add]' );
197		for ( var i = 0; i < buttons.length; i++ ) {
198			var ctx = buttonContext( buttons[ i ] );
199			var added;
200			if ( ctx.isLoop && ctx.isVariable ) {
201				// Tile button for a variable product: filled when ANY saved
202				// entry matches the product; clicking navigates to the page.
203				added = !! findAnyByProduct( ctx.productId );
204			} else if ( ctx.isVariable && ! ctx.complete ) {
205				added = false;
206			} else {
207				added = !! findItem( ctx.productId, ctx.variationId );
208			}
209			buttons[ i ].classList.toggle( 'is-added', added );
210			buttons[ i ].setAttribute( 'aria-pressed', added ? 'true' : 'false' );
211			var label = buttons[ i ].querySelector( '.bmart-wishlist__label' );
212			if ( label ) {
213				label.textContent = added ? cfg.i18n.added : cfg.i18n.add;
214			}
215		}
216	}
217
218	function updateCounts() {
219		var badges = document.querySelectorAll( '[data-bmart-wishlist-count]' );
220		for ( var i = 0; i < badges.length; i++ ) {
221			badges[ i ].textContent = String( state.count );
222			badges[ i ].hidden = state.count < 1;
223		}
224	}
225
226	function onButtonClick( button ) {
227		if ( ! cfg.loggedIn && ! cfg.guestEnabled ) {
228			window.location.href = cfg.accountUrl;
229			return;
230		}
231
232		var ctx = buttonContext( button );
233
234		if ( ctx.isLoop && ctx.isVariable ) {
235			// The variation gets picked on the product page.
236			window.location.href = ctx.permalink;
237			return;
238		}
239
240		if ( ctx.isVariable && ! ctx.complete ) {
241			window.alert( cfg.i18n.selectOptions );
242			return;
243		}
244
245		var existing = findItem( ctx.productId, ctx.variationId );
246		button.disabled = true;
247
248		var request;
249		if ( existing ) {
250			request = api( 'items/' + existing.hash, { method: 'DELETE' } ).then( function ( data ) {
251				state.items = state.items.filter( function ( item ) {
252					return item.hash !== existing.hash;
253				} );
254				setState( state.items, data.count );
255			} );
256		} else {
257			request = api( 'items', {
258				method: 'POST',
259				body: {
260					product_id: ctx.productId,
261					variation_id: ctx.variationId,
262					variation: ctx.attributes,
263				},
264			} ).then( function ( data ) {
265				if ( data.guest_key ) {
266					setCookie( cfg.cookieName, data.guest_key );
267				}
268				if ( data.item ) {
269					state.items.unshift( data.item );
270					trackKlaviyo( data.item );
271				}
272				setState( state.items, data.count );
273			} );
274		}
275
276		request.catch( function ( error ) {
277			if ( 'bmart_wishlist_login_required' === error.code ) {
278				window.location.href = cfg.accountUrl;
279				return;
280			}
281			window.alert( error.message || cfg.i18n.error );
282		} ).then( function () {
283			button.disabled = false;
284		} );
285	}
286
287	// ── Klaviyo ─────────────────────────────────────────────────────
288
289	function trackKlaviyo( item ) {
290		if ( ! cfg.klaviyo || 'undefined' === typeof window.klaviyo || ! window.klaviyo.push ) {
291			return;
292		}
293		window.klaviyo.push( [ 'track', 'Added to Wishlist', {
294			ProductID: item.product_id,
295			VariationID: item.variation_id || null,
296			ProductName: item.name,
297			Price: item.price || null,
298			URL: item.permalink,
299			ImageURL: item.image,
300		} ] );
301	}
302
303	// ── Wishlist page ───────────────────────────────────────────────
304
305	function el( tag, className, text ) {
306		var node = document.createElement( tag );
307		if ( className ) {
308			node.className = className;
309		}
310		if ( text ) {
311			node.textContent = text;
312		}
313		return node;
314	}
315
316	function renderPage() {
317		var root = document.querySelector( '[data-bmart-wishlist-page]' );
318		if ( ! root || ! state.loaded ) {
319			return;
320		}
321
322		root.innerHTML = '';
323
324		if ( ! cfg.loggedIn && ! cfg.guestEnabled ) {
325			var login = el( 'p', 'bmart-wishlist__login-prompt' );
326			var loginLink = el( 'a', '', cfg.i18n.loginRequired );
327			loginLink.href = cfg.accountUrl;
328			login.appendChild( loginLink );
329			root.appendChild( login );
330			return;
331		}
332
333		if ( ! state.items.length ) {
334			var empty = el( 'div', 'bmart-wishlist__empty' );
335			empty.appendChild( el( 'p', '', cfg.i18n.empty ) );
336			var browse = el( 'a', 'button', cfg.i18n.browse );
337			browse.href = cfg.shopUrl;
338			empty.appendChild( browse );
339			root.appendChild( empty );
340			return;
341		}
342
343		var list = el( 'div', 'bmart-wishlist__list' );
344
345		state.items.forEach( function ( item ) {
346			var row = el( 'div', 'bmart-wishlist__row' + ( item.is_available ? '' : ' bmart-wishlist__row--unavailable' ) );
347
348			var media = el( 'a', 'bmart-wishlist__row-image' );
349			media.href = item.permalink || '#';
350			if ( item.image ) {
351				var img = document.createElement( 'img' );
352				img.src = item.image;
353				img.alt = item.name;
354				img.loading = 'lazy';
355				media.appendChild( img );
356			}
357			row.appendChild( media );
358
359			var info = el( 'div', 'bmart-wishlist__row-info' );
360			var name = el( 'a', 'bmart-wishlist__row-name', item.name );
361			name.href = item.permalink || '#';
362			info.appendChild( name );
363			if ( item.variation_label ) {
364				info.appendChild( el( 'div', 'bmart-wishlist__row-variation', item.variation_label ) );
365			}
366			var price = el( 'div', 'bmart-wishlist__row-price' );
367			price.innerHTML = item.price_html || ''; // Server-generated by our own REST endpoint.
368			info.appendChild( price );
369			if ( item.is_available && ! item.in_stock ) {
370				info.appendChild( el( 'div', 'bmart-wishlist__row-stock', cfg.i18n.outOfStock ) );
371			}
372			row.appendChild( info );
373
374			var actions = el( 'div', 'bmart-wishlist__row-actions' );
375			if ( item.is_available ) {
376				var cart = el( 'a', 'button bmart-wishlist__row-cart', item.purchasable && item.in_stock ? cfg.i18n.addToCart : cfg.i18n.viewProduct );
377				cart.href = item.purchasable && item.in_stock && item.add_to_cart_url ? item.add_to_cart_url : item.permalink;
378				actions.appendChild( cart );
379			}
380			var remove = el( 'button', 'bmart-wishlist__row-remove', '×' );
381			remove.type = 'button';
382			remove.setAttribute( 'aria-label', cfg.i18n.remove );
383			remove.setAttribute( 'data-bmart-wishlist-remove', item.hash );
384			actions.appendChild( remove );
385			row.appendChild( actions );
386
387			list.appendChild( row );
388		} );
389
390		root.appendChild( list );
391	}
392
393	function onRemoveClick( button ) {
394		var hash = button.getAttribute( 'data-bmart-wishlist-remove' );
395		button.disabled = true;
396
397		api( 'items/' + hash, { method: 'DELETE' } ).then( function ( data ) {
398			state.items = state.items.filter( function ( item ) {
399				return item.hash !== hash;
400			} );
401			setState( state.items, data.count );
402		} ).catch( function ( error ) {
403			button.disabled = false;
404			window.alert( error.message || cfg.i18n.error );
405		} );
406	}
407
408	// ── Wiring ──────────────────────────────────────────────────────
409
410	// Event delegation: survives re-renders of wishlist rows. It does NOT
411	// recover an interaction that fired before this script executed — the
412	// script must stay out of Perfmatters delay (enforced in PHP via the
413	// perfmatters_delay_js_exclusions filter; verify on staging regardless).
414	document.addEventListener( 'click', function ( event ) {
415		var addButton = event.target.closest( '[data-bmart-wishlist-add]' );
416		if ( addButton ) {
417			event.preventDefault();
418			onButtonClick( addButton );
419			return;
420		}
421		var removeButton = event.target.closest( '[data-bmart-wishlist-remove]' );
422		if ( removeButton ) {
423			event.preventDefault();
424			onRemoveClick( removeButton );
425		}
426	} );
427
428	// Woo's variation events are jQuery-triggered; refresh button state on change.
429	function bindVariationEvents() {
430		var $ = window.jQuery;
431		if ( ! $ ) {
432			return;
433		}
434		$( document.body ).on( 'found_variation reset_data hide_variation', 'form.variations_form', function () {
435			updateButtons();
436		} );
437	}
438
439	// Admins-only mode ships every wishlist element hidden (markup identical
440	// for all visitors — shared page/fragment caches); this script only loads
441	// for users allowed to see them, so reveal here.
442	function revealGated() {
443		var gated = document.querySelectorAll( '[data-bmart-wishlist-gated]' );
444		for ( var i = 0; i < gated.length; i++ ) {
445			gated[ i ].hidden = false;
446			gated[ i ].style.display = '';
447		}
448	}
449
450	function init() {
451		revealGated();
452		bindVariationEvents();
453		paintFromStorage();
454		loadList();
455	}
456
457	if ( 'loading' === document.readyState ) {
458		document.addEventListener( 'DOMContentLoaded', init );
459	} else {
460		init();
461	}
462
463	// Back/forward cache restore: state may be stale — refetch.
464	window.addEventListener( 'pageshow', function ( event ) {
465		if ( event.persisted ) {
466			loadList();
467		}
468	} );
469} )();

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.