PageSourceSearch

https://iheartdogs.com/wp-content/plugins/buy-once-or-subscribe-fo…/assets/js/blocks-cart-plans.js?ver=5.1.2

js iheartdogs.com collected 2026-09-24 15:27:06 UTC 7,158 bytes, 254 lines download raw bytes

1/* global bos4wBlocksCartPlans */
2( function () {
3	'use strict';
4
5	const { __ } = window.wp?.i18n || {};
6	const { subscribe, select } = window.wp?.data || {};
7
8	const updateNamespace = bos4wBlocksCartPlans?.updateNamespace || 'bos4w-cart-plans';
9
10	const sanitizeKeyForClass = ( key ) =>
11		String( key || '' )
12			.toLowerCase()
13			.replace( /[^a-z0-9_-]+/g, '-' )
14			.slice( 0, 80 );
15
16	const hasPlans = ( cartItem ) =>
17		!! cartItem?.extensions?.bos4w?.plans?.length;
18
19	const getCartState = () => {
20		// Prefer the official Blocks cart store.
21		const cartStore = window.wc?.wcBlocksData?.cartStore;
22		if ( cartStore ) {
23			const cart = window.wp?.data?.select( cartStore )?.getCartData?.();
24			if ( cart && Array.isArray( cart.items ) ) {
25				return { items: cart.items };
26			}
27		}
28
29		// Fallback for other environments (older APIs).
30		const legacy = window.wp?.data?.select?.( 'wc/store/cart' );
31		if ( legacy?.getCartItems ) {
32			return { items: legacy.getCartItems() || [] };
33		}
34
35		return { items: [] };
36	};
37
38	const getCartRows = () => {
39		// Common Cart Block row selectors (varies by theme/Blocks version).
40		const selectors = [
41			'.wc-block-cart-items__row',
42			'.wc-block-cart-items__item',
43			'.wc-block-cart-item',
44		];
45		for ( const sel of selectors ) {
46			const nodes = document.querySelectorAll( sel );
47			if ( nodes && nodes.length ) return Array.from( nodes );
48		}
49		return [];
50	};
51
52	const getRowCartItemKey = ( row ) => {
53		if ( ! row ) return '';
54
55		// Direct data attributes (if present in some Blocks/theme versions).
56		const direct =
57			row.getAttribute?.( 'data-cart-item-key' ) ||
58			row.getAttribute?.( 'data-wc-cart-item-key' ) ||
59			row.getAttribute?.( 'data-wc-cart-item' ) ||
60			'';
61		if ( direct ) return String( direct );
62
63		// Try remove link/button URLs like ?remove_item=abc123...
64		const removeEl =
65			row.querySelector?.( 'a[href*="remove_item="]' ) ||
66			row.querySelector?.( 'a[href*="remove-item="]' ) ||
67			row.querySelector?.( 'button[name="remove_item"]' ) ||
68			null;
69
70		const href = removeEl?.getAttribute?.( 'href' ) || '';
71		if ( href ) {
72			try {
73				const url = new URL( href, window.location.href );
74				const key =
75					url.searchParams.get( 'remove_item' ) ||
76					url.searchParams.get( 'remove-item' ) ||
77					'';
78				if ( key ) return key;
79			} catch ( e ) {
80				// ignore
81			}
82
83			// Fallback regex when URL parsing fails.
84			const m = href.match( /(?:remove_item|remove-item)=([^&]+)/ );
85			if ( m?.[ 1 ] ) return decodeURIComponent( m[ 1 ] );
86		}
87
88		// As a last resort, look for any element embedding the key.
89		const any = row.querySelector?.( '[data-cart-item-key],[data-wc-cart-item-key]' );
90		if ( any ) {
91			return (
92				any.getAttribute( 'data-cart-item-key' ) ||
93				any.getAttribute( 'data-wc-cart-item-key' ) ||
94				''
95			);
96		}
97
98		return '';
99	};
100
101	const getExtensionCartUpdate = () => window.wc?.blocksCheckout?.extensionCartUpdate;
102	const getProcessErrorResponse = () => window.wc?.wcBlocksData?.processErrorResponse;
103
104	const decodeHtml = ( input ) => {
105		const html = String( input ?? '' );
106		if ( ! html ) return '';
107		const el = document.createElement( 'textarea' );
108		el.innerHTML = html;
109		return el.value;
110	};
111
112	const buildRadios = ( cartItem ) => {
113		const ext = cartItem.extensions?.bos4w;
114		if ( ! ext?.plans?.length ) return null;
115
116		const wrapper = document.createElement( 'div' );
117		wrapper.className = 'bos4w-blocks-cart-plans';
118
119		const title = document.createElement( 'div' );
120		title.className = 'bos4w-blocks-cart-plans__title';
121		title.textContent = __( 'Choose a plan', 'bos4w' ) || 'Choose a plan';
122		wrapper.appendChild( title );
123
124		const selected = ext.selected || null;
125		const name = `bos4w_blocks_plan_${ sanitizeKeyForClass( cartItem.key ) }`;
126
127		ext.plans.forEach( ( plan ) => {
128			const label = document.createElement( 'label' );
129			label.className = 'bos4w-blocks-cart-plans__option';
130
131			const input = document.createElement( 'input' );
132			input.type = 'radio';
133			input.name = name;
134			input.value = plan.value;
135			input.checked = selected ? plan.value === selected : ! plan.is_subscription;
136
137			input.addEventListener( 'change', () => {
138				const extensionCartUpdate = getExtensionCartUpdate();
139				if ( typeof extensionCartUpdate !== 'function' ) return;
140
141				input.disabled = true;
142				wrapper.setAttribute( 'aria-busy', 'true' );
143
144				extensionCartUpdate( {
145					namespace: updateNamespace,
146					data: {
147						cart_item_key: cartItem.key,
148						selected_plan: plan.value,
149					},
150				} ).catch( ( err ) => {
151					const processErrorResponse = getProcessErrorResponse();
152					if ( typeof processErrorResponse === 'function' ) {
153						processErrorResponse( err );
154					}
155					// eslint-disable-next-line no-console
156					console.warn( 'BOS4W cart plan update failed', err );
157				} ).finally( () => {
158					input.disabled = false;
159					wrapper.removeAttribute( 'aria-busy' );
160					queueInject();
161				} );
162			} );
163
164			const text = document.createElement( 'span' );
165			text.className = 'bos4w-blocks-cart-plans__label';
166			text.textContent = decodeHtml( plan.label );
167
168			label.appendChild( input );
169			label.appendChild( text );
170			wrapper.appendChild( label );
171		} );
172
173		return wrapper;
174	};
175
176	const inject = () => {
177		const { items } = getCartState();
178		if ( ! items.length ) return;
179
180		const rows = getCartRows();
181		if ( ! rows.length ) return;
182
183		// Prefer mapping by cart item key to avoid index mismatches.
184		const rowsByKey = new Map();
185		rows.forEach( ( row ) => {
186			const key = getRowCartItemKey( row );
187			if ( key ) rowsByKey.set( key, row );
188		} );
189
190		items.forEach( ( item, index ) => {
191			if ( ! hasPlans( item ) ) return;
192
193			const row = rowsByKey.get( item.key ) || rows[ index ];
194			if ( ! row ) return;
195
196			// Prefer injecting into the price container for a classic-cart-like UX.
197			const priceContainer =
198				row.querySelector( '.wc-block-cart-item__prices' ) ||
199				row.querySelector( '.wc-block-components-product-price' )?.parentElement ||
200				row;
201
202			if ( ! priceContainer ) return;
203
204			// Avoid duplicates.
205			if ( priceContainer.querySelector( '.bos4w-blocks-cart-plans' ) ) return;
206
207			const radios = buildRadios( item );
208			if ( ! radios ) return;
209
210			priceContainer.appendChild( radios );
211		} );
212	};
213
214	// Observe DOM changes to re-inject after Blocks rerenders.
215	let injectQueued = false;
216	const queueInject = () => {
217		if ( injectQueued ) return;
218		injectQueued = true;
219		setTimeout( () => {
220			injectQueued = false;
221			inject();
222		}, 50 );
223	};
224
225	try {
226		const mo = new MutationObserver( queueInject );
227		mo.observe( document.body, { childList: true, subtree: true } );
228	} catch ( e ) {
229		// ignore
230	}
231
232	// Re-inject when cart changes.
233	let lastSignature = '';
234	subscribe?.( () => {
235		const { items } = getCartState();
236		const sig = items
237			.map( ( it ) => `${ it.key || '' }:${ it.extensions?.bos4w?.selected || '' }` )
238			.join( '|' );
239
240		if ( sig !== lastSignature ) {
241			lastSignature = sig;
242			// Defer to allow Blocks to re-render DOM first.
243			queueInject();
244		}
245	} );
246
247	// Initial inject.
248	if ( document.readyState === 'loading' ) {
249		document.addEventListener( 'DOMContentLoaded', inject );
250	} else {
251		inject();
252	}
253} )();
254

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.