1function fillPACode(form) { 2 var hiddenInput = form.querySelector('input[name="pa_code"]'); 3 var textInput = form.querySelector('.pacode-input'); 4 if ( hiddenInput && textInput ) { 5 hiddenInput.value = 'PA' + textInput.value.replace(/\D/g, ''); 6 } 7} 8 9function checkForResults(form) { 10 var hiddenInput = form.querySelector('input[name="pa_code"]'); 11 if ( ! hiddenInput ) { 12 return; 13 } 14 fillPACode(form); 15 var pa_code = hiddenInput.value; 16 17 jQuery.ajax({ 18 method: "GET", 19 url: `${window.DP_REST_API.v1}/dpapi/proxy/dentist/v1/dentalsearch/dentist/${pa_code}`, 20 success: async function (data) { 21 if ( data.dentists.length === 0 ) { 22 doFormError(form); 23 } else { 24 if ( data.dentists && data.dentists[0].dentistAddress && data.dentists[0].dentistAddress.zip ) { 25 var pa_code_zip = data.dentists[0].dentistAddress.zip; 26 if ( typeof window.overrideLocation === 'function' ) { 27 await window.overrideLocation(pa_code_zip); 28 } 29 } 30 form.submit(); 31 } 32 }, 33 error: function (data) { 34 console.error("pa code search Error: " + pa_code, data); 35 }, 36 }); 37} 38 39function doFormError(form) { 40 var textInput = form.querySelector('.pacode-input'); 41 if ( textInput ) { 42 textInput.classList.add('error'); 43 } 44 form.querySelectorAll('.error-message').forEach(function (el) { 45 el.classList.remove('hidden'); 46 }); 47} 48 49function clearFormError(form) { 50 var textInput = form.querySelector('.pacode-input'); 51 if ( textInput ) { 52 textInput.classList.remove('error'); 53 } 54 form.querySelectorAll('.error-message').forEach(function (el) { 55 el.classList.add('hidden'); 56 }); 57} 58 59function disableFormSubmit(form, disable) { 60 var submitBtn = form.querySelector('[type="submit"]'); 61 if ( submitBtn ) { 62 submitBtn.disabled = disable; 63 } 64} 65 66function isFormSubmitDisabled(form) { 67 var submitBtn = form.querySelector('[type="submit"]'); 68 return submitBtn ? submitBtn.disabled : true; 69} 70 71document.addEventListener("DOMContentLoaded", function () { 72 var forms = document.querySelectorAll('.dentist-pa-code-form'); 73 74 forms.forEach(function (form) { 75 var input = form.querySelector('.pacode-input'); 76 if ( ! input ) { 77 return; 78 } 79 80 input.addEventListener('input', function (event) { 81 input.value = event.target.value.replace(/\D/g, ''); 82 clearFormError(form); 83 disableFormSubmit(form, input.value.length < 1); 84 }); 85 86 input.addEventListener('paste', function (event) { 87 event.preventDefault(); 88 input.value = (event.clipboardData || window.clipboardData).getData("text").replace(/\D/g, ''); 89 clearFormError(form); 90 disableFormSubmit(form, input.value.length < 1); 91 }); 92 }); 93 94 document.body.addEventListener("submit", function (event) { 95 var form = event.target; 96 if ( ! form || ! form.classList || ! form.classList.contains('dentist-pa-code-form') ) { 97 return; 98 } 99 100 event.preventDefault(); 101 if ( ! isFormSubmitDisabled(form) ) { 102 fillPACode(form); 103 checkForResults(form); 104 } 105 }); 106}); 107; 108 109document.addEventListener('DOMContentLoaded', function () { 110 111 ///////////////////////////////////////////////////////////////////////////////////////////// 112 // Define elements. 113 114 let searchFormBlock = null; 115 116 let overlay = null; 117 118 let closeButton = null; 119 120 let searchForm = null; 121 122 let filterByWrapper = null; 123 let filterByToggleButton = null; 124 let filterByOptionsWrapper = null; 125 let filterByOptionsInputs = null; 126 let filterByOptionsInputsArray = null; 127 128 let searchInput = null; 129 130 let autosuggestWrapper = null; 131 let autosuggestContent = null; 132 133 let bootstrapFocusTrapModals = []; 134 135 ///////////////////////////////////////////////////////////////////////////////////////////// 136 // Define state variables. 137 138 let autosuggestAbortController = null; 139 let filterByOptionPreviousValue = null; 140 141 ///////////////////////////////////////////////////////////////////////////////////////////// 142 // Define helpers and handlers. 143 144 function renderMobileHeaderBrand( container ) { 145 const brand = window.dp_search_form && window.dp_search_form.mobile_header_brand; 146 if ( ! container || ! brand || ! brand.home_url ) { 147 return; 148 } 149 const link = document.createElement( 'a' ); 150 link.href = brand.home_url; 151 if ( brand.logo && brand.logo.url ) { 152 link.className = 'site-home-link'; 153 const img = document.createElement( 'img' ); 154 img.src = brand.logo.url; 155 if ( brand.logo.srcset ) { 156 img.setAttribute( 'srcset', brand.logo.srcset ); 157 } 158 if ( brand.logo.sizes ) { 159 img.setAttribute( 'sizes', brand.logo.sizes ); 160 } 161 img.alt = typeof brand.logo.alt === 'string' ? brand.logo.alt : ''; 162 img.decoding = 'async'; 163 link.appendChild( img ); 164 container.appendChild( link ); 165 } else if ( brand.site_title ) { 166 link.textContent = brand.site_title; 167 container.appendChild( link ); 168 } 169 } 170
171 function init() { 172 173 if ( ! searchFormBlock ) { 174 searchFormBlock = document.createElement('div'); 175 searchFormBlock.id = 'dp-site-search-form'; 176 searchFormBlock.classList.add('dp-block-search-form'); 177 178 searchFormBlock.innerHTML = ` 179 <div class="dp-block-search-form__overlay"></div> 180 <div class="dp-block-search-form__inner"> 181 <div class="dp-block-search-form__mobile-header"> 182 <button type="button" class="dp-block-search-form__close" aria-label="Close search"> 183 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 184 <path d="M4 4L20 20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> 185 <path d="M4 20L20 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> 186 </svg> 187 </button> 188 <div class="dp-block-search-form__mobile-logo"></div> 189 </div> 190 <form name="dp-search-form" method="get" action="${window.dp_search_form.form_action}"> 191 <div class="dp-search-form-main"> 192 <div class="dp-search-form__filter-by-dropdown"> 193 <button type="button" class="filter-by-dropdown__toggle" aria-controls="dp-site-search-form__category-options"> 194 <span class="filter-by-dropdown__toggle-label">Category:</span> 195 <span class="filter-by-dropdown__toggle-value">All</span> 196 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="9" viewBox="0 0 14 9" fill="none"> 197 <path d="M12.3135 1.65674L6.65662 7.31359L0.999769 1.65674" stroke="#2445EA" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> 198 </svg> 199 </button> 200 <div id="dp-site-search-form__category-options" class="filter-by-dropdown__options visually-hidden"> 201 <label> 202 <input type="radio" name="facet" value="all" class="visually-hidden" checked /> 203 All 204 </label> 205 ${window.dp_search_form.search_filter_options.map(option => { 206 return ` 207 <label> 208 <input type="radio" name="facet" value="${option.value}" class="visually-hidden" /> 209 ${option.label} 210 </label> 211 `; 212 }).join('')} 213 </div> 214 </div> 215 <input name="s" type="search" placeholder="What are you looking for..." autocomplete="off" required /> 216 <input type="reset" value="Clear" class="clear-search-input" /> 217 <button type="submit" class="submit-search-input"> 218 <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25" fill="none"> 219 <title>Search</title> 220 <circle cx="9.6" cy="9.6" r="8.6" stroke="currentColor" stroke-width="2"/> 221 <path d="M16.8 16.7998L24 23.9998" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> 222 </svg> 223 </button> 224 </div> 225 <div class="dp-search-form-autosuggest"> 226 <div class="dp-search-form-autosuggest__content"></div> 227 <p class="default-message">Need to find a dentist? Look <a href="${window.dp_search_form.dentist_href}">here →</a></p> 228 </div> 229 </form>
230 </div> 231 `; 232 233 document.body.appendChild(searchFormBlock); 234 235 const mobileLogoEl = searchFormBlock.querySelector( '.dp-block-search-form__mobile-logo' ); 236 renderMobileHeaderBrand( mobileLogoEl ); 237 } 238 239 overlay = searchFormBlock.querySelector('.dp-block-search-form__overlay'); 240 241 closeButton = searchFormBlock.querySelector('.dp-block-search-form__close'); 242 243 searchForm = searchFormBlock.querySelector('form[name="dp-search-form"]'); 244 245 filterByWrapper = searchFormBlock.querySelector('.dp-search-form__filter-by-dropdown'); 246 filterByToggleButton = filterByWrapper.querySelector('.filter-by-dropdown__toggle'); 247 filterByOptionsWrapper = filterByWrapper.querySelector('.filter-by-dropdown__options'); 248 filterByOptionsInputs = filterByOptionsWrapper.querySelectorAll('input[name="facet"]'); 249 filterByOptionsInputsArray = Array.from(filterByOptionsInputs); 250 251 searchInput = searchFormBlock.querySelector('input[name="s"]'); 252 253 autosuggestWrapper = searchFormBlock.querySelector('.dp-search-form-autosuggest'); 254 autosuggestContent = autosuggestWrapper.querySelector('.dp-search-form-autosuggest__content'); 255 256 bootstrapFocusTrapModals = []; 257 258 // Define state variables. 259 260 autosuggestAbortController = null; 261 filterByOptionPreviousValue = getFilterByOptionCurrentValue(); 262 263 // Attach event listeners. 264 265 overlay.addEventListener('click', event => { 266 if ( event.target === event.currentTarget ) { 267 // Only toggle visibility if no child element was targeted. 268 toggleSearchFormVisibility(); 269 } 270 }); 271 272 if ( closeButton ) { 273 closeButton.addEventListener('click', () => { 274 if ( document.body.classList.contains('dp-block-search-form--open') ) { 275 toggleSearchFormVisibility(); 276 } 277 }); 278 } 279 280 filterByToggleButton.addEventListener('click', toggleFilterByOptionsVisibility); 281 282 searchForm.addEventListener('reset', event => { 283 setTimeout( 284 () => { 285 // This changes the selected category, so remember the value after reset. 286 // It's now the new "previous" option. 287 filterByOptionPreviousValue = getFilterByOptionCurrentValue(); 288 // Now handle search input change. 289 handleSearchInputChange(event); 290 }, 291 0 // Fire right after this event to ensure the form values have already been reset. 292 ); 293 }); 294 295 searchForm.addEventListener('submit', event => { 296 const query = searchInput.value.trim(); 297 const [ , filterByLabel ] = getFilterByOptionCurrentValue(); 298 dataLayerPush({ 299 event: ( event?.submitter?.classList?.contains('submit-more-results') ? "more_search_success" : "term_search_success" ), 300 search_term: query, 301 search_category: filterByLabel, 302 }); 303 }); 304 305 filterByOptionsInputs.forEach(input => { 306 input.addEventListener('change', handleSearchInputChange); 307 }); 308 309 searchInput.addEventListener('input', handleSearchInputChange); 310 } 311 312 function dataLayerPush(data) { 313 window.dataLayer = window.dataLayer || []; 314 window.dataLayer.push(data); 315 window.console.log('pushed to dataLayer ->', JSON.stringify(data, null, 4)); 316 } 317 318 function toggleSearchFormVisibility() { 319 if ( !! document.body.classList.toggle('dp-block-search-form--open') ) { 320 // Prevent Bootstrap's modal focus trapping from preventing focus. 321 bootstrapFocusTrapModals.length = 0; 322 bsModals = document.querySelectorAll('.modal.show[aria-modal="true"][role="dialog"]'); 323 if ( bsModals ) { 324 bsModals.forEach(modal => { 325 const bsModalInstance = bootstrap.Modal.getInstance(modal); 326 if ( bsModalInstance?._focustrap?._isActive ) { 327 // Only capture modals with focus trapping enabled. 328 bootstrapFocusTrapModals.push(bsModalInstance); 329 bsModalInstance._focustrap.deactivate(); // See https://github.com/twbs/bootstrap/blob/v5.2.3/js/src/modal.js#L70 330 } 331 }); 332 } 333 // Auto-focus the search input field. 334 searchInput.focus(); 335 // Send analytics event. 336 dataLayerPush({ 337 event: "search_overlay_opened", 338 }); 339 } else { 340 // Re-activate Bootstrap modal focus trapping. 341 if ( bootstrapFocusTrapModals ) { 342 bootstrapFocusTrapModals.forEach(modalInstance => { 343 modalInstance._focustrap.activate(); 344 }) 345 } 346 } 347 } 348 349 function toggleFilterByOptionsVisibility() { 350 filterByOptionsWrapper.classList.toggle('visually-hidden'); 351 } 352 353 async function handleSearchInputChange(event) { 354 355 // Ensure current filter option is set. 356 setStateFilterBy(getFilterByOptionCurrent()); 357 358 if (filterByOptionsInputsArray.includes(event.target)) { 359 // Change event from filter category input selection.
360 const [ , prevCategoryLabel ] = filterByOptionPreviousValue; 361 filterByOptionPreviousValue = getFilterByOptionCurrentValue(); 362 const [ , newCategoryLabel ] = filterByOptionPreviousValue; 363 dataLayerPush({ 364 event: "search_category_interest", 365 previous_category: prevCategoryLabel, 366 new_category: newCategoryLabel, 367 }); 368 } 369 370 // Ensure the filter options are hidden while user searches. 371 filterByOptionsWrapper.classList.add('visually-hidden'); 372 373 // Act based on input value. 374 if ( searchInput?.value ) { 375 376 searchFormBlock.classList.add('has-query-input'); 377 378 const query = searchInput.value.trim(); 379 const [ filterByValue, filterByLabel ] = getFilterByOptionCurrentValue(); 380 const limit = 4; 381 const autosuggestResults = await fetchAutosuggestResults(query, filterByValue, limit); 382 if ( 383 'success' === autosuggestResults?.status && 384 autosuggestResults?.data?.topSearchResults?.length 385 ) { 386 // Found search results. 387 388 const topSearchResultsListItems = autosuggestResults.data.topSearchResults.map( 389 post => { 390 if ( ! post?.search_result_item_html ) { 391 return ''; 392 } 393 return `<li>${post.search_result_item_html}</li>`; 394 } 395 ); 396 397 let maybeMoreButton = ``; 398 if ( autosuggestResults?.data?.totalSearchResultsCount > limit ) { 399 maybeMoreButton = `<button type="submit" class="submit-more-results">More...</button>`; 400 } 401 402 autosuggestContent.innerHTML = ` 403 <div class="top-search-results"> 404 <p class="heading">Top Results For <strong>${query}</strong> in <strong>${filterByLabel}</strong></p> 405 <ul> 406 ${topSearchResultsListItems.join("\n")} 407 </ul> 408 ${maybeMoreButton} 409 </div> 410 `; 411 412 autosuggestContent.querySelectorAll('.top-search-results li a').forEach(el => { 413 el.addEventListener('click', () => { 414 const itemTitle = el.querySelector('.search-result-item__title'); 415 dataLayerPush({ 416 event: "suggested_search_success", 417 search_term: query, 418 search_category: filterByLabel, 419 suggested_name: ( itemTitle?.innerText ) ? itemTitle.innerText.trim() : el.innerText.trim(), 420 suggested_url: el.href, 421 }); 422 }); 423 }); 424 } else { 425 // No search results retrieved. 426 427 const viewHoursTooltipHTML = ` 428 <strong>Mon–Fri:</strong> 8:30am – 9pm<br /> 429 <strong>Sat:</strong> 9am – 6pm<br /> 430 <strong>Sun:</strong> Closed<br /> 431 <small>All times are in EST</small> 432 `; 433 434 autosuggestContent.innerHTML = ` 435 <div class="top-search-results no-results"> 436 <p class="heading">We didn't find anything that matches “${query}”.</p> 437 <div class="no-results__cta"> 438 <div class="cta__see-faq"> 439 <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none"> 440 <path d="M2.55996 35.9501C2.99996 33.8101 3.54996 31.6801 4.50996 29.3401C2.92996 26.3301 1.51996 23.1101 1.39996 19.7101C1.17996 13.2801 5.74996 7.46005 11.33 4.25005C13.06 3.26005 14.9 2.45005 16.84 1.98005C23.55 0.340052 31.1 3.16005 35.17 8.74005C39.24 14.3201 39.64 22.3101 36.21 28.3101C32.79 34.3101 25.77 38.0301 18.88 37.5801C15.87 37.3801 12.84 36.4101 10.76 34.2701C8.02996 35.
4400101 5.29996 35.7501 2.54996 35.9501H2.55996Z" stroke="#2445EA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 441 <path d="M15.3096 15.6403C15.4596 13.8203 16.5596 12.0803 18.1896 11.2503C19.8196 10.4203 21.9296 10.5903 23.3596 11.7303C24.7896 12.8703 25.3896 14.9703 24.7096 16.6703C24.2796 17.7403 23.4196 18.5803 22.5096 19.3003C21.7396 19.9103 20.8996 20.4803 20.3496 21.3003C19.7996 22.1203 19.6496 23.3003 20.2796 24.0503" stroke="#2445EA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 442 <path d="M20.3203 28.6702L20.3703 29.1902C20.5403 29.2902 20.8003 29.2202 20.8903 29.0402C20.9803 28.8602 20.8903 28.6102 20.3203 28.6702Z" stroke="#2445EA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 443 <path d="M2.55996 35.9501C2.99996 33.8101 3.54996 31.6801 4.50996 29.3401C2.92996 26.3301 1.51996 23.1101 1.39996 19.7101C1.17996 13.2801 5.74996 7.46005 11.33 4.25005C13.06 3.26005 14.9 2.45005 16.84 1.98005C23.55 0.340052 31.1 3.16005 35.17 8.74005C39.24 14.3201 39.64 22.3101 36.21 28.3101C32.79 34.3101 25.77 38.0301 18.88 37.5801C15.87 37.3801 12.84 36.4101 10.76 34.2701C8.02996 35.
4430101 5.29996 35.7501 2.54996 35.9501H2.55996Z" stroke="#2445EA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 444 <path d="M15.3096 15.6403C15.4596 13.8203 16.5596 12.0803 18.1896 11.2503C19.8196 10.4203 21.9296 10.5903 23.3596 11.7303C24.7896 12.8703 25.3896 14.9703 24.7096 16.6703C24.2796 17.7403 23.4196 18.5803 22.5096 19.3003C21.7396 19.9103 20.8996 20.4803 20.3496 21.3003C19.7996 22.1203 19.6496 23.3003 20.2796 24.0503" stroke="#2445EA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 445 <path d="M20.3203 28.6702L20.3703 29.1902C20.5403 29.2902 20.8003 29.2202 20.8903 29.0402C20.9803 28.8602 20.8903 28.6102 20.3203 28.6702Z" stroke="#2445EA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 446 </svg> 447 <p>Take a look at our frequently asked questions</p> 448 <a href="${dp_search_form.faq_href}" class="dp-button dp-button--tertiary">Go to FAQ</a> 449 </div> 450 <div class="cta__call-us"> 451 <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none"> 452 <g clip-path="url(#clip0_1_828)"> 453 <path d="M20.0001 38.8801C30.4273 38.8801 38.8801 30.4273 38.8801 20.0001C38.8801 9.57298 30.4273 1.12012 20.0001 1.12012C9.57298 1.12012 1.12012 9.57298 1.12012 20.0001C1.12012 30.4273 9.57298 38.8801 20.0001 38.8801Z" stroke="#2445EA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 454 <path d="M9.29004 29.2897C10.09 25.6597 12.76 22.4897 16.21 21.0897C17.93 20.3897 19.8 20.1097 21.57 19.5297C23.34 18.9497 25.1 17.9897 26.01 16.3597C27.04 14.4997 26.68 12.0197 25.3 10.3897C23.92 8.7597 21.67 7.9897 19.55 8.2097C18.48 8.3197 17.42 8.6797 16.57 9.3397C14.9 10.6497 14.31 13.0597 14.93 15.0897C15.55 17.1197 17.22 18.7197 19.18 19.5397C20.93 20.2697 22.89 20.4197 24.64 21.1497C27.97 22.5297 30.25 26.1397 30.06 29.7397" stroke="#2445EA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 455 </g> 456 <defs> 457 <clipPath id="clip0_1_828"> 458 <rect width="40" height="40" fill="white"/> 459 </clipPath> 460 </defs> 461 </svg> 462 <p>Give us a call, we are here to help</p> 463 <button type="button" class="call-us__view-hours" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="top" data-bs-fallbackPlacements="['top', 'bottom', 'left', 'right']" data-bs-title="${viewHoursTooltipHTML}">View Hours</button> 464 <a href="${dp_search_form.dp_phone_href}" class="dp-button dp-phone dp-phone-without-country-code">${dp_search_form.dp_phone_label}</a> 465 </div> 466 </div> 467 </div> 468 `; 469 470 // Enable the Bootstrap tooltip. 471 autosuggestContent.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { 472 return new bootstrap.Tooltip(el); 473 }); 474 } 475 476 // Retain query params on dynamically added hyperlinks. 477 dpSetPhoneNumber(autosuggestContent); 478 } else { 479 // No search input value. 480 searchFormBlock.classList.remove('has-query-input'); 481 autosuggestContent.innerHTML = ''; 482 } 483 } 484 485 function setStateFilterBy(inputEl) { 486 let buttonLabel = 'All'; 487 if ( inputEl?.labels?.length ) { 488 buttonLabel = inputEl.labels[0].innerText; 489 } 490 filterByToggleButton.querySelector('span.filter-by-dropdown__toggle-value').innerText = buttonLabel; 491 filterByOptionsWrapper.classList.add('visually-hidden'); 492 } 493 494 function getFilterByOptionCurrent() { 495 for ( const input of filterByOptionsInputs ) { 496 if ( input.checked ) { 497 return input; 498 } 499 } 500 return null; 501 } 502 503 function getFilterByOptionCurrentValue() { 504 const inputEl = getFilterByOptionCurrent(); 505 506 let value = 'all'; 507 if ( inputEl?.value ) { 508 value = inputEl.value; 509 } 510 511 let label = 'All'; 512 if ( inputEl?.labels?.[0]?.innerText ) { 513 label = inputEl.labels[0].innerText; 514 } 515 516 return [ value, label ]; 517 } 518 519 async function fetchAutosuggestResults(query, filterBy, limit) { 520 521 if ( ! query ) { 522 return null; // Don't fetch for blank queries. 523 } 524 525 // Abort any existing request. 526 if ( autosuggestAbortController ) { 527 autosuggestAbortController.abort(); 528 } 529 530 // Create a new AbortController for the current request. 531 autosuggestAbortController = new AbortController(); 532 533 try { 534 535 const response = await fetch( 536 `${window.DP_REST_API.v1}/search/autosuggest/?s=${query}&facet=${filterBy}&limit=${limit}`, 537 { 538 "method": 'GET', 539 "credentials": 'same-origin', 540 "signal": autosuggestAbortController.signal 541 } 542 ); 543 544 if ( ! response.ok ) { 545 throw new Error('Network response was not ok'); 546 } 547 548 const data = await response.json(); 549 return data; 550 } catch ( error ) { 551 if ( 'AbortError' !== error.name ) { 552 window.console.error('Failed to fetch search autosuggestion results:', error); 553 } 554 } 555 556 return null; // The data couldn't be fetched. 557 } 558 559 ///////////////////////////////////////////////////////////////////////////////////////////// 560 // Attach entry point event listeners. 561 562 document.querySelectorAll('.search-link').forEach(link => { 563 link.addEventListener('click', event => { 564 event.preventDefault();
564 // Do not navigate href. 565 566 // Initialize the search form if it's not already initialized. 567 if ( ! searchFormBlock ) { 568 init(); 569 } 570 571 // Toggle the search form visibility. 572 toggleSearchFormVisibility(); 573 }); 574 }); 575}, { once: true } ); 576; 577/** 578 * Plan search results filter and sort. 579 * Direct links: URL params plans, benefits, sort are applied on load (e.g. ?plans=f_net_cigna&benefits=f_bene_vision&sort=low-price). 580 */ 581var selectedSort = 'featured'; 582var andMode = true; 583 584function sortCards() { 585 jQuery('.insurance-results-block, .cards-results-box').each(function () { 586 let $resultBox = jQuery(this); 587 let $sortedCards = $resultBox.find('.plan.card').sort(function (a, b) { 588 var aVal = parseFloat(jQuery(a).data(selectedSort)) || 0; 589 var bVal = parseFloat(jQuery(b).data(selectedSort)) || 0; 590 if (aVal < bVal) 591 return -1; 592 if (aVal > bVal) 593 return 1; 594 return 0; 595 596 }); 597 $resultBox.html($sortedCards); 598 }); 599} 600 601function filterCards() { 602 603 //cards-results-box 604 let $activeFilters = jQuery('.added-search-pill-box .filter-pill'); 605 606 if ($activeFilters.length) { 607 let $plansFilters = jQuery('.added-search-pill-box .filter-pill.plan'); 608 let $beneFilters = jQuery('.added-search-pill-box .filter-pill.filter'); 609 610 //hide everything 611 jQuery('.plan.card').hide(); 612 613 if ($plansFilters.length && $beneFilters.length && andMode) { 614 615 let planSelector = []; 616 $plansFilters.each(function (idx, el) { 617 planSelector.push('.' + jQuery(this).data('filter')); 618 }); 619 planSelector = planSelector.join(', '); 620 621 let beneSelector = []; 622 $beneFilters.each(function (idx, el) { 623 beneSelector.push('.' + jQuery(this).data('filter')); 624 }); 625 beneSelector = beneSelector.join(', '); 626 627 jQuery(planSelector).filter(beneSelector).show(); 628 629 } else { 630 //show what we should 631 $activeFilters.each(function (idx, el) { 632 jQuery('.plan.card.' + jQuery(this).data('filter')).show(); 633 }); 634 } 635 } else { 636 //if nothing selected show all 637 jQuery('.plan.card').show(); 638 } 639 640 //resort based on sort 641 sortCards(); 642 643 // jQuery(document).trigger("render:cards"); 644 updateResultCounts(); 645} 646 647function updateResultCounts() { 648 649 jQuery('.insurance-results-block, .cards-results-box').each(function () { 650 let $resultBox = jQuery(this); 651 let $header = $resultBox.prev('.card-results-header'); 652 let $activeCards = $resultBox.find('.plan.card:visible'); 653 $header.find('.num_of_plans').text($activeCards.length); 654 if ($activeCards.length > 0) { 655 $header.find('.hide-when-nothing-found,.plan-results-benefits-header').show(); 656 } else { 657 $header.find('.hide-when-nothing-found,.plan-results-benefits-header').hide(); 658 } 659 if ($activeCards.length) { 660 $header.find('.results_found_header').show(); 661 $header.find('.results_not_found_header').hide(); 662 } else { 663 $header.find('.results_not_found_header').show(); 664 $header.find('.results_found_header').hide(); 665 } 666 }); 667} 668 669function hideAllFilterBoxes() { 670 jQuery('.filter-dropdown').removeClass('active'); 671 jQuery(".slider-svg").removeClass("selected"); 672 jQuery(".filter-pop-out-box").addClass("hidden"); 673} 674 675function updateDropDownCounts() { 676 let count = jQuery('.added-search-pill-box .filter-pill.filter').length; 677 678 jQuery('.filter-dropdown.benefits .filter-count-wrap').attr('data-filter-count', count); 679 jQuery('.filter-dropdown.benefits .filter-count').text(count); 680 count = jQuery('.added-search-pill-box .filter-pill.plan').length; 681 jQuery('.filter-dropdown.all-plans-filter-btn .filter-count-wrap').attr('data-filter-count', count); 682 jQuery('.filter-dropdown.all-plans-filter-btn .filter-count').text(count); 683} 684 685/** 686 * Updates the URL to reflect current filter and sort state so the view can be shared or bookmarked. 687 * Preserves zip from the URL, or adds it from .results-zip when present; sets plans, benefits, sort from the DOM. 688 */ 689function syncUrlToFilters() { 690 if ( ! jQuery( '.plan-search-results-filter-layout' ).length ) { 691 return; 692 } 693 var params = new URLSearchParams( window.location.search ); 694 params.delete( 'plans' ); 695 params.delete( 'benefits' ); 696 params.delete( 'sort' );
697 // Preserve zip from URL, or add from displayed .results-zip so shared links include location. 698 if ( ! params.get( 'zip' ) ) { 699 var zipEl = document.querySelector( '.results-zip' ); 700 var zipText = zipEl ? zipEl.textContent.trim() : ''; 701 if ( /^\d{5}$/.test( zipText ) ) { 702 params.set( 'zip', zipText ); 703 } 704 } 705 var planPills = jQuery( '.added-search-pill-box .filter-pill.plan' ); 706 var benePills = jQuery( '.added-search-pill-box .filter-pill.filter' ); 707 var planValues = []; 708 planPills.each( function () { planValues.push( jQuery( this ).data( 'filter' ) ); } ); 709 var beneValues = []; 710 benePills.each( function () { beneValues.push( jQuery( this ).data( 'filter' ) ); } ); 711 if ( planValues.length ) { 712 params.set( 'plans', planValues.join( ',' ) ); 713 } 714 if ( beneValues.length ) { 715 params.set( 'benefits', beneValues.join( ',' ) ); 716 } 717 if ( selectedSort && selectedSort !== 'featured' ) { 718 params.set( 'sort', selectedSort ); 719 } 720 var query = params.toString(); 721 var url = window.location.pathname + ( query ? '?' + query : '' ); 722 window.history.replaceState( null, '', url ); 723} 724 725/** 726 * Pre-apply filters and sort from URL params (plans, benefits, sort). 727 * Supported params: plans (e.g. f_net_delta-dental), benefits (e.g. f_bene_vision), sort (e.g. low-price). 728 * Only applies values that match existing .planfilter elements. 729 */ 730function applyFiltersAndSortFromUrl() { 731 var $pillBox = jQuery('.added-search-pill-box'); 732 if ( ! $pillBox.length || ! jQuery('.cards-results-box .plan.card').length ) { 733 return; 734 } 735 var params = new URLSearchParams( window.location.search ); 736 737 // Sort: match .planfilter[data-sort] in .filter-pop-out-box.sort 738 var sortParam = params.get( 'sort' ); 739 if ( sortParam ) { 740 var $sortFilter = jQuery( '.filter-pop-out-box.sort .planfilter[data-sort="' + sortParam + '"]' ); 741 if ( $sortFilter.length ) { 742 selectedSort = sortParam; 743 jQuery( '.filter-pop-out-box.sort .planfilter' ).removeClass( 'selected' ); 744 $sortFilter.addClass( 'selected' ); 745 var $activeSort = jQuery( '#active-sort' ); 746 if ( $activeSort.length ) { 747 $activeSort.text( $sortFilter.text().trim() ); 748 } 749 sortCards(); 750 } 751 } 752 753 // Plans (network): add pills and mark .planfilter selected in .filter-pop-out-box.plan 754 var plansParam = params.get( 'plans' ); 755 if ( plansParam ) { 756 var planValues = plansParam.split( ',' ).map( function ( s ) { return s.trim(); } ).filter( Boolean ); 757 jQuery( '.filter-pill.plan' ).remove(); 758 jQuery( '.filter-pop-out-box.plan .planfilter' ).removeClass( 'selected' ).find( '.pill-close' ).addClass( 'hidden' ); 759 planValues.forEach( function ( filterValue ) { 760 var $option = jQuery( '.filter-pop-out-box.plan .planfilter[data-filter="' + filterValue + '"]' ); 761 if ( $option.length ) { 762 $option.addClass( 'selected' ).find( '.pill-close' ).removeClass( 'hidden' ); 763 var label = $option.find( '.pill-label' ).text(); 764 var pillHtml = '<div class="filter-pill selected plan" data-filter="' + filterValue + '">' + 765 '<span class="pill-label">' + label + '</span>' + 766 '<span class="pill-close" tabindex="0">' + 767 '<svg width="12" height="12" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg"><title>Remove</title>' + 768 '<path d="M3 1L18 16" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>' + 769 '<path d="M3 16L18 1" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg></span></div>'; 770 $pillBox.append( pillHtml ); 771 } 772 } ); 773 } 774 775 // Benefits: add pills and mark .planfilter selected in .filter-pop-out-box.filter 776 var benefitsParam = params.get( 'benefits' ); 777 if ( benefitsParam ) { 778 var benefitValues = benefitsParam.split( ',' ).map( function ( s ) { return s.trim(); } ).filter( Boolean ); 779 jQuery( '.filter-pill.filter' ).remove(); 780 jQuery( '.filter-pop-out-box.filter .planfilter' ).removeClass( 'selected' ).find( '.pill-close' ).addClass( 'hidden' ); 781 benefitValues.forEach( function ( filterValue ) { 782 var $option = jQuery( '.filter-pop-out-box.filter .planfilter[data-filter="' + filterValue + '"]' ); 783 if ( $option.length ) { 784 $option.addClass( 'selected' ).find( '.pill-close' ).removeClass( 'hidden' ); 785 var label = $option.find( '.pill-label' ).text(); 786 var pillHtml = '<div class="filter-pill selected filter" data-filter="' + filterValue + '">' + 787 '<span class="pill-label">' + label + '</span>' + 788 '<span class="pill-close" tabindex="0">' + 789 '<svg width="12" height="12" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg"><title>Remove</title>' + 790 '<path d="M3 1L18 16" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>' + 791 '<path d="M3 16L18 1" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg></span></div>'; 792 $pillBox.append( pillHtml ); 793 } 794 } ); 795 } 796 797 if ( plansParam || benefitsParam ) { 798 updateDropDownCounts(); 799 filterCards(); 800 } else if ( sortParam ) { 801 updateResultCounts(); 802 } 803} 804 805function setFeatureLabel( attr, text ) { 806 let search_params = new URLSearchParams(window.location.search); 807 let highest = 0;
808 jQuery('.dental-plan.plan.card[data-' + attr + ']').each(function () { 809 let $card = jQuery(this); 810 let currentValue = $card.data(attr); 811 if (currentValue < highest) { 812 highest = currentValue; 813 } 814 }); 815 jQuery('.dental-plan.plan.card[data-' + attr + '="' + highest + '"]') 816 .addClass('has-feature-label ' + attr); 817 jQuery('.dental-plan.plan.card[data-' + attr + '="' + highest + '"] .plan-price') 818 .after('<div class="feature-label"><span>' + text + '</span></div>'); 819} 820 821document.addEventListener("DOMContentLoaded", function () { 822 823 // setFeatureLabel('best-savings', 'Most Savings'); 824 setFeatureLabel('member-count', 'Most Popular'); 825 826 var $pillBox = jQuery('.added-search-pill-box'); 827 828 var inputs = document.querySelectorAll(".planfilter"); 829 830 jQuery('.filter-dropdown').click(function () { 831 hideAllFilterBoxes(); 832 $this = jQuery(this); 833 $this.addClass('active'); 834 $popOut = $this.next('.filter-pop-out-box'); 835 $popOut.removeClass('hidden'); 836 $this.find('.slider-svg').addClass('selected'); 837 //synch sort 838 839 }); 840 841 jQuery('.filter-dropdown').on('keypress', function (e) { 842 var key = e.key; 843 e.preventDefault(); 844 if (key === " " || key === "Enter") { 845 // Only send click events for Space 846 // or Enter keys 847 jQuery(this).click(); // Run the click event for element 848 } 849 }); 850 851 //toggle selected state of filter buttons 852 jQuery('.planfilter').click(function () { 853 let $button = jQuery(this); 854 let $filterSet = $button.parents('.filter-pop-out-box'); 855 if ($filterSet.hasClass('sort')) { 856 $filterSet.find('.planfilter').removeClass('selected'); 857 } 858 $button.toggleClass('selected'); 859 if ($button.hasClass('selected')) { 860 $button.find('.pill-close').removeClass('hidden'); 861 } else { 862 $button.find('.pill-close').addClass('hidden'); 863 } 864 }); 865 866 jQuery('.accept-filter').click(function (e) { 867 let $filterSet = jQuery(this).parents('.filter-pop-out-box'); 868 let $dropDown = $filterSet.prev('.filter-dropdown'); 869 let pillClass = ''; 870 871 //clear out active filters in this class 872 if ($filterSet.hasClass('sort')) { 873 //sort 874 $sortButton = $filterSet.find('.planfilter.selected'); 875 selectedSort = $sortButton.data('sort'); 876 jQuery('#active-sort').text($sortButton.text()); 877 sortCards(); 878 } else { 879 //filter 880 if ($filterSet.hasClass('plan')) { 881 $pillBox.find('.filter-pill.plan').remove(); 882 pillClass = 'plan'; 883 } else if ($filterSet.hasClass('filter')) { 884 $pillBox.find('.filter-pill.filter').remove(); 885 pillClass = 'filter'; 886 } 887 //add the new ones 888 $filterSet.find('.planfilter.selected').each(function () { 889 let pillHtml = ` 890 <div class="filter-pill selected ${pillClass}" data-filter="${jQuery(this).data('filter')}"> 891 <span class="pill-label">${jQuery(this).find('.pill-label').text()}</span> 892 <span class="pill-close" tabindex="0"> 893 <svg width="12" height="12" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg"> 894 <title>Remove</title> 895 <path d="M3 1L18 16" stroke="currentColor" stroke-width="3" stroke-linecap="round"/> 896 <path d="M3 16L18 1" stroke="currentColor" stroke-width="3" stroke-linecap="round"/> 897 </svg> 898 </span> 899 </div> 900 `; 901 $pillBox.append(pillHtml); 902 }); 903 904 updateDropDownCounts(); 905 906 //filter! 907 filterCards(); 908 } 909 hideAllFilterBoxes(); 910 syncUrlToFilters(); 911 }); 912 913 jQuery('.clear-filter').click(function (e) { 914 let $filterSet = jQuery(this).parents('.filter-pop-out-box'); 915 let $dropDown = $filterSet.prev('.filter-dropdown'); 916 917 //clear out active filters in this class 918 if ($filterSet.hasClass('sort')) { 919 selectedSort = 'featured'; 920 $filterSet.find('.planfilter').removeClass('selected'); 921 $sortButton = $filterSet.find('*[data-sort="featured"]').addClass('selected'); 922 jQuery('#active-sort').text($sortButton.text()); 923 sortCards(); 924 } else { 925 $filterSet.find('.planfilter').removeClass('selected'); 926 $filterSet.find('.planfilter .pill-close').addClass('hidden'); 927 if ($filterSet.hasClass('plan')) { 928 $pillBox.find('.filter-pill.plan').remove(); 929 930 } else if ($filterSet.hasClass('filter')) { 931 $pillBox.find('.filter-pill.filter').remove(); 932 933 } 934 //update Count 935 $dropDown.find('.filter-count-wrap').attr('data-filter-count', 0); 936 $dropDown.find('.filter-count').text(0); 937 filterCards(); 938 } 939 940 hideAllFilterBoxes(); 941 syncUrlToFilters(); 942 }); 943 944 jQuery(".plan-search-results-filter-layout").on("click", ".filter-pill.selected", function () { 945 $button = jQuery(this); 946 $filter = jQuery('.filter-pop-out-box [data-filter="' + $button.data('filter') + '"]'); 947 $filter.removeClass('selected'); 948 $filter.find('.pill-close').addClass('hidden'); 949 950 if ($button.parents('.added-search-pill-box').length) { 951 $button.remove(); 952 filterCards(); 953 } 954 955 updateDropDownCounts(); 956 syncUrlToFilters(); 957 }); 958 959 jQuery('.close-button-box').click(function () { 960 hideAllFilterBoxes(); 961 }); 962 963 jQuery('.plan-search-show-button').click(function () { 964 const showing = jQuery(this).parent().hasClass('showing'); 965 966 if (showing) { 967 jQuery(this).parent().removeClass('showing');
968 jQuery('.plan-search-filter-box').slideUp(300); 969 } else { 970 jQuery(this).parent().addClass('showing'); 971 jQuery('.plan-search-filter-box').slideDown(300); 972 } 973 ; 974 975 }); 976 977 // Pre-apply filters/sort from URL (server-rendered results). 978 applyFiltersAndSortFromUrl(); 979 980 updateResultCounts(); 981}, { once: true }); 982 983// Pre-apply filters/sort from URL when results are loaded via AJAX (async plan search). 984document.addEventListener( 'planSearchResultsLoaded', function () { 985 applyFiltersAndSortFromUrl(); 986}, { once: false } ); 987; 988/** 989 * @file Plan comparison tray + compare navigation for plan results. 990 * @requires bootstrap.Modal (Bootstrap 5) for max-selection modal; optional jQuery.modal fallback. 991 * @function getSelectedPlans 992 * @function setPlanComparisonCards 993 * @function renderPlanComparisionCard (returns DocumentFragment of card content) 994 */ 995 996(function () { 997 'use strict'; 998 999 /** 1000 * Escape a string for use in a CSS attribute selector [data-plan-id="â¦"]. 1001 * 1002 * @param {string|number} value 1003 * @return {string} 1004 */ 1005 function escapeCssAttr(value) { 1006 const s = String(value); 1007 if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') { 1008 return CSS.escape(s); 1009 } 1010 return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); 1011 } 1012 1013 /** 1014 * @param {Element} el 1015 * @return {boolean} 1016 */ 1017 function isElementVisible(el) { 1018 if (!el || typeof el.getBoundingClientRect !== 'function') { 1019 return false; 1020 } 1021 const style = window.getComputedStyle(el); 1022 if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) { 1023 return false; 1024 } 1025 const rect = el.getBoundingClientRect(); 1026 return rect.width > 0 && rect.height > 0; 1027 } 1028 1029 /** 1030 * Legacy PHP results use `.dp-plan-toggle`; React v2 plan search sets `data-show-family` on 1031 * `#react-plan-search-results` (see PlanSearchContextProvider). 1032 * 1033 * @return {boolean} 1034 */ 1035 function isFamilyPricingSelectedForCompare() { 1036 const reactMount = document.getElementById('react-plan-search-results'); 1037 if (reactMount && reactMount.getAttribute('data-show-family') === '1') { 1038 return true; 1039 } 1040 const planToggle = document.querySelector('.dp-plan-toggle'); 1041 const toggleSwitch = planToggle ? planToggle.querySelector('.plan-toggle-switch') : null; 1042 return Boolean(toggleSwitch && toggleSwitch.classList.contains('family-selected')); 1043 } 1044 1045 function showComparePlansModal() { 1046 const modalEl = document.getElementById('comparePlansModal'); 1047 if (!modalEl) { 1048 return; 1049 } 1050 if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { 1051 bootstrap.Modal.getOrCreateInstance(modalEl).show(); 1052 return; 1053 } 1054 if (typeof window.jQuery !== 'undefined' && window.jQuery.fn && window.jQuery.fn.modal) { 1055 window.jQuery(modalEl).modal('show'); 1056 } 1057 } 1058 1059 /** 1060 * Plan cards: React v2 uses .dp-plan-search-plan-card; legacy PHP uses .plan.card. 1061 * Prefer a card that exposes a logo (data attribute or .card-image-box img). 1062 * 1063 * @param {string|number} planId 1064 * @return {Element|null} 1065 */ 1066 function findPlanCard(planId) { 1067 const idAttr = escapeCssAttr(planId); 1068 const selector = '.dp-plan-search-plan-card[data-plan-id="' + idAttr + '"]'; 1069 const reactCards = Array.prototype.slice.call(document.querySelectorAll(selector)); 1070 if (reactCards.length) { 1071 const withLogo = reactCards.filter(function (el) { 1072 const fromAttr = el.getAttribute('data-plan-logo'); 1073 if (fromAttr && String(fromAttr).length) { 1074 return true; 1075 } 1076 const img = el.querySelector('.card-image-box img'); 1077 const imgSrc = img ? img.getAttribute('src') : ''; 1078 return Boolean(imgSrc && String(imgSrc).length); 1079 }); 1080 const pick = withLogo.length ? withLogo : reactCards; 1081 return pick[0]; 1082 } 1083 return document.querySelector('.plan.card[data-plan-id="' + idAttr + '"]'); 1084 } 1085 1086 /** 1087 * Set modal title without HTML string concatenation (WordPress VIP JS rules). 1088 * 1089 * @param {string} before Text before the emphasized count. 1090 * @param {string} countText Count shown inside .count (escaped via textContent). 1091 * @param {string} after Text after the count. 1092 */ 1093 function setCompareModalTitleParts(before, countText, after) { 1094 const el = document.querySelector('#comparePlansModal .modal-title'); 1095 if (!el) { 1096 return; 1097 } 1098 el.textContent = ''; 1099 el.appendChild(document.createTextNode(before)); 1100 const span = document.createElement('span'); 1101 span.className = 'count'; 1102 span.textContent = countText; 1103 el.appendChild(span); 1104 el.appendChild(document.createTextNode(after)); 1105 } 1106 1107 function getPlanItems(root) { 1108 return Array.prototype.slice.call(root.querySelectorAll('.plan-item')); 1109 } 1110 1111 function getSelectedPlans(root) { 1112 const planItems = getPlanItems(root); 1113 const maxPlans = planItems.filter(isElementVisible).length; 1114 let planToRemove = ''; 1115 /* Checkboxes live on plan cards site-wide, not inside `.plan-search-compare-selector` (legacy jQuery used `$('.dp-compare input:checked')`). */
1116 const checked = document.querySelectorAll('.dp-compare input:checked'); 1117 let selectedPlans = []; 1118 for (let i = 0; i < checked.length; i++) { 1119 const input = checked[i]; 1120 const id = input.getAttribute('data-plan-id'); 1121 if (id != null && id !== '') { 1122 selectedPlans.push(id); 1123 } 1124 } 1125 selectedPlans = selectedPlans.filter(function (id, index, arr) { 1126 return arr.indexOf(id) === index; 1127 }); 1128 // Legacy jQuery used `$(this).prop('checked', false)` inside a non-bound function (this === window), 1129 // so the second clause was effectively always truthy; keep behavior: trim when over max. 1130 if (selectedPlans.length > maxPlans) { 1131 setCompareModalTitleParts('You can compare up to ', String(maxPlans), ' plans.'); 1132 showComparePlansModal(); 1133 root.classList.add('warning'); 1134 setTimeout(function () { 1135 root.classList.remove('warning'); 1136 }, 1500); 1137 planToRemove = selectedPlans.pop(); 1138 const esc = escapeCssAttr(planToRemove); 1139 const toUncheck = document.querySelector('.dp-compare input[data-plan-id="' + esc + '"]'); 1140 if (toUncheck) { 1141 toUncheck.checked = false; 1142 toUncheck.dispatchEvent(new Event('change', { bubbles: true })); 1143 } 1144 } 1145 const compareBtns = root.querySelectorAll('.compare-plans'); 1146 if (selectedPlans.length > 1) { 1147 compareBtns.forEach(function (btn) { 1148 btn.classList.remove('btn-secondary'); 1149 }); 1150 } else { 1151 compareBtns.forEach(function (btn) { 1152 btn.classList.add('btn-secondary'); 1153 }); 1154 } 1155 return selectedPlans; 1156 } 1157 1158 /** 1159 * Build tray card DOM (no HTML string interpolation â VIP-friendly). 1160 * 1161 * @param {string|number} planId 1162 * @return {DocumentFragment} 1163 */ 1164 function renderPlanComparisionCard(planId) { 1165 const frag = document.createDocumentFragment(); 1166 const planCard = findPlanCard(planId); 1167 if (!planCard) { 1168 return frag; 1169 } 1170 const price = 1171 planCard.getAttribute('data-low-price') || 1172 (planCard.dataset && planCard.dataset.lowPrice) || 1173 '0'; 1174 const planName = 1175 (planCard.dataset && planCard.dataset.itemName) || 1176 planCard.getAttribute('data-item_name') || 1177 planCard.getAttribute('data-item-name') || 1178 ''; 1179 const logoFromAttr = planCard.getAttribute('data-plan-logo') || ''; 1180 const logoImg = planCard.querySelector('.card-image-box img'); 1181 const logoFromImg = logoImg ? logoImg.getAttribute('src') || '' : ''; 1182 const logoImageSrc = logoFromAttr && String(logoFromAttr).length ? logoFromAttr : logoFromImg; 1183 1184 let displayPrice = price; 1185 try { 1186 const pricingAttribute = planCard.getAttribute('data-pricing'); 1187 if (pricingAttribute) { 1188 const pricingData = JSON.parse(pricingAttribute); 1189 const planType = isFamilyPricingSelectedForCompare() ? 'family' : 'individual'; 1190 if (pricingData[planType] && pricingData[planType].monthly != null) { 1191 displayPrice = pricingData[planType].monthly; 1192 } 1193 } 1194 } catch (error) { 1195 console.error('Error parsing pricing data for plan comparison card:', error); 1196 } 1197 1198 const removeLink = document.createElement('a'); 1199 removeLink.href = '#'; 1200 removeLink.className = 1201 'position-absolute top-0 start-100 translate-middle badge rounded-pill remove'; 1202 const removeIcon = document.createElement('img'); 1203 removeIcon.src = 1204 '/wp-content/plugins/dp-shared-plugins/dp-blocks/plan-compare-selector/x-icon.svg'; 1205 removeIcon.alt = 'remove'; 1206 const removeSr = document.createElement('span'); 1207 removeSr.className = 'visually-hidden'; 1208 removeSr.textContent = 'remove'; 1209 removeLink.appendChild(removeIcon); 1210 removeLink.appendChild(removeSr); 1211 1212 const cardBody = document.createElement('div'); 1213 cardBody.className = 'card-body'; 1214 const logoEl = document.createElement('img'); 1215 logoEl.className = 'compare-plan-logo'; 1216 logoEl.setAttribute('src', logoImageSrc); 1217 logoEl.setAttribute('alt', planName); 1218 const titleEl = document.createElement('div'); 1219 titleEl.className = 'card-title fs-5 fw-bold'; 1220 titleEl.textContent = '$' + (Number(displayPrice) || 0).toFixed(2) + '/month'; 1221 cardBody.appendChild(logoEl); 1222 cardBody.appendChild(titleEl); 1223 1224 frag.appendChild(removeLink); 1225 frag.appendChild(cardBody); 1226 return frag; 1227 } 1228 1229 /** 1230 * Update monthly price labels on filled compare-tray slots when individual/family toggles. 1231 * Uses `data-pricing` copied onto each `.plan-item` in `setPlanComparisonCards`. 1232 * 1233 * @param {Element|null} root `.plan-search-compare-selector` 1234 */ 1235 function updateCompareTrayPrices(root) { 1236 if (!root) { 1237 return; 1238 } 1239 const planType = isFamilyPricingSelectedForCompare() ? 'family' : 'individual'; 1240 root.querySelectorAll('.plan-item').forEach(function (item) { 1241 if (!item.getAttribute('data-plan-id')) { 1242 return; 1243 } 1244 let pricingData; 1245 try { 1246 const pricingAttribute = item.getAttribute('data-pricing'); 1247 if (!pricingAttribute) { 1248 return; 1249 } 1250 pricingData = JSON.parse(pricingAttribute); 1251 } catch (error) { 1252 console.error('Error parsing pricing data for comparison card update:', error); 1253 return; 1254 } 1255 const pricing = pricingData[planType]; 1256 if (!pricing || pricing.monthly == null) { 1257 return; 1258 } 1259 const cardTitle = item.querySelector('.card-title'); 1260 if (cardTitle) { 1261 cardTitle.textContent = '$' + pricing.monthly.toFixed(2) + '/month'; 1262 } 1263 }); 1264 } 1265 1266 function setPlanComparisonCards(root, selectedPlans) { 1267 const planItems = getPlanItems(root);
1268 planItems.forEach(function (item) { 1269 item.innerHTML = ''; 1270 item.classList.remove('text-bg-light'); 1271 item.classList.add('text-bg-secondary'); 1272 }); 1273 /* Matches legacy jQuery order: runs after clear, so this set usually no-ops until slots are filled. */ 1274 root.querySelectorAll('.plan-item .card-body *').forEach(function (el) { 1275 el.style.opacity = '0'; 1276 }); 1277 selectedPlans.forEach(function (planId, i) { 1278 const item = planItems[i]; 1279 if (!item) { 1280 return; 1281 } 1282 item.innerHTML = ''; 1283 item.appendChild(renderPlanComparisionCard(planId)); 1284 item.setAttribute('data-plan-id', String(planId)); 1285 item.classList.add('text-bg-light'); 1286 item.classList.remove('text-bg-secondary'); 1287 1288 const originalPlanCard = findPlanCard(planId); 1289 const pricingData = originalPlanCard 1290 ? originalPlanCard.getAttribute('data-pricing') 1291 : null; 1292 if (pricingData) { 1293 item.setAttribute('data-pricing', pricingData); 1294 } 1295 }); 1296 setTimeout(function () { 1297 root.querySelectorAll('.plan-item .card-body *').forEach(function (el) { 1298 el.style.opacity = '1'; 1299 }); 1300 }, 200); 1301 root.setAttribute('data-selected-plans', selectedPlans.join(',')); 1302 root.setAttribute('data-plan-count', String(selectedPlans.length)); 1303 const compareBtns = root.querySelectorAll('.compare-plans'); 1304 if (selectedPlans.length > 1) { 1305 compareBtns.forEach(function (btn) { 1306 btn.classList.remove('btn-secondary'); 1307 }); 1308 } else { 1309 compareBtns.forEach(function (btn) { 1310 btn.classList.add('btn-secondary'); 1311 }); 1312 } 1313 } 1314 1315 /** Native `.dp-compare` checkboxes (legacy PHP cards) â sets `data-plan-count` so fixed tray CSS shows. */ 1316 function syncTrayFromCompareCheckboxes(tray) { 1317 if (!tray) { 1318 return; 1319 } 1320 const selectedPlans = getSelectedPlans(tray); 1321 setPlanComparisonCards(tray, selectedPlans); 1322 } 1323 1324 function initPlanTypePricingRefresh() { 1325 const reactMount = document.getElementById('react-plan-search-results'); 1326 if (reactMount && typeof MutationObserver !== 'undefined') { 1327 const observer = new MutationObserver(function (mutations) { 1328 for (let i = 0; i < mutations.length; i++) { 1329 if ( 1330 mutations[i].type === 'attributes' && 1331 mutations[i].attributeName === 'data-show-family' 1332 ) { 1333 updateCompareTrayPrices(document.querySelector('.plan-search-compare-selector')); 1334 return; 1335 } 1336 } 1337 }); 1338 observer.observe(reactMount, { 1339 attributes: true, 1340 attributeFilter: ['data-show-family'], 1341 }); 1342 } 1343 1344 document.addEventListener('planToggleChanged', function () { 1345 updateCompareTrayPrices(document.querySelector('.plan-search-compare-selector')); 1346 }); 1347 } 1348 1349 function initPlanCompareSelector() { 1350 const root = document.querySelector('.plan-search-compare-selector'); 1351 1352 initPlanTypePricingRefresh(); 1353 1354 window.__dpSetPlanComparisonCards = function (planIds) { 1355 const tray = document.querySelector('.plan-search-compare-selector'); 1356 if (tray) { 1357 setPlanComparisonCards(tray, planIds); 1358 } 1359 }; 1360 1361 if (!window.__dpPlanCompareCheckboxListener) { 1362 window.__dpPlanCompareCheckboxListener = true; 1363 document.addEventListener('change', function (e) { 1364 const t = e.target; 1365 if (!t || t.tagName !== 'INPUT' || t.type !== 'checkbox') { 1366 return; 1367 } 1368 if (!t.closest('.dp-compare')) { 1369 return; 1370 } 1371 syncTrayFromCompareCheckboxes(document.querySelector('.plan-search-compare-selector')); 1372 }); 1373 } 1374 1375 if (!root) { 1376 return; 1377 } 1378 1379 window.addEventListener('pageshow', function () { 1380 if (typeof window.__dpPlanCompareOnPageshow === 'function') { 1381 window.__dpPlanCompareOnPageshow(); 1382 return; 1383 } 1384 const selectedPlans = getSelectedPlans(root); 1385 setPlanComparisonCards(root, selectedPlans); 1386 }); 1387 1388 const navEntry = window.performance.getEntriesByType('navigation')[0]; 1389 const isBackForward = 1390 (navEntry && navEntry.type === 'back_forward') || 1391 (typeof window.performance.navigation !== 'undefined' && 1392 window.performance.navigation.type === 2); 1393 if (isBackForward) { 1394 if (typeof window.__dpPlanCompareOnPageshow === 'function') { 1395 window.__dpPlanCompareOnPageshow(); 1396 } else {
1397 const selectedPlans = getSelectedPlans(root); 1398 setPlanComparisonCards(root, selectedPlans); 1399 } 1400 } 1401 1402 root.addEventListener('click', function (e) { 1403 const removeBtn = e.target.closest('.remove'); 1404 if (!removeBtn || !removeBtn.closest('.plan-item')) { 1405 return; 1406 } 1407 e.preventDefault(); 1408 const planItem = removeBtn.closest('.plan-item'); 1409 const planId = planItem ? planItem.getAttribute('data-plan-id') : null; 1410 if (planId == null) { 1411 return; 1412 } 1413 if (typeof window.__dpPlanCompareRemoveFromTray === 'function') { 1414 window.__dpPlanCompareRemoveFromTray(planId); 1415 } else { 1416 const esc = escapeCssAttr(planId); 1417 const input = document.querySelector('.dp-compare input[data-plan-id="' + esc + '"]'); 1418 if (input) { 1419 input.checked = false; 1420 input.dispatchEvent(new Event('change', { bubbles: true })); 1421 } 1422 } 1423 }); 1424 1425 const compareButtons = root.querySelectorAll('.compare-plans'); 1426 compareButtons.forEach(function (btn) { 1427 btn.addEventListener('click', function (e) { 1428 e.preventDefault(); 1429 const raw = root.getAttribute('data-selected-plans') || ''; 1430 const selectedPlans = raw.split(',').map(function (s) { 1431 return s.trim(); 1432 }); 1433 if (selectedPlans.length > 1) { 1434 const zip = window.userAddress && window.userAddress.postalCode; 1435 const affid = window.DP_AFFID; 1436 const compareUrl = new URL('/compare-plans/', window.location.origin); 1437 const planIdsParam = selectedPlans.filter(Boolean).join(','); 1438 compareUrl.searchParams.set('plan_ids', planIdsParam); 1439 if (zip) { 1440 compareUrl.searchParams.set('zip', String(zip).trim()); 1441 } 1442 if (affid) { 1443 compareUrl.searchParams.set('affid', String(affid).trim()); 1444 } 1445 if (isFamilyPricingSelectedForCompare()) { 1446 compareUrl.searchParams.set('plan_type', 'family'); 1447 } 1448 window.location.assign(compareUrl.href); 1449 } else { 1450 setCompareModalTitleParts('Please select at least ', '2', ' plans to compare.'); 1451 showComparePlansModal(); 1452 } 1453 }); 1454 }); 1455 1456 syncTrayFromCompareCheckboxes(root); 1457 } 1458 1459 if (document.readyState === 'loading') { 1460 document.addEventListener('DOMContentLoaded', initPlanCompareSelector); 1461 } else { 1462 initPlanCompareSelector(); 1463 } 1464})(); 1465;
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.