1/** @format */ 2 3// Wrap all custom JS in an anonymous function that passes the alias "$" to everything so that "jQuery" doesn't have to be used 4(function ($) { 5 // Global variables 6 var megaMenuTarget = $('.menu-item-360'); // Where the mega menu is placed - remove if no mega menu 7 var enableLazyImageFade = $('body').hasClass('lazy-images-fade-in'); 8 9 // Begin JavaScript/jQuery setup 10 // The "on ready" 11 $(function () { 12 checkScroll(); 13 setupYouTube(); 14 setHeaderHeight(); 15 16 // Initialize geolocation tracking cookie (if needed) 17 setupGeolocation(); 18 19 // Prepare links within the body content with the scroll-to class if they point to internal ID anchors 20 $('.content a[href^="#"]').each(function () { 21 // Add class 'scroll-to' to the link 22 $(this).addClass('scroll-to'); 23 24 // Extract the anchor target and set it as a data attribute, minus the hashtag 25 var anchorTarget = $(this).attr('href').substring(1); 26 $(this).attr('data-scroll-to-id', anchorTarget); 27 }); 28 29 // Reposition Gravity Forms labels so the HTML structure works with label CSS 30 // not select or date fields 31 $('.gfield:not(.gfield--type-select, .gfield--type-date)').each(function () { 32 var label = $(this).find('label.gfield_label'); 33 var formControl = $(this).find('.ginput_container').children().first(); 34 label.detach().insertAfter(formControl); 35 }); 36 37 // Video gallery category filter 38 if ($('#video-category-filter').exists()) { 39 $('#video-category-filter').on('change', function () { 40 var selectedCategory = $(this).val(); 41 42 $('.videos-single').each(function () { 43 var categories = $(this).data('category').split(' '); 44 if (selectedCategory == '' || categories.includes(selectedCategory)) { 45 $(this).show(); 46 } else { 47 $(this).hide(); 48 } 49 }); 50 51 animateScroll('videos-container'); 52 }); 53 } 54 55 // Prevent empty searches on forms with the "prevent-blank-search" class 56 document.querySelectorAll('.prevent-blank-search').forEach(function (element) { 57 element.addEventListener( 58 'submit', 59 function (e) { 60 var value = $(this).find('input').val().trim(); 61 62 // Test for whitespace values 63 if (!value || !/\S/.test(value)) { 64 alert('Please enter a search term.'); 65 66 e.preventDefault(); 67 } 68 }, 69 { passive: false } 70 ); 71 }); 72 73 // Initialize mega menu (if needed) 74 if (megaMenuTarget && isDesktopScreen()) { 75 $(megaMenuTarget).addClass('static'); 76 $('#mega-menu').detach().appendTo(megaMenuTarget); 77 } 78 79 // Scroll to ID from a select dropdown. Useful on mobile 80 $('.scroll-to-select').on('change', function (e) { 81 // Get selected option then scroll to it 82 var selectedOption = $(this).find(':selected').val(); 83 animateScroll($(selectedOption).offset().top); 84 }); 85 86 // Copies content from a selected area to a target; used only on some modules 87 $('.content-selector').on('click', function () { 88 $(this).toggleClass('active'); 89 $(this).siblings().removeActive(); 90 91 $('#content-selector-target').removeClass('animated fast fadeIn'); 92 $('#content-selector-target').html($(this).html()); 93 94 setTimeout(function () { 95 $('#content-selector-target').addClass('animated fast fadeIn'); 96 }, 200); 97 }); 98 99 // Accessibility: Ensures main navigation dropdowns remain open for keyboard focus 100 $('.main-navigation-menu li, .mega-menu li') 101 .on('focusin mouseover', function () { 102 $(this).addActive(); 103 $(this).siblings().removeActive(); 104 }) 105 .on('mouseleave', function () { 106 $(this).removeActive(); 107 }); 108 109 // Blog Grid - Masonry and move pagination 110 if ($('#blog-grid').exists()) { 111 if (isDesktopScreen()) { 112 // Initializes Masonry layout; see: https://masonry.desandro.com/options.html 113 $('#blog-grid-inner').masonry({ 114 fitWidth: true, 115 gutter: 30, // Horizontal space between layout items 116 itemSelector: '.post' // Target specific class to avoid other elements being repositioned 117 }); 118 } 119 120 // Move pagination to final position, outside of grid 121 $('.blog-grid .wp-pagenavi').detach().appendTo('#blog-grid-pagination'); 122 } 123 124 // LAZY LOADING WITH LOZAD 125 // Use class "lazy-img" and attribute "data-img-src" to lazy load an <img> and "data-animation-class" to animate 126 // To randomly pick an image from a set, use attribute "data-img-srcs" like so: data-img-srcs="imageurl.
126jpg, imageurl.jpg" 127 var imageLazyLoader = lozad('.lazy-img', { 128 rootMargin: '10px 0px', 129 threshold: 0.1, 130 load: function (elementInView) { 131 if ($(elementInView).attr('data-img-srcs')) { 132 var imagesList = $(elementInView).attr('data-img-srcs').split(','); 133 var imageToLoad = imagesList[Math.floor(Math.random() * imagesList.length)]; 134 } else { 135 var imageToLoad = $(elementInView).attr('data-img-src'); 136 } 137 138 // Trigger fade-in after image loads 139 if (enableLazyImageFade) { 140 $(elementInView).one('load', function () { 141 $(this).addClass('lazy-fade-in-loaded'); 142 }); 143 144 $(elementInView).attr('src', imageToLoad); 145 146 // Fallback for cached images 147 if (elementInView.complete && elementInView.naturalHeight !== 0) { 148 $(elementInView).addClass('lazy-fade-in-loaded'); 149 } 150 } else { 151 $(elementInView).attr('src', imageToLoad); 152 } 153 154 // Optionally, add animation classes at the same time 155 if ($(elementInView).attr('data-animation-class')) { 156 var animateClass = 'animated ' + $(elementInView).attr('data-animation-class'); 157 $(elementInView).addClass(animateClass); 158 } 159 } 160 }); 161 imageLazyLoader.observe(); 162 163 // Use class "lazy-bg" and attribute "data-bg-src" on any element using a CSS background image lazy load 164 // To randomly pick background image from a set, use attribute "data-bg-srcs" like so: data-bg-srcs="imageurl.jpg, imageurl.jpg" 165 var backgroundImageLazyLoader = lozad('.lazy-bg', { 166 rootMargin: '10px 0px', 167 threshold: 0.1, 168 load: function (elementInView) { 169 if ($(elementInView).attr('data-bg-srcs')) { 170 var imagesList = $(elementInView).attr('data-bg-srcs').split(','); 171 var imageToLoad = imagesList[Math.floor(Math.random() * imagesList.length)]; 172 console.log('Lazy loading a randomly-selected background image: ' + imageToLoad); 173 } else { 174 var imageToLoad = $(elementInView).attr('data-bg-src'); 175 console.log('Lazy loading a background image: ' + imageToLoad); 176 } 177 178 $(elementInView).css('background-image', 'url(' + imageToLoad + ')'); 179 } 180 }); 181 backgroundImageLazyLoader.observe(); 182 183 // Add an active class to an element when it enters view 184 // Optionally, add a "data-delay" attribute equal to milliseconds, e.g. 500 for 0.5 seconds 185 var activeInViewTrigger = lozad('.active-in-view', { 186 rootMargin: '10px 0px', 187 threshold: 0.1, 188 load: function (elementInView) { 189 var delayActive = 0; 190 191 if ($(elementInView).attr('data-delay')) { 192 delayActive = $(elementInView).attr('data-delay'); 193 } 194 195 setTimeout(function () { 196 $(elementInView).addActive(); 197 $(elementInView).removeClass('active-in-view'); 198 }, delayActive); 199 } 200 }); 201 activeInViewTrigger.observe(); 202 203 // Trigger animate in view with a specific animation class added 204 // TO USE: Add class "animate-in-view" to an element plus a data attribute "data-animation-class" equal to an animation class name 205 var specificAnimateInViewTrigger = lozad('.animate-in-view', { 206 rootMargin: '10px 0px', 207 threshold: 0.1, 208 load: function (elementInView) { 209 var animToUse = $(elementInView).attr('data-animation-class'); 210 $(elementInView).addClass('animated ' + animToUse); 211 } 212 }); 213 specificAnimateInViewTrigger.observe(); 214 215 // Lazy load Google Map 216 // Requires data attribute "data-embed-map-link" to be set 217 var mapLoadInViewTrigger = lozad('.load-map-in-view', { 218 rootMargin: '10px 0px', 219 threshold: 0.1, 220 load: function (elementInView) { 221 var embedMapWidth = $(elementInView).attr('data-embed-map-width') ? $(elementInView).attr('data-embed-map-width') : 600; 222 var embedMapHeight = $(elementInView).attr('data-embed-map-height') ? $(elementInView).attr('data-embed-map-height') : 450; 223 224 // Add live <iframe> containing Google Map once the element is in view 225 $(elementInView).append( 226 '<iframe src="' + 227 $(elementInView).attr('data-embed-map-link') + 228 '" width="' + 229 embedMapWidth + 230 '" height="' + 231 embedMapHeight + 232 '" frameborder="0" style="border:0;" allowfullscreen="" aria-hidden="false" tabindex="0"></iframe>' 233 ); 234 } 235 }); 236 mapLoadInViewTrigger.observe(); 237 238 // On page print, download images 239 window.onbeforeprint = function () { 240 $('.lazy-img').each(function () { 241 var src = $(this).attr('data-img-src'); 242 $(this).attr('src', src); 243 }); 244 245 $('.lazy-bg').each(function () { 246 var src = $(this).attr('data-bg-src'); 247 $(this).css('background-image', src); 248 }); 249 }; 250 251 // END LAZY LOADING WITH LOZAD 252 }); 253 254 // Window *Load = Entire page including the DOM is loaded 255 $(window).on('load', function () { 256 prepareLinks(); 257 258 // On page load, check inputs for text to add active 259 $('input, textarea').each(function () { 260 activateInputLabel($(this)); 261 }); 262 263 // INPUT ACTIVE CLASS CONTROL 264 // On text inputs, add class when the field holds a value 265 $('input, textarea').on('input change', function () { 266 activateInputLabel($(this)); 267 }); 268 269 // Detects text inside inputs to activate or deactivate label styling 270 function activateInputLabel(thisInput) { 271 // Add validation to ensure we have a valid jQuery object 272 if (!thisInput || !thisInput.length) { 273 return; 274 } 275 276 var thisInputValue = thisInput.val(); 277 278 if (thisInputValue && thisInput.isActive() == false) { 279 thisInput.addActive(); 280 } else if (thisInputValue == '') { 281 thisInput.removeActive(); 282 } 283 } 284 285 // On dropdowns, add class when <select> is used 286 $('select').on('change', function () { 287 $(this).addActive(); 288 }); 289 290 $('select').on('focusin focusout', function () { 291 $(this).closest('.select-wrapper').toggleClass('active'); 292 }); 293 // END INPUT ACTIVE CLASS CONTROL 294 295 // ATTORNEY SEARCH HELPERS 296 // Prevent empty-value form inputs from sending data 297 $('#attorney-search-form').submit(function () { 298 $('input[value=""]:not(.active)').attr('disabled', true); 299 $('option[value=""]').attr('disabled', true); 300 }); 301 302 // If attorney search results are present, scroll to the section 303 if ($('#attorney-search-bar').exists()) { 304 animateScroll('attorney-search-bar', 300); 305 $('#attorney-search-inner').attr('tabindex', '0'); 306 $('#attorney-search-inner').focus(); 307 } 308 // END ATTORNEY SEARCH HELPERS 309 310 // TABLE OF CONTENTS 311 // Scan target for h2s and populate widget with scroll links 312 $('.toc-widget').each(function () { 313 var $widget = $(this); 314 var $list = $widget.find('.toc-list'); 315 var $select = $widget.find('.toc-select'); 316 var target = $widget.data('toc-target') || '.content'; 317 var headings = $(target).find('h2').not('.toc-widget h2'); 318 319 if (headings.length < 2) { 320 var $sidebarTOC = $widget.closest('#sidebar-toc'); 321 if ($sidebarTOC.length) { 322 $sidebarTOC.remove(); 323 } else { 324 $widget.remove(); 325 } 326 return; 327 } 328 329 headings.each(function () { 330 var thisHeadingText = $(this).text().trim(); 331 332 if (thisHeadingText === '') { 333 return; 334 } 335 336 var thisHeadingID = 337 'jump-' + 338 thisHeadingText 339 .toLowerCase() 340 .replace(/ /g, '-') 341 .replace(/[^\w-]+/g, ''); 342 343 $(this).attr('id', thisHeadingID); 344 345 var titleCaseText = thisHeadingText.replace(/\w+/g, function (word) { 346 return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(); 347 }); 348 349 $list.append('<li><button class="scroll-to no-button" data-scroll-to-id="' + thisHeadingID + '">' + titleCaseText + '</button></li>'); 350 $select.append('<option value="#' + thisHeadingID + '">' + titleCaseText + '</option>'); 351 }); 352 }); 353 // END TABLE OF CONTENTS 354 355 // CONTENT TAB BUTTONS 356 // Used on attorney bios 357 $('.content-tab-activate').on('click', function () { 358 $('.content-tab-activate, .content-tab').removeActive(); 359 $("[data-content-tab='" + $(this).attr('data-content-tab') + "']").addActive(); 360 $(this).addActive(); 361 }); 362 363 // Check URL parameters for tab activation on page load 364 var tabParam = getURLParameter('tab'); 365 366 if (tabParam) { 367 // Check if a tab with this parameter exists 368 if ($("[data-content-tab='" + tabParam + "']").length) { 369 // Remove active class from all tabs and content 370 $('.content-tab-activate, .content-tab').removeActive(); 371 // Activate the requested tab and its content 372 $("[data-content-tab='" + tabParam + "']").addActive(); 373 374 // Also update the mobile select dropdown if it exists 375 if ($('#select-content-skip').length) { 376 $('#select-content-skip').val(tabParam); 377 } 378 } 379 } 380 381 $('.mobile-tab').on('click', function () { 382 animateScroll('main'); 383 }); 384 // END CONTENT TAB BUTTONS 385 386 // ACCORDION CONTROL
387 // Toggle accordion open and closed 388 if ($('.accordion-item').length) { 389 // If there are accordions on this page, set their ARIA properties 390 $('.accordion-item-title').attr('aria-expanded', 'false'); 391 392 // Event listener for accordion items 393 $('.accordion-item-title').on('click', function () { 394 var parentAccordionItem = $(this).closest('.accordion-item'); 395 396 // Check data attribute values on the clicked accordion 397 var closeSiblings = $(this).data('close-siblings') !== false; 398 399 // Close sibling accordions, if the data attribute allows it 400 if (closeSiblings) { 401 parentAccordionItem.siblings().removeClass('active'); 402 parentAccordionItem.siblings().find('.accordion-item-title').attr('aria-expanded', 'false'); 403 } 404 405 if (!parentAccordionItem.hasClass('active')) { 406 // Open the clicked accordion 407 parentAccordionItem.addClass('active'); 408 $(this).attr('aria-expanded', 'true'); 409 } else { 410 // Close the clicked accordion 411 parentAccordionItem.removeClass('active'); 412 $(this).attr('aria-expanded', 'false'); 413 } 414 }); 415 } 416 417 // Open first accordion on load 418 if ($('.open-first').exists()) { 419 $('.open-first').each(function () { 420 $(this).find('.accordion-item:first-of-type .accordion-item-title').addClass('active').attr('aria-expanded', true); 421 }); 422 } 423 // END ACCORDION CONTROL 424 }); 425 426 /////////////////////////////////// 427 // Scrolls and On-Screen Reveals // 428 /////////////////////////////////// 429 430 // Gradually scrolls window to element on click of any element with the "scroll-to" class 431 $('body').on('click', '.scroll-to', function (e) { 432 e.preventDefault(); 433 var elementToScrollToID = $(this).attr('data-scroll-to-id'); 434 435 if (!elementToScrollToID) { 436 return; 437 } 438 439 animateScroll(elementToScrollToID); 440 441 if (elementToScrollToID == 'main') { 442 // After scrolling completes, temporarily add tabindex and focus #main 443 $(elementToScrollToID).attr('tabindex', '-1').focus().removeAttr('tabindex'); 444 } 445 }); 446 447 // Scrolls window back to top of page 448 $('.scroll-to-top').on('click', function (e) { 449 e.preventDefault(); 450 animateScroll(0); 451 }); 452 453 // Recurring events on scroll 454 $(window).on('scroll', function () { 455 // Control frequency of scroll checks for performance 456 let scrollDebounceTimer; 457 clearTimeout(scrollDebounceTimer); 458 scrollDebounceTimer = setTimeout(checkScroll, 5); 459 }); 460 461 function checkScroll() { 462 // Add class to body when scrolled beyond top of page 463 if ($(window).scrollTop() > 0) { 464 $('body').addClass('scrolled'); 465 } else { 466 $('body').removeClass('scrolled'); 467 } 468 469 // If scrolled to bottom, activate slideout feature in footer 470 if ($('#bottom-slideout').exists()) { 471 // Detect page bottom 472 if (getScrollPercent() > 0.98) { 473 $('#bottom-slideout').addActive(); 474 } else { 475 $('#bottom-slideout').removeActive(); 476 } 477 } 478 479 // Check elements that are revealed on scroll 480 $('.scroll-reveal').each(function () { 481 // If element is on screen and not yet revealed, add classes 482 var cur = $(this); 483 if (isScrolledIntoView(cur)) { 484 if (!cur.hasClass('animated')) { 485 cur.addClass('animated fadeInUp'); 486 } 487 } 488 }); 489 } 490 491 ///////////////////// 492 // YouTube Embeds // 493 ///////////////////// 494 495 function setupYouTube() { 496 // Prepare YouTube video placeholders that turn into iframe on click 497 if ($('.youtube-player').length == 0) { 498 return; 499 } 500 501 var div; 502 var n; 503 var v = document.getElementsByClassName('youtube-player'); 504 505 for (n = 0; n < v.length; n++) { 506 div = document.createElement('div'); 507 div.setAttribute('data-id', v[n].dataset.id); 508 div.setAttribute('data-list', v[n].dataset.list); 509 div.innerHTML = '<img src="https://img.youtube.com/vi/' + v[n].dataset.id + '/hqdefault.jpg" alt="">'; 510 511 // Add attributes and classes to the div to make interactive 512 $(div).addClass('youtube-player-inner').attr('tabindex', '0'); 513 $(div).addClass('youtube-player-inner').attr('aria-label', 'Play video'); 514 $(div).on('click keypress', getYouTubeFrame); 515 516 // Add div to this player 517 v[n].appendChild(div); 518 } 519 } 520 521 function getYouTubeFrame() { 522 var iframe = document.createElement('iframe'); 523 var embed = 'https://www.youtube.com/embed/ID?autoplay=1'; 524 525 iframe.setAttribute('src', embed.replace('ID', this.dataset.id)); 526 iframe.setAttribute('frameborder', '0'); 527 iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'); 528 iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); 529 iframe.setAttribute('allowfullscreen', ''); 530 iframe.setAttribute('title', 'YouTube video player'); 531 532 // Clear the parent container and append iframe 533 var container = this.parentNode; 534 container.innerHTML = ''; 535 container.appendChild(iframe); 536 } 537 538 //////////// 539 // Misc. // 540 //////////// 541 542 // Returns true if on homepage 543 // Requires WordPress body classes to be in effect 544 function isHomepage() { 545 if (jQuery('body.home').length) { 546 return true; 547 } 548 return false;
549 } 550 551 // Returns true no homepage class is present 552 function isSubpage() { 553 if (jQuery('body.home').length) { 554 return false; 555 } 556 return true; 557 } 558 559 // Returns true if element is scrolled into view 560 // Usage: isScrolledIntoView(element) 561 function isScrolledIntoView(elem) { 562 let docViewTop = $(window).scrollTop(); 563 let docViewBottom = docViewTop + $(window).height(); 564 let elemTop = $(elem).offset().top; 565 let elemBottom = elemTop + $(elem).outerHeight(); 566 567 return elemTop <= docViewBottom && elemBottom >= docViewTop; 568 } 569 570 // Prepare all links with appropriate attributes and classes 571 function prepareLinks() { 572 // External links: Add attributes for external links 573 $('a') 574 .filter(function () { 575 return this.hostname && this.hostname !== location.hostname; 576 }) 577 .attr('target', '_blank') 578 .attr('rel', 'noopener') 579 .addClass('external-link'); 580 581 // PDF links: Open PDFs in new tab 582 $('a[href$=".pdf"]').each(function () { 583 $(this).attr('target', '_blank'); 584 }); 585 586 // Accessibility: Alert the user that a link will open in a new tab 587 $('a[target="_blank"]').each(function () { 588 // Check if the link already has an aria-label 589 var ariaLabel = $(this).attr('aria-label'); 590 591 if (ariaLabel) { 592 // If the link already has an aria-label, append the new text 593 $(this).attr('aria-label', ariaLabel + ' (Opens in a new tab)'); 594 } else { 595 // If no aria-label exists, add a new one with the "Opens in a new tab" text 596 $(this).attr('aria-label', 'Opens in a new tab'); 597 } 598 }); 599 600 // Identify telephone links 601 $('a[href^="tel:"]').each(function () { 602 $(this).addClass('tel-link'); 603 }); 604 } 605 606 // Recurring events on resize 607 window.addEventListener('resize', function () { 608 setHeaderHeight(); 609 }); 610 611 // Update CSS value equal to header height 612 function setHeaderHeight() { 613 const headerID = isMobileScreen() ? 'header-mobile' : 'header-desktop'; 614 var header = document.getElementById(headerID); 615 616 if (header) { 617 const headerHeight = header.offsetHeight; 618 document.documentElement.style.setProperty('--header-height', `${headerHeight}px`); 619 } 620 } 621 622 // Optional geolocation feature 623 // Adds geolocation and other information to contact emails 624 function setupGeolocation() { 625 if (sessionStorage.geolocationReady) { 626 return; 627 } 628 629 // Make AJAX request to store PHP SESSION variables containing geolocation data; requires API key 630 setTimeout(function () { 631 jQuery.ajax({ 632 type: 'get', 633 url: '/wp-content/themes/paperstreet/includes/include-geolocation.php', 634 success: function (response) {} 635 }); 636 637 console.log('Geolocation data saved for this session.'); 638 sessionStorage.geolocationReady = 'true'; 639 }, 2000); 640 } 641})(jQuery); 642 643document.querySelectorAll('a[href^="tel:"]').forEach(link => { 644 link.addEventListener('click', function () { 645 if (typeof gtag === 'function') { 646 gtag('event', 'phone_call', { 647 phone_number: this.href.replace('tel:', ''), 648 page_url: window.location.href 649 }); 650 console.log('gtag phone_call event fired'); 651 } 652 }); 653}); 654 655
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.