1/** 2 * Shopify Care Plan Module 3 * Handles all care plan-related functionality including plan selection and cart operations. 4 * Careplans are currently EN-US only. 5 */ 6 7window.ShopifyCareplan = (function () { 8 'use strict'; 9 10 let config = null; 11 let elements = null; 12 let carePlanData = null; 13 let allVariants = []; // Store all variants for re-filtering 14 let radioButtonsChecked = false; 15 let dynamicDOM = false; // Whether DOM was built dynamically (NID mode) 16 17 /** 18 * Update care plan options based on filter type 19 * 20 * @param {string} filterType - The filter type to apply. 21 * 22 * Example values: 23 * 24 * - 'all' - Show all variants 25 * - 'bundleplan' - Show only bundle plan variants 26 * - 'onlyplan' - Show only care plan variants 27 */ 28 const updateVariantOptions = (filterType = 'all') => { 29 if (!elements.carePlanSelect || !carePlanData) return; 30 31 // Reset dropdown 32 elements.carePlanSelect.innerHTML = '<option value="">' + Drupal.t('Select a plan:') + '</option>'; 33 34 // Store all variants if not already stored 35 if (allVariants.length === 0) { 36 allVariants = [...carePlanData.variants.edges]; 37 } 38 39 // Filter variants based on type 40 let variants = []; 41 42 if (filterType === 'all') { 43 variants = allVariants; 44 } else if (filterType === 'bundleplan') { 45 variants = allVariants.filter(edge => { 46 const variant = edge.node; 47 const metafield = variant.frontVariantType || variant.metafield; 48 return metafield && metafield.value.toLowerCase().includes('bundle'); 49 }); 50 } else if (filterType === 'onlyplan') { 51 variants = allVariants.filter(edge => { 52 const variant = edge.node; 53 const metafield = variant.frontVariantType || variant.metafield; 54 return metafield && metafield.value.toLowerCase().includes('care plan') && !metafield.value.toLowerCase().includes('bundle'); 55 }); 56 } else if (filterType === 'noplan') { 57 // Reset the dropdown and disable the CTA 58 if (elements.carePlanCTA) { 59 elements.carePlanCTA.disabled = true; 60 elements.carePlanCTA.classList.add('disabled'); 61 elements.carePlanCTA.textContent = Drupal.t('Select a plan'); 62 } 63 window.selectedCarePlanVariantId = null; 64 return; 65 } 66 67 // Add variants to dropdown
68 variants.forEach(edge => { 69 const variant = edge.node; 70 const option = document.createElement('option'); 71 option.value = variant.id; 72 73 // Format price 74 const price = parseFloat(variant.price.amount); 75 const formatter = new Intl.NumberFormat(config?.shopifyConfig?.lang || 'en-US', { 76 style: 'currency', 77 currency: 'USD', 78 minimumFractionDigits: 2 79 }); 80 const formattedPrice = formatter.format(price); 81 82 // Create option text 83 let optionText = variant.title; 84 if (variant.title === 'Default Title') { 85 optionText = Drupal.t('Care Plan'); 86 } 87 88 // Check if variant is available 89 if (!variant.availableForSale) { 90 option.disabled = true; 91 optionText += ' ' + Drupal.t('(Out of Stock)'); 92 } 93 94 option.textContent = `${optionText} - ${formattedPrice}`; 95 96 // Store metafield data as a data attribute for easy access 97 const effectiveMeta = variant.frontVariantType || variant.metafield; 98 if (effectiveMeta) { 99 option.dataset.metafieldValue = effectiveMeta.value; 100 } 101 102 elements.carePlanSelect.appendChild(option); 103 }); 104 105 // Disable Add to Cart button initially until a selection is made 106 if (elements.carePlanCTA && !window.selectedCarePlanVariantId) { 107 elements.carePlanCTA.disabled = true; 108 elements.carePlanCTA.classList.add('disabled'); 109 elements.carePlanCTA.textContent = Drupal.t('Select a plan'); 110 } 111 }; 112 113 /** 114 * Handle care plan selection change 115 * 116 * @param {Event} event - The event object. 117 * 118 * Example values: 119 * 120 * - 'all' - Show all variants 121 * - 'bundleplan' - Show only bundle plan variants 122 * - 'onlyplan' - Show only care plan variants 123 */ 124 const handleCarePlanSelection = (event) => { 125 if (!carePlanData) return; 126 127 const variantId = event.target.value; 128 129 if (!elements.carePlanCTA) return; 130 131 if (!variantId) { 132 // No variant selected, select "No thanks" radio 133 const noPlanRadio = document.getElementById('ecom_careplan_none'); 134 if (noPlanRadio) { 135 noPlanRadio.checked = true; 136 } 137 138 // Disable the CTA 139 elements.carePlanCTA.disabled = true; 140 elements.carePlanCTA.classList.add('disabled'); 141 elements.carePlanCTA.textContent = Drupal.t('Select a plan'); 142 window.selectedCarePlanVariantId = null; 143 144 // Show all variants 145 updateVariantOptions('all'); 146 return; 147 } 148 149 // Find selected variant 150 const selectedVariant = carePlanData.variants.edges.find( 151 edge => edge.node.id === variantId 152 )?.node; 153 154 if (!selectedVariant) { 155 elements.carePlanCTA.disabled = true; 156 elements.carePlanCTA.classList.add('disabled'); 157 elements.carePlanCTA.textContent = Drupal.t('Select a plan'); 158 window.selectedCarePlanVariantId = null; 159 return; 160 } 161 162 // Check if variant is available 163 if (!selectedVariant.availableForSale) { 164 elements.carePlanCTA.disabled = true; 165 elements.carePlanCTA.classList.add('disabled'); 166 elements.carePlanCTA.textContent = Drupal.t('Out of Stock'); 167 window.selectedCarePlanVariantId = null; 168 return; 169 } 170 171 // Update global variable for selected variant 172 window.selectedCarePlanVariantId = selectedVariant.id; 173 174 // Check metafield to determine which radio button to select 175 let selectedFilterType = 'onlyplan'; // Default to "Premium Care Plan only" 176 177 const effectiveVariantMeta = selectedVariant.frontVariantType || selectedVariant.metafield; 178 if (effectiveVariantMeta) { 179 const metafieldValue = effectiveVariantMeta.value.toLowerCase(); 180 181 if (metafieldValue.includes('bundle')) { 182 // Select "Product + Premium Care Plan" radio 183 const bundlePlanRadio = document.getElementById('ecom_careplan_bundle'); 184 if (bundlePlanRadio) { 185 bundlePlanRadio.checked = true; 186 selectedFilterType = 'bundleplan'; 187 } 188 } else if (metafieldValue.includes('care plan')) { 189 // Select "Premium Care Plan only" radio 190 const onlyPlanRadio = document.getElementById('ecom_careplan_only'); 191 if (onlyPlanRadio) { 192 onlyPlanRadio.checked = true; 193 selectedFilterType = 'onlyplan'; 194 } 195 } else { 196 // If no specific metafield match, default to "Premium Care Plan only" 197 const onlyPlanRadio = document.getElementById('ecom_careplan_only'); 198 if (onlyPlanRadio) { 199 onlyPlanRadio.checked = true; 200 selectedFilterType = 'onlyplan'; 201 } 202 } 203 } else { 204 // If no metafield, default to "Premium Care Plan only" 205 const onlyPlanRadio = document.getElementById('ecom_careplan_only'); 206 if (onlyPlanRadio) { 207 onlyPlanRadio.checked = true; 208 selectedFilterType = 'onlyplan'; 209 } 210 } 211 212 // Filter the dropdown options based on the selected radio button 213 // Save the current selection 214 const currentSelection = variantId; 215 216 // Update the dropdown options
217 updateVariantOptions(selectedFilterType); 218 219 // Restore the current selection 220 if (elements.carePlanSelect) { 221 elements.carePlanSelect.value = currentSelection; 222 223 // Make sure window.selectedCarePlanVariantId is set correctly 224 window.selectedCarePlanVariantId = currentSelection; 225 } 226 227 // Care plans are always considered available 228 // Enable the button regardless of availableForSale or quantityAvailable 229 elements.carePlanCTA.disabled = false; 230 elements.carePlanCTA.classList.remove('disabled'); 231 elements.carePlanCTA.textContent = Drupal.t('Add to Cart'); 232 }; 233 234 /** 235 * Helper to check and remove a radio button if no options match the filter 236 * @param {string} radioId - The ID of the radio button element 237 * @param {function(HTMLOptionElement): boolean} filterFn - The filter function to test options 238 */ 239 const removeRadioIfNoOptions = (radioId, filterFn) => { 240 const radio = document.getElementById(radioId); 241 const hasOption = Array.from(elements.carePlanSelect.options).some(filterFn); 242 if (!hasOption && radio) { 243 radio.parentElement.remove(); 244 } 245 }; 246 247 /** 248 * Check if radio buttons should be removed based on available options 249 * Only runs once during initialization 250 */ 251 const checkAndRemoveUnusedRadioButtons = () => { 252 if (!elements.carePlanSelect || radioButtonsChecked) return; 253 254 radioButtonsChecked = true; // Set flag to prevent multiple runs 255 256 // Remove "only plan" radio if no matching options 257 removeRadioIfNoOptions('ecom_careplan_only', option => { 258 const metafieldValue = option.dataset.metafieldValue; 259 return metafieldValue && 260 metafieldValue.toLowerCase().includes('care plan') && 261 !metafieldValue.toLowerCase().includes('bundle'); 262 }); 263 264 // Remove "bundle plan" radio if no matching options 265 removeRadioIfNoOptions('ecom_careplan_bundle', option => { 266 const metafieldValue = option.dataset.metafieldValue; 267 return metafieldValue && metafieldValue.toLowerCase().includes('bundle'); 268 }); 269 }; 270 271 /** 272 * Handle radio button selection 273 * 274 * @param {Event} event - The event object. 275 */ 276 const handleRadioSelection = (event) => { 277 const filterType = event.target.value; 278 279 // Update dropdown options based on filter type 280 if (filterType === 'noplan') { 281 // Show all options when "No thanks" is selected 282 updateVariantOptions('all'); 283 } else { 284 updateVariantOptions(filterType); 285 } 286 287 // Reset the selected variant 288 window.selectedCarePlanVariantId = null; 289 290 // Disable the CTA if "No thanks" is selected 291 if (filterType === 'noplan' && elements.carePlanCTA) { 292 elements.carePlanCTA.disabled = true; 293 elements.carePlanCTA.classList.add('disabled'); 294 elements.carePlanCTA.textContent = Drupal.t('Select a plan'); 295 } 296 }; 297 298 /** 299 * Build careplan DOM dynamically for NID mode (front.legacy_careplan). 300 * Mirrors the Twig-rendered markup from igcommerce-products-sub-category-storefront.html.twig 301 * so existing SCSS for #ecomm-careplan-info and #ecomm-careplan applies. 302 */ 303 const buildDynamicDOM = () => { 304 // Place careplan AFTER the ecommerce section (as a sibling), not inside #buybox-addons. 305 // This matches the Twig layout where careplan is a separate box below the main buybox. 306 const ecommSection = document.getElementById('fluke-product-display-ecommerce'); 307 const container = ecommSection?.parentNode; 308 if (!container) return false; 309 310 // Detect locale from URL path for the "Learn more" link 311 const pathParts = window.location.pathname.split('/').filter(Boolean); 312 const lang = pathParts[0] || 'en-us'; 313 314 const wrapper = document.createElement('div'); 315 wrapper.id = 'careplan-dynamic-wrapper'; 316 317 // --- Info section: heading + link + radio buttons --- 318 const info = document.createElement('span'); 319 info.id = 'ecomm-careplan-info'; 320 321 // Heading label with "Learn more" link 322 const headingLabel = document.createElement('label'); 323 headingLabel.textContent = Drupal.t('Add a Fluke Premium Care Plan'); 324 325 const learnMore = document.createElement('a'); 326 learnMore.target = '_blank';
327 learnMore.id = 'careplan-details'; 328 learnMore.className = 'fluke-careplan-information'; 329 learnMore.href = `/${lang}/support/customer-services/premium`; 330 learnMore.textContent = Drupal.t('Learn more about care plans'); 331 headingLabel.appendChild(learnMore); 332 333 info.appendChild(headingLabel); 334 335 // Radio buttons (wrapped in <div> to match Twig structure) 336 const radios = [ 337 { id: 'ecom_careplan_bundle', value: 'bundleplan', label: Drupal.t('Product + Premium Care Plan') }, 338 { id: 'ecom_careplan_only', value: 'onlyplan', label: Drupal.t('Premium Care Plan only (I already own the product)') }, 339 { id: 'ecom_careplan_none', value: 'noplan', label: Drupal.t('No thanks'), checked: true } 340 ]; 341 342 radios.forEach(r => { 343 const div = document.createElement('div'); 344 const input = document.createElement('input'); 345 input.type = 'radio'; 346 input.id = r.id; 347 input.name = 'ecom_careplan'; 348 input.value = r.value; 349 if (r.checked) input.checked = true; 350 div.appendChild(input); 351 div.appendChild(document.createTextNode(r.label)); 352 info.appendChild(div); 353 }); 354 355 wrapper.appendChild(info); 356 357 // --- Plan section: select dropdown + CTA button --- 358 // Matches Twig's shopify-buy-frame structure for SCSS targeting 359 const planSection = document.createElement('span'); 360 planSection.id = 'ecomm-careplan'; 361 planSection.className = 'shopify-buy-frame shopify-buy-frame--product'; 362 363 const productDiv = document.createElement('div'); 364 productDiv.className = 'has-image shopify-buy__layout-vertical shopify-buy__product'; 365 366 // Variant selectors wrapper 367 const variantSelectors = document.createElement('div'); 368 variantSelectors.className = 'shopify-buy__product__variant-selectors'; 369 const optionSelect = document.createElement('div'); 370 optionSelect.className = 'shopify-buy__option-select careplan-select-container'; 371 372 const selectWrapper = document.createElement('div'); 373 selectWrapper.className = 'shopify-buy__option-select-wrapper'; 374 375 const select = document.createElement('select'); 376 select.id = 'careplan-select'; 377 select.className = 'careplan-select shopify-buy__option-select__select'; 378 select.name = 'Size'; 379 380 selectWrapper.appendChild(select); 381 optionSelect.appendChild(selectWrapper); 382 variantSelectors.appendChild(optionSelect); 383 productDiv.appendChild(variantSelectors); 384 385 // CTA button wrapper 386 const btnWrapper = document.createElement('div'); 387 btnWrapper.className = 'shopify-buy__btn-wrapper'; 388 389 const cta = document.createElement('button'); 390 cta.id = 'careplan-select-cta'; 391 cta.className = 'careplan-atc'; 392 cta.disabled = true; 393 cta.textContent = Drupal.t('Select a plan'); 394 395 btnWrapper.appendChild(cta); 396 productDiv.appendChild(btnWrapper); 397 planSection.appendChild(productDiv); 398 wrapper.appendChild(planSection); 399 400 // Insert after the ecommerce section, not inside it 401 ecommSection.insertAdjacentElement('afterend', wrapper); 402 403 // Update element references 404 elements.carePlanSelect = select; 405 elements.carePlanCTA = cta; 406 407 return true; 408 }; 409 410 // Initialize event listeners 411 const initializeEventListeners = () => { 412 // Care plan radio buttons 413 const carePlanRadios = document.querySelectorAll('input[name="ecom_careplan"]'); 414 if (carePlanRadios.length) { 415 carePlanRadios.forEach(radio => { 416 radio.addEventListener('change', handleRadioSelection); 417 }); 418 419 // Set initial filter based on checked radio 420 const checkedRadio = document.querySelector('input[name="ecom_careplan"]:checked'); 421 if (checkedRadio) { 422 // If "No thanks" is checked, show all options 423 if (checkedRadio.value === 'noplan') { 424 updateVariantOptions('all'); 425 } else { 426 updateVariantOptions(checkedRadio.value); 427 } 428 } else { 429 // Default to 'all' if no radio is checked 430 updateVariantOptions('all'); 431 } 432 } else { 433 // No radio buttons found, just show all options 434 updateVariantOptions('all'); 435 } 436 437 // Care plan select change 438 if (elements.carePlanSelect) { 439 elements.carePlanSelect.addEventListener('change', (event) => { 440 handleCarePlanSelection(event); 441 }); 442 } 443 // Check and remove unused radio buttons after initial options are populated 444 checkAndRemoveUnusedRadioButtons(); 445 446 // Care plan Add to Cart button 447 if (elements.carePlanCTA) { 448 elements.carePlanCTA.addEventListener('click', async (event) => { 449 // Prevent event propagation to avoid document click handler 450 event.preventDefault(); 451 event.stopPropagation(); 452 453 if (!window.selectedCarePlanVariantId) { 454 alert(Drupal.t('Please select a care plan')); 455 return; 456 } 457 458 try { 459 await window.ShopifyCart.addToCart(window.selectedCarePlanVariantId, 1); 460 } catch (error) { 461 console.error('Error adding care plan to cart:', error); 462 } 463 }); 464 } 465 }; 466 467 return { 468 init: async function (shopifyConfig, optionalElements) { 469 config = shopifyConfig; 470 elements = optionalElements || {}; 471
472 try { 473 let careplanGid = null; 474 475 if (config.legacyCareplanGid) { 476 // NID mode: GID provided directly from front.legacy_careplan metafield 477 careplanGid = config.legacyCareplanGid; 478 dynamicDOM = true; 479 } else if (config.careplanID) { 480 // Legacy ID mode: numeric ID from Solr/drupalSettings 481 careplanGid = `gid://shopify/Product/${config.careplanID}`; 482 } else { 483 return; 484 } 485 486 // Build dynamic DOM if needed (NID mode â no Twig-rendered markup) 487 if (dynamicDOM) { 488 if (!buildDynamicDOM()) { 489 console.warn('Care plan: could not build dynamic DOM (missing #buybox-addons)'); 490 return; 491 } 492 } 493 494 // Load care plan data 495 const result = await window.ShopifyAPIClient.getProduct(careplanGid); 496 if (result.data?.product) { 497 carePlanData = result.data.product; 498 499 // Initialize care plan features 500 initializeEventListeners(); 501 502 // Ensure the CTA button starts with "Select a plan" text 503 if (elements.carePlanCTA) { 504 elements.carePlanCTA.textContent = Drupal.t('Select a plan'); 505 } 506 } else { 507 console.debug('Care plan data not found'); 508 // Clean up dynamic DOM if careplan product not found 509 if (dynamicDOM) { 510 const wrapper = document.getElementById('careplan-dynamic-wrapper'); 511 if (wrapper) wrapper.remove(); 512 } 513 } 514 } catch (error) { 515 console.error('Error initializing care plan:', error); 516 } 517 } 518 }; 519})();
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.