1(function () { 2 const { mergeObjects, getCurrentBreakpoint } = BreakdanceFrontend.utils; 3 window.breakdancePopupInstances = {}; 4 window.breakdanceHasShownPopup = false; 5 6 class BreakdancePopup { 7 defaultOptions = { 8 keepOpenOnHashlinkClicks: false, 9 closeOnClickOutside: true, 10 closeOnEscapeKey: true, 11 closeAfterMilliseconds: null, 12 showCloseButtonAfterMilliseconds: null, 13 disableScrollWhenOpen: false, 14 avoidMultiple: false, 15 entranceAnimation: null, 16 exitAnimation: null, 17 limitPageLoad: null, 18 limitSession: null, 19 limitForever: null, 20 breakpointConditions: [], 21 triggers: [], 22 }; 23 24 showCount = 0; 25 idleTimeMilliseconds = 0; 26 entranceAnimation = false; 27 exitAnimation = false; 28 lastScrollPosition = window.scrollY; 29 30 boundEvents = {}; 31 32 openClass = "breakdance-popup-open"; 33 animatingClass = "breakdance-popup-animating"; 34 35 constructor(id, options) { 36 if (window.breakdancePopupInstances[id]) { 37 return; 38 } 39 this.id = id; 40 this.options = mergeObjects(this.defaultOptions, options); 41 this.triggers = this.options.triggers; 42 this.storageKey = `breakdance_popup_${id}_count`; 43 window.breakdancePopupInstances[id] = this; 44 } 45 46 init() { 47 this.element = document.querySelector( 48 `[data-breakdance-popup-id="${this.id}"]` 49 ); 50 this.wrapper = this.element.closest(".bde-popup"); 51 52 if (!this.element) { 53 return false; 54 } 55 56 if (!this.wrapper) { 57 return false; 58 } 59 60 this.triggers.forEach((trigger) => { 61 this.initTrigger(trigger.slug, trigger.options); 62 }); 63 64 this.initCloseButton(); 65 66 this.wrapper.addEventListener("breakdance_popup_open", () => { 67 }); 68 69 this.wrapper.addEventListener("breakdance_popup_close", () => { 70 this.handleClose(); 71 }); 72 73 return true; 74 } 75 76 setOptions(options) { 77 this.options = mergeObjects(this.options, options); 78 79 this.init(); 80 } 81 82 open(forceOpen = false) { 83 const event = new Event("breakdance_play_animations", { bubbles: true }); 84 this.element.dispatchEvent(event); 85 86 return new Promise((resolve, reject) => {
87 if (this.shouldHideAtBreakpoint()) { 88 return resolve(); 89 } 90 91 if (this.isOpen() || this.isAnimating()) { 92 return resolve(); 93 } 94 95 if (!forceOpen && this.hasReachedLimit()) { 96 return resolve(); 97 } 98 99 if (this.element.dataset?.breakdancePopupUsingProTriggers) { 100 alert( 101 `This popup is using a Pro-only trigger. The popup would show up now instead of this alert. Get Breakdance Pro to get access to all popup triggers.` 102 ); 103 104 return resolve(); 105 } 106 107 this.incrementShowCounts(); 108 109 this.registerCloseListeners(); 110 111 if (this.options.entranceAnimation) { 112 this.playEntranceAnimation(); 113 } else { 114 this.wrapper.classList.add(this.openClass); 115 } 116 117 this.wrapper.dispatchEvent(new CustomEvent("breakdance_popup_open")); 118 return resolve(); 119 120 }); 121 } 122 123 registerCloseListeners() { 124 this.boundCloseIfEscapeOrClickOutside = this.closeIfEscapeOrClickOutside.bind( 125 this 126 ); 127 128 if (this.options.closeOnEscapeKey) { 129 document.addEventListener( 130 "keydown", 131 this.boundCloseIfEscapeOrClickOutside 132 ); 133 } 134 135 this.boundMaybeAutomaticallyClosePopup = this.maybeAutomaticallyClosePopup.bind(this); 136 this.element.addEventListener("click", this.boundMaybeAutomaticallyClosePopup); 137 138 if (this.options.closeOnClickOutside) { 139 document.addEventListener( 140 "mousedown", 141 this.boundCloseIfEscapeOrClickOutside 142 ); 143 } 144 145 if (this.options.closeAfterMilliseconds) { 146 setTimeout(() => { 147 if (this.isOpen()) { 148 this.close(); 149 } 150 }, this.options.closeAfterMilliseconds); 151 } 152 153 if (this.options.disableScrollWhenOpen) { 154 document.querySelector("html").classList.add("breakdance-noscroll"); 155 } 156 } 157 158 removeCloseListeners() { 159 if (this.boundCloseIfEscapeOrClickOutside) { 160 document.removeEventListener( 161 "keydown", 162 this.boundCloseIfEscapeOrClickOutside 163 ); 164 document.removeEventListener( 165 "mousedown", 166 this.boundCloseIfEscapeOrClickOutside 167 ); 168 this.element.removeEventListener( 169 "click", 170 this.boundMaybeAutomaticallyClosePopup 171 ); 172 } 173 } 174 175 hasReachedLimit() { 176 if ( 177 this.options.avoidMultiple && 178 window.breakdanceHasShownPopup === true 179 ) { 180 return true; 181 } 182 183 if ( 184 this.options.limitPageLoad &&
185 this.getShowCount("page_load") >= this.options.limitPageLoad 186 ) { 187 return true; 188 } 189 190 if ( 191 this.options.limitSession && 192 this.getShowCount("session") >= this.options.limitSession 193 ) { 194 return true; 195 } 196 197 if ( 198 this.options.limitForever && 199 this.getShowCount("local") >= this.options.limitForever 200 ) { 201 return true; 202 } 203 204 return false; 205 } 206 207 isOpen() { 208 return this.wrapper && this.wrapper.classList.contains(this.openClass); 209 } 210 211 isAnimating() { 212 return this.wrapper && this.wrapper.classList.contains(this.animatingClass); 213 } 214 215 closeIfEscapeOrClickOutside(event) { 216 const shouldClose = 217 (event.keyCode === 27 && this.isPopupOnTop()) || 218 event.target === this.wrapper; 219 220 if (shouldClose) { 221 event.preventDefault(); 222 event.stopPropagation(); 223 this.close(); 224 } 225 } 226 227 isPopupOnTop() { 228 const topMostElement = document.elementFromPoint( 229 this.wrapper.offsetLeft, 230 this.wrapper.offsetTop 231 ); 232 return topMostElement === this.wrapper; 233 } 234 235 maybeAutomaticallyClosePopup(event) { 236 237 if (this.options.keepOpenOnHashlinkClicks) { 238 return; 239 } 240 241 /* 242 if a disable-popup-autoclose attribute is present on a parent 243 of the event target, don't automatically close the popup 244 this feature is experimental and may not be available in future versions of Breakdance 245 */ 246 const elementWitDisablePopupAutocloseAttribute = event.target.closest("[disable-popup-autoclose]"); 247 248 if (elementWitDisablePopupAutocloseAttribute) { 249 return; 250 } 251 252 /* 253 autoclose the popup when: 254 - clicking on a link with an attribute of close-popup-on-click (this feature is experimental and may not be available in future versions of Breakdance) 255 - clicking on a link with a href of # 256 - clicking a #hashlink that leads somewhere on the page 257 */ 258 259 if (event.target.closest('[close-popup-on-click]')) { 260 this.close(); 261 return; 262 } 263 264 const link = event.target.closest("a"); 265 if (!link) return; 266 267 if (link.getAttribute('href') === '#') { 268 this.close(); 269 return; 270 } 271 272 const url = new URL(link); 273 if (!url.hash) return; 274 275 const hashAsIdOrName = url.hash.substring(1); 276 277 if (hashAsIdOrName && document.querySelector(`#${hashAsIdOrName}, a[name=${hashAsIdOrName}]`)) { 278 this.close(); 279 return; 280 } 281 282 } 283 284 close() { 285 return new Promise((resolve, reject) => { 286 if (!this.isOpen() || this.isAnimating()) { 287 return resolve(); 288 } 289 290 this.removeCloseListeners(); 291 292 if (this.options.disableScrollWhenOpen) { 293 document 294 .querySelector("html") 295 .classList.remove("breakdance-noscroll"); 296 } 297 298 if (this.options.exitAnimation) { 299 this.playExitAnimation(); 300 } else { 301 this.wrapper.classList.remove(this.openClass); 302 } 303 304 this.wrapper.dispatchEvent(new CustomEvent("breakdance_popup_close")); 305 return resolve(); 306 307 }); 308 } 309 310 toggle() { 311 if (this.isOpen()) { 312 return this.close(); 313 } 314 return this.open(); 315 } 316 317 showPopupOnScrollPercentage(options, callback) { 318 return event => { 319 const targetPercent = options.percent / 100; 320 const scrollTop = window.scrollY; 321 const documentHeight = document.body.offsetHeight; 322 const windowHeight = window.innerHeight; 323 const scrollPercent = scrollTop / (documentHeight - windowHeight); 324 if (scrollPercent > targetPercent) { 325 this.open().then(callback); 326 } 327 }; 328 } 329 330 showPopupOnScrollToSelector(options) { 331 const element = document.querySelector(options.selector); 332 if (!element) { 333 return; 334 } 335 const ob = new IntersectionObserver( 336 (entries, observer) => { 337 entries.forEach(entry => { 338 if (entry.isIntersecting && !this.isOpen()) { 339 this.open().then(() => { 340 observer.unobserve(entry.target); 341 }); 342 } 343 }); 344 }, 345 { 346 root: null, 347 rootMargin: "0px", 348 threshold: 0.2 349 } 350 ); 351 ob.observe(element); 352 } 353 354 showPopupOnScrollUp(callback) { 355 return event => { 356 const scrollTop = window.scrollY; 357 if (scrollTop < this.lastScrollPosition) { 358 this.open().then(callback); 359 } 360 this.lastScrollPosition = scrollTop; 361 }; 362 } 363 364 showPopupAfterInactivityDelay(delayInMilliseconds) { 365 const idleTimeout = 1000; 366 const idleInterval = setInterval(() => { 367 if (this.idleTimeMilliseconds >= delayInMilliseconds) { 368 this.open(); 369 clearInterval(idleInterval); 370 if (boundResetIdleTime) { 371 document.removeEventListener("mousemove", boundResetIdleTime); 372 document.removeEventListener("keydown", boundResetIdleTime); 373 } 374 } else { 375 this.idleTimeMilliseconds += idleTimeout; 376 } 377 }, idleTimeout); 378 const boundResetIdleTime = this.resetIdleTime.bind(this); 379 document.addEventListener("mousemove", boundResetIdleTime); 380 document.addEventListener("keydown", boundResetIdleTime); 381 } 382 383 resetIdleTime() { 384 this.idleTimeMilliseconds = 0; 385 } 386
387 showPopupOnExitIntent(callback) { 388 return event => { 389 const isExitIntent = 390 !event.toElement && !event.relatedTarget && event.clientY < 10; 391 if (isExitIntent) { 392 this.open().then(callback); 393 } 394 }; 395 } 396 397 showPopupOnClickEvent(options, callback) { 398 return event => { 399 if (options.clickType === "anywhere") { 400 return this.open().then(callback); 401 } 402 if (options.selector !== null) { 403 if (event.target.closest(options.selector)) { 404 event.preventDefault(); 405 event.stopPropagation(); 406 return this.open().then(callback); 407 } 408 } 409 }; 410 } 411 412 showPopupOnLoad(delayInMilliseconds) { 413 setTimeout(() => { 414 this.open(); 415 }, delayInMilliseconds); 416 } 417 418 async playEntranceAnimation() { 419 420 if (this.entranceAnimation) { 421 this.entranceAnimation.destroy(); 422 } 423 424 if (this.options.entranceAnimation !== null) { 425 this.entranceAnimation = new BreakdancePopupAnimation( 426 this.element, 427 this.wrapper, 428 this.options.entranceAnimation 429 ); 430 } 431 432 await this.entranceAnimation.play(); 433 434 } 435 436 async playExitAnimation(pauseTweenAtEnd = false) { 437 438 if (this.exitAnimation) { 439 this.exitAnimation.destroy(); 440 } 441 442 if (this.options.exitAnimation !== null) { 443 this.exitAnimation = new BreakdancePopupAnimation( 444 this.element, 445 this.wrapper, 446 this.options.exitAnimation, 447 true 448 ); 449 } 450 451 await this.exitAnimation.play(true); 452 453 if (pauseTweenAtEnd) { 454 this.exitAnimation.tween.pause(0); 455 } 456 457 this.wrapper.classList.remove(this.openClass); 458 this.exitAnimation.destroy(); 459 460 } 461 462 initTrigger(slug, options = {}) { 463 if (slug === "load") { 464 const delay = options.delay ? options.delay * 1000 : 0; 465 this.showPopupOnLoad(delay); 466 } 467 468 if (slug === "inactivity") { 469 const delay = options.delay ? options.delay * 1000 : 0; 470 this.showPopupAfterInactivityDelay(delay); 471 } 472 473 if (slug === "scroll" && options.scrollType === "selector") { 474 this.showPopupOnScrollToSelector(options); 475 } 476 477 if (slug === "scroll" && options.scrollType === "percent") { 478 const scrollEventListener = this.showPopupOnScrollPercentage( 479 options, 480 () => { 481 window.removeEventListener("scroll", scrollEventListener); 482 } 483 ); 484 window.addEventListener("scroll", scrollEventListener); 485 } 486 487 if (slug === "scroll_up") { 488 const scrollUpEventListener = this.showPopupOnScrollUp(() => { 489 window.removeEventListener("scroll", scrollUpEventListener); 490 }); 491 window.addEventListener("scroll", scrollUpEventListener); 492 } 493 494 if (slug === "exit_intent") { 495 const exitIntentEventListener = this.showPopupOnExitIntent
495(() => { 496 document.removeEventListener("mouseout", exitIntentEventListener); 497 }); 498 document.addEventListener("mouseout", exitIntentEventListener); 499 } 500 501 if (slug === "click") { 502 const boundShowPopupOnClickEvent = this.showPopupOnClickEvent( 503 options, 504 () => { 505 if (options.clickType === "anywhere") { 506 document.removeEventListener("click", boundShowPopupOnClickEvent); 507 } 508 } 509 ); 510 document.addEventListener("click", boundShowPopupOnClickEvent); 511 } 512 } 513 514 initCloseButton() { 515 if (this.options.showCloseButtonAfterMilliseconds) { 516 setTimeout(() => { 517 if (!this.isOpen()) { 518 return; 519 } 520 const closeButton = this.element.querySelector( 521 ".breakdance-popup-close-button" 522 ); 523 if (closeButton) { 524 closeButton.classList.remove("hidden"); 525 } 526 }, this.options.showCloseButtonAfterMilliseconds); 527 } 528 } 529 530 incrementShowCounts() { 531 window.breakdanceHasShownPopup = true; 532 533 const sessionStorageValue = this.getShowCount("session"); 534 sessionStorage.setItem( 535 this.storageKey, 536 (sessionStorageValue + 1).toString() 537 ); 538 539 const localStorageValue = this.getShowCount("local"); 540 localStorage.setItem(this.storageKey, (localStorageValue + 1).toString()); 541 542 this.showCount += 1; 543 } 544 545 getShowCount(type) { 546 let showCount = 0; 547 548 if (type === "page_load") { 549 showCount = this.showCount; 550 } 551 552 if (type === "session") { 553 const sessionStorageItem = sessionStorage.getItem(this.storageKey); 554 showCount = sessionStorageItem ? parseInt(sessionStorageItem) : 0; 555 } 556 557 if (type === "local") { 558 const localStorageItem = localStorage.getItem(this.storageKey); 559 showCount = localStorageItem ? parseInt(localStorageItem) : 0; 560 } 561 562 return showCount; 563 } 564
565 shouldHideAtBreakpoint() { 566 const breakpoint = getCurrentBreakpoint(); 567 568 return this.options.breakpointConditions.some(condition => { 569 const conditionApplies = condition.breakpoints.includes(breakpoint.id); 570 571 if (condition.operand === "is one of") { 572 return !conditionApplies; 573 } 574 575 if (condition.operand === "is none of") { 576 return conditionApplies; 577 } 578 579 return false; 580 }); 581 } 582 583 static runAction(popupId, action = "open") { 584 const isBuilder = !!window?.BreakdanceFrontend.utils.isBuilder(); 585 586 if (isBuilder) { 587 return; 588 } 589 const breakdancePopupInstance = window.breakdancePopupInstances[popupId]; 590 if ( 591 breakdancePopupInstance && 592 typeof breakdancePopupInstance[action] === "function" 593 ) { 594 breakdancePopupInstance[action].call(breakdancePopupInstance, true); 595 } 596 } 597 598 handleClose() { 599 pauseYouTubeVideosInsideElement(this.wrapper); 600 pauseVimeoVideosInsideElement(this.wrapper); 601 pauseHtml5VideosInsideElement(this.wrapper); 602 } 603 } 604 605 window.BreakdancePopup = BreakdancePopup; 606 document.addEventListener("click", event => { 607 const popupTrigger = event.target.closest("[data-breakdance-popup-action]"); 608 if (popupTrigger) { 609 const { 610 breakdancePopupReference, 611 breakdancePopupAction 612 } = popupTrigger.dataset; 613 if (breakdancePopupReference) { 614 event.preventDefault(); 615 event.stopPropagation(); 616 BreakdancePopup.runAction( 617 breakdancePopupReference, 618 breakdancePopupAction 619 ); 620 } 621 } 622 }); 623 624 function pauseYouTubeVideosInsideElement(element) { 625 // youtube requires &enablejsapi=1 to be in the embed URL for the below to work 626 element.querySelectorAll( 627 "iframe" 628 ).forEach( 629 (maybeYouTubeVideoFrame) => { 630 maybeYouTubeVideoFrame.contentWindow.postMessage( 631 `{"event":"command","func":"stopVideo","args":[]}`, 632 "*" 633 ); 634 } 635 ); 636 } 637 638 function pauseVimeoVideosInsideElement(element) { 639 element.querySelectorAll( 640 "iframe" 641 ).forEach( 642 (maybeVimeoVideoIframe) => { 643 maybeVimeoVideoIframe.contentWindow.postMessage( 644 '{"method":"pause","value":""}', 645 '*' 646 ); 647 } 648 ); 649 } 650 651 function pauseHtml5VideosInsideElement(element) { 652 element.querySelectorAll( 653 "video" 654 ).forEach( 655 (html5Video) => { 656 html5Video.pause(); 657 } 658 ); 659 } 660 661 662})();
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.