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