1/** 2 * Consent surface guard â captcha forms, maps, and gated embeds. 3 * When the required category is denied, show a theme-matched blocking notice 4 * so visitors do not interact with a broken surface (or lose form input). 5 */ 6(function () { 7 'use strict'; 8 9 if (window.__ucpfConsentSurfaceGuard) { 10 return; 11 } 12 window.__ucpfConsentSurfaceGuard = true; 13 // Legacy flag used by earlier builds. 14 window.__ucpfCaptchaGuard = true; 15 16 /** 17 * When true: skip Elementor Motion FX / sticky layout recovery hacks only. 18 * Consent overlays, captcha covers, and video parking ALWAYS run on every 19 * builder (Elementor, Divi, Gutenberg, Bricks, etc.) â GDPR surfaces are not optional. 20 */ 21 function leaveBuildersAlone() { 22 var c = window.ucpfConfig || {}; 23 return c.leaveBuildersAlone !== false; 24 } 25 26 function isBuilderChrome(node) { 27 if (!node || node.nodeType !== 1) { 28 return false; 29 } 30 try { 31 if (node.closest) { 32 return !!( 33 node.closest('.elementor') || 34 node.closest('.elementor-element') || 35 node.closest('.elementor-widget') || 36 node.closest('[data-elementor-type]') || 37 node.closest('.e-con') || 38 node.closest('.et_pb_section') || 39 node.closest('.et_pb_module') || 40 node.closest('.brxe-section') || 41 node.closest('.fl-builder-content') || 42 node.closest('.oxygen-body') || 43 node.closest('.breakdance') 44 ); 45 } 46 } catch (eB) {} 47 return false; 48 } 49 50 var TOKEN_PROPS = [ 51 '--ucpf-black', 52 '--ucpf-surface', 53 '--ucpf-cream', 54 '--ucpf-muted', 55 '--ucpf-border', 56 '--ucpf-accent', 57 '--ucpf-accent-hover', 58 '--ucpf-accent-active', 59 '--ucpf-accent-2', 60 '--ucpf-on-accent', 61 '--ucpf-label', 62 '--ucpf-focus', 63 '--ucpf-focus-ring', 64 '--ucpf-shadow-panel', 65 '--ucpf-font-sans', 66 '--ucpf-radius-soft', 67 ]; 68 69 /** 70 * Provider-first markers (any theme/plugin), then popular WP builder wrappers. 71 * Broad [class*="â¦"] / [name*="â¦"] patterns are only used via form heuristic 72 * (see formHasCaptchaSignal) to avoid guarding non-form page widgets. 73 */ 74 var CAPTCHA_MARKERS = [ 75 // ââ Cloudflare Turnstile ââ 76 '.cf-turnstile', 77 '#cf-turnstile', 78 '.cf-turnstile-wrapper', 79 '[data-turnstile]', 80 '[name="cf-turnstile-response"]', 81 '[name*="cf-turnstile"]', 82 'iframe[src*="challenges.cloudflare.com"]', 83 'iframe[data-src*="challenges.cloudflare.com"]', 84 'iframe[src*="turnstile"]', 85 'iframe[data-src*="turnstile"]', 86 // ââ Google reCAPTCHA ââ 87 '.g-recaptcha', 88 '.grecaptcha-badge', 89 '.grecaptcha-logo', 90 '#g-recaptcha-response', 91 'textarea[name="g-recaptcha-response"]', 92 '[name*="g-recaptcha"]', 93 '[id*="g-recaptcha"]', 94 'iframe[src*="recaptcha"]', 95 'iframe[data-src*="recaptcha"]', 96 'iframe[src*="google.com/recaptcha"]', 97 'iframe[data-src*="google.com/recaptcha"]', 98 'iframe[title*="reCAPTCHA"]', 99 'iframe[title*="recaptcha"]', 100 // Empty widget hosts (Forminator / Elementor / custom) 101 'div[data-sitekey][data-size]', 102 'div[data-sitekey][data-theme]', 103 'div[data-sitekey][data-callback]', 104 // ââ hCaptcha ââ 105 '.h-captcha', 106 '[name="h-captcha-response"]', 107 '[name*="h-captcha"]', 108 'iframe[src*="hcaptcha.com"]', 109 'iframe[data-src*="hcaptcha.com"]', 110 'iframe[src*="newassets.hcaptcha.com"]', 111 'iframe[title*="hCaptcha"]', 112 'iframe[title*="hcaptcha"]', 113 // ââ Friendly Captcha ââ 114 '.frc-captcha', 115 '[class*="frc-captcha"]', 116 '[name*="frc-"]', 117 'iframe[src*="friendlycaptcha"]', 118 'iframe[data-src*="friendlycaptcha"]', 119 // ââ Gravity Forms ââ 120 '.ginput_recaptcha', 121 '.gform_recaptcha', 122 '.gfield--type-captcha', 123 '.ginput_container_captcha', 124 '.gform_captcha', 125 '.gform_captcha_button', 126 '.gform_turnstile', 127 '.gf-cloudflare-turnstile', 128 '.cf-turnstile-response', 129 '.gform_wrapper .gfield--type-captcha', 130 'form[id^="gform_"] .gfield--type-captcha', 131 'form[id^="gform_"] [data-sitekey]', 132 // ââ Forminator ââ 133 '.forminator-g-recaptcha', 134 '.forminator-hcaptcha', 135 '.forminator-captcha', 136 '.forminator-field-captcha', 137 '.forminator-captcha-left', 138 '.forminator-captcha-right', 139 '[class*="forminator"][class*="recaptcha"]', 140 '[class*="forminator"][class*="hcaptcha"]', 141 '[class*="forminator"][class*="turnstile"]', 142 // ââ WPForms / Fluent / CF7 / Ninja ââ 143 '.wpforms-recaptcha-container', 144 '.wpforms-is-recaptcha', 145 '.wpforms-captcha-invisible', 146 '.ff-el-recaptcha', 147 '.ff-el-hcaptcha', 148 '.wpcf7-form-control.g-recaptcha', 149 '.wpcf7-recaptcha', 150 '.nf-field .g-recaptcha', 151 // ââ Elementor Form ââ 152 '.elementor-field-type-recaptcha', 153 '.elementor-field-type-recaptcha_v3', 154 '.elementor-g-recaptcha', 155 // ââ Formidable / Quform / HappyForms / JetFormBuilder ââ 156 '.frm-g-recaptcha', 157 '[id*="frm_field"][class*="captcha"]', 158 '.frm_form_field[class*="captcha"]', 159 '.quform-captcha',
160 '.quform-recaptcha', 161 '.happyforms-part--recaptcha', 162 '.happyforms-part--captcha', 163 '[class*="jet-form"][class*="recaptcha"]', 164 '[class*="jet-form"][class*="hcaptcha"]', 165 '[class*="jet-form"][class*="turnstile"]', 166 // ââ Kadence / Spectra / Bricks / Divi ââ 167 '[class*="kb-recaptcha"]', 168 '[class*="uagb"][class*="recaptcha"]', 169 '[class*="brxe"][class*="recaptcha"]', 170 '.et_pb_contact_captcha', 171 // ââ WooCommerce captcha plugins ââ 172 '[class*="woo"][class*="recaptcha"]', 173 '[class*="woo"][class*="hcaptcha"]', 174 '[class*="woo"][class*="turnstile"]', 175 // ââ Simple Cloudflare Turnstile / BestWebSoft Google Captcha ââ 176 '[class*="cf-turnstile"]', 177 '.gglcptch', 178 '#gglcptch_recaptcha', 179 // ââ Amelia Booking (captcha often injected on payment step) ââ 180 '#amelia-container .g-recaptcha', 181 '#amelia-container .cf-turnstile', 182 '#amelia-container .h-captcha', 183 '.am-fs__wrapper .g-recaptcha', 184 '.am-fs__wrapper .cf-turnstile', 185 '.am-fs__wrapper .h-captcha', 186 // ââ UCPF-blocked security scripts ââ 187 'script[data-ucpf-category="security"][src*="recaptcha"]', 188 'script[data-ucpf-category="security"][src*="hcaptcha"]', 189 'script[data-ucpf-category="security"][src*="challenges.cloudflare.com"]', 190 'script[data-ucpf-category="security"][src*="friendlycaptcha"]', 191 'script[type="text/plain"][data-src*="recaptcha"]', 192 'script[type="text/plain"][data-src*="hcaptcha"]', 193 'script[type="text/plain"][data-src*="challenges.cloudflare.com"]', 194 'script[type="text/plain"][data-src*="friendlycaptcha"]', 195 ]; 196 197 /** Broad patterns only scanned inside <form> (heuristic pass). */ 198 var CAPTCHA_FORM_INNER_SELECTORS = [ 199 '[class*="g-recaptcha"]', 200 '[class*="grecaptcha"]', 201 '[class*="h-captcha"]', 202 '[class*="hcaptcha"]', 203 '[class*="turnstile"]', 204 '[class*="recaptcha"]', 205 '[class*="frc-captcha"]', 206 '[data-sitekey]', 207 ]; 208 209 var MAP_MARKERS = [ 210 '.wpgmza_map', 211 '.wpgmza_map_container', 212 '.gm-style', 213 '.mapboxgl-map', 214 '.maplibregl-map', 215 '.leaflet-container', 216 '.mapster-wp-maps-container', 217 '.mapster-wp-maps', 218 '.mapster-map', 219 '[id^="mapster-wp-maps"]', 220 '[data-mapbox-map]', 221 '.elementor-widget-google_maps', 222 '[data-widget_type="google_maps.default"]', 223 '[data-widget_type*="google_maps"]', 224 'gmp-map', 225 'gmp-advanced-marker', 226 'iframe[src*="google.com/maps"]', 227 'iframe[data-src*="google.com/maps"]', 228 'iframe[src*="maps.google"]', 229 'iframe[data-src*="maps.google"]', 230 'iframe[src*="openstreetmap.org"]', 231 'iframe[data-src*="openstreetmap.org"]', 232 'iframe[src*="mapbox.com"]', 233 'iframe[data-src*="mapbox.com"]', 234 'iframe[src*="mapme.com"]', 235 'iframe[data-src*="mapme.com"]', 236 'iframe[data-lazy-src*="mapme.com"]', 237 'iframe[src*="maphub.net"]', 238 'iframe[data-src*="maphub.net"]', 239 'iframe[src*="batchgeo.com"]', 240 'iframe[data-src*="batchgeo.com"]', 241 'iframe[src*="zeemaps.com"]', 242 'iframe[data-src*="zeemaps.com"]', 243 'iframe[src*="umap.openstreetmap.fr"]', 244 'iframe[data-src*="umap.openstreetmap.fr"]', 245 'iframe[src*="arcgis.com"]', 246 'iframe[data-src*="arcgis.com"]', 247 'iframe[src*="bing.com/maps"]', 248 'iframe[data-src*="bing.com/maps"]', 249 ]; 250 251 /** Calendly scheduling embeds (inline widgets + iframes; Elementor popups inject late). */ 252 var CALENDLY_MARKERS = [ 253 '.calendly-inline-widget', 254 '.calendly-badge-widget', 255 '.calendly-overlay', 256 '[data-url*="calendly.com"]', 257 'iframe[src*="calendly.com"]', 258 'iframe[data-src*="calendly.com"]', 259 'iframe[data-lazy-src*="calendly.com"]', 260 ]; 261 262 /** Third-party booking / work-request / form embeds (Jobber, Typeform, â¦). */ 263 var WIDGET_EMBED_MARKERS = [ 264 '.jobber-inline-work-request', 265 'iframe.jobber-work-request', 266 'iframe[src*="getjobber.com"]', 267 'iframe[data-src*="getjobber.com"]', 268 'iframe[src*="clienthub.getjobber.com"]', 269 'iframe[data-src*="clienthub.getjobber.com"]', 270 'iframe[src*="typeform.com"]', 271 'iframe[data-src*="typeform.com"]', 272 'iframe[src*="jotform.com"]', 273 'iframe[data-src*="jotform.com"]', 274 'iframe[src*="forms.hsforms.com"]', 275 'iframe[data-src*="forms.hsforms.com"]', 276 'iframe[src*="tally.so"]', 277 'iframe[data-src*="tally.so"]', 278 ]; 279 280 /** Amelia Booking â first-party WP booking UI (like Gravity Forms). Overlay only. */ 281 var AMELIA_FORM_MARKERS = [ 282 '#amelia-container', 283 '.am-fs__wrapper', 284 ]; 285 286 /** Smash Balloon YouTube Feed Pro â shortcode roots (not the #youtube-feed-loader spinner). */ 287 var YOUTUBE_FEED_MARKERS = [ 288 '.sb_youtube', 289 '[id^="sb_youtube"]', 290 '#regular-youtube-container', 291 '#live-youtube-container', 292 ]; 293
294 /** Smash Balloon Social Wall (Instagram / mixed social). */ 295 var SOCIAL_WALL_MARKERS = [ 296 '.sb-wall', 297 '[id^="sb-wall"]', 298 ]; 299 300 var VIDEO_MARKERS = [ 301 'iframe[src*="youtube.com"]', 302 'iframe[data-src*="youtube.com"]', 303 'iframe[data-lazy-src*="youtube"]', 304 'iframe[src*="youtu.be"]', 305 'iframe[data-src*="youtu.be"]', 306 'iframe[src*="youtube-nocookie.com"]', 307 'iframe[data-src*="youtube-nocookie.com"]', 308 'iframe[src*="player.vimeo.com"]', 309 'iframe[data-src*="player.vimeo.com"]', 310 'iframe[data-lazy-src*="vimeo"]', 311 'iframe[src*="vimeo.com"]', 312 'iframe[data-src*="vimeo.com"]', 313 // Lazy / CMP-style attrs â filtered by isVisualEmbedSurface (skips <script> / *.js APIs). 314 '[data-src*="youtube.com"]', 315 '[data-src*="youtu.be"]', 316 '[data-src*="youtube-nocookie.com"]', 317 '[data-src*="player.vimeo.com/video"]', 318 '[data-src*="vimeo.com/video"]', 319 '[data-lazy-src*="youtube"]', 320 '[data-lazy-src*="vimeo"]', 321 '[data-youtube]', 322 '[data-youtube-url]', 323 '[data-vimeo-url]', 324 '[data-vimeo-id]', 325 ]; 326 327 /** Builder / theme video shells that inject iframes later (often empty at paint). */ 328 var VIDEO_SHELL_SELECTORS = [ 329 // Elementor 330 '.elementor-widget-video', 331 '[data-widget_type="video.default"]', 332 '[data-widget_type="video"]', 333 '.elementor-video', 334 '.elementor-wrapper.elementor-open-inline', 335 '.elementor-background-video-container', 336 '.elementor-background-video-embed', 337 // Gutenberg 338 '.wp-block-embed-youtube', 339 '.wp-block-embed-vimeo', 340 '.wp-block-embed.is-provider-youtube', 341 '.wp-block-embed.is-provider-vimeo', 342 '.wp-block-embed.is-type-video', 343 '.wp-block-video', 344 // Divi 345 '.et_pb_video', 346 '.et_pb_video_box', 347 '.et_pb_slide_video', 348 // WPBakery / Visual Composer 349 '.wpb_video_widget', 350 '.wpb_video_wrapper', 351 '.vc_video-bg', 352 // Bricks 353 '.brxe-video', 354 '[data-script-id][class*="brxe-video"]', 355 // Beaver 356 '.fl-module-video', 357 '.fl-video', 358 // Oxygen / Breakdance-ish 359 '.oxy-video', 360 '.breakdance-video', 361 // Theme / generic oEmbed wrappers 362 '.wp-block-embed__wrapper', 363 '.fluidvids', 364 '.rll-youtube-player', 365 '.wp-video', 366 'lite-youtube', 367 'lite-vimeo', 368 ]; 369 370 /** 371 * WooCommerce / checkout surfaces that need Embeds & Widgets (functional) 372 * for PayPal, Stripe, Square, and similar payment widgets. 373 */ 374 var CHECKOUT_SELECTORS = [ 375 'form.woocommerce-checkout', 376 'form.checkout', 377 'form#order_review', 378 '.woocommerce-checkout', 379 '.woocommerce-order-pay', 380 '#payment', 381 '.wc-block-checkout', 382 '.wp-block-woocommerce-checkout', 383 '.wc-block-cart__submit-container', 384 ]; 385 386 /** @type {WeakMap<Element, string>} */ 387 var guarded = new WeakMap(); 388 389 function t(key, fallback) { 390 var i18n = (window.ucpfConfig && window.ucpfConfig.i18n) || {}; 391 return i18n[key] || fallback; 392 } 393 394 function categoryLabel(slug) { 395 var cats = (window.ucpfConfig && window.ucpfConfig.categories) || {}; 396 if (cats[slug] && cats[slug].label) { 397 return cats[slug].label; 398 } 399 return slug.charAt(0).toUpperCase() + slug.slice(1); 400 } 401 402 function hasCategoryConsent(slug) { 403 if (window.__ucpfDiscover) { 404 return true; 405 } 406 if (!slug || slug === 'necessary') { 407 return true; 408 } 409 if (window.UCPF && typeof window.UCPF.hasConsent === 'function') { 410 return !!window.UCPF.hasConsent(slug); 411 } 412 try { 413 var handoff = window.__ucpfConsentHandoff; 414 if (handoff && handoff.categories && handoff.categories[slug]) { 415 return true; 416 } 417 } catch (eHandoff) { /* ignore */ } 418 try { 419 var match = document.cookie.match(/(?:^|; )ucpf_consent=([^;]*)/); 420 if (match) { 421 var data = JSON.parse(decodeURIComponent(match[1])); 422 if (data && data.categories && data.categories[slug]) { 423 return true; 424 } 425 } 426 } catch (e) { /* ignore */ } 427 // Safari may lose the cookie across reload; honor storage backup / bridge. 428 try { 429 var cfg = window.ucpfConfig || {}; 430 var suffix = (cfg.storageSuffix || '').toString().replace(/[^a-zA-Z0-9_-]/g, ''); 431 var keys = []; 432 if (suffix) { 433 keys.push('ucpf_consent_backup_' + suffix, 'ucpf_consent_bridge_' + suffix); 434 } 435 keys.push('ucpf_consent_backup', 'ucpf_consent_bridge'); 436 for (var i = 0; i < keys.length; i++) { 437 var raw = null; 438 if (window.localStorage) { 439 raw = localStorage.getItem(keys[i]); 440 } 441 if (!raw && window.sessionStorage) { 442 raw = sessionStorage.getItem(keys[i]); 443 } 444 if (!raw) { 445 continue; 446 } 447 var parsed = JSON.parse(raw); 448 if (parsed && parsed.categories && parsed.categories[slug]) { 449 return true; 450 } 451 } 452 } catch (e2) { /* ignore */ } 453 return false;
454 } 455 456 function bannerTheme() { 457 var cfg = window.ucpfConfig || {}; 458 var theme = cfg.bannerTheme || 'classic'; 459 return String(theme).replace(/[^a-z0-9_]/gi, '') || 'classic'; 460 } 461 462 function applyTokenMap(el, map) { 463 if (!el || !map) { 464 return; 465 } 466 Object.keys(map).forEach(function (prop) { 467 var v = map[prop]; 468 if (v === undefined || v === null || v === '') { 469 return; 470 } 471 var name = prop.indexOf('--') === 0 ? prop : '--' + prop; 472 el.style.setProperty(name, String(v)); 473 }); 474 } 475 476 function syncThemeOnto(el) { 477 if (!el) { 478 return; 479 } 480 var theme = bannerTheme(); 481 Array.prototype.slice.call(el.classList).forEach(function (cls) { 482 if (cls.indexOf('ucpf-theme-') === 0) { 483 el.classList.remove(cls); 484 } 485 }); 486 el.classList.add('ucpf-theme-' + theme); 487 488 // 1) Authoritative tokens from PHP (active preset + Banner & Branding customs). 489 var cfg = window.ucpfConfig || {}; 490 if (cfg.themeTokens && typeof cfg.themeTokens === 'object') { 491 applyTokenMap(el, cfg.themeTokens); 492 } 493 494 // 2) Live refine from #ucpf-root when present (keeps guard identical to banner). 495 var root = document.getElementById('ucpf-root'); 496 if (root && window.getComputedStyle) { 497 var cs = window.getComputedStyle(root); 498 TOKEN_PROPS.forEach(function (prop) { 499 var v = cs.getPropertyValue(prop); 500 if (v && String(v).trim()) { 501 el.style.setProperty(prop, String(v).trim()); 502 } 503 }); 504 // Also copy any other --ucpf-* the theme may set. 505 try { 506 for (var i = 0; i < cs.length; i++) { 507 var name = cs[i]; 508 if (name && name.indexOf('--ucpf-') === 0) { 509 var val = cs.getPropertyValue(name); 510 if (val && String(val).trim()) { 511 el.style.setProperty(name, String(val).trim()); 512 } 513 } 514 } 515 } catch (e) { 516 /* older browsers */ 517 } 518 } 519 } 520 521 function resyncAllGuards() { 522 var nodes = document.querySelectorAll('.ucpf-consent-guard, .ucpf-captcha-guard'); 523 Array.prototype.forEach.call(nodes, syncThemeOnto); 524 } 525 526 function findFormForNode(node) { 527 if (!node) { 528 return null; 529 } 530 if (node.tagName === 'FORM') { 531 return node; 532 } 533 if (node.closest) { 534 var form = 535 node.closest('form') || 536 node.closest('form.forminator-custom-form') || 537 node.closest('form.forminator-ui') || 538 node.closest('form.wpforms-form') || 539 node.closest('form.wpcf7-form') || 540 node.closest('form.frm-fluent-form') || 541 node.closest('form.nf-form-content') || 542 node.closest('form.elementor-form') || 543 node.closest('form.frm-show-form') || 544 node.closest('form.formidable') || 545 node.closest('form.quform-form') || 546 node.closest('form.happyforms-form') || 547 node.closest('form.jet-form-builder') || 548 node.closest('form.kb-form') || 549 node.closest('form.et_pb_contact_form'); 550 if (form) { 551 return form; 552 } 553 // Captcha sometimes sits in a wrapper just outside / beside the form markup. 554 var shell = 555 node.closest('.gform_wrapper') || 556 node.closest('.gform_body') || 557 node.closest('.forminator-custom-form') || 558 node.closest('.elementor-shortcode') || 559 node.closest('.wpforms-container') || 560 node.closest('.wpcf7') || 561 node.closest('.elementor-widget-form') || 562 node.closest('.frm_forms') || 563 node.closest('.formidable') || 564 node.closest('.quform-form-wrap') || 565 node.closest('.happyforms-form') || 566 node.closest('.jet-form-builder') || 567 node.closest('.kb-form') || 568 node.closest('.et_pb_contact_form'); 569 if (shell) { 570 var nested = 571 shell.querySelector('form') || 572 (shell.tagName === 'FORM' ? shell : null) || 573 (shell.id && String(shell.id).indexOf('gform_') === 0 ? shell : null); 574 if (nested) { 575 return nested; 576 } 577 // GF: cover the wrapper when the <form> node is missing / replaced mid-AJAX. 578 if (shell.classList && shell.classList.contains('gform_wrapper')) { 579 return shell; 580 } 581 } 582 } 583 var parent = node.parentElement; 584 for (var i = 0; parent && i < 8; i++) { 585 if (parent.tagName === 'FORM') { 586 return parent; 587 } 588 parent = parent.parentElement; 589 } 590 return null; 591 } 592 593 function queryAll(sels) { 594 var out = []; 595 var seen = [];
596 sels.forEach(function (sel) { 597 var nodes; 598 try { 599 nodes = document.querySelectorAll(sel); 600 } catch (e) { 601 return; 602 } 603 Array.prototype.forEach.call(nodes, function (node) { 604 for (var i = 0; i < seen.length; i++) { 605 if (seen[i] === node) { 606 return; 607 } 608 } 609 seen.push(node); 610 out.push(node); 611 }); 612 }); 613 return out; 614 } 615 616 function lockFields(form) { 617 var controls = form.querySelectorAll('input, textarea, select, button'); 618 Array.prototype.forEach.call(controls, function (el) { 619 if (el.getAttribute('data-ucpf-captcha-skip') === '1') { 620 return; 621 } 622 if (!el.hasAttribute('data-ucpf-captcha-prev-disabled')) { 623 el.setAttribute('data-ucpf-captcha-prev-disabled', el.disabled ? '1' : '0'); 624 } 625 if (!el.hasAttribute('data-ucpf-captcha-prev-readonly') && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) { 626 el.setAttribute('data-ucpf-captcha-prev-readonly', el.readOnly ? '1' : '0'); 627 } 628 if (el.tagName === 'BUTTON' || (el.tagName === 'INPUT' && /^(submit|button|image)$/i.test(el.type))) { 629 el.disabled = true; 630 } else if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { 631 el.readOnly = true; 632 el.disabled = true; 633 } else { 634 el.disabled = true; 635 } 636 }); 637 form.setAttribute('aria-disabled', 'true'); 638 } 639 640 function unlockFields(form) { 641 var controls = form.querySelectorAll('[data-ucpf-captcha-prev-disabled], [data-ucpf-captcha-prev-readonly]'); 642 Array.prototype.forEach.call(controls, function (el) { 643 var wasDisabled = el.getAttribute('data-ucpf-captcha-prev-disabled'); 644 var wasReadonly = el.getAttribute('data-ucpf-captcha-prev-readonly'); 645 if (wasDisabled !== null) { 646 el.disabled = wasDisabled === '1'; 647 el.removeAttribute('data-ucpf-captcha-prev-disabled'); 648 } 649 if (wasReadonly !== null) { 650 el.readOnly = wasReadonly === '1'; 651 el.removeAttribute('data-ucpf-captcha-prev-readonly'); 652 } 653 }); 654 form.removeAttribute('aria-disabled'); 655 } 656 657 function copyForKind(kind, category, categories) { 658 var label = categoryLabel(category); 659 var cats = Array.isArray(categories) && categories.length ? categories : category ? [category] : []; 660 if (kind === 'checkout') { 661 var needsSec = cats.indexOf('security') !== -1; 662 var needsFun = cats.indexOf('functional') !== -1; 663 if (needsSec && needsFun) { 664 return { 665 title: t('checkoutGuardCombinedTitle', 'Checkout needs Security & Embeds'), 666 body: t( 667 'checkoutGuardCombinedBody', 668 'Checkout uses anti-spam protection (CAPTCHA) and payment / shipping widgets. Enable Security and Embeds & Widgets together before entering details so nothing is lost.' 669 ), 670 enable: t('checkoutGuardCombinedEnable', 'Enable required cookies & continue'), 671 categories: cats, 672 }; 673 } 674 return { 675 title: t('checkoutGuardTitle', 'Checkout needs Embeds & Widgets'), 676 body: t( 677 'checkoutGuardBody', 678 'Payment and shipping widgets (PayPal, Stripe, Square, Shippo, UPS, USPS, and similar) need Embeds & Widgets cookies before checkout can request rates, validate addresses, or load payment buttons. Enable them before entering details so nothing is lost.' 679 ), 680 enable: t('checkoutGuardEnable', 'Enable Embeds & Widgets & continue'), 681 categories: cats.length ? cats : ['functional'], 682 }; 683 } 684 if (kind === 'captcha') { 685 return { 686 title: t('captchaGuardTitle', 'CAPTCHA required before you can use this form'), 687 body: t( 688 'captchaGuardBody', 689 'This form uses anti-spam protection (CAPTCHA / Turnstile) that needs Security cookies. Enable Security before filling fields so your entries are not lost.' 690 ), 691 enable: t('captchaGuardEnable', 'Enable Security & continue'), 692 categories: cats.length ? cats : ['security'], 693 }; 694 } 695 if (kind === 'map') { 696 return { 697 title: t('embedGuardMapTitle', 'Map blocked until you allow Marketing & Embeds'), 698 body: t( 699 'embedGuardMapBody', 700 'This map loads third-party content we cannot fully control. Enable Marketing and Embeds & Widgets so it can load.' 701 ), 702 enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'), 703 categories: ensureEmbedConsentCategories(cats), 704 }; 705 } 706 // YouTube / Vimeo: marketing pixels + embed players often need both. 707 if (kind === 'youtube' || kind === 'vimeo') { 708 var videoCats = ensureEmbedConsentCategories(cats); 709 return { 710 title: t('embedGuardVideoTitle', 'Video blocked until you allow Marketing & Embeds'), 711 body: t( 712 'embedGuardVideoBody', 713 'This embedded video needs Marketing and Embeds & Widgets cookies. Enable both to load the player.' 714 ), 715 enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'), 716 categories: videoCats, 717 }; 718 } 719 if (kind === 'embed' && category === 'marketing') { 720 return { 721 title: t('embedGuardMarketingTitle', 'Content blocked until you allow Marketing & Embeds'), 722 body: t( 723 'embedGuardMarketingBody', 724 'This embedded content may load Marketing and Embeds & Widgets cookies. Enable both to continue.' 725 ), 726 enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'), 727 categories: ensureEmbedConsentCategories(cats.length ? cats : ['marketing']), 728 }; 729 } 730 if (kind === 'calendly') { 731 return { 732 title: t('embedGuardCalendlyTitle', 'Scheduling blocked until you allow Marketing & Embeds'), 733 body: t( 734 'embedGuardCalendlyBody', 735 'This scheduling embed (Calendly) loads third-party content we cannot fully control. Enable Marketing and Embeds & Widgets to continue.' 736 ), 737 enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'), 738 categories: ensureEmbedConsentCategories(cats), 739 }; 740 } 741 if (kind === 'widget' || (kind === 'embed' && category === 'functional')) { 742 return { 743 title: t('embedGuardWidgetTitle', 'Form / widget blocked until you allow Marketing & Embeds'), 744 body: t( 745 'embedGuardWidgetBody', 746 'This embedded form or widget loads third-party content we cannot fully control. Enable Marketing and Embeds & Widgets to continue.' 747 ), 748 enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'), 749 categories: ensureEmbedConsentCategories(cats), 750 }; 751 } 752 return { 753 title: t('embedGuardGenericTitle', 'Content blocked until you allow the required cookies').replace('%s', label), 754 body: t( 755 'embedGuardGenericBody', 756 'This content needs additional cookies before it can load. Enable the required category to continue.' 757 ), 758 enable: (t('embedGuardEnableCategory', 'Enable %s & continue') || 'Enable %s & continue').replace('%s', label), 759 categories: cats.length ? cats : [category], 760 }; 761 } 762 763 /** 764 * Third-party iframes/embeds can load Marketing trackers we cannot inspect â 765 * always unlock Marketing + Embeds together (same rule as video players). 766 */ 767 function ensureEmbedConsentCategories(categories) { 768 var out = []; 769 var seen = {}; 770 function add(slug) { 771 if (!slug || seen[slug]) { 772 return; 773 } 774 seen[slug] = true; 775 out.push(slug); 776 } 777 if (Array.isArray(categories)) {
778 categories.forEach(add); 779 } 780 add('marketing'); 781 add('functional'); 782 return out; 783 } 784 785 /** @deprecated Use ensureEmbedConsentCategories â kept for older call sites. */ 786 function ensureVideoConsentCategories(categories) { 787 return ensureEmbedConsentCategories(categories); 788 } 789 790 function onEnableCategories(categories, e) { 791 if (e) { 792 e.preventDefault(); 793 e.stopPropagation(); 794 } 795 var list = Array.isArray(categories) ? categories.filter(Boolean) : [categories]; 796 if (!list.length) { 797 return; 798 } 799 // Pair Marketing â Embeds for embed unlocks. Pure Security (CAPTCHA) stays alone. 800 var securityOnly = list.indexOf('security') !== -1 && list.indexOf('marketing') === -1 && list.indexOf('analytics') === -1 && list.indexOf('functional') === -1; 801 if (!securityOnly && (list.indexOf('marketing') !== -1 || list.indexOf('functional') !== -1)) { 802 if (list.indexOf('marketing') === -1) { 803 list = list.concat(['marketing']); 804 } 805 if (list.indexOf('functional') === -1) { 806 list = list.concat(['functional']); 807 } 808 } 809 if (!window.UCPF || typeof window.UCPF.getConsent !== 'function' || typeof window.UCPF.setConsent !== 'function') { 810 if (window.UCPF && window.UCPF.openPreferences) { 811 window.UCPF.openPreferences({ highlight: list[0] }); 812 } 813 return; 814 } 815 var current = window.UCPF.getConsent() || {}; 816 var cats = Object.assign({}, current.categories || {}); 817 cats.necessary = true; 818 list.forEach(function (slug) { 819 cats[slug] = true; 820 }); 821 window.UCPF.setConsent({ 822 state: 'custom', 823 categories: cats, 824 services: current.services || {}, 825 uuid: current.uuid || '', 826 }); 827 } 828 829 function onEnableCategory(category, e) { 830 onEnableCategories([category], e); 831 } 832 833 function hasAllCategories(categories) { 834 var list = Array.isArray(categories) ? categories : categories ? [categories] : []; 835 if (!list.length) { 836 return true; 837 } 838 for (var i = 0; i < list.length; i++) { 839 if (!hasCategoryConsent(list[i])) { 840 return false; 841 } 842 } 843 return true; 844 } 845 846 function isCheckoutSurface(node) { 847 if (!node) { 848 return false; 849 } 850 if (node.matches && node.matches(CHECKOUT_SELECTORS.join(','))) { 851 return true; 852 } 853 if (node.closest) { 854 for (var i = 0; i < CHECKOUT_SELECTORS.length; i++) { 855 try { 856 if (node.closest(CHECKOUT_SELECTORS[i])) { 857 return true; 858 } 859 } catch (eClose) { 860 /* ignore invalid selector */ 861 } 862 } 863 } 864 return false; 865 } 866 867 function onOpenPrefs(category, e) { 868 if (e) { 869 e.preventDefault(); 870 e.stopPropagation(); 871 } 872 if (window.UCPF && typeof window.UCPF.openPreferences === 'function') { 873 window.UCPF.openPreferences({ highlight: category }); 874 } 875 } 876 877 function buildPanel(kind, category, categories) { 878 var copy = copyForKind(kind, category, categories); 879 var enableCats = copy.categories && copy.categories.length ? copy.categories : [category]; 880 var panel = document.createElement('div'); 881 panel.className = 'ucpf-consent-guard__panel'; 882 panel.setAttribute('role', 'alert'); 883 panel.setAttribute('aria-live', 'assertive'); 884 885 var title = document.createElement('p'); 886 title.className = 'ucpf-consent-guard__title'; 887 title.textContent = copy.title; 888 889 var body = document.createElement('p'); 890 body.className = 'ucpf-consent-guard__body'; 891 body.textContent = copy.body; 892 893 var actions = document.createElement('div'); 894 actions.className = 'ucpf-consent-guard__actions'; 895 896 var enableBtn = document.createElement('button'); 897 enableBtn.type = 'button'; 898 enableBtn.className = 'ucpf-consent-guard__btn ucpf-consent-guard__btn--primary'; 899 enableBtn.setAttribute('data-ucpf-captcha-skip', '1'); 900 enableBtn.textContent = copy.enable; 901 enableBtn.addEventListener( 902 'click', 903 function (ev) { 904 // Third-party iframes/embeds cannot be inspected â always grant Marketing + Embeds. 905 var dualEmbed = 906 kind === 'youtube' || 907 kind === 'vimeo' || 908 kind === 'widget' || 909 kind === 'calendly' || 910 kind === 'map' || 911 kind === 'embed'; 912 var catsToEnable = dualEmbed ? ensureEmbedConsentCategories(enableCats) : enableCats; 913 onEnableCategories(catsToEnable, ev); 914 }, 915 true 916 ); 917 918 var prefsBtn = document.createElement('button'); 919 prefsBtn.type = 'button'; 920 prefsBtn.className = 'ucpf-consent-guard__btn ucpf-consent-guard__btn--ghost'; 921 prefsBtn.setAttribute('data-ucpf-captcha-skip', '1'); 922 prefsBtn.textContent = t('captchaGuardPrefs', 'Cookie Settings'); 923 prefsBtn.addEventListener('click', function (ev) {
924 onOpenPrefs(enableCats[0] || category, ev); 925 }); 926 927 actions.appendChild(enableBtn); 928 actions.appendChild(prefsBtn); 929 panel.appendChild(title); 930 panel.appendChild(body); 931 panel.appendChild(actions); 932 return panel; 933 } 934 935 function isFormShell(node) { 936 if (!node) { 937 return false; 938 } 939 if (node.tagName === 'FORM') { 940 return true; 941 } 942 if (!node.classList) { 943 return false; 944 } 945 return ( 946 node.classList.contains('woocommerce-checkout') || 947 node.classList.contains('wc-block-checkout') || 948 node.classList.contains('wp-block-woocommerce-checkout') || 949 node.classList.contains('forminator-custom-form') || 950 node.classList.contains('forminator-ui') || 951 node.classList.contains('wpforms-form') || 952 node.classList.contains('wpcf7-form') || 953 node.classList.contains('elementor-form') || 954 node.classList.contains('frm-show-form') || 955 node.classList.contains('formidable') || 956 node.classList.contains('quform-form') || 957 node.classList.contains('happyforms-form') || 958 node.classList.contains('jet-form-builder') || 959 node.classList.contains('kb-form') || 960 node.classList.contains('et_pb_contact_form') || 961 node.id === 'customer_details' || 962 node.id === 'order_review' 963 ); 964 } 965 966 /** 967 * Never decorate / wrap document chrome â wrapping <body> nukes Elementor/GF/sticky. 968 * @param {Element|null} node 969 * @return {boolean} 970 */ 971 function isForbiddenGuardHost(node) { 972 if (!node || node.nodeType !== 1) { 973 return true; 974 } 975 var tag = String(node.tagName || '').toUpperCase(); 976 if (tag === 'HTML' || tag === 'HEAD' || tag === 'BODY') { 977 return true; 978 } 979 if (node.id === 'ucpf-root') { 980 return true; 981 } 982 return false; 983 } 984 985 /** Accessibility toolbar CDN / hosts â never park or cover. */ 986 function isUserWayUrl(url) { 987 var u = String(url || '').toLowerCase(); 988 if (!u) { 989 return false; 990 } 991 return ( 992 u.indexOf('cdn.userway.org') !== -1 || 993 u.indexOf('api.userway.org') !== -1 || 994 u.indexOf('userway.org') !== -1 995 ); 996 } 997 998 /** 999 * UserWay DOM / scripts â fully hands-off (ADA toolbar). 1000 * @param {Element|null} node 1001 * @return {boolean} 1002 */ 1003 function isUserWayNode(node) { 1004 if (!node || node.nodeType !== 1) { 1005 return false; 1006 } 1007 try { 1008 var id = String(node.id || '').toLowerCase(); 1009 if (id.indexOf('userway') !== -1 || id === 'userwayaccessibilityicon') { 1010 return true; 1011 } 1012 var cls = String(node.className || '').toLowerCase(); 1013 if (cls.indexOf('userway') !== -1 || cls.indexOf('uwy') !== -1) { 1014 return true; 1015 } 1016 var blob = ( 1017 (node.getAttribute('data-src') || '') + 1018 ' ' + 1019 (node.getAttribute('src') || '') + 1020 ' ' + 1021 (node.getAttribute('data-account') || '') + 1022 ' ' + 1023 (node.getAttribute('href') || '') 1024 ).toLowerCase(); 1025 if (isUserWayUrl(blob)) { 1026 return true; 1027 } 1028 if (node.closest) { 1029 if ( 1030 node.closest('#userwayAccessibilityIcon') || 1031 node.closest('[id*="userway" i]') || 1032 node.closest('[class*="userway" i]') || 1033 node.closest('.userway_buttons_wrapper') || 1034 node.closest('.uwy') 1035 ) { 1036 return true; 1037 } 1038 } 1039 } catch (eUw) { 1040 /* :i selector unsupported â fall through */ 1041 try { 1042 if (node.closest && (node.closest('.userway_buttons_wrapper') || node.closest('#userwayAccessibilityIcon'))) { 1043 return true; 1044 } 1045 } catch (e2) { /* ignore */ } 1046 } 1047 return false; 1048 } 1049 1050 function isBackgroundVideoOwner(node) { 1051 if (!node || !node.getAttribute) { 1052 return false; 1053 } 1054 var settings = node.getAttribute('data-settings') || ''; 1055 return settings.indexOf('background_video_link') !== -1; 1056 } 1057 1058 function isBackgroundVideoShell(node) { 1059 if (!node || !node.classList) { 1060 return false; 1061 } 1062 if (isBackgroundVideoOwner(node)) { 1063 return true; 1064 } 1065 return ( 1066 node.classList.contains('elementor-background-video-container') || 1067 node.classList.contains('elementor-background-video-embed') 1068 ); 1069 } 1070 1071 /** 1072 * Prefer the Elementor e-con that owns background_video_link. 1073 * Decorating .elementor-background-video-container fights Elementor's absolute fill. 1074 * 1075 * @param {Element} node 1076 * @return {Element|null} 1077 */
1078 function resolveBackgroundVideoHost(node) { 1079 if (!node) { 1080 return null; 1081 } 1082 if (isBackgroundVideoOwner(node)) { 1083 return node; 1084 } 1085 if (node.closest) { 1086 var owner = node.closest('[data-settings*="background_video_link"]'); 1087 if (owner) { 1088 return owner; 1089 } 1090 } 1091 if (node.classList.contains('elementor-background-video-embed')) { 1092 var box = node.closest ? node.closest('.elementor-background-video-container') : node.parentElement; 1093 return resolveBackgroundVideoHost(box || node); 1094 } 1095 if (node.classList.contains('elementor-background-video-container') && node.parentElement) { 1096 return node.parentElement; 1097 } 1098 return node; 1099 } 1100 1101 /** 1102 * Smash Balloon YouTube Feed / Social Wall roots â decorate in place. 1103 * Never treat #youtube-feed-loader (spinner) as a cover host. 1104 * 1105 * @param {Element|null} node 1106 * @return {boolean} 1107 */ 1108 function isYoutubeFeedLoader(node) { 1109 if (!node || node.nodeType !== 1) { 1110 return false; 1111 } 1112 if (node.id === 'youtube-feed-loader') { 1113 return true; 1114 } 1115 try { 1116 return !!(node.closest && node.closest('#youtube-feed-loader')); 1117 } catch (eLoad) { 1118 return false; 1119 } 1120 } 1121 1122 function isSmashBalloonFeedHost(node) { 1123 if (!node || node.nodeType !== 1) { 1124 return false; 1125 } 1126 if (isYoutubeFeedLoader(node)) { 1127 return false; 1128 } 1129 var id = String(node.id || ''); 1130 if (id === 'regular-youtube-container' || id === 'live-youtube-container') { 1131 return true; 1132 } 1133 if (id.indexOf('sb_youtube') === 0 || id.indexOf('sb-wall') === 0) { 1134 return true; 1135 } 1136 if (!node.classList) { 1137 return false; 1138 } 1139 return node.classList.contains('sb_youtube') || node.classList.contains('sb-wall'); 1140 } 1141 1142 function youtubeFeedHostContainer(node) { 1143 if (!node || isYoutubeFeedLoader(node)) { 1144 return null; 1145 } 1146 if (node.classList && node.classList.contains('sb_youtube')) { 1147 return node; 1148 } 1149 if (node.closest) { 1150 var feed = node.closest('.sb_youtube'); 1151 if (feed && !isYoutubeFeedLoader(feed)) { 1152 return feed; 1153 } 1154 } 1155 if (node.querySelector) { 1156 var inner = node.querySelector('.sb_youtube'); 1157 if (inner && !isYoutubeFeedLoader(inner)) { 1158 return inner; 1159 } 1160 } 1161 if (node.id === 'regular-youtube-container' || node.id === 'live-youtube-container') { 1162 return node; 1163 } 1164 return isSmashBalloonFeedHost(node) ? node : null; 1165 } 1166 1167 function socialWallHostContainer(node) { 1168 if (!node || node.nodeType !== 1) { 1169 return null; 1170 } 1171 if (node.classList && node.classList.contains('sb-wall')) { 1172 return node; 1173 } 1174 if (node.closest) { 1175 var wall = node.closest('.sb-wall'); 1176 if (wall) { 1177 return wall; 1178 } 1179 } 1180 if (node.querySelector) { 1181 var innerWall = node.querySelector('.sb-wall'); 1182 if (innerWall) { 1183 return innerWall; 1184 } 1185 } 1186 var id = String(node.id || ''); 1187 if (id.indexOf('sb-wall') === 0) { 1188 return node; 1189 } 1190 return null; 1191 } 1192 1193 function isBuilderShell(node) { 1194 if (!node || !node.classList) { 1195 return false; 1196 } 1197 if (node.getAttribute('data-widget_type') === 'video.default') { 1198 return true; 1199 } 1200 if (node.getAttribute('data-widget_type') === 'html.default') { 1201 return true; 1202 } 1203 if (node.getAttribute('data-widget_type') === 'google_maps.default') { 1204 return true; 1205 } 1206 var wt = node.getAttribute('data-widget_type') || ''; 1207 if (wt.indexOf('google_maps') !== -1) { 1208 return true; 1209 } 1210 if (isBackgroundVideoShell(node) || isBackgroundVideoOwner(node)) { 1211 return true; 1212 } 1213 if (isMapShell(node)) { 1214 return true; 1215 } 1216 return ( 1217 node.classList.contains('elementor-widget-video') || 1218 node.classList.contains('elementor-widget-html') || 1219 node.classList.contains('elementor-widget-google_maps') || 1220 node.classList.contains('elementor-widget-container') || 1221 node.classList.contains('et_pb_video') || 1222 node.classList.contains('wpb_video_widget') || 1223 node.classList.contains('fl-module-video') || 1224 node.classList.contains('brxe-video') || 1225 node.classList.contains('wp-block-embed-youtube') || 1226 node.classList.contains('wp-block-embed-vimeo') || 1227 node.classList.contains('wp-block-video') || 1228 node.classList.contains('wp-block-embed') || 1229 node.classList.contains('jobber-inline-work-request') || 1230 isSmashBalloonFeedHost(node) 1231 ); 1232 } 1233 1234 /** 1235 * Outer map widget shells â decorate in place (do not wrap / nest covers). 1236 * 1237 * @param {Element} node 1238 * @return {boolean} 1239 */ 1240 function isMapShell(node) { 1241 if (!node || !node.classList) { 1242 return false;
1243 } 1244 if (node.classList.contains('elementor-widget-google_maps')) { 1245 return true; 1246 } 1247 if (node.classList.contains('wpgmza_map_container') || node.classList.contains('wpgmza_map')) { 1248 return true; 1249 } 1250 if ( 1251 node.classList.contains('mapster-wp-maps-container') || 1252 node.classList.contains('mapster-wp-maps') || 1253 node.classList.contains('mapster-map') 1254 ) { 1255 return true; 1256 } 1257 var id = String(node.id || ''); 1258 if (id.indexOf('mapster-wp-maps') === 0) { 1259 return true; 1260 } 1261 return false; 1262 } 1263 1264 /** 1265 * Prefer the outermost map host so nested markers (gm-style inside wpgmza, etc.) 1266 * do not each get their own consent cover. 1267 * 1268 * @param {Element} node 1269 * @return {Element|null} 1270 */ 1271 function resolveMapHost(node) { 1272 if (!node || node.nodeType !== 1) { 1273 return null; 1274 } 1275 if (node.closest) { 1276 var elWidget = 1277 node.closest('.elementor-widget-google_maps') || 1278 node.closest('[data-widget_type="google_maps.default"]') || 1279 node.closest('[data-widget_type*="google_maps"]'); 1280 if (elWidget) { 1281 return elWidget; 1282 } 1283 var wpgmzaOuter = node.closest('.wpgmza_map_container'); 1284 if (wpgmzaOuter) { 1285 return wpgmzaOuter; 1286 } 1287 var mapsterOuter = 1288 node.closest('.mapster-wp-maps-container') || 1289 node.closest('.mapster-wp-maps') || 1290 node.closest('[id^="mapster-wp-maps"]'); 1291 if (mapsterOuter) { 1292 return mapsterOuter; 1293 } 1294 var leaflet = node.closest('.leaflet-container'); 1295 if (leaflet) { 1296 return leaflet; 1297 } 1298 var mapbox = node.closest('.mapboxgl-map'); 1299 if (mapbox) { 1300 return mapbox; 1301 } 1302 var maplibre = node.closest('.maplibregl-map'); 1303 if (maplibre && !node.closest('.mapster-wp-maps, .mapster-wp-maps-container')) { 1304 return maplibre; 1305 } 1306 } 1307 // Google's internal .gm-style is never a good cover host by itself. 1308 if (node.classList && node.classList.contains('gm-style')) { 1309 if (node.parentElement) { 1310 var parentMap = resolveMapHost(node.parentElement); 1311 if (parentMap && parentMap !== node) { 1312 return parentMap; 1313 } 1314 return node.parentElement; 1315 } 1316 } 1317 if (node.tagName === 'IFRAME' && node.parentElement) { 1318 if (node.parentElement.classList && node.parentElement.classList.contains('ucpf-consent-guard')) { 1319 return node; 1320 } 1321 var iframeHost = resolveMapHost(node.parentElement); 1322 return iframeHost || node.parentElement; 1323 } 1324 return node; 1325 } 1326 1327 /** 1328 * Elementor (and similar) video widgets that gate before an iframe exists. 1329 * Empty .elementor-video shells measure ~0px â absolute overlays then vanish. 1330 * 1331 * @param {Element} host 1332 * @return {boolean} 1333 */ 1334 function isCollapsedVideoShell(host) { 1335 if (!host || !host.classList) { 1336 return false; 1337 } 1338 if ( 1339 host.classList.contains('elementor-widget-video') || 1340 host.getAttribute('data-widget_type') === 'video.default' || 1341 host.classList.contains('et_pb_video') || 1342 host.classList.contains('wpb_video_widget') || 1343 host.classList.contains('fl-module-video') || 1344 host.classList.contains('brxe-video') || 1345 host.classList.contains('wp-block-embed-youtube') || 1346 host.classList.contains('wp-block-embed-vimeo') 1347 ) { 1348 return true; 1349 } 1350 var kind = host.getAttribute('data-ucpf-guard-kind') || ''; 1351 return kind === 'youtube' || kind === 'vimeo'; 1352 } 1353 1354 /** 1355 * Map widgets that often measure ~0px before tiles paint. 1356 * 1357 * @param {Element} host 1358 * @return {boolean} 1359 */ 1360 function isCollapsedMapShell(host) { 1361 if (!host || host.nodeType !== 1) { 1362 return false; 1363 } 1364 if (isMapShell(host)) { 1365 return true; 1366 } 1367 var kind = host.getAttribute('data-ucpf-guard-kind') || ''; 1368 if (kind === 'map') { 1369 return true; 1370 } 1371 if (!host.classList) { 1372 return false; 1373 } 1374 return ( 1375 host.classList.contains('elementor-widget-google_maps') || 1376 host.classList.contains('wpgmza_map_container') || 1377 host.classList.contains('wpgmza_map') || 1378 host.classList.contains('mapster-wp-maps-container') || 1379 host.classList.contains('mapster-wp-maps') || 1380 host.classList.contains('maplibregl-map') || 1381 host.classList.contains('mapboxgl-map') || 1382 host.classList.contains('leaflet-container') 1383 ); 1384 } 1385 1386 /** 1387 * True for embed/widget consent hosts (not forms, not background video). 1388 * Used for the generic collapsed-shell floor (Calendly, HTML widgets, etc.). 1389 * 1390 * @param {Element} host 1391 * @return {boolean} 1392 */ 1393 function isEmbedGuardHost(host) { 1394 if (!host || host.nodeType !== 1) { 1395 return false;
1396 } 1397 if (host.classList) { 1398 if (host.classList.contains('ucpf-consent-guard--form')) { 1399 return false; 1400 } 1401 if (host.classList.contains('ucpf-consent-guard--bg')) { 1402 return false; 1403 } 1404 if (host.classList.contains('ucpf-consent-guard--embed')) { 1405 return true; 1406 } 1407 } 1408 var kind = host.getAttribute('data-ucpf-guard-kind') || ''; 1409 return ( 1410 kind === 'calendly' || 1411 kind === 'widget' || 1412 kind === 'embed' || 1413 kind === 'youtube' || 1414 kind === 'vimeo' || 1415 kind === 'map' 1416 ); 1417 } 1418 1419 /** 1420 * Apply a modest min-height so absolute inset panels have a paintable box 1421 * when the host is still collapsed (hidden popup, empty widget shell, etc.). 1422 * 1423 * @param {Element} host 1424 * @return {void} 1425 */ 1426 function applyEmbedFloor(host) { 1427 if (!host || host.nodeType !== 1) { 1428 return; 1429 } 1430 try { 1431 host.style.setProperty('min-height', '22rem', 'important'); 1432 host.style.setProperty('width', '100%', 'important'); 1433 host.setAttribute('data-ucpf-size-locked', '1'); 1434 host.setAttribute('data-ucpf-guard-min-h', 'embed-floor'); 1435 host.setAttribute('data-ucpf-aspect-fallback', '1'); 1436 var floorInner = 1437 host.querySelector('.calendly-inline-widget') || 1438 host.querySelector('[data-url*="calendly"]') || 1439 host.querySelector('iframe[data-ucpf-parked="1"]') || 1440 host.querySelector('iframe[data-src]') || 1441 host.querySelector('.elementor-widget-container'); 1442 if (floorInner && floorInner !== host && !floorInner.getAttribute('data-ucpf-aspect-fallback')) { 1443 floorInner.style.setProperty('min-height', '22rem', 'important'); 1444 floorInner.style.setProperty('width', '100%', 'important'); 1445 floorInner.setAttribute('data-ucpf-aspect-fallback', '1'); 1446 } 1447 } catch (eFloor) { /* ignore */ } 1448 } 1449 1450 /** 1451 * Lock the host to its pre-gate size so parked iframes donât collapse the box. 1452 * Video/map get aspect fallbacks; other embeds get a generic floor when collapsed 1453 * (Elementor popups often measure 0 until shown). Forms never invent height. 1454 * 1455 * @param {Element} host 1456 * @return {void} 1457 */ 1458 function preserveEmbedBoxSize(host) { 1459 if (!host || host.nodeType !== 1) { 1460 return; 1461 } 1462 if (host.getAttribute('data-ucpf-size-locked') === '1') { 1463 return; 1464 } 1465 // Background video e-cons: Elementor owns geometry â never invent min-height. 1466 if (isBackgroundVideoOwner(host) || (host.classList && host.classList.contains('ucpf-consent-guard--bg'))) { 1467 return; 1468 } 1469 1470 var measured = 0; 1471 try { 1472 measured = Math.round(host.getBoundingClientRect().height || 0); 1473 } catch (eRect) { 1474 measured = 0; 1475 } 1476 1477 var attrH = 0; 1478 var iframes = []; 1479 try { 1480 if (host.tagName === 'IFRAME') { 1481 iframes = [host]; 1482 } else if (host.querySelectorAll) { 1483 iframes = Array.prototype.slice.call(host.querySelectorAll('iframe')); 1484 } 1485 } catch (eIf) { 1486 iframes = []; 1487 } 1488 for (var i = 0; i < iframes.length; i++) { 1489 var iframe = iframes[i]; 1490 var hAttr = parseInt(iframe.getAttribute('height'), 10) || 0; 1491 var styleH = 0; 1492 try { 1493 var st = iframe.style && iframe.style.height ? parseInt(iframe.style.height, 10) : 0; 1494 styleH = st || 0; 1495 } catch (eSt) { 1496 styleH = 0; 1497 } 1498 var rectH = 0; 1499 try { 1500 rectH = Math.round(iframe.getBoundingClientRect().height || 0); 1501 } catch (eRh) { 1502 rectH = 0; 1503 } 1504 attrH = Math.max(attrH, hAttr, styleH, rectH); 1505 // Keep the iframe itself from collapsing when src is parked. 1506 if (Math.max(hAttr, styleH, rectH) >= 40 && !iframe.getAttribute('data-ucpf-iframe-h')) { 1507 var keep = Math.max(hAttr, styleH, rectH); 1508 iframe.setAttribute('data-ucpf-iframe-h', String(keep)); 1509 try { 1510 iframe.style.setProperty('min-height', keep + 'px', 'important'); 1511 iframe.style.setProperty('height', keep + 'px', 'important'); 1512 } catch (eKeep) { /* ignore */ } 1513 } 1514 } 1515 1516 var targetH = Math.max(measured, attrH); 1517 // Empty Elementor YouTube/Vimeo shells: invent a 16:9 box so the glass overlay is visible. 1518 if (targetH < 80 && isCollapsedVideoShell(host)) {
1519 try { 1520 host.style.setProperty('aspect-ratio', '16 / 9', 'important'); 1521 host.style.setProperty('width', '100%', 'important'); 1522 host.style.setProperty('min-height', '12rem', 'important'); 1523 host.setAttribute('data-ucpf-size-locked', '1'); 1524 host.setAttribute('data-ucpf-guard-min-h', 'aspect-16-9'); 1525 host.setAttribute('data-ucpf-aspect-fallback', '1'); 1526 // Give the inner Elementor fill a height so layout isnât empty under the panel. 1527 var inner = 1528 host.querySelector('.elementor-wrapper') || 1529 host.querySelector('.elementor-video') || 1530 host.querySelector('.elementor-widget-container'); 1531 if (inner && !inner.getAttribute('data-ucpf-aspect-fallback')) { 1532 inner.style.setProperty('aspect-ratio', '16 / 9', 'important'); 1533 inner.style.setProperty('min-height', '12rem', 'important'); 1534 inner.style.setProperty('width', '100%', 'important'); 1535 inner.setAttribute('data-ucpf-aspect-fallback', '1'); 1536 } 1537 } catch (eAspect) { /* ignore */ } 1538 return; 1539 } 1540 // Collapsed map shells: modest 16:9 so Marketing+Embeds cover is visible before tiles. 1541 if (targetH < 80 && isCollapsedMapShell(host)) { 1542 try { 1543 host.style.setProperty('aspect-ratio', '16 / 9', 'important'); 1544 host.style.setProperty('width', '100%', 'important'); 1545 host.style.setProperty('min-height', '14rem', 'important'); 1546 host.setAttribute('data-ucpf-size-locked', '1'); 1547 host.setAttribute('data-ucpf-guard-min-h', 'aspect-16-9-map'); 1548 host.setAttribute('data-ucpf-aspect-fallback', '1'); 1549 var mapInner = 1550 host.querySelector('.elementor-widget-container') || 1551 host.querySelector('.wpgmza_map') || 1552 host.querySelector('.mapster-wp-maps') || 1553 host.querySelector('.maplibregl-map'); 1554 if (mapInner && mapInner !== host && !mapInner.getAttribute('data-ucpf-aspect-fallback')) { 1555 mapInner.style.setProperty('aspect-ratio', '16 / 9', 'important'); 1556 mapInner.style.setProperty('min-height', '14rem', 'important'); 1557 mapInner.style.setProperty('width', '100%', 'important'); 1558 mapInner.setAttribute('data-ucpf-aspect-fallback', '1'); 1559 } 1560 } catch (eMapAspect) { /* ignore */ } 1561 return; 1562 } 1563 // Collapsed Calendly / HTML widgets / other embeds (e.g. Elementor popup not yet shown). 1564 if (targetH < 80) { 1565 if (isEmbedGuardHost(host)) { 1566 applyEmbedFloor(host); 1567 } 1568 return; 1569 } 1570 // Cap runaway measurements (full-page wrappers). 1571 if (targetH > 1200) { 1572 targetH = Math.min(targetH, Math.max(measured, attrH, 480)); 1573 } 1574 try { 1575 host.style.setProperty('min-height', targetH + 'px', 'important'); 1576 host.setAttribute('data-ucpf-size-locked', '1'); 1577 host.setAttribute('data-ucpf-guard-min-h', String(targetH)); 1578 } catch (eLock) { /* ignore */ } 1579 } 1580 1581 /** 1582 * Clear an embed-floor lock and re-measure (e.g. after Elementor popup opens). 1583 * 1584 * @param {Element} host 1585 * @return {void} 1586 */ 1587 function refreshEmbedBoxSize(host) { 1588 if (!host || host.nodeType !== 1) { 1589 return; 1590 } 1591 if (host.getAttribute('data-ucpf-guard-min-h') === 'embed-floor') { 1592 clearEmbedBoxSize(host); 1593 } 1594 preserveEmbedBoxSize(host); 1595 } 1596 1597 /** 1598 * Re-run size lock for active embed guards under a root (popup body, document). 1599 * 1600 * @param {Element|Document|null} root 1601 * @return {void} 1602 */ 1603 function remeasureActiveEmbedGuards(root) { 1604 var scope = root && root.querySelectorAll ? root : document; 1605 var hosts; 1606 try { 1607 hosts = scope.querySelectorAll('.ucpf-consent-guard--embed.ucpf-consent-guard--active'); 1608 } catch (eQ) { 1609 return; 1610 } 1611 Array.prototype.forEach.call(hosts, function (host) { 1612 refreshEmbedBoxSize(host); 1613 }); 1614 } 1615 1616 /** 1617 * Elementor popups often attach guards while display:none â remasure on show. 1618 * 1619 * @return {void} 1620 */ 1621 function bindElementorPopupRemeasure() { 1622 function resolvePopupRoot(id, instance) { 1623 var root = null;
1624 try { 1625 if (instance && instance.$element && instance.$element[0]) { 1626 root = instance.$element[0]; 1627 } else if (instance && instance.$ && instance.$[0]) { 1628 root = instance.$[0]; 1629 } 1630 } catch (eInst) { 1631 root = null; 1632 } 1633 if (!root && id != null && id !== '') { 1634 try { 1635 root = 1636 document.getElementById('elementor-popup-modal-' + id) || 1637 document.querySelector('[data-elementor-id="' + id + '"].elementor-location-popup') || 1638 document.querySelector('.elementor-popup-modal[data-elementor-id="' + id + '"]'); 1639 } catch (eId) { 1640 root = null; 1641 } 1642 } 1643 if (!root) { 1644 try { 1645 root = 1646 document.querySelector('.elementor-popup-modal:not([aria-hidden="true"]) .dialog-lightbox-message') || 1647 document.querySelector('.elementor-popup-modal .dialog-widget-content') || 1648 document.querySelector('.elementor-location-popup'); 1649 } catch (eFb) { 1650 root = null; 1651 } 1652 } 1653 return root; 1654 } 1655 1656 function onPopupShow(event, id, instance) { 1657 var root = resolvePopupRoot(id, instance); 1658 var run = function () { 1659 remeasureActiveEmbedGuards(root || document); 1660 }; 1661 window.setTimeout(run, 50); 1662 window.setTimeout(run, 300); 1663 } 1664 1665 try { 1666 document.addEventListener('elementor/popup/show', function (ev) { 1667 var detail = ev && ev.detail ? ev.detail : null; 1668 var id = detail && detail.id != null ? detail.id : null; 1669 var instance = detail && detail.instance ? detail.instance : null; 1670 onPopupShow(ev, id, instance); 1671 }); 1672 } catch (eNat) { /* ignore */ } 1673 1674 function bindJquery() { 1675 try { 1676 if (!window.jQuery || !window.jQuery.fn) { 1677 return false; 1678 } 1679 window.jQuery(document).on('elementor/popup/show', onPopupShow); 1680 return true; 1681 } catch (eJq) { 1682 return false; 1683 } 1684 } 1685 if (!bindJquery()) { 1686 [100, 500, 2000].forEach(function (ms) { 1687 window.setTimeout(bindJquery, ms); 1688 }); 1689 } 1690 } 1691 1692 /** 1693 * Light fallback when Elementor's popup event is missed: remasure floor-locked 1694 * embeds inside a visible popup / lightbox message. 1695 * 1696 * @return {void} 1697 */ 1698 function remeasureVisiblePopupEmbeds() { 1699 var roots; 1700 try { 1701 roots = document.querySelectorAll( 1702 '.elementor-popup-modal .dialog-lightbox-message, .elementor-location-popup, .dialog-lightbox-message' 1703 ); 1704 } catch (eRoots) { 1705 return; 1706 } 1707 Array.prototype.forEach.call(roots, function (root) { 1708 if (!root || !root.querySelector) { 1709 return; 1710 } 1711 var floor = null; 1712 try { 1713 floor = root.querySelector( 1714 '.ucpf-consent-guard--embed.ucpf-consent-guard--active[data-ucpf-guard-min-h="embed-floor"]' 1715 ); 1716 } catch (eFloor) { 1717 floor = null; 1718 } 1719 if (!floor) { 1720 return; 1721 } 1722 try { 1723 var cs = window.getComputedStyle ? window.getComputedStyle(root) : null; 1724 if (cs && (cs.display === 'none' || cs.visibility === 'hidden')) { 1725 return; 1726 } 1727 } catch (eCs) { /* continue */ } 1728 remeasureActiveEmbedGuards(root); 1729 }); 1730 } 1731 1732 /** 1733 * Undo preserveEmbedBoxSize. 1734 * 1735 * @param {Element} host 1736 * @return {void} 1737 */ 1738 function clearEmbedBoxSize(host) { 1739 if (!host || host.nodeType !== 1) { 1740 return; 1741 } 1742 if (host.getAttribute('data-ucpf-size-locked') === '1') { 1743 try { 1744 host.style.removeProperty('min-height'); 1745 if (host.getAttribute('data-ucpf-aspect-fallback') === '1') { 1746 host.style.removeProperty('aspect-ratio'); 1747 host.style.removeProperty('width'); 1748 } 1749 } catch (eRm) { /* ignore */ } 1750 host.removeAttribute('data-ucpf-size-locked'); 1751 host.removeAttribute('data-ucpf-guard-min-h'); 1752 host.removeAttribute('data-ucpf-aspect-fallback'); 1753 } 1754 try { 1755 var inners = host.querySelectorAll 1756 ? host.querySelectorAll('[data-ucpf-aspect-fallback="1"]') 1757 : []; 1758 Array.prototype.forEach.call(inners, function (inner) { 1759 if (inner === host) { 1760 return; 1761 } 1762 try { 1763 inner.style.removeProperty('aspect-ratio'); 1764 inner.style.removeProperty('min-height'); 1765 inner.style.removeProperty('width'); 1766 } catch (eIn) { /* ignore */ } 1767 inner.removeAttribute('data-ucpf-aspect-fallback'); 1768 }); 1769 } catch (eInner) { /* ignore */ } 1770 try {
1771 var nodes = host.tagName === 'IFRAME' ? [host] : host.querySelectorAll ? host.querySelectorAll('iframe[data-ucpf-iframe-h]') : []; 1772 Array.prototype.forEach.call(nodes, function (iframe) { 1773 try { 1774 iframe.style.removeProperty('min-height'); 1775 iframe.style.removeProperty('height'); 1776 } catch (eI) { /* ignore */ } 1777 iframe.removeAttribute('data-ucpf-iframe-h'); 1778 }); 1779 } catch (eQ) { /* ignore */ } 1780 } 1781 1782 function isEffectivelyHidden(el) { 1783 if (!el) { 1784 return true; 1785 } 1786 // Builder entrance animations (e.g. Elementor .elementor-invisible) use 1787 // visibility:hidden until Motion FX runs. Consent covers must still attach 1788 // before the embed loads â otherwise the section reveals with no overlay. 1789 var entranceAnim = false; 1790 try { 1791 entranceAnim = !!(el.classList && el.classList.contains('elementor-invisible')); 1792 } catch (eInv) { /* ignore */ } 1793 try { 1794 if (window.getComputedStyle) { 1795 var cs = window.getComputedStyle(el); 1796 if (cs.display === 'none') { 1797 return true; 1798 } 1799 if (cs.visibility === 'hidden' && !entranceAnim) { 1800 return true; 1801 } 1802 } 1803 } catch (e) { 1804 /* ignore */ 1805 } 1806 return false; 1807 } 1808 1809 function guardHost(target) { 1810 if (!target || !target.classList) { 1811 return null; 1812 } 1813 if (target.classList.contains('ucpf-consent-guard')) { 1814 return target; 1815 } 1816 var parent = target.parentElement; 1817 if (parent && parent.classList.contains('ucpf-consent-guard')) { 1818 return parent; 1819 } 1820 return null; 1821 } 1822 1823 function ensureWrap(target, mode) { 1824 if (isForbiddenGuardHost(target)) { 1825 return target; 1826 } 1827 var existing = guardHost(target); 1828 if (existing) { 1829 if (isForbiddenGuardHost(existing)) { 1830 return target; 1831 } 1832 return existing; 1833 } 1834 1835 // Elementor background-video e-con: --bg only (never --embed layout rules). 1836 if (mode === 'embed' && isBackgroundVideoOwner(target)) { 1837 target.classList.add('ucpf-consent-guard', 'ucpf-captcha-guard', 'ucpf-consent-guard--bg'); 1838 target.setAttribute('data-ucpf-guard-shell', '1'); 1839 return target; 1840 } 1841 1842 // Decorate in place â wrapping forms/builder shells breaks theme layout. 1843 if ( 1844 (mode === 'form' && isFormShell(target)) || 1845 (mode === 'embed' && isBuilderShell(target)) 1846 ) { 1847 target.classList.add( 1848 'ucpf-consent-guard', 1849 'ucpf-captcha-guard', 1850 mode === 'form' ? 'ucpf-consent-guard--form' : 'ucpf-consent-guard--embed' 1851 ); 1852 target.setAttribute('data-ucpf-guard-shell', '1'); 1853 return target; 1854 } 1855 1856 // Never insert a wrapper around document chrome (would reparent <body>). 1857 if (!target.parentNode || isForbiddenGuardHost(target.parentNode)) { 1858 return target; 1859 } 1860 1861 var wrap = document.createElement('div'); 1862 wrap.className = 'ucpf-consent-guard ucpf-captcha-guard'; 1863 wrap.classList.add(mode === 'form' ? 'ucpf-consent-guard--form' : 'ucpf-consent-guard--embed'); 1864 wrap.setAttribute('data-ucpf-guard-shell', '0'); 1865 if (target.parentNode) { 1866 target.parentNode.insertBefore(wrap, target); 1867 wrap.appendChild(target); 1868 } 1869 return wrap; 1870 } 1871 1872 /** True when URL is a YouTube / Vimeo player embed. */ 1873 function isVideoPlayerUrl(url) { 1874 var u = String(url || '').toLowerCase(); 1875 if (!u) { 1876 return false; 1877 } 1878 return ( 1879 u.indexOf('youtube.com') !== -1 || 1880 u.indexOf('youtu.be') !== -1 || 1881 u.indexOf('youtube-nocookie.com') !== -1 || 1882 u.indexOf('player.vimeo.com') !== -1 || 1883 u.indexOf('vimeo.com') !== -1 1884 ); 1885 } 1886 1887 /** 1888 * Stop players loading under the cover â park src onto data-src until consent. 1889 * Also claim iframes already emptied by network-gate so restore can find them. 1890 * @param {Element} host Guard target (widget / wrapper). 1891 */ 1892 function parkVideoIframes(host) { 1893 if (!host || !host.querySelectorAll) { 1894 return; 1895 } 1896 var nodes = host.querySelectorAll('iframe'); 1897 Array.prototype.forEach.call(nodes, function (iframe) { 1898 var src = iframe.getAttribute('src') || ''; 1899 var parked = iframe.getAttribute('data-src') || ''; 1900 var candidate = src && src !== 'about:blank' ? src : parked; 1901 if (!candidate || !isVideoPlayerUrl(candidate)) { 1902 return; 1903 } 1904 if (!iframe.getAttribute('data-src')) { 1905 iframe.setAttribute('data-src', candidate); 1906 } 1907 iframe.setAttribute('data-ucpf-parked', '1'); 1908 if (!src || src === 'about:blank') { 1909 return; 1910 }
1911 try { 1912 iframe.removeAttribute('src'); 1913 } catch (eRm) { /* ignore */ } 1914 try { 1915 iframe.src = ''; 1916 } catch (eSrc) { /* ignore */ } 1917 }); 1918 } 1919 1920 /** 1921 * Put a video URL back on an iframe after consent. 1922 * Clears gate stamps, assigns src, and replaces the node if the gate re-blocks 1923 * (YouTube often stays blank on a previously emptied iframe). 1924 * 1925 * @param {HTMLIFrameElement} iframe 1926 * @param {string} src 1927 * @return {boolean} 1928 */ 1929 function forceVideoIframeSrc(iframe, src) { 1930 if (!iframe || !src || !isVideoPlayerUrl(src)) { 1931 return false; 1932 } 1933 iframe.removeAttribute('data-ucpf-parked'); 1934 iframe.removeAttribute('data-ucpf-gated'); 1935 iframe.removeAttribute('data-ucpf-category'); 1936 try { 1937 iframe.src = src; 1938 } catch (eProp) { 1939 try { 1940 iframe.setAttribute('src', src); 1941 } catch (eAttr) { 1942 return false; 1943 } 1944 } 1945 iframe.removeAttribute('data-src'); 1946 var live = ''; 1947 try { 1948 live = iframe.getAttribute('src') || iframe.src || ''; 1949 } catch (eLive) { 1950 live = ''; 1951 } 1952 if (live && live !== 'about:blank' && isVideoPlayerUrl(live)) { 1953 return true; 1954 } 1955 // Setter still blocked or player dead â swap in a fresh iframe. 1956 if (!iframe.parentNode) { 1957 return false; 1958 } 1959 var fresh = document.createElement('iframe'); 1960 Array.prototype.slice.call(iframe.attributes || []).forEach(function (attr) { 1961 if (!attr || !attr.name) { 1962 return; 1963 } 1964 if ( 1965 attr.name === 'src' || 1966 attr.name === 'data-src' || 1967 attr.name === 'data-ucpf-gated' || 1968 attr.name === 'data-ucpf-parked' || 1969 attr.name === 'data-ucpf-category' 1970 ) { 1971 return; 1972 } 1973 try { 1974 fresh.setAttribute(attr.name, attr.value); 1975 } catch (eCopy) { /* ignore */ } 1976 }); 1977 if (!fresh.className && iframe.className) { 1978 fresh.className = iframe.className; 1979 } 1980 if (iframe.classList && iframe.classList.contains('elementor-video')) { 1981 fresh.classList.add('elementor-video'); 1982 } 1983 fresh.setAttribute('src', src); 1984 try { 1985 iframe.parentNode.replaceChild(fresh, iframe); 1986 } catch (eRep) { 1987 return false; 1988 } 1989 return true; 1990 } 1991 1992 /** 1993 * Restore parked / gated YouTube / Vimeo iframes after consent. 1994 * Covers both guard parking (data-ucpf-parked) and network-gate (data-ucpf-gated). 1995 * @param {Element} host Guard target (or document-wide root). 1996 */ 1997 function restoreParkedVideoIframes(host) { 1998 if (!host || !host.querySelectorAll) { 1999 return; 2000 } 2001 var roots = [host]; 2002 var wrap = guardHost(host); 2003 if (wrap && wrap !== host) { 2004 roots.push(wrap); 2005 } 2006 roots.forEach(function (root) { 2007 if (!root || !root.querySelectorAll) { 2008 return; 2009 } 2010 Array.prototype.forEach.call( 2011 root.querySelectorAll( 2012 'iframe[data-ucpf-parked="1"], iframe[data-ucpf-gated="1"][data-src], iframe.elementor-video[data-src], iframe[data-src*="youtube"], iframe[data-src*="youtu.be"], iframe[data-src*="youtube-nocookie"], iframe[data-src*="vimeo"]' 2013 ), 2014 function (iframe) { 2015 var live = iframe.getAttribute('src') || ''; 2016 if (live && live !== 'about:blank' && isVideoPlayerUrl(live)) { 2017 iframe.removeAttribute('data-ucpf-parked'); 2018 iframe.removeAttribute('data-ucpf-gated'); 2019 return; 2020 } 2021 var real = iframe.getAttribute('data-src') || ''; 2022 if (!real || !isVideoPlayerUrl(real)) { 2023 iframe.removeAttribute('data-ucpf-parked'); 2024 return; 2025 } 2026 forceVideoIframeSrc(iframe, real); 2027 } 2028 ); 2029 }); 2030 } 2031 2032 /** Document-wide video iframe restore (maps-style â not only under a guard host). */ 2033 function restoreAllParkedVideoIframes() { 2034 restoreParkedVideoIframes(document); 2035 } 2036 2037 function applyGuard(target, kind, category, mode, categories) { 2038 var cats = Array.isArray(categories) && categories.length ? categories : category ? [category] : []; 2039 if (!target || isForbiddenGuardHost(target) || isUserWayNode(target) || hasAllCategories(cats)) { 2040 return; 2041 } 2042 if (mode === 'embed' && isEffectivelyHidden(target) && !isSmashBalloonFeedHost(target)) { 2043 return; 2044 } 2045 var key = kind + ':' + cats.join('+'); 2046 var isVideoKind = kind === 'youtube' || kind === 'vimeo'; 2047 if (guarded.get(target) === key && target.getAttribute('data-ucpf-consent-guarded') === '1') { 2048 var existing = guardHost(target) || target; 2049 if (existing && existing.classList.contains('ucpf-consent-guard')) { 2050 syncThemeOnto(existing); 2051 } 2052 // Retry size lock: first pass often measured 0 before Elementor painted the shell. 2053 if (mode === 'embed' && isVideoKind && existing) { 2054 preserveEmbedBoxSize(existing); 2055 if (existing !== target) { 2056 preserveEmbedBoxSize(target); 2057 } 2058 } 2059 // Elementor may re-inject src after we parked â keep players unloaded.
2060 if (isVideoKind || (mode === 'embed' && isBackgroundVideoOwner(target))) { 2061 parkVideoIframes(target); 2062 if (existing && existing !== target) { 2063 parkVideoIframes(existing); 2064 } 2065 } 2066 return; 2067 } 2068 2069 var isBg = mode === 'embed' && isBackgroundVideoOwner(target); 2070 2071 // Capture Elementor position before any class adds. 2072 var preservePos = ''; 2073 if (isBg) { 2074 try { 2075 preservePos = window.getComputedStyle(target).position || ''; 2076 } catch (eRead) { 2077 preservePos = ''; 2078 } 2079 } 2080 2081 var wrap = ensureWrap(target, mode); 2082 wrap.setAttribute('data-ucpf-guard-kind', kind); 2083 wrap.setAttribute('data-ucpf-guard-category', cats.join(',')); 2084 wrap.classList.add('ucpf-consent-guard--active'); 2085 if (isBg || wrap.classList.contains('ucpf-consent-guard--bg')) { 2086 wrap.classList.add('ucpf-consent-guard--bg'); 2087 // Never apply --embed (relative/min-height/width) to Elementor absolute e-cons. 2088 wrap.classList.remove('ucpf-consent-guard--embed', 'ucpf-consent-guard--form'); 2089 // Only invent a positioning context if Elementor left the host static. 2090 if (!preservePos || preservePos === 'static') { 2091 wrap.style.setProperty('position', 'relative', 'important'); 2092 wrap.setAttribute('data-ucpf-bg-pos', '1'); 2093 } else if (wrap.getAttribute('data-ucpf-bg-pos') === '1') { 2094 // Drop forced absolute from older builds; Elementor owns coords. 2095 wrap.style.removeProperty('position'); 2096 wrap.removeAttribute('data-ucpf-bg-pos'); 2097 } 2098 } else if (mode === 'embed') { 2099 // Measure before parking iframes so the glass overlay keeps the real media box. 2100 preserveEmbedBoxSize(wrap); 2101 if (wrap !== target) { 2102 preserveEmbedBoxSize(target); 2103 } 2104 } 2105 syncThemeOnto(wrap); 2106 2107 var panel = null; 2108 try { 2109 panel = wrap.querySelector(':scope > .ucpf-consent-guard__panel'); 2110 } catch (eScope) { 2111 panel = null; 2112 } 2113 if (!panel) { 2114 var kids = wrap.children; 2115 for (var ci = 0; ci < kids.length; ci++) { 2116 if (kids[ci].classList && kids[ci].classList.contains('ucpf-consent-guard__panel')) { 2117 panel = kids[ci]; 2118 break; 2119 } 2120 } 2121 } 2122 if (!panel) { 2123 panel = wrap.querySelector('.ucpf-consent-guard__panel'); 2124 } 2125 var newPanel = buildPanel(kind, category, cats); 2126 try { 2127 if (panel && panel.parentNode === wrap) { 2128 wrap.replaceChild(newPanel, panel); 2129 } else { 2130 var anchor = wrap === target ? wrap.firstChild : target; 2131 if (anchor && anchor.parentNode === wrap) { 2132 wrap.insertBefore(newPanel, anchor); 2133 } else if (wrap.firstChild && wrap.firstChild.parentNode === wrap) { 2134 wrap.insertBefore(newPanel, wrap.firstChild); 2135 } else { 2136 wrap.appendChild(newPanel); 2137 } 2138 } 2139 } catch (eInsert) { 2140 try { 2141 wrap.appendChild(newPanel); 2142 } catch (eAppend) { 2143 /* ignore â Elementor may reparent mid-refresh */ 2144 } 2145 } 2146 2147 if (mode === 'form') { 2148 lockFields(target); 2149 } 2150 if (isVideoKind || isBg) { 2151 parkVideoIframes(target); 2152 parkVideoIframes(wrap); 2153 } 2154 target.setAttribute('data-ucpf-consent-guarded', '1'); 2155 guarded.set(target, key); 2156 } 2157 2158 function removeGuard(target) { 2159 if (!target || target.getAttribute('data-ucpf-consent-guarded') !== '1') { 2160 return; 2161 } 2162 if (target.tagName === 'FORM') { 2163 unlockFields(target); 2164 } 2165 restoreParkedVideoIframes(target); 2166 target.removeAttribute('data-ucpf-consent-guarded'); 2167 2168 var wrap = guardHost(target) || (target.classList.contains('ucpf-consent-guard') ? target : null); 2169 if (wrap && wrap.classList.contains('ucpf-consent-guard')) { 2170 restoreParkedVideoIframes(wrap); 2171 clearEmbedBoxSize(wrap); 2172 clearEmbedBoxSize(target); 2173 var panel = wrap.querySelector('.ucpf-consent-guard__panel'); 2174 if (panel) { 2175 panel.remove(); 2176 } 2177 wrap.classList.remove('ucpf-consent-guard--active'); 2178 2179 var inPlace = wrap.getAttribute('data-ucpf-guard-shell') === '1' || wrap === target; 2180 if (inPlace) { 2181 wrap.classList.remove( 2182 'ucpf-consent-guard', 2183 'ucpf-captcha-guard', 2184 'ucpf-consent-guard--embed', 2185 'ucpf-consent-guard--form', 2186 'ucpf-consent-guard--bg' 2187 ); 2188 wrap.removeAttribute('data-ucpf-guard-shell'); 2189 wrap.removeAttribute('data-ucpf-guard-kind'); 2190 wrap.removeAttribute('data-ucpf-guard-category'); 2191 if (wrap.getAttribute('data-ucpf-bg-pos') === '1') { 2192 wrap.style.removeProperty('position'); 2193 wrap.removeAttribute('data-ucpf-bg-pos'); 2194 } 2195 } else if (wrap.parentNode) { 2196 wrap.parentNode.insertBefore(target, wrap); 2197 wrap.remove(); 2198 } 2199 } else { 2200 clearEmbedBoxSize(target); 2201 } 2202 guarded.set(target, 'off'); 2203 2204 // Overlay is gone â remount the player immediately if restore left a blank shell. 2205 var videoHost = 2206 (target.classList && 2207 (target.classList.contains('elementor-widget-video') || 2208 target.getAttribute('data-widget_type') === 'video.default' || 2209 target.classList.contains('elementor-wrapper'))) || 2210 (target.querySelector && 2211 target.querySelector('.elementor-wrapper.elementor-open-inline, iframe.elementor-video, iframe[data-src*="youtube"], iframe[data-src*="vimeo"]')); 2212 if (videoHost && hasCategoryConsent('marketing') && hasCategoryConsent('functional')) { 2213 var remountRoot = 2214 (target.closest && (target.closest('.elementor-widget-video') || target.closest('[data-widget_type="video.default"]'))) || 2215 target; 2216 var kick = function (allowCreate) { 2217 restoreParkedVideoIframes(remountRoot); 2218 injectElementorOpenInlineVideo(remountRoot, { allowCreate: !!allowCreate }); 2219 dedupeElementorOpenInlineVideos(remountRoot); 2220 }; 2221 // Restore Elementor's frame first; create only if still empty shortly after. 2222 kick(false); 2223 window.setTimeout(function () { 2224 kick(false); 2225 }, 200); 2226 window.setTimeout(function () { 2227 kick(true); 2228 }, 1000); 2229 } 2230 } 2231 2232 function kindForPlaceholder(node) { 2233 var service = (node.getAttribute('data-ucpf-service') || '').toLowerCase(); 2234 var src = (node.getAttribute('data-src') || node.getAttribute('src') || '').toLowerCase(); 2235 if (service.indexOf('map') !== -1 || /maps\.google|google\.com\/maps|openstreetmap|mapbox|maplibre/.test(src)) { 2236 return 'map'; 2237 } 2238 if (service.indexOf('youtube') !== -1 || src.indexOf('youtube') !== -1 || src.indexOf('youtu.be') !== -1) { 2239 return 'youtube'; 2240 } 2241 if (service.indexOf('vimeo') !== -1 || src.indexOf('vimeo') !== -1) { 2242 return 'vimeo'; 2243 } 2244 if (service.indexOf('calendly') !== -1 || src.indexOf('calendly.com') !== -1) { 2245 return 'calendly'; 2246 } 2247 if ( 2248 service.indexOf('jobber') !== -1 || 2249 /getjobber\.com|clienthub\.getjobber|work_request|typeform\.com|jotform\.com|hsforms\.com|tally\.so/.test(src) 2250 ) { 2251 return 'widget'; 2252 } 2253 return 'embed'; 2254 } 2255 2256 function parseDataSettings(el) { 2257 if (!el || !el.getAttribute) { 2258 return null; 2259 } 2260 var raw = el.getAttribute('data-settings') || el.getAttribute('data-elementor-settings') || ''; 2261 if (!raw) { 2262 return null; 2263 } 2264 try { 2265 return JSON.parse(raw); 2266 } catch (e1) {
2267 try { 2268 return JSON.parse(raw.replace(/"/g, '"').replace(/'/g, "'")); 2269 } catch (e2) { 2270 return null; 2271 } 2272 } 2273 } 2274 2275 /** 2276 * Detect YouTube / Vimeo from builder widgets even when the iframe is not painted yet. 2277 * @param {Element} el 2278 * @returns {{ kind: string, category: string }|null} 2279 */ 2280 function hostMatchesPage(hostname) { 2281 var a = String(hostname || '') 2282 .replace(/^www\./i, '') 2283 .toLowerCase(); 2284 var b = String(window.location.hostname || '') 2285 .replace(/^www\./i, '') 2286 .toLowerCase(); 2287 return !!(a && b && a === b); 2288 } 2289 2290 /** Same-origin (or relative / blob / data) media â first-party, never consent-gated. */ 2291 function isSameOriginMediaUrl(url) { 2292 var raw = String(url || '').trim(); 2293 if (!raw) { 2294 return false; 2295 } 2296 if (/^(blob:|data:)/i.test(raw)) { 2297 return true; 2298 } 2299 if (raw.charAt(0) === '/' && raw.charAt(1) !== '/') { 2300 return true; 2301 } 2302 try { 2303 var u = new URL(raw, window.location.href); 2304 return hostMatchesPage(u.hostname); 2305 } catch (e) { 2306 return false; 2307 } 2308 } 2309 2310 /** 2311 * Elementor Self Hosted / HTML5 <video> / same-origin iframe â do not cover. 2312 * @param {Element} el 2313 * @return {boolean} 2314 */ 2315 function isSelfHostedVideoSurface(el) { 2316 if (!el || el.nodeType !== 1) { 2317 return false; 2318 } 2319 var settings = parseDataSettings(el); 2320 if ((!settings || typeof settings !== 'object') && el.closest) { 2321 var host = el.closest('[data-settings]'); 2322 settings = parseDataSettings(host); 2323 } 2324 if (settings && typeof settings === 'object') { 2325 var vtype = String(settings.video_type || '').toLowerCase(); 2326 if (vtype === 'hosted' || vtype === 'self' || vtype === 'self_hosted' || vtype === 'file') { 2327 return true; 2328 } 2329 if (settings.hosted_url && isSameOriginMediaUrl(settings.hosted_url)) { 2330 return true; 2331 } 2332 var bg = String(settings.background_video_link || settings.background_background || ''); 2333 if (bg && isSameOriginMediaUrl(bg) && /\.(mp4|webm|ogg|ogv|m4v)(\?|#|$)/i.test(bg)) { 2334 return true; 2335 } 2336 } 2337 if (el.tagName === 'VIDEO') { 2338 var vsrc = el.getAttribute('src') || el.getAttribute('data-src') || ''; 2339 if (vsrc && isSameOriginMediaUrl(vsrc)) { 2340 return true; 2341 } 2342 var sources = el.querySelectorAll ? el.querySelectorAll('source[src], source[data-src]') : []; 2343 for (var si = 0; si < sources.length; si++) { 2344 var ss = sources[si].getAttribute('src') || sources[si].getAttribute('data-src') || ''; 2345 if (ss && isSameOriginMediaUrl(ss)) { 2346 return true; 2347 } 2348 } 2349 } 2350 if (el.querySelector) { 2351 var video = el.querySelector('video[src], video source[src], video[data-src], video source[data-src]'); 2352 if (video) { 2353 var nested = 2354 (video.tagName === 'SOURCE' ? video : video).getAttribute('src') || 2355 (video.tagName === 'SOURCE' ? video : video).getAttribute('data-src') || 2356 ''; 2357 if (!nested && video.querySelector) { 2358 var nestedSrc = video.querySelector('source[src], source[data-src]'); 2359 nested = nestedSrc 2360 ? nestedSrc.getAttribute('src') || nestedSrc.getAttribute('data-src') || '' 2361 : ''; 2362 } 2363 if (nested && isSameOriginMediaUrl(nested)) { 2364 return true; 2365 } 2366 if (video.tagName === 'VIDEO' && isSelfHostedVideoSurface(video)) { 2367 return true; 2368 } 2369 } 2370 } 2371 if (el.tagName === 'IFRAME') { 2372 var iframeSrc = el.getAttribute('src') || el.getAttribute('data-src') || ''; 2373 if (iframeSrc && isSameOriginMediaUrl(iframeSrc) && !isVideoPlayerUrl(iframeSrc)) { 2374 return true; 2375 } 2376 } 2377 return false; 2378 } 2379 2380 function detectVideoProvider(el) { 2381 if (!el || el.nodeType !== 1) { 2382 return null; 2383 } 2384 if (isSelfHostedVideoSurface(el)) { 2385 return null; 2386 } 2387 2388 var blob = ''; 2389 var attrs = [ 2390 'src', 2391 'data-src', 2392 'data-lazy-src', 2393 'data-youtube', 2394 'data-youtube-url', 2395 'data-vimeo-url', 2396 'data-vimeo-id', 2397 'href', 2398 'data-url', 2399 ]; 2400 for (var a = 0; a < attrs.length; a++) { 2401 blob += ' ' + (el.getAttribute(attrs[a]) || ''); 2402 } 2403 blob += ' ' + (el.className || ''); 2404 blob += ' ' + (el.getAttribute('data-widget_type') || ''); 2405 2406 var settings = parseDataSettings(el); 2407 if (settings && typeof settings === 'object') { 2408 var vtypeEarly = String(settings.video_type || '').toLowerCase(); 2409 if (vtypeEarly === 'hosted' || vtypeEarly === 'self' || vtypeEarly === 'self_hosted' || vtypeEarly === 'file') { 2410 return null; 2411 } 2412 blob += 2413 ' ' +
2414 (settings.video_type || '') + 2415 ' ' + 2416 (settings.youtube_url || '') + 2417 ' ' + 2418 (settings.vimeo_url || '') + 2419 ' ' + 2420 (settings.dailymotion_url || '') + 2421 ' ' + 2422 (settings.hosted_url || '') + 2423 ' ' + 2424 (settings.background_video_link || '') + 2425 ' ' + 2426 (settings.background_background || ''); 2427 } 2428 2429 // Walk parents for Elementor widget / container data-settings (background videos live on e-con). 2430 if (el.parentElement) { 2431 var settingsHost = el.closest 2432 ? el.closest('[data-settings*="background_video"], [data-settings*="youtube_url"], [data-settings*="vimeo_url"], [data-settings]') 2433 : el.parentElement; 2434 var parentSettings = parseDataSettings(el.parentElement) || parseDataSettings(settingsHost); 2435 if (parentSettings && typeof parentSettings === 'object') { 2436 var pv = String(parentSettings.video_type || '').toLowerCase(); 2437 if (pv === 'hosted' || pv === 'self' || pv === 'self_hosted' || pv === 'file') { 2438 return null; 2439 } 2440 blob += 2441 ' ' + 2442 (parentSettings.video_type || '') + 2443 ' ' + 2444 (parentSettings.youtube_url || '') + 2445 ' ' + 2446 (parentSettings.vimeo_url || '') + 2447 ' ' + 2448 (parentSettings.background_video_link || '') + 2449 ' ' + 2450 (parentSettings.background_background || ''); 2451 } 2452 } 2453 2454 blob = blob.toLowerCase(); 2455 // Hosted-only blob with no third-party player signal. 2456 if (/hosted/.test(blob) && !/youtube|youtu\.be|vimeo|dailymotion/.test(blob)) { 2457 return null; 2458 } 2459 if (/vimeo|player\.vimeo/.test(blob)) { 2460 return { kind: 'vimeo', category: 'functional' }; 2461 } 2462 if (/youtube|youtu\.be|youtube-nocookie/.test(blob)) { 2463 return { kind: 'youtube', category: 'marketing' }; 2464 } 2465 return null; 2466 } 2467 2468 function calendlyHostContainer(node) { 2469 if (!node || node.nodeType !== 1) { 2470 return null; 2471 } 2472 if (node.classList && (node.classList.contains('calendly-inline-widget') || node.classList.contains('calendly-badge-widget'))) { 2473 return node.closest('.elementor-widget-container') || node.closest('.elementor-widget') || node; 2474 } 2475 if (node.tagName === 'IFRAME' && node.parentElement) { 2476 return ( 2477 node.closest('.calendly-inline-widget') || 2478 node.closest('.elementor-widget-container') || 2479 node.parentElement 2480 ); 2481 } 2482 var dataUrl = (node.getAttribute('data-url') || '').toLowerCase(); 2483 if (dataUrl.indexOf('calendly.com') !== -1) { 2484 return node.closest('.elementor-widget-container') || node.closest('.elementor-widget') || node; 2485 } 2486 return node; 2487 } 2488 2489 /** 2490 * Prefer a sized mount box for Jobber / Typeform / Elementor HTML embeds. 2491 * 2492 * @param {Element} node 2493 * @return {Element|null} 2494 */ 2495 function widgetEmbedHostContainer(node) { 2496 if (!node || node.nodeType !== 1) { 2497 return null; 2498 } 2499 // UserWay accessibility toolbar â never promote a cover host. 2500 if (isUserWayNode(node) || isUserWayUrl((node.getAttribute('data-src') || '') + ' ' + (node.getAttribute('src') || ''))) { 2501 return null; 2502 } 2503 if (node.classList && node.classList.contains('jobber-inline-work-request')) { 2504 return node; 2505 } 2506 var blob = ( 2507 (node.getAttribute('data-src') || '') + 2508 ' ' + 2509 (node.getAttribute('src') || '') + 2510 ' ' + 2511 (node.id || '') 2512 ).toLowerCase(); 2513 // Calendly script often lives under <body> â cover the widget box, never the page. 2514 if (blob.indexOf('calendly') !== -1 && node.ownerDocument) { 2515 var cal = 2516 (node.closest && node.closest('.calendly-inline-widget, .calendly-badge-widget')) || 2517 node.ownerDocument.querySelector('.calendly-inline-widget[data-url], .calendly-badge-widget, .calendly-inline-widget'); 2518 if (cal && !isForbiddenGuardHost(cal)) { 2519 return cal; 2520 } 2521 } 2522 if (node.closest) { 2523 // Prefer the Elementor / builder layout box â not the parked iframe (collapses to 0). 2524 var box = 2525 node.closest('[data-widget_type="html.default"]') || 2526 node.closest('.elementor-widget-html') || 2527 node.closest('[data-widget_type="tp-meeting-scheduler.default"]') || 2528 node.closest('.elementor-widget-tp-meeting-scheduler') || 2529 node.closest('.jobber-inline-work-request') || 2530 node.closest('[class*="jobber-inline"]') || 2531 node.closest('.elementor-widget-container') || 2532 node.closest('.elementor-widget'); 2533 if (box && !isForbiddenGuardHost(box)) { 2534 return box; 2535 } 2536 } 2537 if (node.tagName === 'IFRAME' && node.parentElement && !isForbiddenGuardHost(node.parentElement)) { 2538 return node.parentElement; 2539 } 2540 // Scripts parked under <body>/<head> must not promote the document chrome as a host. 2541 if (node.tagName === 'SCRIPT') { 2542 return null; 2543 } 2544 if (isForbiddenGuardHost(node)) { 2545 return null; 2546 } 2547 return node; 2548 } 2549 2550 /** 2551 * Whether a gated script is a form/widget embed (not analytics). 2552 * 2553 * @param {Element} script 2554 * @return {boolean} 2555 */ 2556 function isEmbedLikeScript(script) { 2557 if (!script || script.tagName !== 'SCRIPT') { 2558 return false;
2559 } 2560 var blob = ( 2561 (script.getAttribute('data-src') || '') + 2562 ' ' + 2563 (script.getAttribute('src') || '') + 2564 ' ' + 2565 (script.getAttribute('form_url') || '') + 2566 ' ' + 2567 (script.getAttribute('typehub_id') || '') + 2568 ' ' + 2569 (script.id || '') + 2570 ' ' + 2571 (script.className || '') 2572 ).toLowerCase(); 2573 // Accessibility toolbar â never treat as a third-party form/widget cover. 2574 if (isUserWayUrl(blob) || blob.indexOf('userway') !== -1) { 2575 return false; 2576 } 2577 if ( 2578 /(google-analytics|googletagmanager|gtag\/js|facebook\.net|fbevents|hotjar|clarity\.ms|doubleclick|googlesyndication)/.test( 2579 blob 2580 ) 2581 ) { 2582 return false; 2583 } 2584 if ( 2585 /(jobber|getjobber|work_request|typeform|jotform|hsforms|hubspot\.com\/.*form|calendly|wufoo|fillout\.com|tally\.so|forms\.office)/.test( 2586 blob 2587 ) 2588 ) { 2589 return true; 2590 } 2591 if (script.getAttribute('form_url') || script.getAttribute('typehub_id') || script.getAttribute('data-form-id')) { 2592 return true; 2593 } 2594 // Elementor HTML widgets often host third-party form snippets. 2595 if (script.closest) { 2596 return !!( 2597 script.closest('.elementor-widget-html') || 2598 script.closest('[data-widget_type="html.default"]') || 2599 script.closest('.jobber-inline-work-request') 2600 ); 2601 } 2602 return false; 2603 } 2604 2605 /** 2606 * Calendly only scans the DOM once on script load. Elementor popups inject 2607 * .calendly-inline-widget later â re-run init after Functional consent. 2608 */ 2609 function reinitCalendlyWidgets() { 2610 if (!hasCategoryConsent('functional')) { 2611 return; 2612 } 2613 if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function') { 2614 try { 2615 window.UCPFLoader.applyConsent(); 2616 } catch (eLoad) { 2617 /* ignore */ 2618 } 2619 } 2620 var attempt = 0; 2621 function tryInit() { 2622 attempt += 1; 2623 var C = window.Calendly; 2624 var nodes = document.querySelectorAll('.calendly-inline-widget[data-url], .calendly-badge-widget[data-url]'); 2625 if (!nodes.length) { 2626 return; 2627 } 2628 if (C && typeof C === 'object') { 2629 try { 2630 if (typeof C.initInlineWidgets === 'function') { 2631 C.initInlineWidgets(); 2632 return; 2633 } 2634 } catch (eInitAll) { 2635 /* fall through per-node */ 2636 } 2637 Array.prototype.forEach.call(nodes, function (el) { 2638 if (el.querySelector && el.querySelector('iframe')) { 2639 return; 2640 } 2641 var url = el.getAttribute('data-url'); 2642 if (!url) { 2643 return; 2644 } 2645 try { 2646 if (typeof C.initInlineWidget === 'function') { 2647 C.initInlineWidget({ url: url, parentElement: el }); 2648 } 2649 } catch (eOne) { 2650 /* ignore */ 2651 } 2652 }); 2653 return; 2654 } 2655 if (attempt < 20) { 2656 window.setTimeout(tryInit, 250); 2657 } 2658 } 2659 tryInit(); 2660 } 2661 2662 /** 2663 * Extract a YouTube video id from watch / embed / shorts / youtu.be URLs. 2664 * @param {string} raw 2665 * @return {string} 2666 */ 2667 function youtubeIdFromUrl(raw) { 2668 var url = String(raw || '').trim(); 2669 if (!url) { 2670 return ''; 2671 } 2672 var m = 2673 url.match(/youtu\.be\/([A-Za-z0-9_-]{6,})/i) || 2674 url.match(/youtube\.com\/shorts\/([A-Za-z0-9_-]{6,})/i) || 2675 url.match(/youtube(?:-nocookie)?\.com\/embed\/([A-Za-z0-9_-]{6,})/i) || 2676 url.match(/[?&]v=([A-Za-z0-9_-]{6,})/i) || 2677 url.match(/youtube\.com\/live\/([A-Za-z0-9_-]{6,})/i); 2678 return m && m[1] ? m[1] : ''; 2679 } 2680 2681 /** 2682 * @param {string} raw 2683 * @param {object} [settings] 2684 * @return {string} 2685 */ 2686 function youtubeEmbedSrc(raw, settings) { 2687 var id = youtubeIdFromUrl(raw); 2688 if (!id) { 2689 return ''; 2690 } 2691 var params = ['feature=oembed', 'enablejsapi=1']; 2692 var rawStr = String(raw || ''); 2693 var preserved = {}; 2694 try { 2695 var q = rawStr.split('?')[1] || ''; 2696 if (q) { 2697 q.replace(/^\?/, '') 2698 .split('&') 2699 .forEach(function (pair) { 2700 var parts = pair.split('='); 2701 var k = decodeURIComponent(parts[0] || '').toLowerCase(); 2702 var v = decodeURIComponent((parts[1] || '').replace(/\+/g, ' ')); 2703 if (k === 'list' || k === 'start' || k === 'si' || k === 'index') { 2704 preserved[k] = v; 2705 } 2706 if (k === 't' || k === 'time_continue') { 2707 var secs = v; 2708 if (/^\d+$/.test(v)) { 2709 preserved.start = v; 2710 } else { 2711 var tm = String(v).match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/i); 2712 if (tm) { 2713 preserved.start = String( 2714 (parseInt(tm[1] || '0', 10) || 0) * 3600 + 2715 (parseInt(tm[2] || '0', 10) || 0) * 60 + 2716 (parseInt(tm[3] || '0', 10) || 0) 2717 ); 2718 } 2719 } 2720 } 2721 }); 2722 } 2723 } catch (eYtQ) { /* ignore */ } 2724
2725 Object.keys(preserved).forEach(function (k) { 2726 if (preserved[k] !== '' && preserved[k] != null) { 2727 params.push(encodeURIComponent(k) + '=' + encodeURIComponent(preserved[k])); 2728 } 2729 }); 2730 2731 if (settings && String(settings.controls) === 'yes') { 2732 params.push('controls=1'); 2733 } 2734 if (settings && (settings.autoplay === 'yes' || settings.autoplay === true)) { 2735 params.push('autoplay=1'); 2736 if (params.indexOf('mute=1') === -1) { 2737 params.push('mute=1'); 2738 } 2739 } 2740 if (settings && (settings.mute === 'yes' || settings.mute === true)) { 2741 if (params.indexOf('mute=1') === -1) { 2742 params.push('mute=1'); 2743 } 2744 } 2745 if (settings && (settings.loop === 'yes' || settings.loop === true)) { 2746 params.push('loop=1'); 2747 if (!preserved.list) { 2748 params.push('playlist=' + encodeURIComponent(id)); 2749 } 2750 } 2751 return 'https://www.youtube-nocookie.com/embed/' + encodeURIComponent(id) + '?' + params.join('&'); 2752 } 2753 2754 /** 2755 * Build a Vimeo player URL without dropping unlisted privacy hashes. 2756 * Accepts vimeo.com/{id}/{hash}, ?h=, and player.vimeo.com/video/{id}?h=. 2757 * 2758 * @param {string} raw 2759 * @return {string} 2760 */ 2761 function vimeoEmbedSrc(raw) { 2762 var url = String(raw || '').trim(); 2763 if (!url) { 2764 return ''; 2765 } 2766 2767 function hashFromQuery(u) { 2768 var m = String(u).match(/[?&#]h=([a-zA-Z0-9]+)/i); 2769 return m && m[1] ? m[1] : ''; 2770 } 2771 2772 var playerM = url.match(/player\.vimeo\.com\/video\/(\d+)/i); 2773 if (playerM && playerM[1]) { 2774 var ph = hashFromQuery(url); 2775 return ( 2776 'https://player.vimeo.com/video/' + 2777 encodeURIComponent(playerM[1]) + 2778 (ph ? '?h=' + encodeURIComponent(ph) : '') 2779 ); 2780 } 2781 2782 // vimeo.com/1027049072/366bb7c771 or vimeo.com/video/123 2783 var pathM = url.match(/vimeo\.com\/(?:video\/)?(\d+)(?:\/([a-zA-Z0-9]+))?/i); 2784 if (!pathM || !pathM[1]) { 2785 return ''; 2786 } 2787 var id = pathM[1]; 2788 var hash = pathM[2] || hashFromQuery(url); 2789 // Path segments that are not privacy hashes. 2790 if (hash && /^(channels|manage|album|groups|ondemand|showcase|event|review|staffpick)$/i.test(hash)) { 2791 hash = hashFromQuery(url); 2792 } 2793 return ( 2794 'https://player.vimeo.com/video/' + 2795 encodeURIComponent(id) + 2796 (hash ? '?h=' + encodeURIComponent(hash) : '') 2797 ); 2798 } 2799 2800 /** 2801 * Prefer an exact existing iframe URL over rebuilding (keeps h=, list, etc.). 2802 * 2803 * @param {Element} root 2804 * @return {string} 2805 */ 2806 function existingEmbedUrl(root) { 2807 if (!root || !root.querySelector) { 2808 return ''; 2809 } 2810 var iframe = root.querySelector( 2811 'iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="youtube-nocookie"], iframe[src*="vimeo"],' + 2812 'iframe[data-src*="youtube"], iframe[data-src*="youtu.be"], iframe[data-src*="youtube-nocookie"], iframe[data-src*="vimeo"]' 2813 ); 2814 if (!iframe) { 2815 return ''; 2816 } 2817 return String(iframe.getAttribute('src') || iframe.getAttribute('data-src') || '').trim(); 2818 } 2819 2820 /** 2821 * Build a plain iframe for non-Elementor empty shells only. 2822 * Do not invent Elementor background cover CSS â that breaks native styling. 2823 * 2824 * @param {string} src 2825 * @param {string} title 2826 * @return {HTMLIFrameElement} 2827 */ 2828 function createConsentVideoIframe(src, title) { 2829 var iframe = document.createElement('iframe'); 2830 iframe.className = 'elementor-video-iframe'; 2831 iframe.setAttribute('src', src); 2832 iframe.setAttribute('title', title || 'Video player'); 2833 iframe.setAttribute('frameborder', '0'); 2834 iframe.setAttribute( 2835 'allow', 2836 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen' 2837 ); 2838 iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); 2839 iframe.style.cssText = 'width:100%;height:100%;border:0;'; 2840 return iframe; 2841 } 2842 2843 /** 2844 * Elementor-style cover size (px width/height), matching disabled-plugin markup. 2845 * 2846 * @param {HTMLIFrameElement} iframe 2847 * @param {Element} container 2848 */
2849 function sizeElementorBackgroundIframe(iframe, container) { 2850 var cw = (container && (container.clientWidth || container.offsetWidth)) || 0; 2851 var ch = (container && (container.clientHeight || container.offsetHeight)) || 0; 2852 if (cw < 2 || ch < 2) { 2853 cw = window.innerWidth || 1920; 2854 ch = window.innerHeight || 1080; 2855 } 2856 var ratio = 16 / 9; 2857 var width; 2858 var height; 2859 if (cw / ch > ratio) { 2860 width = cw; 2861 height = cw / ratio; 2862 } else { 2863 height = ch; 2864 width = ch * ratio; 2865 } 2866 // Slight overscan like Elementor's Vimeo background sizing. 2867 width = Math.ceil(width * 1.11); 2868 height = Math.ceil(height * 1.11); 2869 iframe.setAttribute('width', '426'); 2870 iframe.setAttribute('height', '240'); 2871 iframe.style.width = width + 'px'; 2872 iframe.style.height = height + 'px'; 2873 } 2874 2875 /** 2876 * Consent/park races can leave Elementor's BG shell with a live iframe but still 2877 * elementor-loading + elementor-invisible (blank hero). Heal without adding a second iframe. 2878 * Elementor may re-add those classes if its YT onReady never fired â CSS + observer stick. 2879 * 2880 * @param {Element} box 2881 * @param {HTMLIFrameElement} [iframe] 2882 * @return {boolean} 2883 */ 2884 function healElementorBackgroundVideo(box, iframe) { 2885 if (!box) { 2886 return false; 2887 } 2888 var live = 2889 iframe || 2890 box.querySelector('iframe.elementor-background-video-embed[src]:not(.elementor-video-iframe)') || 2891 box.querySelector('iframe.elementor-background-video-embed[src]'); 2892 if (!live) { 2893 return false; 2894 } 2895 var src = ''; 2896 try { 2897 src = live.getAttribute('src') || live.src || ''; 2898 } catch (eSrc) { 2899 src = ''; 2900 } 2901 if (!src || src === 'about:blank' || /^javascript:/i.test(src)) { 2902 return false; 2903 } 2904 try { 2905 box.classList.remove('elementor-loading', 'elementor-invisible'); 2906 box.classList.add('ucpf-bg-video-ready'); 2907 box.setAttribute('data-ucpf-bg-healed', '1'); 2908 } catch (eCls) { /* ignore */ } 2909 try { 2910 // Belt-and-suspenders: Elementor CSS uses visibility:hidden on .elementor-invisible. 2911 box.style.visibility = 'visible'; 2912 box.style.opacity = '1'; 2913 } catch (eStyle) { /* ignore */ } 2914 try { 2915 sizeElementorBackgroundIframe(live, box); 2916 } catch (eSize) { /* ignore */ } 2917 return true; 2918 } 2919 2920 /** 2921 * Scan every Elementor background-video shell with a live player and heal stuck loading state. 2922 * 2923 * @return {number} 2924 */ 2925 function healAllElementorBackgroundVideos() { 2926 var n = 0; 2927 try { 2928 queryAll(['.elementor-background-video-container']).forEach(function (box) { 2929 if (healElementorBackgroundVideo(box)) { 2930 n += 1; 2931 } 2932 }); 2933 } catch (eAll) { /* ignore */ } 2934 return n; 2935 } 2936 2937 var bgVideoHealObserver = null; 2938 2939 /** 2940 * If Elementor re-adds loading/invisible after a missed YT ready, strip again. 2941 */ 2942 function watchElementorBackgroundVideos() { 2943 if (bgVideoHealObserver || typeof MutationObserver !== 'function') { 2944 return; 2945 } 2946 try { 2947 bgVideoHealObserver = new MutationObserver(function (records) { 2948 var need = false; 2949 for (var i = 0; i < records.length; i++) { 2950 var t = records[i].target; 2951 if ( 2952 t && 2953 t.classList && 2954 t.classList.contains('elementor-background-video-container') && 2955 (t.classList.contains('elementor-loading') || t.classList.contains('elementor-invisible')) 2956 ) { 2957 need = true; 2958 break; 2959 } 2960 } 2961 if (need) { 2962 healAllElementorBackgroundVideos(); 2963 } 2964 }); 2965 bgVideoHealObserver.observe(document.documentElement, { 2966 subtree: true, 2967 attributes: true, 2968 attributeFilter: ['class'], 2969 }); 2970 } catch (eObs) { 2971 bgVideoHealObserver = null; 2972 } 2973 } 2974 2975 /** 2976 * Player URL matching Elementor's background Vimeo/YouTube iframe (keep privacy hash). 2977 * 2978 * @param {string} link 2979 * @return {{src:string,isVimeo:boolean}} 2980 */ 2981 function elementorBackgroundPlayerUrl(link) { 2982 var isVimeo = /vimeo/i.test(String(link || '')); 2983 var src = ''; 2984 if (isVimeo) { 2985 src = vimeoEmbedSrc(link); 2986 if (src) { 2987 var join = src.indexOf('?') === -1 ? '?' : '&'; 2988 // Same query shape Elementor emits (plus app_id Elementor adds). 2989 if (src.indexOf('autoplay=') === -1) { 2990 src += join + 'muted=1&autoplay=1&loop=1&background=1&app_id=122963'; 2991 } else if (src.indexOf('app_id=') === -1) { 2992 src += '&app_id=122963'; 2993 } 2994 } 2995 } else { 2996 src = youtubeEmbedSrc(link, { autoplay: 'yes', mute: 'yes', loop: 'yes' }); 2997 } 2998 return { src: src, isVimeo: isVimeo }; 2999 } 3000 3001 /** 3002 * Inject background iframe that mirrors Elementor-disabled markup: 3003 * keep presentation div; sibling iframe.elementor-background-video-embed with px size. 3004 * 3005 * @param {Element} owner 3006 * @return {boolean} 3007 */
3008 function injectElementorNativeBackground(owner) { 3009 if (!owner) { 3010 return false; 3011 } 3012 var box = owner.querySelector('.elementor-background-video-container'); 3013 if (!box) { 3014 return false; 3015 } 3016 // Already has a native Elementor iframe (not our old custom hydrate). 3017 var existing = box.querySelector('iframe.elementor-background-video-embed:not(.elementor-video-iframe)'); 3018 if (existing && existing.getAttribute('src') && box.getAttribute('data-vimeo-initialized') === 'true') { 3019 return false; 3020 } 3021 3022 var settings = parseDataSettings(owner) || {}; 3023 var link = String(settings.background_video_link || ''); 3024 if (!link) { 3025 return false; 3026 } 3027 var isVimeo = /vimeo/i.test(link); 3028 if (isVimeo && !hasCategoryConsent('functional')) { 3029 return false; 3030 } 3031 if (!isVimeo && !(hasCategoryConsent('marketing') && hasCategoryConsent('functional'))) { 3032 return false; 3033 } 3034 3035 // Remove prior UCPF hydrates only. 3036 box.querySelectorAll('iframe.elementor-video-iframe').forEach(function (iframe) { 3037 iframe.remove(); 3038 }); 3039 // If a broken/partial iframe remains without native class sizing, replace it. 3040 box.querySelectorAll('iframe').forEach(function (iframe) { 3041 if (iframe.classList.contains('elementor-video-iframe')) { 3042 iframe.remove(); 3043 } 3044 }); 3045 if (box.querySelector('iframe.elementor-background-video-embed')) { 3046 return false; 3047 } 3048 3049 var placeholder = box.querySelector('div.elementor-background-video-embed'); 3050 if (!placeholder) { 3051 placeholder = document.createElement('div'); 3052 placeholder.className = 'elementor-background-video-embed'; 3053 placeholder.setAttribute('role', 'presentation'); 3054 box.insertBefore(placeholder, box.firstChild); 3055 } else if (!placeholder.getAttribute('role')) { 3056 placeholder.setAttribute('role', 'presentation'); 3057 } 3058 3059 var built = elementorBackgroundPlayerUrl(link); 3060 if (!built.src) { 3061 return false; 3062 } 3063 3064 var iframe = document.createElement('iframe'); 3065 iframe.className = 'elementor-background-video-embed'; 3066 iframe.setAttribute('src', built.src); 3067 iframe.setAttribute('frameborder', '0'); 3068 iframe.setAttribute( 3069 'allow', 3070 'autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share' 3071 ); 3072 iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); 3073 iframe.setAttribute('title', 'Background video'); 3074 iframe.setAttribute('data-ready', 'true'); 3075 sizeElementorBackgroundIframe(iframe, box); 3076 box.appendChild(iframe); 3077 box.setAttribute('data-vimeo-initialized', 'true'); 3078 box.setAttribute('data-ucpf-bg-native', '1'); 3079 owner.setAttribute('data-ucpf-bg-native', '1'); 3080 owner.removeAttribute('data-ucpf-video-hydrated'); 3081 box.removeAttribute('data-ucpf-video-hydrated'); 3082 healElementorBackgroundVideo(box, iframe); 3083 return true; 3084 } 3085 3086 /** 3087 * Undo UCPF-injected background iframes and restore Elementor's empty embed shell. 3088 * 3089 * @param {Element} owner Container with background_video_link settings. 3090 */ 3091 function restoreElementorBackgroundShell(owner) { 3092 if (!owner) { 3093 return null; 3094 } 3095 var box = owner.querySelector('.elementor-background-video-container'); 3096 if (!box) { 3097 return null; 3098 } 3099 try { 3100 box.querySelectorAll('iframe.elementor-video-iframe').forEach(function (iframe) { 3101 iframe.remove(); 3102 }); 3103 var placeholder = box.querySelector('.elementor-background-video-embed[role="presentation"]'); 3104 if (!placeholder) { 3105 placeholder = box.querySelector('div.elementor-background-video-embed:not(iframe)'); 3106 } 3107 if (!placeholder) { 3108 placeholder = document.createElement('div'); 3109 placeholder.className = 'elementor-background-video-embed'; 3110 placeholder.setAttribute('role', 'presentation'); 3111 box.insertBefore(placeholder, box.firstChild); 3112 } 3113 box.removeAttribute('data-ucpf-video-hydrated'); 3114 owner.removeAttribute('data-ucpf-video-hydrated'); 3115 } catch (eRestore) { /* ignore */ } 3116 return box; 3117 } 3118 3119 function whenPlayerApiReady(isVimeo, cb) { 3120 var ready = function () { 3121 if (isVimeo) { 3122 return typeof window.Vimeo !== 'undefined'; 3123 } 3124 return typeof window.YT !== 'undefined' || true; 3125 }; 3126 if (ready()) { 3127 cb(); 3128 return; 3129 } 3130 var n = 0; 3131 var t = window.setInterval(function () { 3132 n += 1; 3133 if (ready() || n > 50) { 3134 window.clearInterval(t); 3135 cb(); 3136 } 3137 }, 100); 3138 } 3139 3140 /** 3141 * Live YouTube / Vimeo iframe already playing inside a root. 3142 * 3143 * @param {Element} root 3144 * @return {boolean} 3145 */ 3146 function hasLiveVideoPlayer(root) { 3147 if (!root || !root.querySelector) { 3148 return false;
3149 } 3150 return !!root.querySelector( 3151 'iframe[src*="youtube.com"], iframe[src*="youtu.be"], iframe[src*="youtube-nocookie.com"], iframe[src*="player.vimeo.com"], iframe[src*="vimeo.com"]' 3152 ); 3153 } 3154 3155 /** 3156 * Prefer a single Elementor-native player. Our late inject must not stack a 3157 * second iframe.elementor-video next to Elementor's (hard-refresh showed both). 3158 * 3159 * @param {Element} [root] Widget or document. 3160 */ 3161 function dedupeElementorOpenInlineVideos(root) { 3162 var scope = root && root.querySelectorAll ? root : document; 3163 var widgets = []; 3164 if ( 3165 scope.classList && 3166 (scope.classList.contains('elementor-widget-video') || scope.getAttribute('data-widget_type') === 'video.default') 3167 ) { 3168 widgets = [scope]; 3169 } else { 3170 widgets = Array.prototype.slice.call( 3171 scope.querySelectorAll 3172 ? scope.querySelectorAll('.elementor-widget-video, [data-widget_type="video.default"]') 3173 : [] 3174 ); 3175 } 3176 widgets.forEach(function (widget) { 3177 var wrap = widget.querySelector('.elementor-wrapper.elementor-open-inline') || widget; 3178 if (!wrap || !wrap.querySelectorAll) { 3179 return; 3180 } 3181 var iframes = Array.prototype.slice.call( 3182 wrap.querySelectorAll('iframe.elementor-video, iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="youtube-nocookie"], iframe[src*="vimeo"]') 3183 ); 3184 if (iframes.length <= 1) { 3185 return; 3186 } 3187 var keep = null; 3188 // 1) Live Elementor player (not our inject). 3189 iframes.forEach(function (iframe) { 3190 if (keep) { 3191 return; 3192 } 3193 var src = iframe.getAttribute('src') || ''; 3194 if ( 3195 iframe.getAttribute('data-ucpf-injected-video') !== '1' && 3196 src && 3197 src !== 'about:blank' && 3198 isVideoPlayerUrl(src) 3199 ) { 3200 keep = iframe; 3201 } 3202 }); 3203 // 2) Any live player. 3204 if (!keep) { 3205 iframes.forEach(function (iframe) { 3206 if (keep) { 3207 return; 3208 } 3209 var src = iframe.getAttribute('src') || ''; 3210 if (src && src !== 'about:blank' && isVideoPlayerUrl(src)) { 3211 keep = iframe; 3212 } 3213 }); 3214 } 3215 // 3) Non-injected shell (parked Elementor frame). 3216 if (!keep) { 3217 iframes.forEach(function (iframe) { 3218 if (keep) { 3219 return; 3220 } 3221 if (iframe.getAttribute('data-ucpf-injected-video') !== '1') { 3222 keep = iframe; 3223 } 3224 }); 3225 } 3226 if (!keep) { 3227 keep = iframes[0]; 3228 } 3229 iframes.forEach(function (iframe) { 3230 if (iframe !== keep) { 3231 try { 3232 iframe.remove(); 3233 } catch (eRm) { /* ignore */ } 3234 } 3235 }); 3236 }); 3237 } 3238 3239 /** 3240 * Additional method for Elementor Video widgets in open-inline mode. 3241 * Restores the existing iframe.elementor-video when possible; only creates a 3242 * new frame when the shell is empty (and marks it so we can dedupe later). 3243 * 3244 * @param {Element} widget 3245 * @param {{ allowCreate?: boolean }} [opts] 3246 * @return {boolean} 3247 */ 3248 function injectElementorOpenInlineVideo(widget, opts) { 3249 opts = opts || {}; 3250 var allowCreate = opts.allowCreate !== false; 3251 3252 if (!widget || !widget.querySelector) { 3253 return false; 3254 } 3255 if (widget.querySelector('.elementor-background-video-container') || isBackgroundVideoOwner(widget)) { 3256 return false; 3257 } 3258 if (isSelfHostedVideoSurface(widget)) { 3259 return false; 3260 } 3261 3262 var wrap = widget.querySelector('.elementor-wrapper.elementor-open-inline'); 3263 if (!wrap) { 3264 return false; 3265 } 3266 3267 // Already have a live player â only strip duplicates. 3268 if (hasLiveVideoPlayer(wrap) || hasLiveVideoPlayer(widget)) { 3269 dedupeElementorOpenInlineVideos(widget); 3270 return true; 3271 } 3272 3273 var settings = parseDataSettings(widget) || {}; 3274 var videoType = String(settings.video_type || '').toLowerCase(); 3275 if (videoType === 'hosted') { 3276 return false; 3277 } 3278 3279 var ytLink = String(settings.youtube_url || ''); 3280 var vimLink = String(settings.vimeo_url || ''); 3281 var isVimeo = 3282 videoType === 'vimeo' || 3283 (!!vimLink && !ytLink) || 3284 /vimeo/i.test(vimLink || existingEmbedUrl(wrap) || existingEmbedUrl(widget)); 3285 3286 if (isVimeo) { 3287 if (!hasCategoryConsent('functional')) { 3288 return false;
3289 } 3290 } else if (!(hasCategoryConsent('marketing') && hasCategoryConsent('functional'))) { 3291 return false; 3292 } 3293 3294 // Prefer Elementor's exact parked URL (youtube.com/â¦) over rebuilding nocookie. 3295 var src = existingEmbedUrl(wrap) || existingEmbedUrl(widget); 3296 if (!src || !isVideoPlayerUrl(src)) { 3297 if (isVimeo) { 3298 src = vimeoEmbedSrc(vimLink || ytLink); 3299 } else { 3300 src = youtubeEmbedSrc(ytLink || vimLink, settings); 3301 } 3302 } 3303 if (!src) { 3304 return false; 3305 } 3306 3307 var iframe = 3308 wrap.querySelector('iframe.elementor-video:not([data-ucpf-injected-video])') || 3309 wrap.querySelector('iframe.elementor-video') || 3310 wrap.querySelector('iframe[data-ucpf-parked="1"]') || 3311 wrap.querySelector('iframe[data-src]') || 3312 wrap.querySelector('iframe'); 3313 3314 if (iframe) { 3315 if (!iframe.classList.contains('elementor-video')) { 3316 iframe.classList.add('elementor-video'); 3317 } 3318 if (!forceVideoIframeSrc(iframe, src)) { 3319 return false; 3320 } 3321 dedupeElementorOpenInlineVideos(widget); 3322 return true; 3323 } 3324 3325 // Empty shell only â wait for Elementor unless allowCreate (late fallback). 3326 if (!allowCreate) { 3327 return false; 3328 } 3329 3330 iframe = document.createElement('iframe'); 3331 iframe.className = 'elementor-video'; 3332 iframe.setAttribute('data-ucpf-injected-video', '1'); 3333 iframe.setAttribute('frameborder', '0'); 3334 iframe.setAttribute('allowfullscreen', ''); 3335 iframe.setAttribute( 3336 'allow', 3337 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' 3338 ); 3339 iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); 3340 iframe.setAttribute('title', 'Video player'); 3341 iframe.setAttribute('width', '640'); 3342 iframe.setAttribute('height', '360'); 3343 wrap.appendChild(iframe); 3344 if (!forceVideoIframeSrc(iframe, src)) { 3345 try { 3346 iframe.remove(); 3347 } catch (eRm) { /* ignore */ } 3348 return false; 3349 } 3350 3351 widget.setAttribute('data-ucpf-open-inline', '1'); 3352 wrap.setAttribute('data-ucpf-open-inline', '1'); 3353 dedupeElementorOpenInlineVideos(widget); 3354 return true; 3355 } 3356 3357 /** 3358 * After consent: try Elementor handlers, then native-matching iframe fallback. 3359 * 3360 * @return {boolean} 3361 */ 3362 function reinitElementorVideos() { 3363 var hasEl = 3364 !!( 3365 window.elementorFrontend && 3366 elementorFrontend.elementsHandler && 3367 typeof elementorFrontend.elementsHandler.runReadyTrigger === 'function' 3368 ); 3369 var run = function (el) { 3370 if (!hasEl) { 3371 return; 3372 } 3373 try { 3374 elementorFrontend.elementsHandler.runReadyTrigger(el); 3375 } catch (eRun) { /* ignore */ } 3376 }; 3377 3378 queryAll(['[data-settings*="background_video_link"]']).forEach(function (owner) { 3379 var box = owner.querySelector('.elementor-background-video-container'); 3380 if (!box) { 3381 return; 3382 } 3383 // Native Elementor success â clear stuck loading/invisible, then leave alone. 3384 if ( 3385 owner.getAttribute('data-ucpf-bg-native') === '1' || 3386 box.getAttribute('data-ucpf-bg-native') === '1' 3387 ) { 3388 var native = box.querySelector('iframe.elementor-background-video-embed:not(.elementor-video-iframe)'); 3389 if (native && native.getAttribute('src')) { 3390 healElementorBackgroundVideo(box, native); 3391 return; 3392 } 3393 } 3394 if ( 3395 box.getAttribute('data-vimeo-initialized') === 'true' && 3396 box.querySelector('iframe.elementor-background-video-embed:not(.elementor-video-iframe)') && 3397 owner.getAttribute('data-ucpf-video-hydrated') !== '1' 3398 ) { 3399 healElementorBackgroundVideo(box); 3400 return; 3401 } 3402 3403 // Live iframe already present (Elementor or restored) â heal, do not strip/reinject. 3404 if (healElementorBackgroundVideo(box)) { 3405 return; 3406 } 3407 3408 // Strip broken UCPF custom iframes; keep/restore presentation div. 3409 restoreElementorBackgroundShell(owner); 3410 box.removeAttribute('data-vimeo-initialized'); 3411 run(owner); 3412 3413 // Elementor often will not re-bind background video after a failed first pass. 3414 // Fall back to markup that matches plugin-disabled Elementor output. 3415 window.setTimeout(function () { 3416 var existing = box.querySelector('iframe.elementor-background-video-embed:not(.elementor-video-iframe)'); 3417 if (existing && existing.getAttribute('src')) {
3418 healElementorBackgroundVideo(box, existing); 3419 return; 3420 } 3421 whenPlayerApiReady(/vimeo/i.test(String((parseDataSettings(owner) || {}).background_video_link || '')), function () { 3422 injectElementorNativeBackground(owner); 3423 }); 3424 }, 350); 3425 }); 3426 3427 queryAll(['.elementor-widget-video', '[data-widget_type="video.default"]']).forEach(function (widget) { 3428 // Always restore parked Elementor frames first; never invent a sibling yet. 3429 restoreParkedVideoIframes(widget); 3430 dedupeElementorOpenInlineVideos(widget); 3431 3432 if (widget.getAttribute('data-ucpf-video-hydrated') === '1') { 3433 try { 3434 widget.querySelectorAll('iframe.elementor-video-iframe').forEach(function (iframe) { 3435 iframe.remove(); 3436 }); 3437 widget.removeAttribute('data-ucpf-video-hydrated'); 3438 } catch (eW) { /* ignore */ } 3439 } 3440 3441 if (hasLiveVideoPlayer(widget)) { 3442 dedupeElementorOpenInlineVideos(widget); 3443 return; 3444 } 3445 3446 run(widget); 3447 // Restore-only passes while Elementor remounts. 3448 window.setTimeout(function () { 3449 restoreParkedVideoIframes(widget); 3450 injectElementorOpenInlineVideo(widget, { allowCreate: false }); 3451 dedupeElementorOpenInlineVideos(widget); 3452 }, 200); 3453 // Late create only if Elementor left the open-inline shell empty. 3454 window.setTimeout(function () { 3455 if (!hasLiveVideoPlayer(widget)) { 3456 injectElementorOpenInlineVideo(widget, { allowCreate: true }); 3457 } 3458 dedupeElementorOpenInlineVideos(widget); 3459 }, 1200); 3460 }); 3461 3462 return hasEl; 3463 } 3464 3465 /** 3466 * Ensure the mount has sizing so the absolute iframe is visible. 3467 * @param {Element} mount 3468 * @param {Element} widget 3469 */ 3470 function ensureVideoMountBox(mount, widget) { 3471 if (!mount) { 3472 return; 3473 } 3474 try { 3475 var cs = window.getComputedStyle(mount); 3476 if (cs.position === 'static') { 3477 mount.style.position = 'relative'; 3478 } 3479 var h = mount.getBoundingClientRect().height; 3480 if (h < 40) { 3481 var wrap = widget && widget.querySelector ? widget.querySelector('.elementor-wrapper') : null; 3482 if (wrap && wrap.classList.contains('elementor-fit-aspect-ratio')) { 3483 /* Elementor aspect-ratio CSS should size it */ 3484 } else if (!mount.style.paddingBottom && !mount.style.aspectRatio) { 3485 mount.style.aspectRatio = '16 / 9'; 3486 mount.style.width = '100%'; 3487 mount.style.minHeight = '200px'; 3488 } 3489 } 3490 } catch (eBox) { 3491 /* ignore */ 3492 } 3493 } 3494 3495 /** 3496 * Elementor / Divi / Gutenberg leave empty shells until their player JS runs. 3497 * After Marketing + Embeds consent, inject the iframe ourselves (Shorts-safe). 3498 */ 3499 function hydrateBuilderVideos() { 3500 var allowYt = hasCategoryConsent('marketing') && hasCategoryConsent('functional'); 3501 var allowVim = hasCategoryConsent('functional'); 3502 if (!allowYt && !allowVim) { 3503 return 0; 3504 } 3505 3506 var count = 0; 3507 3508 function alreadyHasPlayer(root) { 3509 if (!root || !root.querySelector) { 3510 return true; 3511 } 3512 // Live or deferred (data-src) players â never clobber; loader restores data-src verbatim. 3513 return !!( 3514 root.querySelector( 3515 'iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="youtube-nocookie"], iframe[src*="vimeo"],' + 3516 'iframe[data-src*="youtube"], iframe[data-src*="youtu.be"], iframe[data-src*="youtube-nocookie"], iframe[data-src*="vimeo"]' 3517 ) || root.querySelector('video[src], video source[src]') 3518 ); 3519 } 3520 3521 function resolveEmbedSrc(root, mount, built) { 3522 var exact = existingEmbedUrl(root) || existingEmbedUrl(mount); 3523 if (exact && /youtube|youtu\.be|vimeo/i.test(exact)) { 3524 return exact; 3525 } 3526 return built || ''; 3527 } 3528 3529 function injectInto(mount, src, title, widget) { 3530 if (!mount || !src || mount.querySelector('iframe')) { 3531 return false; 3532 } 3533 ensureVideoMountBox(mount, widget); 3534 mount.appendChild(createConsentVideoIframe(src, title)); 3535 mount.setAttribute('data-ucpf-video-hydrated', '1'); 3536 return true; 3537 } 3538 3539 // Elementor Video widgets â never invent markup; Elementor sizes/styles natively. 3540 // (Handled in reinitElementorVideos / runReadyTrigger below.) 3541 3542 // Do NOT inject Elementor background videos here â that breaks Elementor's 3543 // cover sizing (width/height px). reinitElementorVideos restores the shell 3544 // and runs Elementor's own Vimeo/YouTube background handler. 3545 3546 // Gutenberg / Divi / WPBakery / Bricks empty shells with URL in attrs/classes. 3547 queryAll([ 3548 '.wp-block-embed-youtube', 3549 '.wp-block-embed.is-provider-youtube', 3550 '.wp-block-embed-vimeo', 3551 '.wp-block-embed.is-provider-vimeo', 3552 '.et_pb_video', 3553 '.wpb_video_widget', 3554 '.fl-module-video', 3555 '.brxe-video',
3556 ]).forEach(function (shell) { 3557 if (alreadyHasPlayer(shell) || shell.getAttribute('data-ucpf-video-hydrated') === '1') { 3558 return; 3559 } 3560 // Skip if this shell is inside Elementor â Elementor owns those. 3561 if (shell.closest && shell.closest('.elementor-element, .elementor')) { 3562 return; 3563 } 3564 var blob = 3565 (shell.getAttribute('data-src') || '') + 3566 ' ' + 3567 (shell.getAttribute('data-url') || '') + 3568 ' ' + 3569 (shell.getAttribute('data-video-url') || '') + 3570 ' ' + 3571 (shell.innerHTML || ''); 3572 var mount = 3573 shell.querySelector('.wp-block-embed__wrapper') || 3574 shell.querySelector('.et_pb_video_box') || 3575 shell.querySelector('.fluid-width-video-wrapper') || 3576 shell; 3577 var src = ''; 3578 var cls = (shell.className || '').toLowerCase(); 3579 var built = ''; 3580 if (/vimeo/.test(cls + blob)) { 3581 if (!allowVim) { 3582 return; 3583 } 3584 var vm = blob.match(/https?:\/\/[^\s"'<>]*vimeo\.com\/[^\s"'<>]+/i); 3585 built = vimeoEmbedSrc(vm ? vm[0] : blob); 3586 } else { 3587 if (!allowYt) { 3588 return; 3589 } 3590 var ym = blob.match(/https?:\/\/[^\s"'<>]*(?:youtube\.com|youtu\.be)\/[^\s"'<>]+/i); 3591 built = youtubeEmbedSrc(ym ? ym[0] : blob, {}); 3592 } 3593 src = resolveEmbedSrc(shell, mount, built); 3594 if (injectInto(mount, src, 'Embedded video', shell)) { 3595 shell.setAttribute('data-ucpf-video-hydrated', '1'); 3596 count += 1; 3597 } 3598 }); 3599 3600 return count; 3601 } 3602 3603 function ensureVideosIfNeeded() { 3604 // Always hydrate videos after consent â leaveBuildersAlone only skips layout/Motion FX 3605 // recovery, never GDPR unlock for YouTube/Vimeo (same rule as maps). 3606 if (!hasCategoryConsent('marketing') || !hasCategoryConsent('functional')) { 3607 return; 3608 } 3609 watchElementorBackgroundVideos(); 3610 var runRestore = function () { 3611 restoreAllParkedVideoIframes(); 3612 dedupeElementorOpenInlineVideos(document); 3613 }; 3614 var runHydrate = function (allowCreate) { 3615 restoreAllParkedVideoIframes(); 3616 // Live BG iframes often exist before YT/Vimeo APIs â clear stuck loading immediately. 3617 healAllElementorBackgroundVideos(); 3618 reinitElementorVideos(); 3619 healAllElementorBackgroundVideos(); 3620 hydrateBuilderVideos(); 3621 queryAll(['.elementor-widget-video', '[data-widget_type="video.default"]']).forEach(function (widget) { 3622 injectElementorOpenInlineVideo(widget, { allowCreate: !!allowCreate }); 3623 dedupeElementorOpenInlineVideos(widget); 3624 }); 3625 dedupeElementorOpenInlineVideos(document); 3626 }; 3627 // Immediate: put Elementor's parked URL back + heal stuck BG shells (no API wait). 3628 runRestore(); 3629 healAllElementorBackgroundVideos(); 3630 runHydrate(false); 3631 // Do not wait for Vimeo on YouTube-only pages (was whenPlayerApiReady(true) â 5s delay). 3632 whenPlayerApiReady(false, function () { 3633 runHydrate(false); 3634 }); 3635 window.setTimeout(function () { 3636 runHydrate(false); 3637 }, 300); 3638 window.setTimeout(function () { 3639 runHydrate(false); 3640 }, 800); 3641 // Late fallback create only if Elementor never mounted a player. 3642 window.setTimeout(function () { 3643 runHydrate(true); 3644 }, 1400); 3645 window.setTimeout(function () { 3646 runHydrate(true); 3647 healAllElementorBackgroundVideos(); 3648 }, 2500); 3649 window.setTimeout(healAllElementorBackgroundVideos, 4000); 3650 } 3651 3652 /** 3653 * After Marketing+Embeds: Smash Balloon feeds were parked. One-shot resize so 3654 * list/masonry layouts measure after the overlay is removed. Do not call 3655 * Smash Balloon internals or loop applyConsent (Mapster freeze). 3656 */ 3657 var smashBalloonHydrated = false; 3658 3659 function ensureSmashBalloonIfNeeded() { 3660 if (smashBalloonHydrated) { 3661 return; 3662 } 3663 if (!hasCategoryConsent('marketing') || !hasCategoryConsent('functional')) { 3664 return; 3665 } 3666 var hasFeed = false; 3667 try { 3668 hasFeed = !!document.querySelector('.sb_youtube, .sb-wall, [id^="sb_youtube"], [id^="sb-wall"]'); 3669 } catch (eQ) { 3670 hasFeed = false;
3671 } 3672 if (!hasFeed) { 3673 return; 3674 } 3675 smashBalloonHydrated = true; 3676 try { 3677 if (window.jQuery) { 3678 window.jQuery(window).trigger('resize'); 3679 } 3680 } catch (eJq) { 3681 /* ignore */ 3682 } 3683 try { 3684 window.dispatchEvent(new Event('resize')); 3685 } catch (eEv) { 3686 /* ignore */ 3687 } 3688 } 3689 3690 function videoHostContainer(node) { 3691 if (!node) { 3692 return null; 3693 } 3694 // Prefer a sized shell so the overlay has layout (empty .elementor-video is fine). 3695 if (node.classList) { 3696 if (node.classList.contains('elementor-widget-video') || node.getAttribute('data-widget_type') === 'video.default') { 3697 return node; 3698 } 3699 if ( 3700 isBackgroundVideoOwner(node) || 3701 node.classList.contains('elementor-background-video-container') || 3702 node.classList.contains('elementor-background-video-embed') 3703 ) { 3704 return resolveBackgroundVideoHost(node); 3705 } 3706 if (node.classList.contains('elementor-wrapper') || node.classList.contains('elementor-video')) { 3707 return node.closest('.elementor-widget-video') || node.closest('[data-widget_type="video.default"]') || node; 3708 } 3709 if ( 3710 node.classList.contains('et_pb_video') || 3711 node.classList.contains('wpb_video_widget') || 3712 node.classList.contains('fl-module-video') || 3713 node.classList.contains('brxe-video') || 3714 node.classList.contains('wp-block-embed-youtube') || 3715 node.classList.contains('wp-block-embed-vimeo') || 3716 node.classList.contains('wp-block-video') || 3717 node.classList.contains('wp-block-embed') 3718 ) { 3719 return node; 3720 } 3721 } 3722 if (node.tagName === 'IFRAME' && node.parentElement) { 3723 var parent = node.parentElement; 3724 if (parent.classList.contains('ucpf-consent-guard')) { 3725 return node; 3726 } 3727 // Prefer known media shells; otherwise guard the iframe itself (avoid wrapping huge parents). 3728 if ( 3729 parent.classList.contains('elementor-wrapper') || 3730 parent.classList.contains('elementor-video') || 3731 parent.classList.contains('elementor-fit-aspect-ratio') || 3732 parent.classList.contains('wp-block-embed__wrapper') || 3733 parent.classList.contains('fluidvids') || 3734 parent.classList.contains('et_pb_video_box') || 3735 parent.classList.contains('fl-video') 3736 ) { 3737 return ( 3738 parent.closest('.elementor-widget-video') || 3739 parent.closest('[data-widget_type="video.default"]') || 3740 parent.closest('.et_pb_video') || 3741 parent.closest('.wp-block-embed') || 3742 parent 3743 ); 3744 } 3745 return node; 3746 } 3747 return node; 3748 } 3749 3750 /** 3751 * True for blocked library scripts / JS APIs â not playable embeds. 3752 * 3753 * @param {string} src 3754 * @return {boolean} 3755 */ 3756 function isNonVisualMediaUrl(src) { 3757 src = String(src || '').toLowerCase(); 3758 if (!src) { 3759 return false; 3760 } 3761 if (/\/api\/player\.js/.test(src)) { 3762 return true; 3763 } 3764 // Script assets ending in .js â keep player.vimeo.com/video/... allowed. 3765 if (/\.js(\?|#|$)/.test(src)) { 3766 return true; 3767 } 3768 return false; 3769 } 3770 3771 /** 3772 * Surface overlays only belong on visual embeds (iframe/map/shell), never on 3773 * blocked library scripts (e.g. GTM4WP player.vimeo.com/api/player.js). 3774 * 3775 * @param {Element} node 3776 * @return {boolean} 3777 */ 3778 function isVisualEmbedSurface(node) { 3779 if (!node || !node.tagName) { 3780 return false; 3781 } 3782 if (isForbiddenGuardHost(node)) { 3783 return false; 3784 } 3785 var tag = node.tagName.toUpperCase(); 3786 if ( 3787 tag === 'SCRIPT' || 3788 tag === 'LINK' || 3789 tag === 'STYLE' || 3790 tag === 'META' || 3791 tag === 'NOSCRIPT' || 3792 tag === 'TEMPLATE' || 3793 tag === 'SOURCE' || 3794 tag === 'BASE' 3795 ) { 3796 return false; 3797 } 3798 if (/gtm4wp/i.test(node.id || '') || /gtm4wp/i.test(node.className || '')) { 3799 return false; 3800 } 3801 var src = 3802 node.getAttribute('data-src') || 3803 node.getAttribute('src') || 3804 node.getAttribute('data-lazy-src') || 3805 ''; 3806 if (isNonVisualMediaUrl(src)) { 3807 return false;
3808 } 3809 return true; 3810 } 3811 3812 /** Tear down guards wrongly applied to scripts / empty non-visual wrappers. */ 3813 function teardownNonVisualGuards() { 3814 teardownDocumentChromeGuards(); 3815 // Heal any cover accidentally placed on the accessibility toolbar. 3816 try { 3817 queryAll([ 3818 '#userwayAccessibilityIcon.ucpf-consent-guard', 3819 '.userway_buttons_wrapper.ucpf-consent-guard', 3820 '[id*="userway"].ucpf-consent-guard', 3821 '[class*="userway"].ucpf-consent-guard', 3822 '.uwy.ucpf-consent-guard', 3823 ]).forEach(function (el) { 3824 removeGuard(el); 3825 }); 3826 } catch (eUwHeal) { /* ignore */ } 3827 // Un-park UserWay scripts left gated by older builds (Preferences / Embeds). 3828 try { 3829 queryAll([ 3830 'script[data-src*="userway"]', 3831 'script[data-src*="cdn.userway.org"]', 3832 'script[type="text/plain"][data-src*="userway"]', 3833 ]).forEach(function (script) { 3834 var src = script.getAttribute('data-src') || ''; 3835 if (!isUserWayUrl(src)) { 3836 return; 3837 } 3838 if (script.getAttribute('src')) { 3839 script.removeAttribute('data-ucpf-gated'); 3840 script.removeAttribute('data-ucpf-category'); 3841 script.removeAttribute('data-ucpf-service'); 3842 return; 3843 } 3844 script.type = 'text/javascript'; 3845 script.src = src; 3846 script.removeAttribute('data-src'); 3847 script.removeAttribute('data-ucpf-gated'); 3848 script.removeAttribute('data-ucpf-category'); 3849 script.removeAttribute('data-ucpf-service'); 3850 }); 3851 } catch (eUwPark) { /* ignore */ } 3852 queryAll(['script[data-ucpf-consent-guarded="1"]']).forEach(function (script) { 3853 removeGuard(script); 3854 }); 3855 // Migrate legacy overlays that decorated the Elementor video fill instead of the e-con owner. 3856 queryAll(['.elementor-background-video-container.ucpf-consent-guard']).forEach(function (box) { 3857 if (box.getAttribute('data-ucpf-consent-guarded') === '1') { 3858 removeGuard(box); 3859 } 3860 }); 3861 // Older builds forced position:absolute !important + --embed on bg e-cons (broke layout). 3862 queryAll(['.ucpf-consent-guard--bg']).forEach(function (host) { 3863 host.classList.remove('ucpf-consent-guard--embed', 'ucpf-consent-guard--form'); 3864 if (host.getAttribute('data-ucpf-bg-pos') === '1') { 3865 host.style.removeProperty('position'); 3866 host.removeAttribute('data-ucpf-bg-pos'); 3867 } 3868 }); 3869 queryAll(['.ucpf-consent-guard--embed']).forEach(function (wrap) { 3870 // In-place shells (Elementor widget etc.) keep their own markup children â never tear those down here. 3871 if (wrap.getAttribute('data-ucpf-guard-shell') === '1') { 3872 return; 3873 } 3874 // Heal wraps that somehow contain <body>. 3875 for (var bi = 0; bi < wrap.children.length; bi++) { 3876 if (wrap.children[bi] === document.body || String(wrap.children[bi].tagName || '').toUpperCase() === 'BODY') { 3877 teardownDocumentChromeGuards(); 3878 return; 3879 } 3880 } 3881 var kids = []; 3882 for (var i = 0; i < wrap.children.length; i++) { 3883 var child = wrap.children[i]; 3884 if (!child.classList.contains('ucpf-consent-guard__panel')) { 3885 kids.push(child); 3886 } 3887 } 3888 if (kids.length === 1 && !isVisualEmbedSurface(kids[0])) { 3889 removeGuard(kids[0]); 3890 } 3891 }); 3892 } 3893 3894 /** 3895 * Heal catastrophic wraps: consent guard that reparented <body> (breaks Elementor MO/sticky). 3896 */ 3897 function teardownDocumentChromeGuards() { 3898 try { 3899 var body = document.body; 3900 if (body && body.parentElement && body.parentElement.classList && body.parentElement.classList.contains('ucpf-consent-guard')) { 3901 var wrap = body.parentElement; 3902 var grand = wrap.parentNode; 3903 if (grand) { 3904 while (wrap.firstChild) { 3905 var child = wrap.firstChild; 3906 if (child.classList && child.classList.contains('ucpf-consent-guard__panel')) { 3907 wrap.removeChild(child); 3908 continue; 3909 } 3910 grand.insertBefore(child, wrap); 3911 } 3912 if (wrap.parentNode === grand) { 3913 grand.removeChild(wrap); 3914 } 3915 } 3916 } 3917 } catch (eUnwrap) { /* ignore */ } 3918 3919 [document.documentElement, document.head, document.body].forEac
3919h(function (el) { 3920 if (!el || !el.classList) { 3921 return; 3922 } 3923 try { 3924 el.classList.remove( 3925 'ucpf-consent-guard', 3926 'ucpf-captcha-guard', 3927 'ucpf-consent-guard--active', 3928 'ucpf-consent-guard--embed', 3929 'ucpf-consent-guard--form', 3930 'ucpf-consent-guard--bg' 3931 ); 3932 el.removeAttribute('data-ucpf-consent-guarded'); 3933 el.removeAttribute('data-ucpf-guard-shell'); 3934 el.removeAttribute('data-ucpf-guard-kind'); 3935 el.removeAttribute('data-ucpf-guard-category'); 3936 el.removeAttribute('data-ucpf-size-locked'); 3937 el.removeAttribute('data-ucpf-guard-min-h'); 3938 el.style.removeProperty('min-height'); 3939 var panels = []; 3940 try { 3941 panels = el.querySelectorAll(':scope > .ucpf-consent-guard__panel'); 3942 } catch (eScope) { 3943 for (var pi = 0; pi < el.children.length; pi++) { 3944 if (el.children[pi].classList && el.children[pi].classList.contains('ucpf-consent-guard__panel')) { 3945 panels.push(el.children[pi]); 3946 } 3947 } 3948 } 3949 Array.prototype.forEach.call(panels, function (p) { 3950 if (p && p.parentNode === el) { 3951 p.parentNode.removeChild(p); 3952 } 3953 }); 3954 } catch (eClean) { /* ignore */ } 3955 }); 3956 } 3957 3958 /** 3959 * True when a form contains any captcha / security widget signal 3960 * (covers custom themes that invent class names). 3961 * 3962 * @param {Element} form 3963 * @return {boolean} 3964 */ 3965 function formHasCaptchaSignal(form) { 3966 if (!form || !form.querySelector) { 3967 return false; 3968 } 3969 var i; 3970 for (i = 0; i < CAPTCHA_MARKERS.length; i++) { 3971 try { 3972 if (form.querySelector(CAPTCHA_MARKERS[i])) { 3973 return true; 3974 } 3975 } catch (e1) { 3976 /* invalid selector */ 3977 } 3978 } 3979 for (i = 0; i < CAPTCHA_FORM_INNER_SELECTORS.length; i++) { 3980 try { 3981 if (form.querySelector(CAPTCHA_FORM_INNER_SELECTORS[i])) { 3982 return true; 3983 } 3984 } catch (e2) { 3985 /* invalid selector */ 3986 } 3987 } 3988 // Name / URL heuristics for unknown markup. 3989 var named = form.querySelectorAll('[name], iframe[src], iframe[data-src], script[src], script[data-src]'); 3990 for (i = 0; i < named.length; i++) { 3991 var el = named[i]; 3992 var blob = ( 3993 (el.getAttribute('name') || '') + 3994 ' ' + 3995 (el.getAttribute('src') || '') + 3996 ' ' + 3997 (el.getAttribute('data-src') || '') + 3998 ' ' + 3999 (el.getAttribute('data-ucpf-category') || '') 4000 ).toLowerCase(); 4001 if (/(recaptcha|h-captcha|hcaptcha|cf-turnstile|turnstile|frc-|friendlycaptcha)/.test(blob)) { 4002 return true; 4003 } 4004 if ( 4005 el.getAttribute('data-ucpf-category') === 'security' && 4006 /(recaptcha|hcaptcha|turnstile|friendlycaptcha|challenges\.cloudflare)/.test(blob) 4007 ) { 4008 return true; 4009 } 4010 } 4011 return false; 4012 } 4013 4014 function collectTargets() { 4015 /** @type {{ target: Element, kind: string, category: string, categories: string[], mode: string }[]} */ 4016 var items = []; 4017 var seen = []; 4018 4019 function push(target, kind, category, mode, categories) { 4020 if (!target || isForbiddenGuardHost(target) || isUserWayNode(target)) { 4021 return; 4022 } 4023 if (mode === 'embed' && !isVisualEmbedSurface(target)) { 4024 return; 4025 } 4026 if (mode === 'embed' && isEffectivelyHidden(target) && !isSmashBalloonFeedHost(target)) { 4027 return; 4028 } 4029 for (var i = 0; i < seen.length; i++) { 4030 if (seen[i] === target) { 4031 return; 4032 } 4033 // Maps: one cover on the outermost host â skip nested markers already covered, 4034 // or replace an inner host when a better outer host is collected later. 4035 if (kind === 'map' && seen[i] && seen[i].nodeType === 1 && target.nodeType === 1) { 4036 try { 4037 if (seen[i].contains && seen[i].contains(target)) { 4038 return; 4039 } 4040 if (target.contains && target.contains(seen[i])) { 4041 var inner = seen[i]; 4042 seen.splice(i, 1); 4043 for (var j = items.length - 1; j >= 0; j--) { 4044 if (items[j].target === inner) { 4045 items.splice(j, 1); 4046 } 4047 } 4048 i--; 4049 } 4050 } catch (eNest) { /* ignore */ } 4051 } 4052 } 4053 seen.push(target);
4054 var cats = Array.isArray(categories) && categories.length ? categories : category ? [category] : []; 4055 items.push({ 4056 target: target, 4057 kind: kind, 4058 category: category || cats[0] || '', 4059 categories: cats, 4060 mode: mode, 4061 }); 4062 } 4063 4064 // WooCommerce checkout first â one combined Security + Embeds panel (no stacked overlays). 4065 queryAll(CHECKOUT_SELECTORS).forEach(function (node) { 4066 var host = node; 4067 if (node.tagName !== 'FORM') { 4068 host = (node.querySelector && (node.querySelector('form.checkout') || node.querySelector('form.woocommerce-checkout'))) || node; 4069 } 4070 push(host, 'checkout', 'functional', 'form', ['security', 'functional']); 4071 }); 4072 4073 // CAPTCHA-backed forms â Security (skip checkout hosts â already covered above). 4074 // Always cover builder-hosted forms (Elementor etc.) â leaveBuildersAlone must 4075 // never suppress GDPR surface overlays. 4076 queryAll(CAPTCHA_MARKERS).forEach(function (node) { 4077 var form = findFormForNode(node); 4078 if (isCheckoutSurface(form) || isCheckoutSurface(node)) { 4079 return; 4080 } 4081 push(form, 'captcha', 'security', 'form', ['security']); 4082 }); 4083 4084 // Any <form> with captcha-ish descendants (custom themes / unknown plugins). 4085 Array.prototype.forEach.call(document.querySelectorAll('form'), function (form) { 4086 if (isCheckoutSurface(form)) { 4087 return; 4088 } 4089 if (formHasCaptchaSignal(form)) { 4090 push(form, 'captcha', 'security', 'form', ['security']); 4091 } 4092 }); 4093 4094 // Gravity Forms wrappers (AJAX / Elementor shortcode) â cover even if markers sit oddly. 4095 queryAll(['.gform_wrapper', 'form[id^="gform_"]']).forEach(function (node) { 4096 if (isCheckoutSurface(node)) { 4097 return; 4098 } 4099 var form = 4100 node.tagName === 'FORM' 4101 ? node 4102 : (node.querySelector && (node.querySelector('form[id^="gform_"]') || node.querySelector('form'))) || node; 4103 if ( 4104 formHasCaptchaSignal(form) || 4105 (form.querySelector && 4106 form.querySelector( 4107 '.gfield--type-captcha, .ginput_recaptcha, .gform_recaptcha, .gform_captcha, [data-sitekey]' 4108 )) 4109 ) { 4110 push(form, 'captcha', 'security', 'form', ['security']); 4111 } 4112 }); 4113 4114 // Catalog / blocker placeholders + network-gate parked iframes. 4115 queryAll([ 4116 '.ucpf-iframe-placeholder[data-ucpf-category]', 4117 'iframe[data-ucpf-category][data-src]', 4118 'iframe[data-ucpf-gated="1"]', 4119 'iframe[data-ucpf-category]', 4120 ]).forEach(function (node) { 4121 var category = node.getAttribute('data-ucpf-category') || 'marketing'; 4122 var kind = kindForPlaceholder(node); 4123 if (kind === 'embed' && category === 'functional') { 4124 kind = 'widget'; 4125 } 4126 var cats = 4127 kind === 'youtube' || kind === 'vimeo' || kind === 'widget' || kind === 'calendly' || kind === 'map' || kind === 'embed' 4128 ? ensureEmbedConsentCategories([category]) 4129 : [category]; 4130 var host = node; 4131 if (node.tagName === 'IFRAME' && node.parentElement) { 4132 host = widgetEmbedHostContainer(node) || node.parentElement; 4133 } 4134 push(host, kind, category, 'embed', cats); 4135 }); 4136 4137 // Live map widgets / iframes not yet replaced. 4138 queryAll(MAP_MARKERS).forEach(function (node) { 4139 var host = resolveMapHost(node); 4140 if (!host) { 4141 return; 4142 } 4143 push(host, 'map', 'functional', 'embed', ensureEmbedConsentCategories(['functional'])); 4144 }); 4145 4146 // Calendly inline widgets / iframes (Elementor popups inject these late). 4147 queryAll(CALENDLY_MARKERS).forEach(function (node) { 4148 if (isEffectivelyHidden(node)) { 4149 return; 4150 } 4151 var host = calendlyHostContainer(node); 4152 if (host) { 4153 push(host, 'calendly', 'functional', 'embed', ensureEmbedConsentCategories(['functional'])); 4154 } 4155 }); 4156 4157 // Amelia Booking â first-party SPA (Gravity Forms model): Security overlay only 4158 // when captcha is configured. Never treat as a third-party embed / don't park scripts.
4159 queryAll(AMELIA_FORM_MARKERS).forEach(function (node) { 4160 if (isEffectivelyHidden(node)) { 4161 return; 4162 } 4163 var host = node.id === 'amelia-container' ? node : (node.closest && node.closest('#amelia-container')) || node; 4164 // Always cover booking until Security â Amelia loads reCAPTCHA with the form (HAR). 4165 push(host, 'captcha', 'security', 'form', ['security']); 4166 }); 4167 4168 // Jobber / Typeform / similar booking & form embeds. 4169 queryAll(WIDGET_EMBED_MARKERS).forEach(function (node) { 4170 if (isEffectivelyHidden(node)) { 4171 return; 4172 } 4173 var host = widgetEmbedHostContainer(node); 4174 if (host) { 4175 push(host, 'widget', 'functional', 'embed', ensureEmbedConsentCategories(['functional'])); 4176 } 4177 }); 4178 4179 // Gated embed scripts (Jobber CloudFront snippet, etc.) â cover the host box 4180 // even when the iframe has not been injected yet. 4181 queryAll(['script[data-ucpf-gated="1"]', 'script[data-ucpf-category][data-src]', 'script[type="text/plain"][data-src]']).forEac
4181h( 4182 function (script) { 4183 if (isUserWayNode(script) || isUserWayUrl(script.getAttribute('data-src') || script.getAttribute('src') || '')) { 4184 return; 4185 } 4186 if (!isEmbedLikeScript(script)) { 4187 return; 4188 } 4189 var host = widgetEmbedHostContainer(script); 4190 if (!host || isEffectivelyHidden(host) || isUserWayNode(host)) { 4191 return; 4192 } 4193 var category = script.getAttribute('data-ucpf-category') || 'functional'; 4194 if (category !== 'functional' && category !== 'marketing' && category !== 'analytics') { 4195 category = 'functional'; 4196 } 4197 push(host, 'widget', category, 'embed', ensureEmbedConsentCategories([category])); 4198 } 4199 ); 4200 // Smash Balloon YouTube Feed â cover the feed root, never the spinner icon. 4201 queryAll(YOUTUBE_FEED_MARKERS).forEach(function (node) { 4202 if (isYoutubeFeedLoader(node)) { 4203 return; 4204 } 4205 var host = youtubeFeedHostContainer(node); 4206 if (host && !isYoutubeFeedLoader(host)) { 4207 push(host, 'youtube', 'marketing', 'embed', ensureVideoConsentCategories(['marketing'])); 4208 } 4209 }); 4210 4211 // Smash Balloon Social Wall (Instagram CDN / lightbox) â Marketing + Embeds. 4212 queryAll(SOCIAL_WALL_MARKERS).forEach(function (node) { 4213 var host = socialWallHostContainer(node); 4214 if (host) { 4215 push(host, 'embed', 'marketing', 'embed', ensureEmbedConsentCategories(['marketing'])); 4216 } 4217 }); 4218 4219 // YouTube / Vimeo always â including Elementor when leaveBuildersAlone is on. 4220 // Layout skips stay elsewhere; video covers must still run before consent. 4221 // Same-origin / Elementor Self Hosted media must never be covered. 4222 queryAll(VIDEO_MARKERS).forEach(function (node) { 4223 if (!isVisualEmbedSurface(node)) { 4224 return; 4225 } 4226 if (node.closest && (node.closest('.sb_youtube') || node.closest('.sb-wall'))) { 4227 return; 4228 } 4229 if (isSelfHostedVideoSurface(node)) { 4230 return; 4231 } 4232 var detected = detectVideoProvider(node); 4233 if (!detected) { 4234 var src = (node.getAttribute('src') || node.getAttribute('data-src') || '').toLowerCase(); 4235 if (src && isSameOriginMediaUrl(src)) { 4236 return; 4237 } 4238 if (src.indexOf('vimeo') !== -1) { 4239 detected = { kind: 'vimeo', category: 'functional' }; 4240 } else if (src && isVideoPlayerUrl(src)) { 4241 detected = { kind: 'youtube', category: 'marketing' }; 4242 } 4243 } 4244 if (detected) { 4245 var host = videoHostContainer(node); 4246 if (host && isSelfHostedVideoSurface(host)) { 4247 return; 4248 } 4249 if (host && isVisualEmbedSurface(host)) { 4250 push(host, detected.kind, detected.category, 'embed', ensureVideoConsentCategories([detected.category])); 4251 } 4252 } 4253 }); 4254 4255 // Builder shells (Elementor/Divi/Gutenberg/etc.) â iframe often injected later.
4256 queryAll(VIDEO_SHELL_SELECTORS).forEach(function (node) { 4257 if (isEffectivelyHidden(node)) { 4258 return; 4259 } 4260 if (isSelfHostedVideoSurface(node)) { 4261 return; 4262 } 4263 var detected = detectVideoProvider(node); 4264 if (!detected) { 4265 var cls = (node.className || '').toLowerCase(); 4266 if (cls.indexOf('vimeo') !== -1) { 4267 detected = { kind: 'vimeo', category: 'functional' }; 4268 } else if (cls.indexOf('youtube') !== -1) { 4269 detected = { kind: 'youtube', category: 'marketing' }; 4270 } 4271 } 4272 if (!detected && (node.classList.contains('elementor-widget-video') || node.getAttribute('data-widget_type') === 'video.default')) { 4273 // Never invent YouTube for Self Hosted / same-origin Elementor videos. 4274 if (isSelfHostedVideoSurface(node)) { 4275 return; 4276 } 4277 var s = parseDataSettings(node); 4278 var vt = s && String(s.video_type || '').toLowerCase(); 4279 if (vt === 'hosted' || vt === 'self' || vt === 'self_hosted' || vt === 'file') { 4280 return; 4281 } 4282 if (vt === 'vimeo' || (s && s.vimeo_url)) { 4283 detected = { kind: 'vimeo', category: 'functional' }; 4284 } else if (vt === 'youtube' || (s && (s.youtube_url || s.youtube_id))) { 4285 detected = { kind: 'youtube', category: 'marketing' }; 4286 } else { 4287 detected = detectVideoProvider(node); 4288 } 4289 } 4290 if (!detected && isBackgroundVideoShell(node)) { 4291 detected = detectVideoProvider(node); 4292 } 4293 if (detected) { 4294 var host = videoHostContainer(node); 4295 if (isBackgroundVideoShell(node) || (host && isBackgroundVideoShell(host))) {
4296 host = resolveBackgroundVideoHost(node); 4297 } 4298 if (host && isSelfHostedVideoSurface(host)) { 4299 return; 4300 } 4301 push(host, detected.kind, detected.category, 'embed', ensureVideoConsentCategories([detected.category])); 4302 } 4303 }); 4304 4305 queryAll(['[data-settings*="background_video_link"]']).forEach(function (node) { 4306 if (isEffectivelyHidden(node)) { 4307 return; 4308 } 4309 if (isSelfHostedVideoSurface(node)) { 4310 return; 4311 } 4312 var box = node.querySelector('.elementor-background-video-container'); 4313 if (box && isEffectivelyHidden(box)) { 4314 return; 4315 } 4316 var detected = detectVideoProvider(node) || (box ? detectVideoProvider(box) : null); 4317 if (detected) { 4318 push( 4319 resolveBackgroundVideoHost(node), 4320 detected.kind, 4321 detected.category, 4322 'embed', 4323 ensureVideoConsentCategories([detected.category]) 4324 ); 4325 } 4326 }); 4327 4328 return items; 4329 } 4330 4331 function activateLoaderFallback() { 4332 if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function' && window.UCPF) { 4333 try { 4334 window.UCPFLoader.applyConsent(window.UCPF.getConsent()); 4335 } catch (e) { 4336 /* ignore */ 4337 } 4338 } 4339 } 4340 4341 var refreshBusy = false; 4342 function refresh() { 4343 if (window.__ucpfDiscover || refreshBusy) { 4344 return; 4345 } 4346 refreshBusy = true; 4347 try { 4348 teardownNonVisualGuards(); 4349 var items = collectTargets(); 4350 items.forEach(function (item) { 4351 var cats = item.categories && item.categories.length ? item.categories : [item.category]; 4352 if (hasAllCategories(cats)) { 4353 removeGuard(item.target); 4354 } else { 4355 applyGuard(item.target, item.kind, item.category, item.mode, cats); 4356 } 4357 }); 4358 } finally { 4359 refreshBusy = false; 4360 } 4361 } 4362 4363 function ensureCalendlyIfNeeded() { 4364 if (!hasCategoryConsent('functional')) { 4365 return; 4366 } 4367 var nodes = document.querySelectorAll('.calendly-inline-widget[data-url], .calendly-badge-widget[data-url]'); 4368 if (!nodes.length) { 4369 return; 4370 } 4371 var needs = false; 4372 Array.prototype.forEach.call(nodes, function (el) { 4373 if (!el.querySelector || !el.querySelector('iframe')) { 4374 needs = true; 4375 } 4376 }); 4377 if (needs) { 4378 reinitCalendlyWidgets(); 4379 } 4380 } 4381 4382 /** 4383 * After Security consent: captcha APIs may load after GF already tried once. 4384 * Re-render empty widgets and nudge Gravity Forms (same idea as Calendly). 4385 */ 4386 function captchaWidgetNeedsRender(el) { 4387 if (!el) { 4388 return false; 4389 } 4390 var hasLiveFrame = !!( 4391 el.querySelector && 4392 el.querySelector( 4393 'iframe[src*="recaptcha"], iframe[src*="hcaptcha"], iframe[src*="turnstile"], iframe[title*="reCAPTCHA"], iframe[title*="hCaptcha"]' 4394 ) 4395 ); 4396 var hasResponseBox = !!( 4397 el.querySelector && 4398 el.querySelector('textarea.g-recaptcha-response, textarea[name="g-recaptcha-response"], textarea[name="h-captcha-response"], [name="cf-turnstile-response"]') 4399 ); 4400 // Gravity Forms: already bound (see .ginput_recaptcha[data-widget-id] + gform-initialized). 4401 if (el.getAttribute('data-widget-id') != null && String(el.getAttribute('data-widget-id')) !== '') { 4402 if (hasLiveFrame || hasResponseBox) { 4403 return false; 4404 } 4405 // Stale widget-id with no live frame â clear so GF can bind again (new tab / bfcache). 4406 try { 4407 el.removeAttribute('data-widget-id'); 4408 } catch (eWid) { /* ignore */ } 4409 if (el.classList) { 4410 el.classList.remove('gform-initialized'); 4411 } 4412 return true; 4413 } 4414 if (el.classList && el.classList.contains('gform-initialized') && (hasLiveFrame || hasResponseBox)) { 4415 return false; 4416 } 4417 if (hasLiveFrame) { 4418 return false; 4419 } 4420 if (el.querySelector && el.querySelector('iframe') && hasResponseBox) { 4421 return false; 4422 } 4423 // Invisible widgets may have no iframe yet â still need an API render pass. 4424 return true; 4425 } 4426 4427 /** True when a captcha host on the page still looks unbound / empty. */ 4428 function captchaSurfacesNeedHelp() { 4429 var nodes = document.querySelectorAll( 4430 '.g-recaptcha, .h-captcha, .cf-turnstile, .ginput_recaptcha, .gform_recaptcha' 4431 ); 4432 if (!nodes.length) { 4433 return false;
4434 } 4435 for (var i = 0; i < nodes.length; i++) { 4436 if (captchaWidgetNeedsRender(nodes[i])) { 4437 return true; 4438 } 4439 } 4440 return false; 4441 } 4442 4443 function nudgeGravityFormsCaptcha() { 4444 try { 4445 if (!window.jQuery) { 4446 return; 4447 } 4448 var $ = window.jQuery; 4449 // Clear stale GF init flags on empty captcha hosts so post_render rebinds. 4450 $('.ginput_recaptcha, .gform_recaptcha').each(function () { 4451 var el = this; 4452 if (captchaWidgetNeedsRender(el) && el.classList) { 4453 el.classList.remove('gform-initialized'); 4454 } 4455 }); 4456 $('.gform_wrapper').each(function () { 4457 var wrap = this; 4458 var formId = 0; 4459 try { 4460 var m = (wrap.id || '').match(/gform_wrapper_(\d+)/); 4461 if (m) { 4462 formId = parseInt(m[1], 10) || 0; 4463 } 4464 } catch (eId) { 4465 formId = 0; 4466 } 4467 try { 4468 $(document).trigger('gform_post_render', [formId, 1]); 4469 } catch (eTrig) { 4470 /* ignore */ 4471 } 4472 }); 4473 } catch (eGf) { 4474 /* ignore */ 4475 } 4476 } 4477 4478 var captchaReinitScheduled = false; 4479 4480 function reinitCaptchaWidgets() { 4481 if (!hasCategoryConsent('security')) { 4482 return; 4483 } 4484 if (captchaReinitScheduled) { 4485 return; 4486 } 4487 captchaReinitScheduled = true; 4488 if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function') { 4489 try { 4490 window.UCPFLoader.applyConsent(); 4491 } catch (eLoad) { 4492 /* ignore */ 4493 } 4494 } 4495 var attempt = 0; 4496 function tryInit() { 4497 attempt += 1; 4498 4499 try { 4500 var gr = window.grecaptcha; 4501 if (gr && typeof gr.render === 'function') { 4502 var ready = typeof gr.ready === 'function' ? gr.ready.bind(gr) : function (fn) { 4503 fn(); 4504 }; 4505 ready(function () { 4506 // Generic .g-recaptcha only â GF .ginput_recaptcha is owned by gform_post_render. 4507 Array.prototype.forEach.call(document.querySelectorAll('.g-recaptcha'), function (el) { 4508 if (!captchaWidgetNeedsRender(el)) { 4509 return; 4510 } 4511 if (el.getAttribute('data-ucpf-captcha-rendered') === '1') { 4512 return; 4513 } 4514 try { 4515 gr.render(el); 4516 el.setAttribute('data-ucpf-captcha-rendered', '1'); 4517 } catch (eRender) { 4518 /* already rendered or missing sitekey â leave for GF nudge */ 4519 } 4520 }); 4521 }); 4522 } 4523 } catch (eGr) { 4524 /* ignore */ 4525 } 4526 4527 try { 4528 var hc = window.hcaptcha; 4529 if (hc && typeof hc.render === 'function') { 4530 Array.prototype.forEach.call(document.querySelectorAll('.h-captcha'), function (el) { 4531 if (!captchaWidgetNeedsRender(el)) { 4532 return; 4533 } 4534 if (el.getAttribute('data-ucpf-captcha-rendered') === '1') { 4535 return; 4536 } 4537 try { 4538 hc.render(el); 4539 el.setAttribute('data-ucpf-captcha-rendered', '1'); 4540 } catch (eHc) { 4541 /* ignore */ 4542 } 4543 }); 4544 } 4545 } catch (eH) { 4546 /* ignore */ 4547 } 4548 4549 try { 4550 var ts = window.turnstile; 4551 if (ts && typeof ts.render === 'function') { 4552 Array.prototype.forEach.call(document.querySelectorAll('.cf-turnstile'), function (el) { 4553 if (!captchaWidgetNeedsRender(el)) { 4554 return; 4555 } 4556 if (el.getAttribute('data-ucpf-captcha-rendered') === '1') { 4557 return; 4558 } 4559 try { 4560 ts.render(el); 4561 el.setAttribute('data-ucpf-captcha-rendered', '1'); 4562 } catch (eTs) { 4563 /* ignore */ 4564 } 4565 }); 4566 } 4567 } catch (eT) { 4568 /* ignore */ 4569 } 4570 4571 // Always nudge GF while surfaces still look empty (API present â widget live). 4572 if (window.grecaptcha || window.hcaptcha || window.turnstile || attempt > 2) { 4573 nudgeGravityFormsCaptcha(); 4574 } 4575 4576 var stillNeed = captchaSurfacesNeedHelp(); 4577 if (!stillNeed || attempt >= 24) { 4578 captchaReinitScheduled = false;
4579 return; 4580 } 4581 window.setTimeout(tryInit, 250); 4582 } 4583 tryInit(); 4584 } 4585 4586 function ensureCaptchasIfNeeded() { 4587 if (!hasCategoryConsent('security')) { 4588 return; 4589 } 4590 var nodes = document.querySelectorAll( 4591 '.g-recaptcha, .h-captcha, .cf-turnstile, .ginput_recaptcha, .gform_recaptcha, .gform_wrapper .gfield--type-captcha, .gfield--type-captcha, form[id^="gform_"] [data-sitekey]' 4592 ); 4593 if (!nodes.length) { 4594 return; 4595 } 4596 if (!captchaSurfacesNeedHelp()) { 4597 return; 4598 } 4599 reinitCaptchaWidgets(); 4600 } 4601 4602 /** 4603 * After Marketing + Embeds: restore parked map iframes once and nudge Elementor. 4604 * Never cache-bust / reload live iframes in a loop â that spam-refreshes embeds. 4605 */ 4606 var mapsHydrateStarted = false; 4607 var mapsHydrated = false; 4608 var mapsHydrateRetryScheduled = false; 4609 var animationsHydrateStarted = false; 4610 4611 function ensureAnimationsIfNeeded() { 4612 // GSAP CDN is never gated â only heal leftover parked tags from older builds. 4613 if (animationsHydrateStarted) { 4614 return; 4615 } 4616 var hasParkedAnim = !!document.querySelector( 4617 'script[type="text/plain"][data-src*="gsap"], script[data-ucpf-gated="1"][data-src*="gsap"], script[type="text/plain"][data-src*="lottie"], script[type="text/plain"][data-src*="dotlottie"], script[type="text/plain"][data-ucpf-inline-animation="1"]' 4618 ); 4619 if (!hasParkedAnim) { 4620 return; 4621 } 4622 animationsHydrateStarted = true; 4623 if (window.UCPFLoader && typeof window.UCPFLoader.refireAnimationDependents === 'function') { 4624 try { 4625 window.UCPFLoader.refireAnimationDependents(); 4626 } catch (eAnim) { /* ignore */ } 4627 } 4628 } 4629 4630 function isPlaceholderEmbedSrc(url) { 4631 var u = String(url || '').trim(); 4632 if (!u || u === 'about:blank') { 4633 return true; 4634 } 4635 var lower = u.toLowerCase(); 4636 return lower.indexOf('data:') === 0 || lower.indexOf('blob:') === 0 || lower.indexOf('javascript:') === 0; 4637 } 4638 4639 function resolveDeferredIframeUrl(iframe) { 4640 if (!iframe || iframe.tagName !== 'IFRAME') { 4641 return ''; 4642 } 4643 if (typeof window.__ucpfResolveDeferredEmbedUrl === 'function') { 4644 return window.__ucpfResolveDeferredEmbedUrl(iframe); 4645 } 4646 var parked = iframe.getAttribute('data-src') || iframe.getAttribute('data-lazy-src') || ''; 4647 var live = iframe.getAttribute('src') || ''; 4648 if (parked && !isPlaceholderEmbedSrc(parked)) { 4649 if (isPlaceholderEmbedSrc(live)) { 4650 return parked; 4651 } 4652 } 4653 if (live && !isPlaceholderEmbedSrc(live)) { 4654 return live; 4655 } 4656 return parked || live || ''; 4657 } 4658 4659 function restoreParkedMapIframe(iframe) { 4660 if (!iframe || iframe.tagName !== 'IFRAME') { 4661 return false; 4662 } 4663 if (iframe.getAttribute('data-ucpf-map-restored') === '1') { 4664 return false; 4665 } 4666 var parked = iframe.getAttribute('data-src') || ''; 4667 var live = iframe.getAttribute('src') || ''; 4668 var gated = iframe.getAttribute('data-ucpf-gated') === '1'; 4669 var blank = !live || live === 'about:blank' || isPlaceholderEmbedSrc(live); 4670 // Only restore when gated/blank â do not touch a healthy live maps iframe. 4671 if (!gated && !blank) { 4672 if (isMapEmbedSrc(live)) { 4673 iframe.setAttribute('data-ucpf-map-restored', '1'); 4674 } 4675 return false; 4676 } 4677 var src = resolveDeferredIframeUrl(iframe); 4678 if (!isMapEmbedSrc(src)) { 4679 return false; 4680 } 4681 // Strip any leftover cache-bust from older builds. 4682 src = String(src).replace(/([?&])ucpf_r=\d+/g, '$1').replace(/[?&]$/, ''); 4683 try { 4684 iframe.src = src; 4685 } catch (eSet) { 4686 return false; 4687 } 4688 iframe.setAttribute('data-ucpf-map-restored', '1'); 4689 iframe.removeAttribute('data-src'); 4690 iframe.removeAttribute('data-ucpf-gated'); 4691 iframe.removeAttribute('data-ucpf-category'); 4692 iframe.removeAttribute('data-ucpf-parked'); 4693 return true; 4694 } 4695 4696 function isMapEmbedSrc(src) { 4697 var u = String(src || '').toLowerCase(); 4698 return !!( 4699 u && 4700 u !== 'about:blank' && 4701 !isPlaceholderEmbedSrc(u) && 4702 (/maps\.google|google\.com\/maps|mapbox\.com|openstreetmap\.org|maplibre|bing\.com\/maps|virtualearth|mapme\.com|maphub\.net|batchgeo\.com|zeemaps\.com|umap\.openstreetmap\.fr|arcgis\.com/i.test( 4703 u 4704 )) 4705 ); 4706 } 4707 4708 function mapIframeIsLive(iframe) { 4709 if (!iframe || iframe.tagName !== 'IFRAME') { 4710 return false;
4711 } 4712 if (iframe.getAttribute('data-ucpf-gated') === '1') { 4713 return false; 4714 } 4715 var live = resolveDeferredIframeUrl(iframe); 4716 return isMapEmbedSrc(live); 4717 } 4718 4719 function reinitMapWidgets() { 4720 if (!hasCategoryConsent('marketing') || !hasCategoryConsent('functional')) { 4721 return; 4722 } 4723 if (mapsHydrated) { 4724 return; 4725 } 4726 if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function') { 4727 try { 4728 window.UCPFLoader.applyConsent(); 4729 } catch (eLoad) { /* ignore */ } 4730 } 4731 4732 queryAll(MAP_MARKERS).forEach(function (node) { 4733 if (node.querySelectorAll) { 4734 node.querySelectorAll('iframe').forEach(restoreParkedMapIframe); 4735 } 4736 if (node.tagName === 'IFRAME') { 4737 restoreParkedMapIframe(node); 4738 } 4739 }); 4740 document 4741 .querySelectorAll( 4742 'iframe[data-ucpf-gated="1"][data-src*="maps.google"], iframe[data-ucpf-gated="1"][data-src*="google.com/maps"], iframe[data-ucpf-gated="1"][data-src*="mapme.com"], iframe[data-ucpf-gated="1"][data-src*="maphub.net"], iframe[data-ucpf-gated="1"][data-src*="batchgeo.com"], iframe[data-ucpf-gated="1"][data-src*="zeemaps.com"], iframe[data-ucpf-gated="1"][data-src*="umap.openstreetmap.fr"], iframe[data-ucpf-gated="1"][data-src*="arcgis.com"], iframe[data-ucpf-gated="1"][data-src*="bing.com/maps"]' 4743 ) 4744 .forEach(restoreParkedMapIframe); 4745 4746 var hasEl = 4747 !!( 4748 window.elementorFrontend && 4749 elementorFrontend.elementsHandler && 4750 typeof elementorFrontend.elementsHandler.runReadyTrigger === 'function' 4751 ); 4752 if (hasEl) { 4753 queryAll([ 4754 '.elementor-widget-google_maps', 4755 '[data-widget_type="google_maps.default"]', 4756 '[data-widget_type*="google_maps"]', 4757 ]).forEach(function (widget) { 4758 try { 4759 widget.querySelectorAll('iframe').forEach(restoreParkedMapIframe); 4760 // Only re-boot Elementor when the map iframe is still missing/gated. 4761 var live = false; 4762 widget.querySelectorAll('iframe').forEach(function (iframe) { 4763 if (mapIframeIsLive(iframe)) { 4764 live = true; 4765 } 4766 }); 4767 if (!live) { 4768 elementorFrontend.elementsHandler.runReadyTrigger(widget); 4769 } 4770 } catch (eRun) { /* ignore */ } 4771 }); 4772 } 4773 4774 try { 4775 if (typeof window.initMap === 'function') { 4776 window.initMap(); 4777 } 4778 } catch (eInit) { /* ignore */ } 4779 try { 4780 if (window.WPGMZA && typeof window.WPGMZA.maps !== 'undefined' && window.jQuery) { 4781 window.jQuery('.wpgmza_map').each(function () { 4782 try { 4783 window.jQuery(this).trigger('init'); 4784 } catch (eOne) { /* ignore */ } 4785 }); 4786 } 4787 } catch (eWpg) { /* ignore */ } 4788 try { 4789 // Mapster / MapLibre / Mapbox: force-refire plugin bootstrap at most once per page. 4790 // Skip when a live canvas already exists (avoid nested MapLibre after soft+force clone). 4791 if ( 4792 !window.__ucpfMapsterForceDone && 4793 !mapsterMapIsLive() && 4794 window.UCPFLoader && 4795 typeof window.UCPFLoader.refireMapDependents === 'function' 4796 ) { 4797 window.__ucpfMapsterForceDone = true; 4798 window.UCPFLoader.refireMapDependents({ forceMapster: true }); 4799 } else if (mapsterMapIsLive()) { 4800 window.__ucpfMapsterForceDone = true; 4801 } 4802 } catch (eMapster) { /* ignore */ } 4803 try { 4804 if (window.jQuery) { 4805 window.jQuery(document).trigger('google.maps.loaded'); 4806 window.jQuery(window).trigger('resize'); 4807 } 4808 } catch (eJq) { /* ignore */ } 4809 try { 4810 window.dispatchEvent(new CustomEvent('ucpf:maps:ready')); 4811 } catch (eEv) { /* ignore */ } 4812 4813 var mapsterLive = mapsterMapIsLive(); 4814 var canvasLive = !!document.querySelector( 4815 '.leaflet-container canvas, .mapboxgl-canvas, .maplibregl-canvas, .gm-style canvas, .wpgmza_map canvas' 4816 ); 4817 var iframeWithMapContent = false; 4818 try { 4819 document 4820 .querySelectorAll('iframe[src*="google.com/maps"]:not([data-ucpf-gated="1"]), iframe[src*="maps.google"]:not([data-ucpf-gated="1"])') 4821 .forEach(function (iframe) { 4822 if (mapIframeIsLive(iframe) && iframe.offsetHeight > 0) { 4823 iframeWithMapContent = true; 4824 } 4825 }); 4826 } catch (eIfLive) { /* ignore */ } 4827 if (mapsterLive || canvasLive || iframeWithMapContent) { 4828 mapsHydrated = true; 4829 } 4830 } 4831 4832 /** Mapster canvas / MapLibre map finished past the initial loader. */ 4833 function mapsterMapIsLive() { 4834 try { 4835 if ( 4836 document.querySelector( 4837 '.mapster-wp-maps .maplibregl-map, .mapster-wp-maps .mapboxgl-map, .mapster-wp-maps-container .maplibregl-map, .mapster-wp-maps-container .mapboxgl-map, .mapster-map .maplibregl-map, .mapster-map .mapboxgl-map' 4838 ) 4839 ) { 4840 return true; 4841 } 4842 var hosts = document.querySelectorAll(
4843 '.mapster-wp-maps, [id^="mapster-wp-maps"], .mapster-map' 4844 ); 4845 for (var i = 0; i < hosts.length; i++) { 4846 var host = hosts[i]; 4847 if (!host || !host.children || !host.children.length) { 4848 continue; 4849 } 4850 var loader = host.querySelector('.mapster-map-loader-initial, .mapster-wp-maps-loader-container .mapster-map-loader-initial'); 4851 // Sibling loader outside the map node â check container. 4852 var container = host.closest ? host.closest('.mapster-wp-maps-container') : null; 4853 var containerLoader = container 4854 ? container.querySelector('.mapster-map-loader-initial') 4855 : null; 4856 var stillLoading = !!( 4857 (loader && loader.offsetParent !== null) || 4858 (containerLoader && containerLoader.offsetParent !== null) 4859 ); 4860 if (!stillLoading && host.children.length > 0) { 4861 // Canvas / SVG / div map chrome appeared. 4862 if (host.querySelector('canvas, .maplibregl-canvas, .mapboxgl-canvas, svg')) { 4863 return true; 4864 } 4865 } 4866 } 4867 } catch (eLive) { /* ignore */ } 4868 return false; 4869 } 4870 4871 function whenMapsApiReady(cb) { 4872 var n = 0; 4873 var t = window.setInterval(function () { 4874 n += 1; 4875 var ready = 4876 (window.google && window.google.maps) || 4877 typeof window.mapboxgl !== 'undefined' || 4878 typeof window.maplibregl !== 'undefined' || 4879 (window.L && typeof window.L.map === 'function') || 4880 document.querySelector( 4881 'iframe[src*="google.com/maps"]:not([data-ucpf-gated="1"]), iframe[src*="maps.google"]:not([data-ucpf-gated="1"]), iframe[data-ucpf-gated="1"][data-src*="maps.google"], iframe[data-ucpf-gated="1"][data-src*="google.com/maps"]' 4882 ); 4883 if (ready || n > 40) { 4884 window.clearInterval(t); 4885 cb(); 4886 } 4887 }, 100); 4888 } 4889 4890 function ensureMapsIfNeeded() { 4891 if (!hasCategoryConsent('marketing') || !hasCategoryConsent('functional')) { 4892 mapsHydrateStarted = false; 4893 mapsHydrated = false; 4894 mapsHydrateRetryScheduled = false; 4895 return; 4896 } 4897 if (mapsHydrated) { 4898 return; 4899 } 4900 if (mapsHydrateStarted) { 4901 return; 4902 } 4903 var hasMapSurface = !!document.querySelector( 4904 MAP_MARKERS.join(',') + 4905 ',script[data-src*="maps.googleapis"],script[src*="maps.googleapis"],script[data-src*="mapbox"],script[data-src*="mapster"],script[src*="mapster"],script[data-src*="maplibre"],script[src*="maplibre"],iframe[data-src*="google.com/maps"],iframe[data-src*="maps.google"],iframe[data-ucpf-gated="1"][data-src*="maps.google"]' 4906 ); 4907 if (!hasMapSurface) { 4908 return; 4909 } 4910 mapsHydrateStarted = true; 4911 // One restore pass + a single delayed retry (idempotent via data-ucpf-map-restored). 4912 whenMapsApiReady(reinitMapWidgets); 4913 window.setTimeout(reinitMapWidgets, 600); 4914 // Mapster/MapLibre: one extra retry if still on the initial loader after APIs activate. 4915 if (!mapsHydrateRetryScheduled) { 4916 mapsHydrateRetryScheduled = true; 4917 window.setTimeout(function () { 4918 if (mapsHydrated) { 4919 return; 4920 } 4921 if (mapsterMapIsLive()) { 4922 mapsHydrated = true; 4923 return; 4924 } 4925 // Allow one more hydrate pass for canvas maps that missed the first refire. 4926 mapsHydrateStarted = false; 4927 whenMapsApiReady(reinitMapWidgets); 4928 window.setTimeout(reinitMapWidgets, 400); 4929 }, 1800); 4930 } 4931 } 4932 4933 /** 4934 * Elementor sticky / Motion FX mutate the DOM constantly (sticky spacers, 4935 * data-settings). Re-scanning guards mid-flight races prepareOptions and 4936 * surfaces as: Cannot read properties of undefined (reading 'translateY'), 4937 * leaving widgets stuck with .elementor-invisible (untouchable on mobile). 4938 */ 4939 /** 4940 * Captcha / map / calendly / gated iframe injections must wake refresh even 4941 * when builders mutate the DOM constantly. 4942 * @param {MutationRecord|null} mutation 4943 * @param {Node|null} target 4944 */ 4945 function isConsentSurfaceSignal(mutation, target) { 4946 function nodeIsSurface(node) { 4947 if (!node || node.nodeType !== 1) { 4948 return false;
4949 } 4950 var el = /** @type {Element} */ (node); 4951 var tag = String(el.tagName || '').toUpperCase(); 4952 if (tag === 'IFRAME') { 4953 var src = (el.getAttribute('src') || el.getAttribute('data-src') || '').toLowerCase(); 4954 if ( 4955 isVideoPlayerUrl(src) || 4956 el.getAttribute('data-ucpf-gated') === '1' || 4957 el.getAttribute('data-ucpf-category') || 4958 /recaptcha|hcaptcha|turnstile|challenges\.cloudflare|calendly|google\.com\/maps|maps\.google|mapbox|openstreetmap|getjobber|jobber|typeform|jotform|hsforms|tally\.so/.test( 4959 src 4960 ) 4961 ) { 4962 return true; 4963 } 4964 } 4965 if (tag === 'SCRIPT') { 4966 if (el.getAttribute('data-ucpf-gated') === '1' || el.getAttribute('form_url') || el.getAttribute('typehub_id')) { 4967 return true; 4968 } 4969 } 4970 if (!el.classList) { 4971 return false; 4972 } 4973 return !!( 4974 el.classList.contains('g-recaptcha') || 4975 el.classList.contains('cf-turnstile') || 4976 el.classList.contains('h-captcha') || 4977 el.classList.contains('grecaptcha-badge') || 4978 el.classList.contains('calendly-inline-widget') || 4979 el.classList.contains('jobber-inline-work-request') || 4980 el.classList.contains('ucpf-iframe-placeholder') || 4981 el.classList.contains('gm-style') || 4982 el.classList.contains('mapboxgl-map') || 4983 el.classList.contains('maplibregl-map') || 4984 el.classList.contains('mapster-wp-maps') || 4985 el.classList.contains('mapster-wp-maps-container') || 4986 el.classList.contains('mapster-map') 4987 ); 4988 } 4989 if (target && nodeIsSurface(target)) { 4990 return true; 4991 } 4992 if (!mutation || mutation.type !== 'childList') { 4993 return false; 4994 } 4995 var lists = [mutation.addedNodes, mutation.removedNodes]; 4996 for (var li = 0; li < lists.length; li++) { 4997 var list = lists[li]; 4998 if (!list || !list.length) { 4999 continue; 5000 } 5001 for (var ni = 0; ni < list.length; ni++) { 5002 if (nodeIsSurface(list[ni])) { 5003 return true; 5004 } 5005 try { 5006 var child = list[ni]; 5007 if (child && child.querySelector && child.querySelector('iframe, .g-recaptcha, .cf-turnstile, .h-captcha, .calendly-inline-widget, .jobber-inline-work-request, script[data-ucpf-gated], script[form_url]')) { 5008 return true; 5009 } 5010 } catch (eQ) { /* ignore */ } 5011 } 5012 } 5013 return false; 5014 } 5015 5016 /** 5017 * Elementor sticky / Motion FX mutate the DOM constantly (sticky spacers, 5018 * data-settings). Re-scanning guards mid-flight races prepareOptions and 5019 * surfaces as: Cannot read properties of undefined (reading 'translateY'), 5020 * leaving widgets stuck with .elementor-invisible (untouchable on mobile). 5021 */ 5022 function isElementorBuilderNoise(mutation, target) { 5023 // Video / captcha / map signals must still wake the guard. 5024 if (isVideoIframeSignal(mutation, target)) { 5025 return false; 5026 } 5027 if (isConsentSurfaceSignal(mutation, target)) { 5028 return false; 5029 } 5030 if (!mutation) { 5031 return false; 5032 } 5033 if (mutation.type === 'attributes') { 5034 var attr = mutation.attributeName || ''; 5035 if (attr === 'data-settings') { 5036 return true; 5037 } 5038 if ((attr === 'src' || attr === 'data-src') && target && target.tagName) { 5039 var tag = String(target.tagName).toUpperCase(); 5040 if (tag === 'IFRAME' && isVideoPlayerUrl(target.getAttribute('src') || target.getAttribute('data-src') || '')) { 5041 return false; 5042 } 5043 if (tag === 'AUDIO' || tag === 'VIDEO' || tag === 'SOURCE') { 5044 return true; 5045 } 5046 } 5047 // Ignore transform/style thrash from Motion FX / sticky when leaveBuildersAlone. 5048 if (leaveBuildersAlone() && (attr === 'style' || attr === 'data-motion' || attr.indexOf('data-') === 0)) { 5049 return isBuilderChrome(target); 5050 } 5051 return false; 5052 } 5053 if (mutation.type !== 'childList') { 5054 return false; 5055 } 5056 function isStickySpacer(node) { 5057 return !!( 5058 node && 5059 node.nodeType === 1 && 5060 node.classList && 5061 node.classList.contains('elementor-sticky__spacer') 5062 ); 5063 } 5064 function isEmptyText(node) { 5065 return !!(node && node.nodeType === 3 && !String(node.textContent || '').trim()); 5066 } 5067 var lists2 = [mutation.addedNodes, mutation.removedNodes]; 5068 var saw = false;
5069 for (var lj = 0; lj < lists2.length; lj++) { 5070 var list2 = lists2[lj]; 5071 if (!list2 || !list2.length) { 5072 continue; 5073 } 5074 for (var nj = 0; nj < list2.length; nj++) { 5075 var n = list2[nj]; 5076 if (isEmptyText(n)) { 5077 continue; 5078 } 5079 saw = true; 5080 if (!isStickySpacer(n)) { 5081 return false; 5082 } 5083 } 5084 } 5085 return saw; 5086 } 5087 5088 /** 5089 * True only for real video player nodes â never bare .elementor-wrapper 5090 * (Elementor uses that class site-wide; matching it re-ran refresh on every 5091 * Motion FX / sticky mutation and tore down captcha covers). 5092 * @param {Element|Node} node 5093 */ 5094 function nodeLooksLikeVideoEmbed(node) { 5095 if (!node || node.nodeType !== 1) { 5096 return false; 5097 } 5098 var el = /** @type {Element} */ (node); 5099 if (String(el.tagName || '').toUpperCase() === 'IFRAME') { 5100 return isVideoPlayerUrl(el.getAttribute('src') || el.getAttribute('data-src') || ''); 5101 } 5102 if (el.classList) { 5103 if ( 5104 el.classList.contains('elementor-widget-video') || 5105 el.classList.contains('elementor-video') || 5106 el.classList.contains('elementor-background-video-container') || 5107 el.classList.contains('elementor-background-video-embed') 5108 ) { 5109 return true; 5110 } 5111 // Inline video shell only â not every .elementor-wrapper on the page. 5112 if (el.classList.contains('elementor-wrapper') && el.classList.contains('elementor-open-inline')) { 5113 return true; 5114 } 5115 } 5116 if (el.getAttribute) { 5117 var wt = el.getAttribute('data-widget_type') || ''; 5118 if (wt === 'video.default' || wt === 'video') { 5119 return true; 5120 } 5121 } 5122 return false; 5123 } 5124 5125 /** 5126 * Wake the guard only when a YouTube/Vimeo iframe (or video widget) actually 5127 * appears or changes src â not when Elementor mutates a parent wrapper. 5128 * @param {MutationRecord|null} mutation 5129 * @param {Node|null} target 5130 */ 5131 function isVideoIframeSignal(mutation, target) { 5132 if (!mutation) { 5133 return !!(target && String(target.tagName || '').toUpperCase() === 'IFRAME' && nodeLooksLikeVideoEmbed(target)); 5134 } 5135 if (mutation.type === 'attributes') { 5136 var attr = mutation.attributeName || ''; 5137 if ((attr === 'src' || attr === 'data-src') && target && String(target.tagName || '').toUpperCase() === 'IFRAME') { 5138 return isVideoPlayerUrl( 5139 /** @type {Element} */ (target).getAttribute('src') || 5140 /** @type {Element} */ (target).getAttribute('data-src') || 5141 '' 5142 ); 5143 } 5144 return false; 5145 } 5146 if (mutation.type !== 'childList') { 5147 return false; 5148 } 5149 var lists = [mutation.addedNodes, mutation.removedNodes]; 5150 for (var li = 0; li < lists.length; li++) { 5151 var list = lists[li]; 5152 if (!list || !list.length) { 5153 continue; 5154 } 5155 for (var ni = 0; ni < list.length; ni++) { 5156 if (nodeLooksLikeVideoEmbed(list[ni])) { 5157 return true; 5158 } 5159 } 5160 } 5161 return false; 5162 } 5163 5164 /** 5165 * When OS "Reduce motion" is on, Elementor Pro Motion FX may crash and never 5166 * clear .elementor-invisible (visibility:hidden â no taps). Reveal stuck nodes. 5167 * Also run a delayed pass without reduced-motion: gate/consent races can leave 5168 * the same stuck class after Elementor should have animated. 5169 */ 5170 function unhideReducedMotionElementor() { 5171 try { 5172 var preferReduce = 5173 window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 5174 var nodes = document.querySelectorAll('.elementor-invisible'); 5175 var n = nodes.length; 5176 if (!n) { 5177 return 0; 5178 } 5179 // Immediate unhide only for reduced-motion (Motion FX crash path). 5180 if (!preferReduce) { 5181 return 0; 5182 } 5183 Array.prototype.forEach.call(nodes, function (el) { 5184 el.classList.remove('elementor-invisible'); 5185 }); 5186 return n; 5187 } catch (eUnhide) { 5188 return 0; 5189 } 5190 } 5191 5192 /** Late safety net: reveal Elementor nodes still invisible long after frontend init. */ 5193 function unhideStuckElementorInvisible() { 5194 try { 5195 var nodes = document.querySelectorAll('.elementor-invisible'); 5196 if (!nodes.length) { 5197 return 0; 5198 } 5199 // If Elementor never booted (script error / gate race), unhide so the page is usable 5200 // even when the visitor declined marketing cookies (no Accept reload / CF bypass). 5201 var elReady = !!(window.elementorFrontend && elementorFrontend.elementsHandler); 5202 if (elReady && document.readyState !== 'complete') { 5203 return 0; 5204 } 5205 Array.prototype.forEach.call(nodes, function (el) { 5206 el.classList.remove('elementor-invisible'); 5207 }); 5208 return nodes.length; 5209 } catch (eStuck) { 5210 return 0; 5211 } 5212 } 5213 5214 var consentChangeTimer = null; 5215 var consentChangeRunning = false;
5216 5217 function onConsentChanged() { 5218 // Accept/Reject hard-reload: skip heavy hydrate â it freezes the tab (Mapster/Elementor). 5219 if (window.__ucpfConsentReloadPending) { 5220 try { 5221 refresh(); 5222 } catch (eRefreshOnly) { /* ignore */ } 5223 return; 5224 } 5225 // document + window + UCPF.on all fire the same event â coalesce to one pass. 5226 if (consentChangeTimer) { 5227 window.clearTimeout(consentChangeTimer); 5228 } 5229 consentChangeTimer = window.setTimeout(runConsentChanged, 50); 5230 } 5231 5232 function runConsentChanged() { 5233 consentChangeTimer = null; 5234 if (consentChangeRunning || window.__ucpfConsentReloadPending) { 5235 return; 5236 } 5237 consentChangeRunning = true; 5238 try { 5239 // refresh() applies/removes covers and restores parked YouTube/Vimeo iframe src. 5240 refresh(); 5241 if ( 5242 hasCategoryConsent('security') || 5243 hasCategoryConsent('functional') || 5244 hasCategoryConsent('marketing') 5245 ) { 5246 activateLoaderFallback(); 5247 } 5248 ensureCalendlyIfNeeded(); 5249 ensureCaptchasIfNeeded(); 5250 // Always hydrate videos after consent â builders must not block GDPR unlock. 5251 ensureVideosIfNeeded(); 5252 ensureSmashBalloonIfNeeded(); 5253 // Maps: same class of failure as Vimeo (API parked, widget already ran). 5254 ensureMapsIfNeeded(); 5255 ensureAnimationsIfNeeded(); 5256 // Layout-only Elementor Motion FX recovery stays behind leaveBuildersAlone. 5257 if (!leaveBuildersAlone()) { 5258 unhideReducedMotionElementor(); 5259 window.setTimeout(unhideStuckElementorInvisible, 400); 5260 } 5261 // Ensure GTM4WP Vimeo/YouTube helpers re-run after player APIs activate. 5262 // Skip when loader already ran from the cancelled-navigation fallback. 5263 if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function') { 5264 try { 5265 window.UCPFLoader.applyConsent(); 5266 } catch (eApply) { /* ignore */ } 5267 } 5268 // Force-clear any leftover checkout panels (Safari cancel-navigation path). 5269 if (hasAllCategories(['security', 'functional'])) {
5270 queryAll(CHECKOUT_SELECTORS).forEach(function (node) { 5271 var host = node; 5272 if (node.tagName !== 'FORM') { 5273 host = (node.querySelector && (node.querySelector('form.checkout') || node.querySelector('form.woocommerce-checkout'))) || node; 5274 } 5275 removeGuard(host); 5276 if (host && host !== node) { 5277 removeGuard(node); 5278 } 5279 }); 5280 queryAll(['.ucpf-consent-guard--active[data-ucpf-guard-kind="checkout"]']).forEach(function (wrap) { 5281 wrap.classList.remove('ucpf-consent-guard--active'); 5282 var panel = wrap.querySelector('.ucpf-consent-guard__panel'); 5283 if (panel) { 5284 panel.remove(); 5285 } 5286 if (wrap.tagName === 'FORM') { 5287 unlockFields(wrap); 5288 } 5289 }); 5290 } 5291 } finally { 5292 consentChangeRunning = false; 5293 } 5294 } 5295 5296 function boot() { 5297 refresh(); 5298 resyncAllGuards(); 5299 ensureVideosIfNeeded(); 5300 ensureSmashBalloonIfNeeded(); 5301 ensureMapsIfNeeded(); 5302 ensureAnimationsIfNeeded(); 5303 ensureCaptchasIfNeeded(); 5304 if (!leaveBuildersAlone()) { 5305 unhideReducedMotionElementor(); 5306 } 5307 // Banner root + late builder/lazy iframes â re-scan and re-copy tokens. 5308 [50, 250, 800, 2000, 4000].forEach(function (ms) { 5309 window.setTimeout(function () { 5310 refresh(); 5311 resyncAllGuards(); 5312 ensureVideosIfNeeded(); 5313 ensureMapsIfNeeded(); 5314 ensureAnimationsIfNeeded(); 5315 ensureCaptchasIfNeeded(); 5316 if (!leaveBuildersAlone()) { 5317 unhideReducedMotionElementor(); 5318 if (ms >= 2000) { 5319 unhideStuckElementorInvisible(); 5320 } 5321 } 5322 }, ms); 5323 }); 5324 window.addEventListener('load', function () { 5325 ensureVideosIfNeeded(); 5326 ensureMapsIfNeeded(); 5327 ensureAnimationsIfNeeded(); 5328 ensureCaptchasIfNeeded(); 5329 window.setTimeout(ensureVideosIfNeeded, 1200); 5330 window.setTimeout(ensureMapsIfNeeded, 1200); 5331 window.setTimeout(ensureAnimationsIfNeeded, 1200); 5332 window.setTimeout(ensureCaptchasIfNeeded, 1200); 5333 if (leaveBuildersAlone()) { 5334 return; 5335 } 5336 unhideReducedMotionElementor(); 5337 window.setTimeout(unhideReducedMotionElementor, 1200); 5338 window.setTimeout(unhideStuckElementorInvisible, 2500); 5339 window.setTimeout(unhideStuckElementorInvisible, 5000); 5340 }); 5341 // Elementor may finish Motion FX after our early passes. 5342 try { 5343 document.addEventListener('elementor/frontend/init', function () { 5344 window.setTimeout(function () { 5345 refresh(); 5346 ensureVideosIfNeeded(); 5347 ensureMapsIfNeeded(); 5348 ensureAnimationsIfNeeded(); 5349 ensureCaptchasIfNeeded(); 5350 }, 150); 5351 window.setTimeout(ensureVideosIfNeeded, 900); 5352 window.setTimeout(ensureMapsIfNeeded, 900); 5353 window.setTimeout(ensureAnimationsIfNeeded, 900); 5354 window.setTimeout(ensureCaptchasIfNeeded, 900); 5355 if (!leaveBuildersAlone()) { 5356 window.setTimeout(unhideReducedMotionElementor, 100); 5357 window.setTimeout(unhideReducedMotionElementor, 800); 5358 window.setTimeout(unhideStuckElementorInvisible, 2000); 5359 } 5360 }); 5361 } catch (eEl) { /* ignore */ } 5362 bindElementorPopupRemeasure(); 5363 document.addEventListener('ucpf:consent:changed', onConsentChanged); 5364 document.addEventListener('ucpf:consent:accepted_all', onConsentChanged); 5365 document.addEventListener('ucpf:consent:rejected_all', onConsentChanged); 5366 window.addEventListener('ucpf:consent:changed', onConsentChanged); 5367 window.addEventListener('ucpf:consent:accepted_all', onConsentChanged); 5368 window.addEventListener('ucpf:consent:rejected_all', onConsentChanged); 5369 var ucpfBound = false; 5370 function bindUcpf() { 5371 if (ucpfBound) { 5372 return true; 5373 } 5374 if (window.UCPF && typeof window.UCPF.on === 'function') { 5375 window.UCPF.on('ucpf:consent:changed', onConsentChanged); 5376 window.UCPF.on('ucpf:consent:accepted_all', onConsentChanged); 5377 window.UCPF.on('ucpf:consent:rejected_all', onConsentChanged); 5378 ucpfBound = true; 5379 return true; 5380 } 5381 return false;
5382 } 5383 if (!bindUcpf()) { 5384 [50, 200, 800, 2000].forEach(function (ms) { 5385 window.setTimeout(bindUcpf, ms); 5386 }); 5387 } 5388 // Safari bfcache: re-evaluate guards when restoring a frozen checkout page. 5389 window.addEventListener('pageshow', function () { 5390 captchaReinitScheduled = false; 5391 refresh(); 5392 resyncAllGuards(); 5393 ensureVideosIfNeeded(); 5394 ensureMapsIfNeeded(); 5395 ensureCaptchasIfNeeded(); 5396 if (!leaveBuildersAlone()) { 5397 unhideReducedMotionElementor(); 5398 } 5399 }); 5400 5401 if (typeof MutationObserver === 'function') { 5402 var timer = null; 5403 var obs = new MutationObserver(function (mutations) { 5404 var relevant = false; 5405 for (var i = 0; i < mutations.length; i++) { 5406 var m = mutations[i]; 5407 var t = m.target; 5408 if (!t || t.nodeType !== 1) { 5409 // childList may only have addedNodes 5410 if (m.type === 'childList' && (m.addedNodes.length || m.removedNodes.length)) { 5411 if (isElementorBuilderNoise(m, t)) { 5412 continue; 5413 } 5414 relevant = true; 5415 break; 5416 } 5417 continue; 5418 } 5419 // Never react to our own UI or Elementor animation class thrash. 5420 if (t.id === 'ucpf-root' || (t.closest && t.closest('#ucpf-root, .ucpf-consent-guard__panel'))) { 5421 continue; 5422 } 5423 if (m.type === 'attributes' && m.attributeName === 'class') { 5424 // Ignore pure Elementor motion / sticky / animation class toggles. 5425 continue; 5426 } 5427 if (isElementorBuilderNoise(m, t)) { 5428 continue; 5429 } 5430 relevant = true; 5431 break; 5432 } 5433 if (!relevant) { 5434 return; 5435 } 5436 if (timer) { 5437 window.clearTimeout(timer); 5438 } 5439 // Longer debounce so Elementor entrance animations can settle. 5440 timer = window.setTimeout(function () { 5441 refresh(); 5442 resyncAllGuards(); 5443 ensureCalendlyIfNeeded(); 5444 remeasureVisiblePopupEmbeds(); 5445 unhideReducedMotionElementor(); 5446 }, 400); 5447 }); 5448 // Do NOT watch `class` â Elementor fade/sticky/motion toggle classes constantly; 5449 // that used to re-scan the whole page and break Motion Effects + Turnstile. 5450 // data-settings is Elementor Motion FX / sticky state â filtered as noise above. 5451 obs.observe(document.documentElement, { 5452 childList: true, 5453 subtree: true, 5454 attributes: true, 5455 attributeFilter: ['data-src', 'src', 'data-settings', 'data-url'], 5456 }); 5457 } 5458 } 5459 5460 if (document.readyState === 'loading') { 5461 document.addEventListener('DOMContentLoaded', boot); 5462 } else { 5463 boot(); 5464 } 5465})();
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.