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.