1/*----------------------------------------------------------------------------------- 2 3Theme Name: revize. 4Front-end developer: Samir Alley 5Author Design: Samir Alley @samiralley 6Author URI: https://www.revize.com/ 7Date: Dec 12 2025 8Version: 1.1987987 9 10-----------------------------------------------------------------------------------*/ 11 12RZBarAddButton = RZBarAddButton || function() {} 13 14function RZRenderMini(elementList) { 15 elementList.querySelectorAll(".tiny-deferred").forEach(function(el) { 16 window.RZTinyCarousel(el); 17 }); 18 elementList.querySelectorAll(".masonry-deferred").forEach(function(el) { 19 window.RZMasonry(el) 20 }); 21} 22 23// Link to correct news pages 24(function () { 25 function fixNewsLinks(root) { 26 root = root || document; 27 28 var links = root.querySelectorAll('.rz-news-card-wrapper a.rz-news-button[href]'); 29 for (var i = 0; i < links.length; i++) { 30 var a = links[i]; 31 var href = (a.getAttribute('href') || '').trim(); 32 if (!href) continue; 33 34 var low = href.toLowerCase(); 35 if (low.indexOf('http://') === 0 || low.indexOf('https://') === 0 || href.indexOf('//') === 0) continue; 36 if (low.indexOf('mailto:') === 0 || low.indexOf('tel:') === 0 || href.indexOf('#') === 0) continue; 37 38 if (href.charAt(0) === '/') href = href.substring(1); 39 if (href.indexOf('./') !== 0 && href.indexOf('../') !== 0) href = './' + href; 40 41 a.setAttribute('href', href); 42 } 43 } 44 45// If a "tns-item" is off the screen , any focusable children (links, buttons, etc) will not be focusable. This prevents users from having to tab through every single quicklink/news story and only allows focus the visible elements on the screen. 46// To view more news stories, the user will use the prev/next arrows as intended. Also, tiny slider adds aria-hidden and tabindex=-1 to the "tns-item" elements when they are off screen. 47// If aria-hidden=true is on a parent "tns-item" element, the children cannot be focusable (or aria-hidden="false") or you will get ADA error for: "ARIA hidden element must not be focusable or contain focusable elements". 48function observeSlides() { 49 function updateTabindex() { 50 $(".tns-item").each(function () { 51 const hidden = $(this).attr("aria-hidden") === "true"; 52 $(this) 53 .find('a, button, input, select, textarea, summary, iframe, [tabindex]') 54 .each(function () { 55 if (hidden) { 56 $(this).attr("tabindex", "-1"); 57 } else { 58 $(this).removeAttr("tabindex"); 59 } 60 }); 61 }); 62 } 63 64 const observer = new MutationObserver(updateTabindex); 65 66 $(".tns-item").each(function () { 67 observer.observe(this, { 68 attributes: true, 69 attributeFilter: ["class", "aria-hidden"] 70 }); 71 }); 72 73 updateTabindex(); 74} 75 76observeSlides(); 77 78 // ADA, Refocus last clicked calendar event 79 function initializeModalFocus() { 80 let $lastFocused = null; 81 const $modal = $("#event-modal"); 82 83 function setupEventHandlers() { 84 // Track last focused event 85 $(document).on("click keydown", ".cal-event", function (e) { 86 if (e.type === "click" || (e.type === "keydown" && e.which === 13)) { 87 $lastFocused = $(this); 88 if (e.type === "keydown") $(this).trigger("click"); 89 } 90 }); 91 92 // Handle modal closing 93 const closeModal = () => { 94 $modal.hide(); 95 $modal.removeClass("show"); 96 $modal.css("display", "none"); 97 98 setTimeout(() => { 99 if ($lastFocused?.length) { 100 $lastFocused.focus(); 101 } 102 }, 100); 103 }; 104 105 // Close modal via button or escape key 106 $(document).on("click", "#modal-close-button, .modal-footer .btn-secondary", closeModal); 107 108 // Handle escape key 109 $(document).on("keydown", function (e) { 110 // Get computed style directly 111 const modalElement = document.getElementById("event-modal"); 112 if (!modalElement) return; 113 const computedStyle = window.getComputedStyle(modalElement); 114 const isModalVisible = computedStyle.display !== "none"; 115 116 if (e.key === "Escape" && isModalVisible) { 117 e.preventDefault(); 118 closeModal(); 119 } 120 }); 121 } 122 123 setupEventHandlers(); 124 } 125 126 initializeModalFocus(); 127
128 function init() { 129 fixNewsLinks(document); 130 131 var wraps = document.querySelectorAll('.rz-news-card-wrapper'); 132 for (var w = 0; w < wraps.length; w++) { 133 (function (wrap) { 134 var obs = new MutationObserver(function () { 135 fixNewsLinks(wrap); 136 }); 137 obs.observe(wrap, { childList: true, subtree: true }); 138 })(wraps[w]); 139 } 140 } 141 142 if (document.readyState === 'loading') { 143 document.addEventListener('DOMContentLoaded', init); 144 } else { 145 init(); 146 } 147})(); 148 149// column border animations 150(function () { 151 function initAnimatedBorders() { 152 var cols = document.querySelectorAll('.rz-column-block[data-border-animated="true"]'); 153 if (!cols.length) return; 154 155 // If IntersectionObserver isn't supported, just animate immediately 156 if (!('IntersectionObserver' in window)) { 157 cols.forEach(function (el) { el.classList.add('animate-border'); }); 158 return; 159 } 160 161 var io = new IntersectionObserver(function (entries) { 162 entries.forEach(function (entry) { 163 if (!entry.isIntersecting) return; 164 entry.target.classList.add('animate-border'); 165 io.unobserve(entry.target); 166 }); 167 }, { threshold: 0.2 }); 168 169 cols.forEach(function (el) { 170 if (el.classList.contains('animate-border')) return; 171 io.observe(el); 172 }); 173 } 174 175 if (document.readyState === 'loading') { 176 document.addEventListener('DOMContentLoaded', initAnimatedBorders); 177 } else { 178 initAnimatedBorders(); 179 } 180})(); 181 182// Responsive YouTube videos for all 183function wrapYouTubeIframes(context = document) { 184 context 185 .querySelectorAll('iframe[src*="youtube"], iframe[src*="youtu.be"]') 186 .forEach(function (el) { 187 if (el.closest(".ratio") || el.closest(".youtube-background")) return; 188 el.removeAttribute("width"); 189 el.removeAttribute("height"); 190 const wrapper = document.createElement("div"); 191 wrapper.className = "ratio ratio-16x9"; 192 el.parentNode.insertBefore(wrapper, el); 193 wrapper.appendChild(el); 194 }); 195} 196document.addEventListener("DOMContentLoaded", function () { 197 wrapYouTubeIframes(); 198}); 199 200document.addEventListener("shown.bs.offcanvas", function (e) { 201 wrapYouTubeIframes(e.target); 202}); 203 204// Add "Today" to Catalogue modal hours and hours Module 205document.addEventListener('DOMContentLoaded', function () { 206 const fullDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 207 const abbrDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 208 const todayIndex = new Date().getDay(); 209 const todayFull = fullDays[todayIndex].toLowerCase(); 210 const todayAbbr = abbrDays[todayIndex].toLowerCase(); 211 const labelHTML = ` 212 <small class="today-badge bg-success-subtle text-success-emphasis border-radius py-1 px-2 d-inline-block lh-sm ms-1 rounded"> 213 Today 214 </small> 215 `; 216 document.querySelectorAll('.table.hours-of-op-table tbody tr').forEach(row => { 217 const dayCell = row.querySelector('th'); 218 if (!dayCell) return; 219 const text = dayCell.textContent.trim().toLowerCase(); 220 if (text.startsWith(todayFull) || text.startsWith(todayAbbr)) { 221 dayCell.insertAdjacentHTML('beforeend', labelHTML); 222 row.classList.add('today-row'); 223 } 224 }); 225}); 226 227$(function() { 228 var $builderBtn = $('.builder-btn-wrap'); 229 var $emptyPost = $('.post-content .rz-builder-empty-data'); 230 var $builderInfo = $('.post-content .rz-builder-info'); 231 232 if ($builderBtn.length && $emptyPost.length) { 233 var $cloneBuilderBtn = $builderBtn.clone(true, true); 234 var $emptyPostHeader = $('<h6 class="builder-empty-header mb-3">Click below to add a module or content to this page</h6>'); 235 var $wrapper = $('<div class="builder-btn-inserted"></div>'); 236 $wrapper.append($emptyPostHeader).append($cloneBuilderBtn); 237 $emptyPost.empty().append($wrapper); 238 } 239 240 if ($builderInfo.length && !$builderInfo.parent().hasClass('container')) { 241 $builderInfo.wrap('<div class="container-fluid add-module-alert"></div>'); 242 } 243}); 244 245/** 246 * Place the logo in the middle of the nav 247 **/ 248$(function () { 249 const $headerWrap = $('.site-header-wrap'); 250 const $nav = $('#nav'); 251 252 const $textLogo = $('.main-logo.main-logo-text').first(); 253 const $imageLogo = $('.main-logo.main-logo-image').first(); 254 255 let $logos = $(); 256 if ($textLogo.length && $imageLogo.length) { 257 $logos = $textLogo.add($imageLogo); 258 } else if ($imageLogo.length) { 259 $logos = $imageLogo; 260 } else if ($textLogo.length) { 261 $logos = $textLogo; 262 } 263 264 if (!$logos.length || !$nav.length || !$headerWrap.length) return; 265 266 const $placeholder = $('<span class="main-logo-placeholder"></span>'); 267 $logos.first().before($placeholder); 268 269 function moveMainLogo() { 270 const moveAllowed = $headerWrap.attr('data-logo-move') === 'true'; 271 const isDesktop = window.innerWidth >= 992; 272 273 if (!moveAllowed || !isDesktop) { 274 if (!$placeholder.next().is($logos.first())) { 275 $logos.detach(); 276 $placeholder.after($logos); 277 } 278 return; 279 } 280 281 const count = $nav.children('li').length; 282 if (!count) return; 283 284 const middlePoint = Math.floor(((count - 1) / 2)); 285 $logos.detach(); 286 $logos.insertAfter('#nav > li:nth-child(' + middlePoint + ')'); 287 } 288 289 moveMainLogo(); 290 $(window).on('resize', moveMainLogo); 291 }); 292 293/** 294 * Fix any mis-rendered Material Icons on the page. 295 * Converts: 296 * <i class="material-icons|home"></i> 297 * <i class="material-icons-outlined|airport_shuttle"></i> 298 * to: 299 * <span class="material-icons">home</span> 300 * <span class="material-icons-outlined">airport_shuttle</span> 301 */ 302document.addEventListener('DOMContentLoaded', () => { 303 const selector = '[class*="material-icons|"], [class*="material-icons-outlined|"]'; 304 305 document.querySelectorAll(selector).forEach(el => { 306 const m = (el.className || '').match(/\b(material-icons(?:-outlined)?)\|([a-z0-9_]+)\b/i); 307 if (!m) return; 308
309 const klass = m[1]; 310 const name = m[2]; 311 312 const span = document.createElement('i'); 313 314 const newClasses = el.className.replace(m[0], klass).trim(); 315 span.className = /\brz-btn-icon\b/.test(newClasses) 316 ? newClasses + ' notranslate' 317 : newClasses + ' rz-btn-icon notranslate'; 318 319 span.setAttribute('translate', 'no'); 320 span.setAttribute('lang', 'en'); 321 322 span.textContent = name; 323 324 // copy other attributes (except class/style) 325 for (const attr of [...el.attributes]) { 326 if (attr.name !== 'class' && attr.name !== 'style') { 327 span.setAttribute(attr.name, attr.value); 328 } 329 } 330 331 if (el.style.cssText) span.style.cssText = el.style.cssText; 332 333 el.replaceWith(span); 334 }); 335 }); 336 337if (window.revizeCalendar) { 338 339 window.revizeCalendar.editformPrefix = "/revize/plugins/zbuilder/v1/website/editforms/revize_calendar"; 340 341 window.revizeCalendar_proto.fns.resetModal = function(eInfo) { 342 $('#event-modal #ics-export').remove(); 343 $('#event-modal #modal-event-duration').remove(); 344 $('#event-modal .modal-action-buttons').prepend(`<button id="ics-export" onclick="javascript:icalExport.download('${eInfo.title}');return false;" class="btn btn-light flex-fill border-0"><i class="bi bi-calendar2-plus me-1"></i> Add to Calendar</button>`); 345 $('#event-modal .event-modal-header').css('background','none'); 346 $('#event-modal .modal-event-date-wrap').css('background-color', window.revizeCalendar.fns.getEventC
346olor(eInfo)); 347 $('#event-edit, #event-delete').remove(); 348 $('#event-modal .modal-footer .btn').css('background-color', window.revizeCalendar.fns.getEventColor(eInfo)); 349 $('#modal-event-location').html(''); 350 } 351 352 window.revizeCalendar.fns.createModalHeader = function(eInfo) { 353 354 var defaultImage = window.revizeCalendarDefaultImage ?? "./_assets_/plugins/revizeCalendar/images/default-event.jpg"; 355 356 if (eInfo.image !== '') { 357 var $img = $(decodeURIComponent(eInfo.image)); 358 var src = $img.attr('src'); 359 if (!src || src.indexOf("noimage.gif") >= 0 || src.indexOf("/revize/plugins/") >= 0) { 360 eInfo.image = defaultImage; 361 } else { 362 eInfo.image = src; 363 } 364 } 365 if (eInfo.image === '') { 366 eInfo.image = defaultImage; 367 } 368 369 $('#event-modal .event-modal-header').css('background', "url('" + eInfo.image + "') center center / cover no-repeat"); 370 } 371 372 window.revizeCalendar.fns.createModalBody = function(eInfo) { 373 374 375 $('#event-modal .event-border-color').css('background-color', window.revizeCalendar.fns.getEventC
375olor(eInfo)); 376 $('#modal-event-title').text(eInfo.title); 377 $('#modal-event-description').html(eInfo.desc.replace(/href=\"\.\.\//g,"href=\"")); 378 379 $("#event-modal-dialog").each(function(i, el) { 380 var eventDate = eInfo.start.format('dddd, MMMM D'); 381 el.querySelector(".modal-event-date").innerHTML = `<i class="bi bi-calendar4-event me-2"></i>${eventDate}`; 382 383 var eventTimes = ""; 384 if (eInfo.allDay) { 385 if (eInfo.end.isAfter(eInfo.start, 'day')) { 386 eventTimes = '<span class="dur-start">' + eInfo.start.format('MMM Do ') + '</span> ' 387 eventTimes += '- <span class="dur-end">' + eInfo.end.format('MMM Do ') + '</span>'; 388 } else { 389 eventTimes = "All Day"; 390 } 391 } else { 392 eventTimes = '<span class="dur-start">' + eInfo.start.format('hh:mm a') + '</span> '; 393 if (typeof eInfo.options === "string" && eInfo.options.indexOf("\"hide_end_date\":true") === -1) { 394 var endString = eInfo.end.format('hh:mm a'); 395 if (eInfo.end.isAfter(eInfo.start, 'year')) { 396 endString = eInfo.end.format(', YYYY ') + endString; 397 } 398 if (eInfo.end.isAfter(eInfo.start, 'day')) { 399 endString = eInfo.end.format('MMM Do ') + endString; 400 } 401 eventTimes += '- <span class="dur-end">' + endString + '</span>'; 402 } 403 } 404 405 el.querySelector(".modal-event-time").innerHTML = `<i class="bi bi-clock me-2"></i>${eventTimes}`; 406 407 if (eInfo.location !== '' && eInfo.location !== 'none') { 408 el.querySelector(".modal-location").innerHTML = `<i class="bi bi-geo-alt me-2"></i>${eInfo.location}`; 409 } 410 411 }); 412 413 if (eInfo.location !== '' && eInfo.location !== 'none') {
414 $('#modal-event-location').html('<iframe class="rz-business-inner-map" height="175" frameborder="0" style="width:100%;margin:10px 0;border:1px solid '+window.revizeCalendar.fns.getEventColor(eInfo)+'" src=https://www.google.com/maps/embed/v1/place?key=AIzaSyC5cMe92Yw3JgHrkvgIyHgdS2lTsH4C95k&q="'+encodeURIComponent(eInfo.location)+'" allowfullscreen="true"></iframe>') 415 } 416 } 417 418 window.revizeCalendar.fns.createModalFooter = function(eInfo) { 419 if(eInfo.url.length){ 420 $('#modal-event-readmore').attr('href', eInfo.url).show(); 421 } else { 422 $('#modal-event-readmore').hide(); 423 } 424 425 if(typeof RZ !== 'undefined' && RZ.login && eInfo.edit && typeof eInfo.rid === "string" && eInfo.rid.indexOf("RevizeCalendar") < 0) { 426 427 if (window.revizeCalendar.fns.editPermitted(eInfo)) { 428 $("#modal-event-body").addClass("has-edit-buttons yes-has-edit-buttons"); 429 $("#modal-event-body .rz-btns-container").html(` 430 <a id="event-edit" href="${eInfo.edit}" class="rzBtn rzBtn-sm">Edit event</a> 431 <a id="event-delete" href="javascript:RZdelete( \'${eInfo.rid}\', \'rzevents\' )" class="rzBtn rzBtn-sm bg-danger" style="border-color: rgb(180, 7, 7);background:rgb(223, 7, 7)!important;color:#fff!important">Delete event</a>`); 432 } 433 } 434 } 435} 436 437document.querySelectorAll("[data-masonry]").forEach(function(el) { 438 $(el).one('layoutComplete', function(ev, items, c) { 439 if (!items.length) { return; } 440 441 const layout = items[0].layout; 442 ev.target.querySelectorAll("img").forEach(function(img) { 443 img.addEventListener('load', function() { layout.$element.masonry('layout'); }); 444 }); 445 }); 446}); 447 448// Immediately Invoked Function Expression (IIFE) to scope jQuery and avoid global conflicts 449(function ($) { 450// Enable strict mode for better error checking and cleaner JavaScript 451 "use strict"; 452 453// jQuery references to commonly used DOM elements for performance and clarity 454 var $window = $(window), 455 $body = $("body"), 456 $html = $("html"), 457 $rzBarWrap = $("#rz-bar-wrap"), 458 $rzBtnsContainer = $(".rz-btns-container"), 459 $siteHeader = $('.site-header'), 460 $freeformSlider = $('.rz-freeform-slider'), 461 $heroSlider = $('.hero'), 462 $contentWrap = $('#rz-content-wrap'); 463 464/* ===================== 465 RZ Class + remove revize bar 466===================== */ 467// Check if the Revize (RZ) object exists to adjust body class based on login state 468 if (typeof RZ !== "undefined") { 469 if (RZ.login) { 470 $body.addClass("user-logged-in"); 471 } else { 472 $body.addClass("user-not-logged-in"); 473 } 474 } 475 476 /* ===================== 477 UserWay 478 ===================== */ 479 (function(d) { 480 const s = d.createElement("script"); 481 const cssColor = getComputedStyle(document.documentElement) 482 .getPropertyValue('--userway-color') 483 .trim(); 484 485 s.setAttribute("data-position", 5); 486 s.setAttribute("data-color", cssColor); 487 s.setAttribute("data-account", "sudb1U5E4V"); 488 s.setAttribute("src", "https://cdn.userway.org/widget.js"); 489 (d.body || d.head).appendChild(s); 490 })(document); 491 492 document.addEventListener("DOMContentLoaded", () => { 493 const adaBtns = document.querySelectorAll(".rz-ada-btn-element"); 494 495 adaBtns.forEach(btn => { 496 let adaText = btn.querySelector("span"); 497 const ignoreText = adaText && adaText.classList.contains("visually-hidden"); 498 const hasText = adaText && !ignoreText; 499 const originalText = hasText ? adaText.innerText : null; 500 501 let spinner; 502 if (!hasText) { 503 spinner = document.createElement("div"); 504 spinner.className = "spinner-border text-dark position-absolute w-100 h-100 start-0 top-0 d-none"; 505 spinner.role = "status"; 506 spinner.setAttribute("aria-hidden", "true"); 507 const button = btn.querySelector("button"); 508 if (button) { 509 button.classList.add("position-relative"); 510 button.appendChild(spinner); 511 } 512 } 513 514 btn.addEventListener("click", () => { 515 if (hasText) { 516 adaText.innerText = "Loading tools..."; 517 } 518 519 if (!hasText && spinner) { 520 spinner.classList.remove("d-none"); 521 } 522 523 if (typeof UserWay !== "undefined" && typeof UserWay.widgetOpen === "function") { 524 UserWay.widgetOpen(); 525 } 526 527 function observeUwy() { 528 const target = document.querySelector(".uwy"); 529 if (!target) { 530 setTimeout(observeUwy, 100); 531 return; 532 } 533 534 const observer = new MutationObserver(() => { 535 if (target.classList.contains("uon")) { 536 if (hasText) { 537 adaText.innerText = originalText; 538 } 539 if (!hasText && spinner) { 540 spinner.classList.add("d-none"); 541 } 542 observer.disconnect(); 543 } 544 }); 545 546 observer.observe(target, { attributes: true });
547 548 if (target.classList.contains("uon")) { 549 if (hasText) adaText.innerText = originalText; 550 if (!hasText && spinner) spinner.classList.add("d-none"); 551 observer.disconnect(); 552 } 553 } 554 555 observeUwy(); 556 }); 557 }); 558 }); 559 560// Initialize Bootstrap tooltips for elements using [data-bs-toggle='tooltip'] 561 const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); 562// Initialize Bootstrap tooltips for elements using [data-bs-toggle='tooltip'] 563 const tooltipList = [...tooltipTriggerList].map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl)); 564 565// Photo Gallery Initialization (may include lightbox or slider) 566/* ===================== 567 Photo Gallery from https://www.lightgalleryjs.com/demos/thumbnails/ 568===================== */ 569document.addEventListener('DOMContentLoaded', function() { 570 document.querySelectorAll('.lightgallery').forEach(function(galleryEl) { 571 lightGallery(galleryEl, { 572 selector: '.rz-gallery-image', 573 plugins: [lgThumbnail, lgVideo], 574 licenseKey: "E85B5C22-0F6A-4E84-8A38-013E4E9E31E2", 575 speed: 500, 576 zoomFromOrigin: false, 577 loop: true, // <-- Infinite scrolling! 578 slideEndAnimation: true // <-- Optional for smooth looping 579 }); 580 }); 581}); 582 583 584 /* ===================== 585 Mega font 586 ===================== */ 587 fitty(".mega-font", { 588 minSize: 32, 589 maxSize: 800, 590 }); 591 592/* =====================
593 YouTube Background initializer https://github.com/stamat/youtube-background 594 ===================== */ 595 function initYouTubeBackgrounds() { 596 if (window.VideoBackgrounds) { 597 new VideoBackgrounds('[data-vbg]'); 598 } 599 } 600 601 initYouTubeBackgrounds(); 602 603 $(document).on("change", ".selectall", function () { 604 var $dropdown = $(this).closest(".dropdown"); 605 var checked = $(this).is(":checked"); 606 607 $dropdown.find(".option").prop("checked", checked); 608 var total = $dropdown.find('input[name="options[]"]:checked').length; 609 610 $dropdown.find(".dropdown-text").html("(" + total + ") Selected"); 611 $dropdown.find(".select-text").html(checked ? " Deselect" : " Select"); 612 }); 613 614 $(document).on("change", "input[type='checkbox'].justone", function () { 615 var $dropdown = $(this).closest(".dropdown"); 616 var $allOptions = $dropdown.find("input[type='checkbox'].justone"); 617 var $selectAll = $dropdown.find(".selectall"); 618 619 var allChecked = $allOptions.length === $allOptions.filter(":checked").length; 620 621 $selectAll.prop("checked", allChecked); 622 $dropdown.find(".select-text").html(allChecked ? " Deselect" : " Select"); 623 624 var total = $dropdown.find('input[name="options[]"]:checked').length; 625 $dropdown.find(".dropdown-text").html("(" + total + ") Selected"); 626 }); 627 628/* ===================== 629 sticky header 630 ===================== */ 631 $(function() { 632 var stickyHeader = $('[data-sticky="true"]'); 633 if (!stickyHeader.length) return; 634 635 var logoNoScroll = $(".logo-no-scroll"); 636 var logoOnScroll = $(".logo-on-scroll"); 637 638 // store the original offset on load 639 var elementOffset = stickyHeader.offset() ? stickyHeader.offset().top : 0; 640 641 $(window).on("scroll", function () { 642 var scrollTop = $(this).scrollTop(); 643 644 // Always remove sticky when scrolling back above offset 645 if (scrollTop > elementOffset) { 646 stickyHeader.addClass("sticky-top"); 647 logoOnScroll.removeClass("d-none"); 648 logoNoScroll.addClass("d-none"); 649 } else { 650 stickyHeader.removeClass("sticky-top"); 651 logoOnScroll.addClass("d-none"); 652 logoNoScroll.removeClass("d-none"); 653 } 654 }); 655 }); 656 657 // $(window).on("resize", function() { 658 // elementOffset = stickyHeader.offset() ? stickyHeader.offset().top : 0; 659 // }); 660 661 662/* ===================== This is for 663 the case of not having a hero slider on the freeform page and the navigation being set to absolute which causes for 664overlapping ===================== */ 665 // var siteHeader = $('.site-header'), 666 // siteHeaderHeight = siteHeader.height(), 667 // rzFreeformSlider = $('.rz-freeform-slider'); 668 // 669 // if ((siteHeader.css('position') === 'absolute') & (rzFreeformSlider.length === 0)){ 670 671/* ===================== 672 Header is positioned absolute used for hooking 673===================== */ 674 // $body.addClass("header-is-absolute no-hero-slider"); 675 // $('#rz-content-wrap').css('padding-top', siteHeaderHeight + 'px'); 676 // } 677 678/* ===================== This is for 679 the case of not having a hero slider on the freeform page and the navigation being set to absolute which causes for 680 overlapping ===================== */ 681 function adjustHeaderPadding() { 682 // Only run above 996px 683 if (window.innerWidth <= 996) { 684 $body.removeClass('header-is-absolute no-hero-slider'); 685 $contentWrap.css('padding-top', ''); 686 return; 687 } 688 689 var hdrPos = $siteHeader.css('position'), 690 hdrH = $siteHeader.outerHeight() || 0, 691 noSlider = $freeformSlider.length === 0, 692 noHeroSlider = $heroSlider.length === 0; 693 694 // Header is positioned absolute used for hooking 695 if (hdrPos === 'absolute' && noSlider && noHeroSlider) { 696 $body.addClass('header-is-absolute no-hero-slider'); 697 $contentWrap.css('padding-top', hdrH + 'px'); 698 } else { 699 $body.removeClass('header-is-absolute no-hero-slider'); 700 $contentWrap.css('padding-top', ''); 701 } 702 } 703 704 // run on load and on resize 705 $window.on('load resize', adjustHeaderPadding); 706 707 /* ===================== 708 Fade out element 709 ===================== */ 710 document.addEventListener('DOMContentLoaded', function () { 711 const els = Array.from(document.querySelectorAll('.fade-out-element')); 712 if (!els.length) return; 713 714 const DEFAULT_START = 100; 715 const DEFAULT_END = 300; 716 717 let ticking = false; 718
719 function update() { 720 const pageY = window.scrollY; 721 722 els.forEach(el => { 723 const start = Number(el.dataset.fadeStart ?? DEFAULT_START); 724 const end = Number(el.dataset.fadeEnd ?? DEFAULT_END); 725 726 const elTopPageY = pageY + el.getBoundingClientRect().top; 727 const fadeStartY = elTopPageY - start; 728 const fadeEndY = elTopPageY + end; 729 730 let opacity; 731 if (pageY <= fadeStartY) { 732 opacity = 1; 733 } else if (pageY >= fadeEndY) { 734 opacity = 0; 735 } else { 736 opacity = 1 - ((pageY - fadeStartY) / (fadeEndY - fadeStartY)); 737 } 738 739 opacity = Math.max(0, Math.min(1, opacity)); 740 el.style.opacity = String(opacity); 741 el.style.pointerEvents = opacity === 0 ? 'none' : ''; 742 }); 743 744 ticking = false; 745 } 746 747 function onScroll() { 748 if (!ticking) { 749 ticking = true; 750 requestAnimationFrame(update); 751 } 752 } 753 754 window.addEventListener('scroll', onScroll, { passive: true }); 755 window.addEventListener('resize', onScroll, { passive: true }); 756 update(); 757 }); 758/* ===================== 759 Font resizer 760===================== */ 761// Function definition: textResizer 762 function textResizer() { 763 var $dec, $inc; 764 $inc = $("#increase-font"); 765 $dec = $("#decrease-font"); 766 $inc.on("click", function (e) { 767 return $("body").css({ 768 "font-size": "+=1.5", 769 }); 770 }); 771 $dec.on("click", function (e) { 772 return $("body").css({ 773 "font-size": "-=1.5", 774 }); 775 }); 776 return $("#font-resizer a").on("click", function (e) { 777 return e.preventDefault(); 778 }); 779 } 780 textResizer(); 781 782/* ===================== 783 Clone icons to each navigation link/span 784===================== */ 785 $('[data-nav-icons="true"] #navicons li').each(function () { 786 var navIconIndex = $(this).index(); 787 $(this).find(".navicon").prependTo($("#nav>li").eq(navIconIndex).children("a,span")); 788 }); 789 790/* ===================== 791 Add class to active nav item 792 ===================== */ 793// selected page link 794 if ($body.attr('id') === 'homepage') { 795 $('#nav > li:first-child > a').addClass('active-page'); 796 } 797 798 var cleanUrl = window.location.href 799 .split('/') 800 .map(function(part) { 801 return part.replace(/_/g, ''); 802 }) 803 .join('/'); 804 805 $('#nav > li').each(function() { 806 var link = $(this).find('> a'); 807 var linkText = link.text().toLowerCase().replace(/[^a-z]/g, ''); 808 809 if (cleanUrl.includes(linkText)) { 810 link.addClass('active-page'); 811 } 812 }); 813 814/* ===================== 815 If no submenu items add placeholder 816 ===================== */ 817 document.addEventListener('DOMContentLoaded', function () { 818 const submenu = document.querySelector('#submenu'); 819 if (!submenu) return; 820 821 const hasLinks = submenu.querySelectorAll('a[href]').length > 0; 822 823 if (!hasLinks) { 824 submenu.outerHTML = ` 825 <ul id="submenu"> 826 <li class="menuLI level0 menuSelectedChild nav-item position-relative" style="display: flex; align-items: center;"><a class="menuA level0 menuSelectedChild nav-link" href="#" style="flex: 1 1 auto;">These links are just placeholders</a></li> 827 <li class="menuLI level0 menuSelectedChild nav-item position-relative" style="display: flex; align-items: center;"><a class="menuA level0 menuSelectedChild nav-link" href="#" style="flex: 1 1 auto;">Add Links or pages here</a></li> 828 <li class="menuLI level0 menuSelectedChild nav-item position-relative" style="display: flex; align-items: center;"><a class="menuA level0 menuSelectedChild nav-link" href="#" style="flex: 1 1 auto;">Click "Edit Submenu"</a></li> 829 </ul> 830 `; 831 } 832 }); 833/* ===================== 834 Keyboard Navigation: Nav, flyout 835===================== */ 836 var isClick = false; 837 $("#nav li a, #submenu li a, a, button, .toggle, .toggle2").on("focusin", function (e) { 838 if (isClick === false) { 839 $(".focused").removeClass("focused"); 840 $(e.currentTarget).parents("#nav li, #submenu li").addClass("focused"); 841 $(".opened:not(.focused) ul:visible,.opened2:not(.focused) ul:visible").slideUp().parent().removeClass("opened opened2"); 842 } else { 843 $(".focused").removeClass("focused"); 844 isClick = false; 845 } 846 }); 847 848/* ===================== 849 for counters from https://codepen.io/fflorim/pen/eYYdRPB 850 ===================== */ 851 (function () { 852 function easeOutQuad(t){ return t*(2-t); } 853 function formatNumber(val, decimals, sep){ 854 var n = Number(val); if (!isFinite(n)) n = 0; 855 var s = (decimals > 0) ? n.toFixed(decimals) : Math.round(n).toString(); 856 if (sep){ 857 var parts = s.split('.'); 858 parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); 859 s = parts.join('.'); 860 } 861 return s; 862 } 863 function animate(el){ 864 var start = parseFloat(el.getAttribute('data-counter-start')) || 0; 865 var end = parseFloat(el.getAttribute('data-counter-end')) || 0; 866 var dur = parseInt(el.getAttribute('data-counter-duration'),10) || 2000; 867 868 var decAttr = (el.getAttribute('data-counter-decimals') || '').trim(); 869 var dec = decAttr === '' ? 0 : parseInt(decAttr, 10); 870 if (isNaN(dec)) dec = 0; 871 if (dec < 0) dec = 0; 872 if (dec > 2) dec = 2; 873 874 var sep = el.getAttribute('data-counter-sep') === '1'; 875 876 // Write once up front, then animate. 877 el.textContent = formatNumber(start, dec, sep); 878 879 var t0; 880 function step(ts){ 881 if(!t0) t0 = ts; 882 var p = Math.min(1, (ts - t0)/dur); 883 var eased = easeOutQuad(p); 884 var val = start + (end - start) * eased; 885 el.textContent = formatNumber(val, dec, sep); 886 if (p < 1) requestAnimationFrame(step); 887 else el.textContent = formatNumber(end, dec, sep); 888 } 889 requestAnimationFrame(step); 890 } 891 892 var counters = document.querySelectorAll('.rz-element-wrapper .rz-counter'); 893 if (!counters.length) return; 894 895 if ('IntersectionObserver' in window) { 896 var io = new IntersectionObserver(function(entries, obs){
897 entries.forEach(function(e){ 898 if (e.isIntersecting) { 899 animate(e.target); 900 obs.unobserve(e.target); 901 } 902 }); 903 }, { threshold: 0.35 }); 904 counters.forEach(function(el){ io.observe(el); }); 905 } else { 906 counters.forEach(animate); 907 } 908 })(); 909 910/* ===================== 911 for revize buttons container 912===================== */ 913 $rzBtnsContainer.each(function () { 914 var $container = $(this); 915 if ($container.html().trim() != "") { 916 $container.closest(".has-edit-buttons").addClass("yes-has-edit-buttons"); 917 } 918 }); 919 $(".has-edit-buttons").not(".yes-has-edit-buttons").removeClass("has-edit-buttons"); 920 921/* ===================== 922 responsive views from revize bar 923===================== */ 924 var urlParams = new URLSearchParams(window.location.search); //get all parameters 925 var hidebar = urlParams.get("hidebar"); //extract the hidebar parameter - this will return NULL if hidebar isn't a parameter 926 927 if (hidebar) { 928 //check if hidebar parameter is set to anything 929 $body.removeClass("user-logged-in").addClass("hide-edit-btns"); 930 } 931 932/* ===================== 933 prevent focused class changes on click - This way arrows wont pop when clicking nav links 934===================== */ 935 $("#nav a,#submenu a").on("mousedown", function () { 936 isClick = true; 937 }); 938 939/* ===================== 940 divider's 941 ===================== */ 942document.addEventListener('DOMContentLoaded', function() { 943 var animatedDividers = document.querySelectorAll('.rz-animate-on-visible'); 944 animatedDividers.forEach(function(divider) { 945 divider.classList.remove( 946 'divider-anim-grow-center', 'divider-anim-grow-left', 'divider-anim-grow-right', 947 'divider-anim-grow-center-vertical', 'divider-anim-grow-top', 'divider-anim-grow-bottom' 948 ); 949 }); 950 if ('IntersectionObserver' in window) { 951 var observer = new IntersectionObserver(function(entries, obs) { 952 entries.forEach(function(entry) { 953 var el = entry.target; 954 var anim = el.getAttribute('data-anim-class'); 955 if (!anim) return; 956 if (entry.isIntersecting) { 957 el.classList.add(anim); 958 // Stop observing after first animation 959 obs.unobserve(el); 960 } 961 }); 962 }, { threshold: 0.25 }); 963 animatedDividers.forEach(function(divider) { 964 observer.observe(divider); 965 }); 966 } else { 967 // Fallback: Add the anim right away 968 animatedDividers.forEach(function(divider) { 969 var anim = divider.getAttribute('data-anim-class'); 970 if (anim) divider.classList.add(anim); 971 }); 972 } 973}); 974 975 /* ===================== 976 Create links for accordion's 977 ===================== */ 978document.addEventListener('DOMContentLoaded', function() { 979 // Assign friendly sequential IDs to all accordions on the page (acc_0, acc_1, etc) 980 let accordions = document.querySelectorAll('.accordion'); 981 accordions.forEach(function(accordion, accIdx) { 982 // Use existing doc_center_ prefix if it's a doc center 983 let isDocCenter = accordion.closest('.doc-center-wrap') ? true : false; 984 let baseId = isDocCenter ? 'doc_center_' + accIdx : 'acc_' + accIdx; 985 accordion.id = baseId; 986 987 // Assign friendly, sequential IDs to all items, headings, panels 988 accordion.querySelectorAll('.accordion-item').forEach(function(item, i) { 989 let headingId = baseId + '_heading_' + i; 990 let panelId = baseId + '_panel_' + i; 991 992 // Button/heading 993 let button = item.querySelector('.accordion-button'); 994 if (button) { 995 button.setAttribute('data-bs-toggle', 'collapse'); 996 button.setAttribute('data-bs-target', '#' + panelId); 997 button.setAttribute('aria-controls', panelId); 998 button.setAttribute('aria-expanded', 'false'); 999 button.id = headingId; 1000 } 1001 1002 // Collapse panel 1003 let collapse = item.querySelector('.accordion-collapse'); 1004 if (collapse) { 1005 collapse.id = panelId; 1006 collapse.setAttribute('aria-labelledby', headingId); 1007 collapse.setAttribute('data-bs-parent', '#' + baseId); 1008 } 1009 }); 1010 });
1011 1012 // lightGallery: Enable ONLY for doc centers and rz-agendalist-card-wrapper 1013 document.querySelectorAll('.doc-center-wrap,.rz-agendalist-card-wrapper').forEach(function(wrap) { 1014 lightGallery(wrap, { selector: '.open-pdf' }); 1015 }); 1016 1017 // File type icons: Only for document centers 1018 var docTypes = { 1019 binary: ["bin"], 1020 code: ["css", "js"], 1021 excel: ["xlsx", "xls"], 1022 image: ["gif", "jpg", "jpeg", "png", "bmp"], 1023 pdf: ["pdf"], 1024 play: ["mp4", "mp3"], 1025 ppt: ["ppt", "pptx"], 1026 richtext: ["rtf"], 1027 text: ["txt"], 1028 word: ["doc", "docx"], 1029 zip: ["zip"] 1030 }; 1031 1032 document.querySelectorAll( 1033 '.doc-center-wrap .list-group-item > [data-src]' 1034 ).forEach(function(el) { 1035 var src = el.dataset.src || ''; 1036 1037 // Remove query string and hash before checking extension 1038 var cleanSrc = src.split('?')[0].split('#')[0]; 1039 var ext = cleanSrc.split('.').pop().toLowerCase(); 1040 1041 var className = "file"; 1042 1043 Object.entries(docTypes).forEach(function([type, arr]) { 1044 if (arr.includes(ext)) { 1045 className = type; 1046 } 1047 }); 1048 1049 var icon = el.querySelector("i[class*='bi-file']"); 1050 if (!icon) return; 1051 1052 icon.className = ""; 1053 1054 // Restore the required Bootstrap Icons base class 1055 icon.classList.add("bi", "fs-5", "lh-1", "py-2"); 1056 1057 if (className !== "file") { 1058 icon.classList.add( 1059 "text-" + className, 1060 "bi-file-earmark-" + className 1061 ); 1062 } else { 1063 icon.classList.add("bi-file-earmark"); 1064 } 1065 }); 1066 1067 // Update hash when an accordion opens 1068 document.querySelectorAll('.accordion').forEach(function(acc) { 1069 acc.querySelectorAll('.accordion-collapse').forEach(function(collapse) { 1070 collapse.addEventListener('show.bs.collapse', function() { 1071 if (history.replaceState) { 1072 history.replaceState( 1073 null, 1074 '', 1075 window.location.pathname + window.location.search + '#' + collapse.id 1076 ); 1077 } else { 1078 location.hash = '#' + collapse.id; 1079 } 1080 }); 1081 }); 1082 }); 1083 1084 // On load: open panel from hash, if found 1085 var hash = window.location.hash.replace('#', ''); 1086 if (hash) { 1087 var el = document.getElementById(hash); 1088 if (el && el.classList.contains('accordion-collapse') && el.closest('.accordion')) { 1089 var bsCollapse = bootstrap.Collapse.getOrCreateInstance(el, { toggle: false }); 1090 bsCollapse.show(); 1091 } 1092 } 1093 }); 1094 1095 /* ===================== 1096 custom-cursor for gallery 1097 ===================== */ 1098 function setupCustomCursorForGallery() { 1099 document.querySelectorAll('.rz-gallery-image').forEach(area => { 1100 // Prevent double init 1101 if (area.__customCursorAttached) return; 1102 area.__customCursorAttached = true; 1103 1104 const cursor = area.querySelector('.custom-cursor.follow-cursor'); 1105 if (!cursor) { return; } 1106 let visible = false; 1107 let target = { x: 0, y: 0 }; 1108 let pos = { x: 0, y: 0 }; 1109 let initialized = false; 1110 1111 function animateCursor() { 1112 pos.x += (target.x - pos.x) * 0.8; 1113 pos.y += (target.y - pos.y) * 0.8; 1114 let scale = visible ? 1 : 0; 1115 cursor.style.transform = `translate(-50%, -50%) translate(${pos.x}px, ${pos.y}px) scale(${scale})`; 1116 cursor.style.opacity = visible ? 1 : 0; 1117 requestAnimationFrame(animateCursor); 1118 } 1119 animateCursor(); 1120 1121 area.addEventListener('mouseenter', (e) => { 1122 const rect = area.getBoundingClientRect(); 1123 let x = (e && e.clientX) ? (e.clientX - rect.left) : rect.width / 2; 1124 let y = (e && e.clientY) ? (e.clientY - rect.top) : rect.height / 2; 1125 target.x = pos.x = x; 1126 target.y = pos.y = y; 1127 visible = true; 1128 initialized = true; 1129 cursor.classList.add('active'); 1130 }); 1131 1132 area.addEventListener('mouseleave', () => { 1133 visible = false; 1134 cursor.classList.remove('active'); 1135 }); 1136 1137 area.addEventListener('mousemove', (e) => { 1138 const rect = area.getBoundingClientRect(); 1139 target.x = e.clientX - rect.left; 1140 target.y = e.clientY - rect.top; 1141 if (!initialized) { 1142 pos.x = target.x; 1143 pos.y = target.y; 1144 initialized = true; 1145 cursor.classList.add('active'); 1146 } 1147 }); 1148 1149 cursor.style.opacity = 0; 1150 cursor.style.transform = `translate(-50%, -50%) scale(0)`; 1151 }); 1152 } 1153
1154 // Initial setup 1155 setupCustomCursorForGallery(); 1156 1157 // Re-apply every time the slider changes slides (Tiny Slider triggers events) 1158 document.querySelectorAll('.tiny-carousel').forEach(carousel => { 1159 carousel.addEventListener('transitionend', setupCustomCursorForGallery); 1160 }); 1161 1162 // OR: Use MutationObserver for *any* DOM change in the gallery 1163 const galleryWrapper = document.querySelector('.lightgallery') || document.querySelector('.tiny-carousel'); 1164 if (galleryWrapper) { 1165 const observer = new MutationObserver(() => { 1166 setupCustomCursorForGallery(); 1167 }); 1168 observer.observe(galleryWrapper, { childList: true, subtree: true }); 1169 } 1170 1171 /*! 1172 Name: Estimated Reading Time 1173 Dependencies: jQuery 1174 Author: Michael Lynch 1175 Author URL: http://michaelynch.com 1176 Date Created: August 14, 2013 1177 Licensed under the MIT license 1178 */ 1179 1180 $.fn.estimatedReadingTime = function (options) { 1181 //return if no element was bound 1182 //so chained events can continue 1183 if (!this.length) { 1184 return this; 1185 } 1186 //define default parameters 1187 var defaults = { 1188 readingTimeTarget: "eta", 1189 wordCountTarget: "words", 1190 readingSpeed: 200, 1191 round: true, 1192 }; 1193 //define plugin 1194 var plugin = this; 1195 //define element 1196 var el = $(this); 1197 //merge defaults and options 1198 plugin.settings = $.extend({}, defaults, options); 1199 //define vars 1200 var readingTimeTarget = plugin.settings.readingTimeTarget; 1201 var wordCountTarget = plugin.settings.wordCountTarget; 1202 var readingSpeed = plugin.settings.readingSpeed; 1203 var round = plugin.settings.round; 1204 //define text 1205 var text = el.text(); 1206 //split text by spaces to define total words 1207 var totalWords = text.split(" ").length; 1208 //define words per second based on words per minute (readingSpeed) 1209 var wordsPerSecond = readingSpeed / 60; 1210 //define total reading time in seconds 1211 var totalReadingTimeSeconds = totalWords / wordsPerSecond; 1212 //define reading time in minutes 1213 var readingTimeMinutes = Math.round(totalReadingTimeSeconds / 60); 1214 //define remaining reading time seconds 1215 var readingTimeSeconds = Math.round(totalReadingTimeSeconds - readingTimeMinutes * 60); 1216 //if round is set to true 1217 if (round === true) { 1218 //if minutes are greater than 0 1219 if (readingTimeMinutes > 0) { 1220 //set reading time by the minute 1221 $("#" + readingTimeTarget).text(readingTimeMinutes + " Min read"); 1222 } else { 1223 //set reading time as less than a minute 1224 $("#" + readingTimeTarget).text("Less than a minute read"); 1225 } 1226 //if round is set to false 1227 } else { 1228 //format reading time 1229 var readingTime = readingTimeMinutes + ":" + readingTimeSeconds; 1230 //set reading time in minutes and seconds 1231 $("#" + readingTimeTarget).text(readingTime + " Min"); 1232 } 1233 1234 //if word count container isn't blank or undefined 1235 if (wordCountTarget !== "" && wordCountTarget !== undefined) { 1236 //set word count 1237 $("#" + wordCountTarget).text(totalWords); 1238 } 1239 }; 1240 1241 (function ($) { 1242 var ertTimer = null; 1243 1244 function runERT() { 1245 if (!$.fn.estimatedReadingTime) return; 1246 var $target = $("#news-full-details"); 1247 if (!$target.length) return; 1248 $target.estimatedReadingTime(); 1249 } 1250 1251 function scheduleERT(delay) { 1252 clearTimeout(ertTimer); 1253 ertTimer = setTimeout(runERT, delay || 0); 1254 } 1255 1256 $(function () { 1257 scheduleERT(3000); 1258 1259 $(document).on("click", ".rz-news-card .rz-news-button", function () { 1260 scheduleERT(300); 1261 }); 1262 }); 1263 })(jQuery); 1264 1265/* ===================== 1266 back to top 1267===================== */ 1268 $(window).scroll(function () { 1269 if ($(this).scrollTop() > 500) { 1270 $("#back-to-top").css({ 1271 opacity: 1, 1272 transform: "translateX(-1rem)", 1273 transition: "all .5s", 1274 }); 1275 } else { 1276 $("#back-to-top").css({ 1277 opacity: 0, 1278 transform: "translateX(100px)", 1279 transition: "all .5s", 1280 }); 1281 } 1282 }); 1283 1284 $($("a[href='#top']")).click(function () { 1285 $("body,html").animate( 1286 { 1287 scrollTop: 0, 1288 }, 1289 800 1290 ); 1291 return false; 1292 }); 1293 1294/* ===================== 1295 clone elements in header to offcanvas for mobile 1296 ===================== */ 1297// old 1298 // var isClonedHeaderElement = false; 1299 // $(window).on("load resize", function() { 1300 // if($window.width() < 992) { 1301 // if(!isClonedHeaderElement) { 1302 // $('.site-header-wrap .move-to-offcanvas').clone().addClass('d-flex').appendTo('.offcanvasHeaderItems-offcanvas-body'); 1303 // isClonedHeaderElement = true; 1304 // } 1305 // } 1306 // else { 1307 // if(isClonedHeaderElement) { 1308 // $('.offcanvasHeaderItems-offcanvas-body .move-to-offcanvas').remove(); 1309 // isClonedHeaderElement = false; 1310 // } 1311 // } 1312 // }); 1313var isClonedHeaderElement = false; 1314 var savedInlineStyles = new Map(); 1315 1316 $(window).on("load resize", function () { 1317 var isMobile = window.matchMedia("(max-width: 991.98px)").matches; 1318 1319 if (isMobile) { 1320 if (!isClonedHeaderElement) { 1321 var $toClone = $('.site-header-wrap .move-to-offcanvas'); 1322 1323 $toClone.add($toClone.find('*')).each(function () { 1324 var $el = $(this); 1325 var style = $el.attr('style'); 1326 if (style) { 1327 savedInlineStyles.set(this, style); 1328 $el.removeAttr('style'); 1329 } 1330 }); 1331 1332 var $clone = $toClone.clone(true, true).addClass('d-flex'); 1333 $clone.appendTo('.offcanvasHeaderItems-offcanvas-body'); 1334 isClonedHeaderElement = true; 1335 } 1336 } else { 1337 if (isClonedHeaderElement) { 1338 $('.offcanvasHeaderItems-offcanvas-body .move-to-offcanvas').remove(); 1339 isClonedHeaderElement = false; 1340
1341 savedInlineStyles.forEach(function (style, el) { 1342 $(el).attr('style', style); 1343 }); 1344 savedInlineStyles.clear(); 1345 } 1346 } 1347 }); 1348 1349// submenu AKA flyout 1350$(function () { 1351 var isClonedSubnavElement = false; 1352 1353 function handleSubnav() { 1354 if (window.matchMedia('(max-width: 991.98px)').matches) { 1355 if (!isClonedSubnavElement) { 1356 $('#submenu').clone().appendTo('#offcanvasNav .offcanvas-body'); 1357 $('#offcanvasNav .offcanvas-body > #submenu') 1358 .prepend('<h5 class="px-3 py-4 mt-2 m-0 w-100">Sub Menu</h5>'); 1359 isClonedSubnavElement = true; 1360 } 1361 } else { 1362 if (isClonedSubnavElement) { 1363 $('#offcanvasNav .offcanvas-body > #submenu').remove(); 1364 isClonedSubnavElement = false; 1365 } 1366 } 1367 } 1368 1369 $(window).on('load resize', handleSubnav); 1370}); 1371 1372 1373(function () { 1374 var root = document.getElementById('submenu'); 1375 if (!root) return; 1376 if (!(window.bootstrap && bootstrap.Dropdown)) return; 1377 1378 root.classList.add('nav'); 1379 root.style.display = 'flex'; 1380 root.style.flexWrap = 'wrap'; 1381 1382 var dropdowns = new Map(); 1383 1384 Array.from(root.querySelectorAll(':scope > li')).forEach(function (li) { 1385 li.classList.add('nav-item', 'position-relative'); 1386 li.style.display = 'flex'; 1387 li.style.alignItems = 'center'; 1388 1389 var aMain = li.querySelector(':scope > a'); 1390 var menu = li.querySelector(':scope > ul'); 1391 if (!aMain) return; 1392 1393 aMain.classList.add('nav-link'); 1394 aMain.style.flex = '1 1 auto'; 1395 1396 if (!menu) return; 1397 1398 li.classList.add('dropdown' ,'dropend'); 1399 1400 var toggler = document.createElement('a'); 1401 toggler.href = '#'; 1402 toggler.className = 'nav-link dropdown-toggle dropdown-toggle-split ms-auto w-auto d-flex align-items-center justify-content-center px-lg-3'; 1403 toggler.setAttribute('role', 'button'); 1404 toggler.setAttribute('data-bs-toggle', 'dropdown'); 1405 toggler.setAttribute('aria-expanded', 'false'); 1406 toggler.setAttribute('aria-label', 'Toggle submenu'); 1407 toggler.style.flex = '0 0 auto'; 1408 aMain.insertAdjacentElement('afterend', toggler); 1409 1410 menu.classList.add('dropdown-menu'); 1411 menu.classList.remove('menuHidden'); 1412 1413 Array.from(menu.querySelectorAll(':scope > li')).forEach(function (subLi) { 1414 subLi.classList.remove('menuHidden'); 1415 var subA = subLi.querySelector(':scope > a'); 1416 if (subA) subA.classList.add('dropdown-item'); 1417 }); 1418 1419 var dd = new bootstrap.Dropdown(toggler, { autoClose: 'outside' }); 1420 dropdowns.set(toggler, dd); 1421 1422 toggler.addEventListener('click', function (e) { 1423 e.preventDefault(); 1424 dd.toggle(); 1425 }); 1426 1427 aMain.addEventListener('click', function (e) { 1428 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 1429 var href = aMain.getAttribute('href'); 1430 if (!href || href === '#' || href.toLowerCase().startsWith('javascript:')) return; 1431 e.stopPropagation(); 1432 window.location.href = href; 1433 }, true); 1434 1435 li.addEventListener('mouseenter', function () { 1436 dropdowns.forEach(function (d, other) { if (other !== toggler) d.hide(); }); 1437 dd.show(); 1438 }); 1439 li.addEventListener('mouseleave', function (ev) { 1440 if (!li.contains(ev.relatedTarget)) dd.hide(); 1441 }); 1442 }); 1443 1444 root.addEventListener('show.bs.dropdown', function (ev) { 1445 var tg = ev.target; 1446 dropdowns.forEach(function (dd, otherTg) { if (otherTg !== tg) dd.hide(); }); 1447 }); 1448})(); 1449 1450 1451 1452 1453/* ===================== 1454 E-Notify Auto submit 1455===================== */ 1456 $.urlParam = function (n) { 1457 var e = new RegExp("[?&]" + n + "=([^]*)").exec(window.location.href); 1458 return null == e ? null : e[1] || 0; 1459 }; 1460 var $enotify = $('iframe[src*="/revize/plugins/notify/notify.jsp"]'); 1461 if ($enotify.length > 0) { 1462 var emailStr = $.urlParam("email"); 1463 if (emailStr != null) { 1464 $enotify.attr("src", $enotify.attr("src") + "&email=" + emailStr); 1465 } 1466 } 1467 1468/* ===================== 1469 Make sure all calendars have unique ids 1470===================== */ 1471 $('iframe[name="calendar"]').each(function (index, calendar) { 1472 calendar.id = "calendar-" + index; 1473 }); 1474 1475/* ===================== 1476 Start Frame Resizer 1477===================== */ 1478// Function definition: resizeIframe 1479 function resizeIframe(height, frameElement) { 1480 if (frameElement) { 1481 frameElement.height = ""; 1482 frameElement.height = height; 1483 } 1484 } 1485 1486 var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; 1487 var eventHandler = window[eventMethod]; 1488 var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message"; 1489 eventHandler(messageEvent, function (e) { 1490 if (Array.isArray(e.data)) {
1491 if (e.data[0] === "setCalHeight" || e.data[0] === "setNotifyHeight") { 1492 var frames = document.querySelectorAll("iframe"); 1493 for (var i = 0; i < frames.length; i++) { 1494 if (frames[i].contentWindow === e.source) { 1495 resizeIframe(e.data[1], frames[i]); 1496 } 1497 } 1498 } 1499 } 1500 }); 1501 1502/* ===================== 1503 Alert Close Caching 1504===================== */ 1505 if ($("div.alert").length) { 1506 var hide = window.sessionStorage && parseInt(window.sessionStorage.getItem("alertClosed")) > 1; 1507 if (!hide || $(".user-logged-in").length != 0) { 1508 $("div.alert").addClass("show"); 1509 } 1510 $("div.alert .close-alert").on("click", function (e) { 1511 if (window.sessionStorage) { 1512 window.sessionStorage.setItem("alertClosed", parseInt(window.sessionStorage.getItem("alertClosed") || 0) + 1); 1513 } 1514 }); 1515 } 1516 1517/* ===================== 1518 revizeWeather #todo check on live server 1519===================== */ 1520 if (typeof $.fn.revizeWeather !== "undefined") { 1521 (function() { 1522 const zip = (document.body.getAttribute('data-zipcode') || '').trim(); 1523 if (!zip) { $('.header-weather-wrap').hide(); return; } 1524 1525 $.fn.revizeWeather({ 1526 zip: zip, 1527 city_name: '', 1528 unit: 'f', 1529 success: function(weather) { 1530 const d = new Date(); 1531 const parts = new Intl.DateTimeFormat('en-US', { month: 'long', day: 'numeric', year: 'numeric' }).formatToParts(d);
1532 const month = parts.find(p => p.type === 'month')?.value || ''; 1533 const day = parts.find(p => p.type === 'day')?.value || ''; 1534 const year = parts.find(p => p.type === 'year')?.value || ''; 1535 const iso = d.toISOString().split('T')[0]; 1536 1537 const unit = (weather.unit || 'F').toUpperCase() === 'C' ? 'C' : 'F'; 1538 const unitLong = unit === 'C' ? 'degrees Celsius' : 'degrees Fahrenheit'; 1539 const dateLabel = `${month} ${day}, ${year}`; 1540 1541 const html = 1542 '<time class="weather-date" datetime="'+ iso +'" aria-label="'+ dateLabel +'">' + 1543 '<span class="weather-month">' + month + '</span> ' + 1544 '<span class="weather-day">' + day + '</span>, ' + 1545 '<span class="weather-year">' + year + '</span>' + 1546 '</time>' + 1547 '<div class="weather-details d-flex align-items-center gap-2" role="group" aria-label="Current weather">' + 1548 '<span class="weather-temp" aria-label="Temperature ' + weather.temp + ' ' + unitLong + '">' + 1549 weather.temp + 1550 '<span aria-hidden="true">°</span>' + 1551 '<span class="sr-only"> ' + unitLong + '</span>' + 1552 '</span>' + 1553 '<span class="weather-forecast" aria-label="Conditions ' + weather.forecast + '"> ' + 1554 weather.forecast + 1555 '</span>' + 1556 '<i class="fst-normal weather-icon fs-1 ' + weather.icon + '" aria-hidden="true"></i>' + 1557 '</div>'; 1558 1559 $('#weather').html(html); 1560 }, 1561 error: function(err) { 1562 $('.header-weather-wrap').hide(); 1563 console.log(err); 1564 } 1565 }); 1566 })(); 1567 } 1568 1569// Function definition: removeCookieString 1570function removeCookieString(value, domain) { 1571 domain = domain ? "domain=" + domain + "; " : ""; 1572 return value + "; expires=Thu, 01 Jan 1970 00:00:01 GMT; " + domain + "path=/"; 1573} 1574 1575// Function definition: unsetGoogtransCookie 1576function unsetGoogtransCookie() { 1577 for (var domains = window.location.hostname.split("."); domains.length >= 2; domains.shift()) { 1578 var cookieString = removeCookieString("googtrans=unset", domains.join(".")); 1579 document.cookie = cookieString; 1580 if (domains.length === 2) { 1581 cookieString = removeCookieString("googtrans=unset", "." + domains.join(".")); 1582 document.cookie = cookieString; 1583 } 1584 } 1585 document.cookie = removeCookieString("googtrans=unset"); 1586} 1587 1588if ( 1589document.cookie.split(";").some(function (item) { 1590 return item.trim().startsWith("googtrans=/auto/en"); 1591}) 1592) { 1593 unsetGoogtransCookie(); 1594} 1595 1596/* ===================== 1597 Masonry Deferred 1598===================== */ 1599window.RZMasonry = function(el) { 1600 var masonryAttr = el.dataset['masonryDeferred']; 1601 var masonryOpts = {}; 1602 1603 if (masonryAttr) { 1604 try { 1605 masonryOpts = JSON.parse(masonryAttr); 1606 } catch (e) { 1607 // Bummer 1608 } 1609 } 1610 new Masonry(el, masonryOpts); 1611} 1612 1613/* ===================== 1614 Tiny Slider 1615===================== */ 1616if (typeof tns !== "undefined") { 1617 1618 window.RZTinyCarousel = function(el) { 1619 var $el = $(el); 1620 var tinyItemCount = $el.children().length; 1621 1622 if (tinyItemCount === 0) return; 1623 1624 var tinyItemData = $el.data("tinyItems") || 1; 1625 var tinyItemDataSm = $el.data("tinyItemsSm") || 1; 1626 var tinyItemDataMd = $el.data("tinyItemsMd") || 1; 1627 var tinyGapData = $el.data("tinyGap") || 0; 1628 var tinyNavData = $el.data("tiny-nav"); 1629 var tinyControlsData = $el.data("tiny-controls"); 1630 var tinyAutoplayButtonsData = $el.data("tiny-autoplay-buttons"); 1631 var tinyAutoplayData = $el.data("tiny-autoplay"); 1632 var tinyEdgePaddingData = $el.data("tiny-padding-x"); 1633 var tinyModeData = $el.data("tiny-mode"); 1634 var tinySpeed = $el.data("tiny-speed") || 300; 1635 1636 var tinyCar = tns({ 1637 container: el, 1638 rewind: true, 1639 items: Math.min(tinyItemCount, tinyItemData), 1640 animateIn: "fadeIn", 1641 animateOut: "fadeOut", 1642 speed: tinySpeed, 1643 responsive: { 1644 0: { 1645 items: Math.min(tinyItemCount, tinyItemDataSm), 1646 }, 1647 768: { 1648 items: Math.min(tinyItemCount, tinyItemDataMd), 1649 }, 1650 1200: { 1651 items: Math.min(tinyItemCount, tinyItemData), 1652 }, 1653 }, 1654 lazyload: true, 1655 nav: tinyNavData, 1656 center: false, 1657 gutter: tinyGapData, 1658 edgePadding: tinyEdgePaddingData, 1659 lazyloadSelector: ".tns-lazy-img", 1660 preventScrollOnTouch: "auto", 1661 controls: tinyControlsData, 1662 autoplay: tinyAutoplayData, 1663 mouseDrag: true, 1664 autoplayButtonOutput: tinyAutoplayButtonsData, 1665 mode: tinyModeData, 1666 autoplayText: [ 1667 '<i class="bi bi-play-fill"></i>', 1668 '<i class="bi bi-pause-fill"></i>' 1669 ], 1670 controlsText: [ 1671 '<i class="bi bi-chevron-left" aria-hidden="true"></i>', 1672 '<i class="bi bi-chevron-right" aria-hidden="true"></i>' 1673 ], 1674 });
1675 1676 // Mark prev / next nav dots 1677 if (tinyCar && tinyCar.events) { 1678 tinyCar.events.on('indexChanged', function () { 1679 var info = tinyCar.getInfo(); 1680 var nav = info && info.navContainer; 1681 if (!nav) return; 1682 1683 var dots = nav.querySelectorAll('button'); 1684 dots.forEach(function (b) { 1685 b.classList.remove('is-prev', 'is-next'); 1686 }); 1687 1688 var active = nav.querySelector('.tns-nav-active'); 1689 if (!active) return; 1690 1691 if (active.previousElementSibling) { 1692 active.previousElementSibling.classList.add('is-prev'); 1693 } 1694 1695 if (active.nextElementSibling) { 1696 active.nextElementSibling.classList.add('is-next'); 1697 } 1698 }); 1699 } 1700 1701 if (tinyCar && typeof tinyCar.getInfo === 'function') { 1702 var navControls = tinyCar.getInfo().navContainer; 1703 1704 $(navControls).parent().find(".tns-nav,[data-action]").wrapAll('<div class="nav-controls absolute"></div>'); 1705 1706 // Auto-center the active dot on mobile (NO page scroll) 1707 function centerActiveTinyDot(tinyCar) { 1708 1709 var info = tinyCar.getInfo(); 1710 var nav = info && info.navContainer; 1711 if (!nav || nav.scrollWidth <= nav.clientWidth) return; 1712 1713 var active = nav.querySelector('.tns-nav-active'); 1714 if (!active) return; 1715 1716 requestAnimationFrame(function () { 1717 var navRect = nav.getBoundingClientRect(); 1718 var dotRect = active.getBoundingClientRect(); 1719 1720 var current = nav.scrollLeft; 1721 var delta = (dotRect.left - navRect.left) - (nav.clientWidth / 2) + (dotRect.width / 2); 1722 var next = current + delta; 1723 1724 next = Math.max(0, Math.min(next, nav.scrollWidth - nav.clientWidth)); 1725 nav.scrollTo({ left: next, behavior: 'smooth' }); 1726 }); 1727 } 1728 1729 // attach 1730 tinyCar.events.on('indexChanged', function () { centerActiveTinyDot(tinyCar); }); 1731 // optionally more stable: 1732 tinyCar.events.on('transitionEnd', function () { centerActiveTinyDot(tinyCar); }); 1733 1734 // also center on load (after tns builds nav) 1735 setTimeout(function(){ centerActiveTinyDot(tinyCar); }, 0); 1736 1737 } 1738 }; 1739 1740 $(".tiny-carousel,.tiny-slider").not(".tiny-deferred").each(function (i, el) { 1741 window.RZTinyCarousel(el); 1742 }); 1743 1744 $(".iwantto-slider").each(function (i, el) { 1745 var $el = $(el); 1746 var tinyItemCount = $el.children().length; 1747 var tinyItemData = $el.data("tinyItems") ? $el.data("tinyItems") : 1; 1748 var tinyItemDataSm = $el.data("tinyItemsSm") ? $el.data("tinyItemsSm") : 1; 1749 var tinyItemDataMd = $el.data("tinyItemsMd") ? $el.data("tinyItemsMd") : 1; 1750 var tinyGapData = $el.data("tinyGap") ? $el.data("tinyGap") : 0; 1751 var tinyNavData = $(el).data("tiny-nav"); 1752 var tinyControlsData = $(el).data("tiny-controls"); 1753 var tinyAutoplayButtonsData = $(el).data("tiny-autoplay-buttons"); 1754 var tinyAutoplayData = $(el).data("tiny-autoplay"); 1755 var tinyEdgePaddingData = $(el).data("tiny-padding-x"); 1756 1757 tns({ 1758 container: el, 1759 items: Math.min(tinyItemCount, tinyItemData), 1760 responsive: { 1761 0: { 1762 items: Math.min(tinyItemCount, tinyItemDataSm), 1763 }, 1764 768: { 1765 items: Math.min(tinyItemCount, tinyItemDataMd), 1766 }, 1767 1200: { 1768 items: Math.min(tinyItemCount, tinyItemData), 1769 }, 1770 }, 1771 nav: tinyNavData, 1772 loop: false, 1773 axis:"vertical", 1774 center: true, 1775 gutter: Math.min(tinyGapData, tinyGapData), 1776 edgePadding: tinyEdgePaddingData, 1777 preventScrollOnTouch: "auto", 1778 controls: tinyControlsData, 1779 autoplay: tinyAutoplayData, 1780 autoplayButtonOutput: tinyAutoplayButtonsData, 1781 autoplayText: ['<i class="bi bi-play-fill"></i>', '<i class="bi bi-pause-fill"></i>'], 1782 controlsText: ['<span class="bi bi-chevron-left" aria-hidden="true"></span>Prev', 'Next<span class="bi bi-chevron-right" aria-hidden="true"></span>'], 1783 });
1784 }); 1785} 1786/* ===================== 1787 Sets aria-labels for carousel buttons (Previous/Next) 1788===================== */ 1789(function () { 1790 function applyTnsA11yFix(ctx) { 1791 const $ctx = ctx ? $(ctx) : $(document); 1792 1793 $ctx.find('.tns-controls, .tns-controls button') 1794 .removeAttr('tabindex') 1795 .removeAttr('aria-label'); 1796 1797 $ctx.find('.tns-controls button[data-controls="prev"]') 1798 .attr('aria-label', 'Previous Slide'); 1799 1800 $ctx.find('.tns-controls button[data-controls="next"]') 1801 .attr('aria-label', 'Next Slide'); 1802 1803 $ctx.find('.tns-liveregion') 1804 .attr('aria-live', 'off') 1805 .attr('aria-atomic', 'false'); 1806 } 1807 1808 $(applyTnsA11yFix); 1809 1810 const mo = new MutationObserver(muts => { 1811 for (const m of muts) { 1812 if (!m.addedNodes || !m.addedNodes.length) continue; 1813 for (const n of m.addedNodes) { 1814 if (!(n instanceof HTMLElement)) continue; 1815 if ( 1816 n.classList?.contains('tns-controls') || 1817 n.classList?.contains('tns-liveregion') || 1818 n.querySelector?.('.tns-controls, .tns-liveregion') 1819 ) { 1820 applyTnsA11yFix(n); 1821 } 1822 } 1823 } 1824 }); 1825 1826 mo.observe(document.body, { childList: true, subtree: true }); 1827})(); 1828 1829// ADA - Prevent focus from offscreen elements, use slider controls to bring more elements into view 1830// #todo 1831 1832 1833/* ===================== 1834 AOS Animations 1835===================== */ 1836AOS.init({ 1837 disable: "phone, tablet, mobile", // accepts following values: 'phone', 'tablet', 'mobile', boolean, expression or function 1838 startEvent: "DOMContentLoaded", // name of the event dispatched on the document, that AOS should initialize on 1839 initClassName: "aos-init", // class applied after initialization 1840 animatedClassName: "aos-animate", // class applied on animation 1841 useClassNames: false, // if true, will add content of `data-aos` as classes on scroll 1842 disableMutationObserver: false, // disables automatic mutations' detections (advanced) 1843 debounceDelay: 50, // the delay on debounce used while resizing window (advanced) 1844 throttleDelay: 99, // the delay on throttle used while scrolling the page (advanced) 1845 1846/* ===================== 1847 Settings that can be overridden on per-element basis, by `data-aos-*` attributes: 1848===================== */ 1849 offset: 0, // offset (in px) from the original trigger point 1850 delay: 400, // values from 0 to 3000, with step 50ms 1851 duration: 400, // values from 0 to 3000, with step 50ms 1852 easing: "ease", // default easing for AOS animations 1853 once: true, // whether animation should happen only once - while scrolling down 1854 mirror: false, // whether elements should animate out while scrolling past them 1855 anchorPlacement: "top-bottom", // defines which position of the element regarding to window should trigger the animation 1856}); 1857 1858document.addEventListener('DOMContentLoaded', () => { 1859 const baseDelay = 100; 1860 const letterDelay = 100; 1861 const duration = 1000; // << SLOWER animation! 1862 1863 document.querySelectorAll('.stagger-fadeup').forEach(el => { 1864 const text = el.textContent.trim(); 1865 const animation = el.getAttribute('data-aos') || 'fade-up'; 1866 let html = ''; 1867 let idx = 0; 1868 1869 for (const char of text) { 1870 if (char === ' ') { 1871 html += ' '; 1872 } else { 1873 const delay = baseDelay + idx * letterDelay; 1874 html += `<span style="opacity:1;overflow:hidden;line-height:1;display:inline-block"><span style="opacity:1;overflow:hidden;display:inline-block;" data-aos="${animation}" data-aos-delay="${delay}" data-aos-duration="${duration}" class="aos-init">${char}</span></span>`; 1875 idx++; 1876 } 1877 } 1878 el.innerHTML = html; 1879 }); 1880 1881 if (window.AOS) { 1882 AOS.refreshHard(); 1883 } 1884}); 1885 1886/* ===================== 1887 Lazy load 1888===================== */ 1889$(function () { 1890 $(".lazy,.rz-card-img,.rz-gallery-image img,.rz-qlink-img,.rz-tile-img,.featured-image-wrap").lazy(); 1891}); 1892 1893/* ===================== 1894 Responsive Tables 1895===================== */ 1896$(".post table:not(.layout-table):not(.not-responsive)").wrap('<div class="table-responsive"></div>'); 1897$(".layout-table").attr("role", "presentation"); 1898 1899/* ===================== 1900 Preloader 1901===================== */ 1902$window.on("load", function () { 1903 setTimeout(function () { 1904 $body.addClass("loaded"); 1905 $(".loader-wrapper").fadeOut(); 1906 }, 600); 1907}); 1908 1909/* ===================== 1910 Smooth Scroll 1911===================== */ 1912$window.ready(function () { 1913 //#Smooth Scrolling 1914 $('a[href*="#"]') 1915 .not('[href="#"]') 1916 .not('[href*="#collapse"]') 1917 .click(function (event) { 1918 if (location.pathname.replace(/^\//, "") === this.pathname.replace(/^\//, "") && location.hostname === this.hostname) { 1919 var target = $(this.hash); 1920 target = target.length ? target : $("[name=" + this.hash.slice(1) + "]"); 1921 if (target.length) { 1922 event.preventDefault(); 1923 $("html,body").animate( 1924 { 1925 scrollTop: target.offset().top, 1926 }, 1927 1000, 1928 function () { 1929 var $target = $(target);
1930 $target.focus(); 1931 if ($target.is(":focus")) { 1932 return false; 1933 } else { 1934 $target.attr("tabindex", "-1"); 1935 $target.focus(); 1936 $target.removeAttr("tabindex"); 1937 } 1938 } 1939 ); 1940 } 1941 } 1942 }); 1943 1944//play button in video hero 1945document.querySelectorAll('button.play-toggle[role="switch"]').forEach(function(btn) { 1946 // Prefer existing aria-pressed value, fallback to aria-checked, else set to "false" 1947 let checked = "false"; 1948 if (btn.hasAttribute('aria-pressed')) { 1949 checked = btn.getAttribute('aria-pressed') === "true" ? "true" : "false"; 1950 btn.removeAttribute('aria-pressed'); 1951 } else if (btn.hasAttribute('aria-checked')) { 1952 checked = btn.getAttribute('aria-checked') === "true" ? "true" : "false"; 1953 } 1954 btn.setAttribute('aria-checked', checked); 1955 1956 // OPTIONAL: Toggle aria-checked on click for demonstration 1957 btn.addEventListener('click', function() { 1958 let newChecked = btn.getAttribute('aria-checked') === "true" ? "false" : "true"; 1959 btn.setAttribute('aria-checked', newChecked); 1960 }); 1961 }); 1962 1963/* ===================== 1964 Removes aria-live attribute which reads to screen reader when slide is changed. Also sets aria-hidden to true so this element 1965===================== */ 1966 // is ignored from the dom. - Michael S. 1967 $(".tns-liveregion.tns-visually-hidden").attr("aria-hidden", "true").removeAttr("aria-live"); 1968 1969 }); // Ready 1970})(jQuery); 1971 1972 1973// nav 1.0 1974// Get the threshold from data-mega-at on .site-header-wrap 1975const megaAt = parseInt($('.site-header-wrap').attr('data-mega-at'), 10) || 8; 1976 1977// nav 1.0 1978$("#nav > li > ul").each(function () { 1979 const $submenu = $(this); 1980 const $parentLi = $submenu.parent(); 1981 const itemCount = $submenu.find("li").length; 1982 1983 if (itemCount >= megaAt) { 1984 1985 /* ===================== 1986 Mega menu 1987 ===================== */ 1988 $submenu 1989 .addClass("make-mega dropdown-menu") 1990 .wrapInner('<li class="mega-wrapper d-lg-flex gap-3 w-100"><div class="mega-wrap w-100"><ul class="mega-wrap-inner bg-transparent"></ul></div></li>'); 1991 $parentLi.addClass("static dropdown dropdown-center"); 1992 1993 } else { 1994 1995 /* ===================== 1996 Regular dropdown 1997 ===================== */ 1998 $submenu.addClass("normal-menu dropdown-menu"); 1999 $parentLi.addClass("relative dropdown"); 2000 } 2001}); 2002 2003 2004/* ===================== 2005 Handle nested submenus inside normal-menu items 2006===================== */ 2007 $(".normal-menu ul").each(function () { 2008 const $nestedMenu = $(this); 2009 const $parentLi = $nestedMenu.parent(); 2010 $nestedMenu.addClass("dropdown-menu menu-level-3"); 2011 $parentLi.addClass("dropdown static dropend"); 2012 }); 2013 2014/* ===================== 2015 Apply Bootstrap dropdown behavior to all dropdown links 2016===================== */ 2017 $("#nav .dropdown > a") 2018 .addClass("dropdown-toggle") 2019 .attr({ 2020 'data-bs-toggle': "dropdown", 2021 'data-bs-auto-close': "outside", 2022 "aria-expanded": "false", 2023 'data-bs-offset':"0,0" 2024 }); 2025 2026(function applyNavToggles() { 2027 function applyToggles() { 2028 const $spans = $("#nav .dropdown > span"); 2029 if (!$spans.length) return; 2030 2031 $spans 2032 .addClass("dropdown-toggle") 2033 .attr({ 2034 "data-bs-toggle": "dropdown", 2035 "data-bs-auto-close": "outside", 2036 "aria-expanded": "false", 2037 "data-bs-offset": "0,0", 2038 "role": "button", 2039 "tabindex": "0" 2040 }); 2041 } 2042 2043 applyToggles(); 2044 2045 const nav = document.getElementById("nav"); 2046 const root = nav ? nav : document.body; 2047 2048 const obs = new MutationObserver(() => applyToggles()); 2049 obs.observe(root, { childList: true, subtree: true }); 2050 })(); 2051 2052// Function definition: initDropdowns 2053 function initDropdowns() { 2054 if (window.innerWidth < 992) return; 2055 2056 const nav = document.getElementById('nav'); 2057 2058 document.querySelectorAll('#nav .dropdown-toggle').forEach((toggle) => { 2059 const li = toggle.closest('li'); 2060 const menu = toggle.nextElementSibling; 2061 2062 if (!menu || !menu.classList.contains('dropdown-menu')) return; 2063 2064/* ===================== 2065 Temporarily show the dropdown menu to measure its width 2066===================== */ 2067 menu.style.visibility = 'hidden'; 2068 menu.style.display = 'block'; 2069 2070 const dropdownWiderThanNav = menu.offsetWidth > nav.offsetWidth; 2071 const normalMenu = toggle.closest('.normal-menu'); 2072 2073/* ===================== 2074 Restore visibility 2075===================== */ 2076 menu.style.display = ''; 2077 menu.style.visibility = ''; 2078 2079/* ===================== 2080 Determine correct reference element 2081===================== */ 2082 let referenceEl; 2083 if (dropdownWiderThanNav) { 2084 referenceEl = nav; 2085 } else if (normalMenu) { 2086 referenceEl = normalMenu; 2087 } else { 2088 referenceEl = li; 2089 } 2090 2091 new bootstrap.Dropdown(toggle, { 2092 reference: referenceEl, 2093 popperConfig: (defaultBsPopperConfig) => { 2094 return { 2095 ...defaultBsPopperConfig, 2096 modifiers: [ 2097 ...(defaultBsPopperConfig.modifiers || []), 2098 { 2099 name: 'preventOverflow', 2100 options: { 2101 boundary: 'viewport', 2102 padding: 0 2103 } 2104 } 2105 ] 2106 }; 2107 } 2108 }); 2109 });
2110 } 2111 2112/* ===================== 2113 Run on load if ⥠992 2114===================== */ 2115 initDropdowns(); 2116 2117/* ===================== 2118 Re-run on resize when crossing the 992px threshold 2119===================== */ 2120 let wasWide = window.innerWidth >= 992; 2121 2122 window.addEventListener('resize', () => { 2123 const isWide = window.innerWidth >= 992; 2124 if (isWide && !wasWide) { 2125 initDropdowns(); 2126 } 2127 wasWide = isWide; 2128 }); 2129 2130 document.addEventListener("DOMContentLoaded", function(){ 2131 2132/* ===================== 2133 make it as accordion for smaller screens 2134===================== */ 2135 if (window.innerWidth > 992) { 2136 2137 document.querySelectorAll('.navbar .dropdown').forEach(function(everyitem){ 2138 2139 everyitem.addEventListener('mouseover', function(e){ 2140 2141 let el_link = this.querySelector('a[data-bs-toggle]'); 2142 2143 if(el_link != null){ 2144 let nextEl = el_link.nextElementSibling; 2145 el_link.classList.add('show'); 2146 nextEl.classList.add('show'); 2147 } 2148 2149 }); 2150 everyitem.addEventListener('mouseleave', function(e){ 2151 let el_link = this.querySelector('a[data-bs-toggle]'); 2152 2153 if(el_link != null){ 2154 let nextEl = el_link.nextElementSibling; 2155 el_link.classList.remove('show'); 2156 nextEl.classList.remove('show'); 2157 } 2158 2159 2160 }) 2161 }); 2162 2163 } 2164 2165/* ===================== 2166 end if innerWidth 2167===================== */ 2168 }); 2169 2170/* ===================== 2171 DOMContentLoaded end 2172===================== */ 2173 2174 2175/* ===================== 2176 Add 'navbar' class to #nav if menu-onhover is enabled 2177===================== */ 2178 // $('[data-menu-onhover="true"] #nav').addClass('navbar'); 2179$(document).ready(function () { 2180 function initMenuHover() { 2181 // Remove previous bindings 2182 $('#nav .dropdown').off('mouseenter mouseleave click'); 2183 2184 if ($('.site-header-wrap').attr('data-menu-onhover') === 'true' && $(window).width() > 992) { 2185 // Hover behavior 2186 $('#nav .dropdown').hover( 2187 function () { 2188 $(this).children('[data-bs-toggle="dropdown"]').dropdown('show'); 2189 }, 2190 function () { 2191 $(this).children('[data-bs-toggle="dropdown"]').dropdown('hide'); 2192 } 2193 ); 2194 2195 // Make parent link clickable 2196 $('#nav .dropdown > a[data-bs-toggle="dropdown"]').on('click', function (e) { 2197 var href = $(this).attr('href'); 2198 if (href && href !== '#') { 2199 window.location = href; // Go to the link 2200 } 2201 }); 2202 } 2203 } 2204 2205 initMenuHover(); 2206 $(window).on('resize', initMenuHover); 2207 }); 2208 2209/* ===================== 2210 Clone .mega-button elements into their matching mega menu slots 2211===================== */ 2212 const $headerItems = $('#nav > li').not('.nav-border'); 2213 2214 $('.mega-button').each(function () { 2215 const index = $(this).index(); 2216 $(this).clone().appendTo( 2217 $headerItems.eq(index).find('.make-mega .mega-wrapper') 2218 ); 2219 }); 2220 2221/* ===================== 2222 Create hover border effect under nav items 2223===================== */ 2224 const $siteHeaderNav = $('#nav'); 2225 2226 // Only add the border element if it doesn't exist 2227 if (!$siteHeaderNav.find('.nav-border').length) { 2228 $('<li class="nav-border d-none d-lg-block"></li>').appendTo($siteHeaderNav); 2229 } 2230 2231 const $border = $('.nav-border'); 2232 let borderInitialized = false; 2233 2234 // Set up hover behavior for nav items 2235 $headerItems.each(function () { 2236 $(this).on('mouseover', function () { 2237 const elRect = this.getBoundingClientRect(); 2238 const parentRect = this.parentNode.getBoundingClientRect(); 2239 2240 const left = elRect.left - parentRect.left; 2241 const width = elRect.width; 2242 const paddingOffset = 20; // total 10px on each side 2243 2244 $border.css({ 2245 opacity: 1, 2246 width: width - paddingOffset, 2247 transform: `translateX(${left}px)` 2248 }); 2249 });
2250 2251 $(this).on('mouseout', function () { 2252 $border.css({ 2253 opacity: 0, 2254 width: 0 2255 }); 2256 }); 2257 2258 // Set initial hover position on first item 2259 if (!borderInitialized) { 2260 $(this).trigger('mouseover').trigger('mouseout'); 2261 borderInitialized = true; 2262 } 2263 }); 2264 2265 $('#nav .dropdown-toggle').each(function(){ 2266 // Clone the current dropdown-toggle 2267 let clonedToggle = $(this) 2268 .clone() 2269 .addClass('dropdown-header icon-link icon-link-hover d-flex align-items-center') 2270 .removeClass('dropdown-toggle') 2271 .removeAttr('data-bs-toggle data-bs-auto-close aria-expanded data-bs-offset') 2272 .append('<i class="bi bi-arrow-right d-flex align-items-center"></i>'); 2273 2274 // Wrap cloned toggle in <li> 2275 let liWrapper = $('<li class="dropdown-header-item"></li>').append(clonedToggle); 2276 2277 // Find the dropdown menu 2278 let nextDropdownMenu = $(this).closest('.dropdown').find('.dropdown-menu').first(); 2279 2280 // Prepend the new <li> into the dropdown 2281 if (nextDropdownMenu.length) { 2282 nextDropdownMenu.prepend(liWrapper); 2283 } 2284 }); 2285 2286// offcanvas links 2287 const OPENERS = { 2288 jobDetails: (id) => window.openJobDetails?.(id), 2289 bidDetails: (id) => window.openBidDetails?.(id), 2290 newsDetails: (id) => window.openNewsDetails?.(id), 2291 catalogueDetails: (id) => window.openCatalogueDetails?.(id), 2292 }; 2293 2294 const u = () => new URL(location.href); 2295 const get = (k) => u().searchParams.get(k); 2296 const set = (url,k,v) => (v==null||v==='') ? url.searchParams.delete(k) : url.searchParams.set(k,v); 2297 2298 let SUPPRESS = false; 2299 2300 window.pushOcState = function(ocId, id) { 2301 if (SUPPRESS) return; 2302 const url = u(); 2303 set(url, 'rz', ocId); 2304 set(url, 'id', (id!=null && id!=='') ? String(id) : ''); 2305 history.pushState({ oc: ocId, id: String(id || '') }, '', url); 2306 }; 2307 2308 function tryDeepLinkNow(ocId) { 2309 const url = u(); 2310 const oc = url.searchParams.get('rz'); 2311 const id = url.searchParams.get('id'); 2312 if (oc === ocId && id) { 2313 SUPPRESS = true; 2314 OPENERS[ocId]?.(String(id)); 2315 setTimeout(() => { SUPPRESS = false; }, 0); 2316 } 2317 } 2318 2319 // Deep link ?rz=<offcanvasId>&id=<recordId> 2320 function applyDeepLink() { 2321 const ocId = get('rz'); 2322 const recId = get('id'); 2323 if (!ocId) return; 2324 2325 SUPPRESS = true; 2326 if (OPENERS[ocId] && recId) { 2327 OPENERS[ocId](recId); 2328 } else { 2329 const el = document.getElementById(ocId); 2330 if (el) bootstrap.Offcanvas.getOrCreateInstance(el).show(); 2331 } 2332 setTimeout(() => { SUPPRESS = false; }, 0); 2333 } 2334 2335 document.addEventListener('show.bs.offcanvas', (ev) => { 2336 if (SUPPRESS) return; 2337 const ocId = ev.target?.id; 2338 if (!ocId) return; 2339 const recId = ev.relatedTarget?.getAttribute?.('data-id') || ev.relatedTarget?.dataset?.id || ''; 2340 const url = u(); 2341 set(url, 'rz', ocId); 2342 set(url, 'id', recId); 2343 history.pushState({ oc: ocId, id: recId }, '', url); 2344 }); 2345 2346 document.addEventListener('hide.bs.offcanvas', () => { 2347 if (SUPPRESS) return; 2348 const url = u(); 2349 set(url, 'rz', null); 2350 set(url, 'id', null); 2351 history.pushState({}, '', url); 2352 }); 2353 2354 window.addEventListener('popstate', applyDeepLink); 2355 document.addEventListener('DOMContentLoaded', applyDeepLink); 2356 2357 2358/* RZ Search/Filter for Document Center + Accordion 2359 - Categories come from accordion headers 2360 - Uses <template class="rz-cat-row"> inside each .rz-element-acc-search-wrapper 2361 - Shows a spinner while filtering, updates result/section counts and per-category badges 2362*/ 2363(function () { 2364 const DEBOUNCE_MS = 150; 2365 const LOAD_HANDLES = new WeakMap(); 2366 const normalize = (s) => (s || '').toString().toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, ''); 2367 const debounce = (fn, ms) => { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; }; 2368 const getNextModule = (searchWrap) => { 2369 const wrapper = searchWrap.closest('.rz-element-wrapper'); 2370 if (!wrapper) return null; 2371 const candidates = Array.from(wrapper.querySelectorAll('.doc-center-wrap, .accordion-wrap')); 2372 if (!candidates.length) return null; 2373 const children = Array.from(wrapper.children);
2374 const searchIdx = children.indexOf(searchWrap.closest('.rz-element-search-wrapper')); 2375 let best = null, bestIdx = Infinity; 2376 candidates.forEach((el) => { 2377 const idx = children.indexOf(el); 2378 if (idx > searchIdx && idx < bestIdx) { best = el; bestIdx = idx; } 2379 }); 2380 return best || candidates[0] || null; 2381 }; 2382 function scheduleWithSpinner(searchRoot, workFn) { 2383 const spinner = searchRoot.querySelector('.rz-loader'); 2384 if (!spinner) return; 2385 spinner.classList.remove('d-none', 'fade-out'); 2386 spinner.classList.add('fade-in'); 2387 const prev = LOAD_HANDLES.get(searchRoot); 2388 if (prev) { cancelAnimationFrame(prev.f1); cancelAnimationFrame(prev.f2); } 2389 const f1 = requestAnimationFrame(() => { 2390 const f2 = requestAnimationFrame(() => { 2391 const start = performance.now(); 2392 try { workFn(); } 2393 finally { 2394 const minVisible = 400; 2395 const elapsed = performance.now() - start; 2396 const delay = Math.max(0, minVisible - elapsed); 2397 setTimeout(() => { 2398 spinner.classList.remove('fade-in'); 2399 spinner.classList.add('fade-out'); 2400 setTimeout(() => spinner.classList.add('d-none'), 400); 2401 }, delay); 2402 } 2403 }); 2404 LOAD_HANDLES.set(searchRoot, { f1, f2 }); 2405 }); 2406 LOAD_HANDLES.set(searchRoot, { f1, f2: 0 }); 2407 } 2408 function wireDebounced(searchRoot, apply) { 2409 const debouncedApply = debounce(() => scheduleWithSpinner(searchRoot, apply), DEBOUNCE_MS); 2410 const menu = searchRoot.querySelector('#categoryDropdownMenu'); 2411 const input = searchRoot.querySelector('input[type="search"]'); 2412 if (input) input.addEventListener('input', debouncedApply); 2413 if (menu) menu.addEventListener('rz-categories-change', debouncedApply); 2414 apply(); 2415 } 2416 function updateTopCounts(searchRoot, totalItems, matchedSections) { 2417 const r = searchRoot.querySelector('.rz-results-count'); 2418 const s = searchRoot.querySelector('.rz-sections-count'); 2419 if (r) r.textContent = String(totalItems); 2420 if (s) s.textContent = String(matchedSections); 2421 } 2422 function buildCategoryUIFromHeaders(searchRoot, headers) { 2423 const menu = searchRoot.querySelector('#categoryDropdownMenu'); 2424 const tpl = searchRoot.querySelector('.rz-cat-row'); 2425 if (!menu || !tpl) return { getSelected: () => [], map: new Map() }; 2426 const keep = Array.from(menu.children).slice(0, 2); 2427 menu.innerHTML = ''; 2428 keep.forEach(n => menu.appendChild(n)); 2429 const catMap = new Map(); 2430 headers.forEach((labelTxt) => { 2431 const clean = (labelTxt || '').trim(); 2432 if (!clean) return; 2433 const node = tpl.content.firstElementChild.cloneNode(true); 2434 const cb = node.querySelector('input[type="checkbox"]'); 2435 const label = node.querySelector('.rz-cat-label'); 2436 const badge = node.querySelector('.rz-cat-badge'); 2437 if (cb) cb.value = clean; 2438 if (label) label.textContent = clean; 2439 if (badge) badge.textContent = '0'; 2440 catMap.set(clean, { cb, badge }); 2441 menu.appendChild(node); 2442 }); 2443 const selectAll = menu.querySelector('input.selectall'); 2444 if (selectAll) { 2445 selectAll.checked = false; 2446 selectAll.addEventListener('change', () => { 2447 menu.querySelectorAll('input[type="checkbox"]:not(.selectall)').forEach(cb => cb.checked = selectAll.checked); 2448 menu.dispatchEvent(new Event('rz-categories-change')); 2449 }); 2450 } 2451 menu.addEventListener('change', (e) => { 2452 if (e.target && e.target.matches('input[type="checkbox"]') && !e.target.classList.contains('selectall')) { 2453 if (selectAll && selectAll.checked) selectAll.checked = false; 2454 menu.dispatchEvent(new Event('rz-categories-change')); 2455 } 2456 }); 2457 return { map: catMap, getSelected: () => Array.from(menu.querySelectorAll('input[type="checkbox"]:not(.selectall):checked')).map(cb => cb.value) }; 2458 } 2459 2460 function wireDocCenter(searchRoot, docCenter) { 2461 const input = searchRoot.querySelector('input[type="search"]'); 2462 const sections = Array.from(docCenter.querySelectorAll('.accordion-item')); 2463 2464 const sectionObjs = sections.map(sec => { 2465 const btn = sec.querySelector('.accordion-button'); 2466 const panel = sec.querySelector('.accordion-collapse'); 2467 const header = (btn ? btn.textContent : sec.textContent).trim(); 2468 const items = Array.from(sec.querySelectorAll('.list-group-item')).map(li => { 2469 const nameEl = li.querySelector('.doc-name'); 2470 const text = normalize(nameEl ? nameEl.textContent : li.textContent); 2471 return { li, text }; 2472 }); 2473 const initialOpen = !!(panel && panel.classList.contains('show')); 2474 const totalDocs = items.length; 2475 return { sec, btn, panel, header, items, initialOpen, totalDocs }; 2476 });
2477 2478 const headers = sectionObjs.map(s => s.header).filter(Boolean); 2479 const catUI = buildCategoryUIFromHeaders(searchRoot, headers); 2480 2481 const apply = () => { 2482 const q = normalize(input && input.value); 2483 const selected = catUI.getSelected(); 2484 const filterByCat = selected.length > 0; 2485 const activeFilter = !!q || filterByCat; 2486 2487 catUI.map.forEach(({ badge }) => { if (badge) badge.textContent = '0'; }); 2488 2489 let totalShown = 0; 2490 let matchedSections = 0; 2491 2492 sectionObjs.forEach(({ sec, btn, panel, header, items, initialOpen, totalDocs }) => { 2493 let shownInSection = 0; 2494 2495 if (!activeFilter) { 2496 items.forEach(({ li }) => { li.style.removeProperty('display'); }); 2497 sec.style.removeProperty('display'); 2498 shownInSection = totalDocs; 2499 2500 if (panel && btn) { 2501 panel.classList.toggle('show', initialOpen); 2502 btn.classList.toggle('collapsed', !initialOpen); 2503 btn.setAttribute('aria-expanded', initialOpen ? 'true' : 'false'); 2504 2505 const badge = 2506 btn.querySelector('.badge.bg-success-subtle.ms-auto.text-dark.rounded-1') || 2507 btn.querySelector('.badge'); 2508 if (badge) badge.textContent = String(totalDocs); 2509 } 2510 } else { 2511 items.forEach(({ li, text }) => { 2512 let ok = !q || text.includes(q); 2513 if (ok && filterByCat) ok = selected.includes(header); 2514 2515 if (ok) { 2516 li.style.removeProperty('display'); 2517 shownInSection++; 2518 } else { 2519 li.style.setProperty('display', 'none', 'important'); 2520 } 2521 }); 2522 2523 const hasVisible = shownInSection > 0; 2524 2525 if (panel && btn) { 2526 panel.classList.toggle('show', hasVisible); 2527 btn.classList.toggle('collapsed', !hasVisible); 2528 btn.setAttribute('aria-expanded', hasVisible ? 'true' : 'false'); 2529 2530 const badge = 2531 btn.querySelector('.badge.bg-success-subtle.ms-auto.text-dark.rounded-1') || 2532 btn.querySelector('.badge'); 2533 if (badge) badge.textContent = shownInSection ? String(shownInSection) : '0'; 2534 } 2535 2536 if (hasVisible) { 2537 sec.style.removeProperty('display'); 2538 } else { 2539 sec.style.setProperty('display', 'none', 'important'); 2540 } 2541 } 2542 2543 const catBadge = catUI.map.get(header)?.badge; 2544 if (catBadge) catBadge.textContent = String(shownInSection); 2545 2546 totalShown += shownInSection; 2547 if (shownInSection > 0) matchedSections++; 2548 }); 2549 2550 updateTopCounts(searchRoot, totalShown, matchedSections); 2551 }; 2552 2553 wireDebounced(searchRoot, apply); 2554 } 2555 2556 function wireAccordion(searchRoot, accWrap) { 2557 const input = searchRoot.querySelector('input[type="search"]'); 2558 const items = Array.from(accWrap.querySelectorAll('.accordion-item')).map(item => { 2559 const btn = item.querySelector('.accordion-button'); 2560 const panel = item.querySelector('.accordion-collapse'); 2561 const headerTxt = item.querySelector('.accordion-header')?.textContent?.trim() || ''; 2562 const bodyTxt = panel?.textContent || ''; 2563 return { 2564 item, btn, panel, 2565 header: headerTxt, 2566 text: normalize((headerTxt + ' ' + bodyTxt).trim()), 2567 initialOpen: !!(panel && panel.classList.contains('show')) 2568 }; 2569 }); 2570 2571 const headers = items.map(i => i.header).filter(Boolean); 2572 const catUI = buildCategoryUIFromHeaders(searchRoot, headers); 2573 2574 const apply = () => { 2575 const q = normalize(input && input.value); 2576 const selected = catUI.getSelected(); 2577 const filterByCat = selected.length > 0; 2578 const activeFilter = !!q || filterByCat; 2579 2580 catUI.map.forEach(({ badge }) => { if (badge) badge.textContent = '0'; }); 2581 2582 let totalShown = 0; 2583 let matchedSections = 0; 2584 2585 items.forEach(({ item, btn, panel, header, text, initialOpen }) => { 2586 let isVisible = false; 2587 2588 if (!activeFilter) { 2589 isVisible = true; 2590 item.style.removeProperty('display'); 2591 if (panel && btn) { 2592 panel.classList.toggle('show', initialOpen); 2593 btn.classList.toggle('collapsed', !initialOpen); 2594 btn.setAttribute('aria-expanded', initialOpen ? 'true' : 'false'); 2595 const b = btn.querySelector('.badge'); 2596 if (b) b.textContent = ''; 2597 } 2598 } else { 2599 let ok = !q || text.includes(q); 2600 if (ok && filterByCat) ok = selected.includes(header); 2601 2602 isVisible = ok; 2603 2604 if (ok) item.style.removeProperty('display'); 2605 else item.style.setProperty('display', 'none', 'important'); 2606 2607 if (panel && btn) { 2608 panel.classList.toggle('show', ok); 2609 btn.classList.toggle('collapsed', !ok); 2610 btn.setAttribute('aria-expanded', ok ? 'true' : 'false'); 2611 } 2612 } 2613 2614 if (isVisible) { 2615 totalShown += 1;
2616 matchedSections += 1; 2617 const catBadge = catUI.map.get(header)?.badge; 2618 if (catBadge) catBadge.textContent = '1'; 2619 } 2620 }); 2621 2622 updateTopCounts(searchRoot, totalShown, matchedSections); 2623 }; 2624 2625 wireDebounced(searchRoot, apply); 2626 } 2627 2628 document.querySelectorAll('.rz-element-acc-search-wrapper').forEach((searchWrap) => { 2629 const module = getNextModule(searchWrap); 2630 if (!module) return; 2631 if (module.classList.contains('doc-center-wrap')) { 2632 wireDocCenter(searchWrap, module); 2633 } else if (module.classList.contains('accordion-wrap')) { 2634 wireAccordion(searchWrap, module); 2635 } 2636 }); 2637})(); 2638 2639/* ====================================================== 2640 RZ Element Search + Filter + Pagination + Masonry 2641 2642 - Text search with keyword highlighting 2643 - Optional category filter (checkbox dropdown with counts) 2644 - Optional date range filter via [data-search-date="from"/"to"] 2645 - Supports two modes: 2646 ⢠Classic pagination via data-cards-per-page (default 10) 2647 ⢠Infinite âShow moreâ mode when data-infinite="true" 2648 - Skips search/pagination when inside tiny slider/carousel 2649 - Non-visible cards are removed from the DOM for performance 2650 - Loader spinner: 2651 ⢠Shown at the TOP for search/filter/sort/pagination 2652 ⢠Shown at the BOTTOM only while "Show more" is loading 2653 - "No results" message with Show more hidden when there are 0 matches 2654 - Infinite mode: 2655 ⢠Appends new cards without destroying Masonry 2656 ⢠Soft Masonry relayout to avoid scroll jumps 2657 ⢠Cards fade/zoom up in (CSS animation) without affecting Masonry layout 2658 - Masonry-safe: 2659 ⢠Transitions on Masonry items are disabled 2660 ⢠Works with RZ.initMasonry / data-masonry / data-masonry-deferred 2661 * Works well * 2662====================================================== */ 2663(function () { 2664 // ------------ Helpers ------------ 2665 2666 function normalize(str) { 2667 return (str || "") 2668 .toString() 2669 .toLowerCase() 2670 .normalize("NFD") 2671 .replace(/[\u0300-\u036f]/g, ""); 2672 } 2673 2674 function safeParseDate(str) { 2675 if (!str) return null; 2676 var d = new Date(str); 2677 return isNaN(d.getTime()) ? null : d; 2678 } 2679 2680 function getDateFromInput(input) { 2681 if (!input || !input.value) return null; 2682 return safeParseDate(input.value); 2683 } 2684 2685 function highlightMatch(el, tokens) { 2686 if (!el || !tokens || !tokens.length) return; 2687 2688 // Ignore any tokens that contain angle brackets â they can break HTML 2689 tokens = tokens.filter(function (t) { 2690 return t && !/[<>]/.test(t); 2691 }); 2692 if (!tokens.length) return; 2693 2694 function escapeRegExp(str) { 2695 return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 2696 } 2697 2698 // Single regex for all tokens, case-insensitive 2699 var pattern = new RegExp( 2700 "(" + tokens.map(escapeRegExp).join("|") + ")", 2701 "gi" 2702 ); 2703 2704 var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false); 2705 var textNodes = []; 2706 var node; 2707 while ((node = walker.nextNode())) textNodes.push(node); 2708 2709 textNodes.forEach(function (n) { 2710 var text = n.nodeValue; 2711 if (!pattern.test(text)) { 2712 pattern.lastIndex = 0; // reset for next node 2713 return; 2714 } 2715 pattern.lastIndex = 0; 2716 2717 var frag = document.createDocumentFragment(); 2718 var lastIndex = 0; 2719 2720 text.replace(pattern, function (match, _group, offset) { 2721 // text before match 2722 if (offset > lastIndex) { 2723 frag.appendChild( 2724 document.createTextNode(text.slice(lastIndex, offset)) 2725 ); 2726 } 2727 // highlighted match 2728 var mark = document.createElement("mark"); 2729 mark.textContent = match; // keep original casing 2730 frag.appendChild(mark); 2731 2732 lastIndex = offset + match.length; 2733 return match; 2734 }); 2735 2736 // trailing text 2737 if (lastIndex < text.length) { 2738 frag.appendChild(document.createTextNode(text.slice(lastIndex))); 2739 } 2740 2741 if (n.parentNode) { 2742 n.parentNode.replaceChild(frag, n); 2743 } 2744 });
2745 } 2746 2747 // Wait until cards (.rz-element) exist in a container 2748 function whenCardsReady(container, cb) { 2749 var current = container.querySelectorAll(".rz-element"); 2750 if (current.length) { 2751 cb(Array.prototype.slice.call(current)); 2752 return; 2753 } 2754 var mo = new MutationObserver(function () { 2755 var cards = container.querySelectorAll(".rz-element"); 2756 if (cards.length) { 2757 mo.disconnect(); 2758 cb(Array.prototype.slice.call(cards)); 2759 } 2760 }); 2761 mo.observe(container, { childList: true, subtree: true }); 2762 } 2763 2764 // Masonry helpers 2765 function killMasonry(grid) { 2766 if (!grid) return; 2767 if (grid._masonryInstance && typeof grid._masonryInstance.destroy === "function") { 2768 grid._masonryInstance.destroy(); 2769 grid._masonryInstance = null; 2770 } 2771 grid.querySelectorAll(".rz-element").forEach(function (el) { 2772 el.style.position = ""; 2773 el.style.left = ""; 2774 el.style.top = ""; 2775 }); 2776 grid.style.height = ""; 2777 } 2778 2779 function readMasonryOptions(grid) { 2780 var defaults = { itemSelector: ".rz-element", percentPosition: true }; 2781 try { 2782 var raw = grid.getAttribute("data-masonry"); 2783 if (!raw) return defaults; 2784 var tmp = document.createElement("div"); 2785 tmp.innerHTML = raw; 2786 var decoded = tmp.textContent || tmp.innerText || raw; 2787 var obj = JSON.parse(decoded); 2788 for (var k in defaults) { 2789 if (!obj.hasOwnProperty(k)) obj[k] = defaults[k]; 2790 } 2791 return obj; 2792 } catch (e) { 2793 return defaults; 2794 } 2795 } 2796 2797 function patchMasonryNoTransition(grid) { 2798 if (!grid) return; 2799 2800 // We only care if data-masonry (or deferred) is present 2801 var raw = 2802 grid.getAttribute("data-masonry") || 2803 grid.getAttribute("data-masonry-deferred"); 2804 if (!raw) return; 2805 2806 try { 2807 // RZ tends to HTML-encode this, so decode first 2808 var tmp = document.createElement("div"); 2809 tmp.innerHTML = raw; 2810 var decoded = tmp.textContent || tmp.innerText || raw; 2811 2812 var obj = JSON.parse(decoded || "{}"); 2813 2814 // Only touch if not explicitly set 2815 if (obj.transitionDuration == null) { 2816 // 0 = no animation, which prevents "flying in" 2817 obj.transitionDuration = 0; 2818 } 2819 2820 var json = JSON.stringify(obj); 2821 2822 // Write back so BOTH RZ.initMasonry and our fallback see it 2823 if (grid.hasAttribute("data-masonry")) { 2824 grid.setAttribute("data-masonry", json); 2825 } 2826 if (grid.hasAttribute("data-masonry-deferred")) { 2827 grid.setAttribute("data-masonry-deferred", json); 2828 } 2829 } catch (e) { 2830 // swallow â worst case, Masonry keeps its default transitions 2831 } 2832 } 2833 2834 function waitImagesOnce(grid, cb) { 2835 if (grid._imgsReadyOnce) return cb(); 2836 var imgs = Array.prototype.slice.call(grid.querySelectorAll("img")); 2837 if (!imgs.length) { 2838 grid._imgsReadyOnce = true; 2839 return cb(); 2840 } 2841 var pending = imgs.filter(function (img) { return !img.complete; }).length; 2842 if (pending === 0) { 2843 grid._imgsReadyOnce = true; 2844 return cb(); 2845 } 2846 function done() { 2847 if (!grid._imgsReadyOnce) { 2848 grid._imgsReadyOnce = true; 2849 cb(); 2850 } 2851 } 2852 imgs.forEach(function (img) { 2853 if (img.complete) return; 2854 img.addEventListener("load", function () { 2855 if (--pending <= 0) done(); 2856 }, { once: true }); 2857 img.addEventListener("error", function () { 2858 if (--pending <= 0) done(); 2859 }, { once: true }); 2860 }); 2861 } 2862 2863 function scheduleMasonryRestore(grid) { 2864 if (!grid) return; 2865 if (grid._mzRestoreT) clearTimeout(grid._mzRestoreT); 2866 2867 function pokeOwners() { 2868 grid.dispatchEvent(new CustomEvent("rz:masonry:requestInit", { bubbles: true })); 2869 grid.dispatchEvent(new CustomEvent("rz:masonry:requestLayout", { bubbles: true })); 2870 if (window.RZ && typeof window.RZ.initMasonry === "function") window.RZ.initMasonry(grid); 2871 else if (typeof window.RZInitMasonry === "function") window.RZInitMasonry(grid); 2872 else if (typeof window.initMasonry === "function") window.initMasonry(grid); 2873 } 2874 2875 function fallbackInit() { 2876 if (grid._masonryInstance || typeof window.Masonry !== "function") return; 2877 var opts = readMasonryOptions(grid); 2878 grid._masonryInstance = new Masonry(grid, opts); 2879 grid._masonryInstance.reloadItems();
2880 grid._masonryInstance.layout(); 2881 } 2882 2883 requestAnimationFrame(function () { 2884 setTimeout(function () { 2885 waitImagesOnce(grid, function () { 2886 pokeOwners(); 2887 setTimeout(function () { 2888 grid.dispatchEvent(new CustomEvent("rz:masonry:requestLayout", { bubbles: true })); 2889 if (!grid._masonryInstance) fallbackInit(); 2890 }, 80); 2891 }); 2892 }, 24); 2893 }); 2894 2895 grid._mzRestoreT = setTimeout(function () { 2896 pokeOwners(); 2897 if (!grid._masonryInstance) fallbackInit(); 2898 }, 280); 2899 } 2900 2901 function softMasonryRelayout(grid) { 2902 if (!grid) return; 2903 2904 // Ask any RZ Masonry owners to just re-layout, not re-init 2905 grid.dispatchEvent( 2906 new CustomEvent("rz:masonry:requestLayout", { bubbles: true }) 2907 ); 2908 2909 // If our fallback instance exists, just reload + layout 2910 if (grid._masonryInstance && typeof grid._masonryInstance.layout === "function") { 2911 if (typeof grid._masonryInstance.reloadItems === "function") { 2912 grid._masonryInstance.reloadItems(); 2913 } 2914 grid._masonryInstance.layout(); 2915 } 2916 } 2917 2918 // ------------ Main init ------------ 2919 2920 function initRZElementSearch() { 2921 // Only run on wrappers that explicitly opt-in via data-rz-search-module 2922 var wrappers = document.querySelectorAll( 2923 '.rz-element-wrapper[data-rz-search-module]' 2924 ); 2925 2926 wrappers.forEach(function (wrapper) { 2927 2928 // completely skip any wrapper that is part of an agenda list 2929 if ( 2930 wrapper.closest(".rz-agendalist-card-wrapper") || 2931 wrapper.querySelector(".rz-agendalist-card-wrapper, .rz-agendalist-card") 2932 ) { 2933 return; 2934 } 2935 2936 var cardContainer = wrapper.querySelector(".row"); 2937 if (!cardContainer) return; 2938 2939 // Skip tiny slider 2940 var tinyParent = cardContainer.closest( 2941 ".tiny-carousel-wrap, .tiny-slider, .tiny-slider-wrap, .rz-slider" 2942 ); 2943 if (tinyParent) return; 2944 2945 // Optional search wrapper + search input 2946 var searchWrapper = wrapper.querySelector(".rz-element-search-wrapper"); 2947 var searchInput = searchWrapper 2948 ? searchWrapper.querySelector('input[type="search"]') 2949 : null; 2950 2951 if (!wrapper.style.position) wrapper.style.position = "relative"; 2952 if (!cardContainer.style.position) cardContainer.style.position = "relative"; 2953 2954 var masonryContainer = 2955 cardContainer.hasAttribute("data-masonry") || 2956 cardContainer.hasAttribute("data-masonry-deferred") 2957 ? cardContainer 2958 : null; 2959 2960 // Wait until cards exist (RZ may inject them later) 2961 whenCardsReady(cardContainer, function (initialCards) { 2962 if (!initialCards.length) return; 2963 if (wrapper._rzSearchInit) return; 2964 wrapper._rzSearchInit = true; 2965 2966 // detect infinite "show more" mode 2967 var infiniteScrollEnabled = (function () { 2968 var val = 2969 cardContainer.getAttribute("data-infinite") || 2970 cardContainer.getAttribute("data-Infinite") || 2971 wrapper.getAttribute("data-infinite") || 2972 wrapper.getAttribute("data-Infinite") || 2973 ""; 2974 return String(val).toLowerCase() === "true"; 2975 })(); 2976 2977 // Disable Masonry transition animation on ALL Masonry grids 2978 // (prevents "flying cards" and lets CSS height transition handle smoothness) 2979 if (masonryContainer) { 2980 patchMasonryNoTransition(masonryContainer); 2981 } 2982 2983 // ------------- Core state ------------- 2984 2985 var allCards = initialCards.slice(); 2986 allCards.forEach(function (card, idx) { 2987 if (card.__origIndex == null) card.__origIndex = idx; 2988 if (!card.__origHTML) card.__origHTML = card.innerHTML; 2989 if (!card.__dateValue && card.dataset && card.dataset.date) { 2990 card.__dateValue = safeParseDate(card.dataset.date); 2991 } 2992 if (!card.__searchText) { 2993 card.__searchText = normalize(card.textContent || ""); 2994 } 2995 }); 2996 2997 // Pagination config from data-cards-per-page 2998 var cardsPerPage = (function () { 2999 var raw = 3000 (cardContainer.getAttribute("data-cards-per-page") || 3001 wrapper.getAttribute("data-cards-per-page") || 3002 "" 3003 ).trim(); 3004 3005 // If attribute missing/empty -> default 10 3006 if (!raw) return 10; 3007 3008 var v = parseInt(raw, 10); 3009 if (!Number.isFinite(v) || v <= 0) return 0; // 0 = no pagination / infinite batch size 3010 return v; 3011 })(); 3012 3013 // If infinite mode is enabled, we do NOT show classic pagination 3014 var enablePagination = !infiniteScrollEnabled && cardsPerPage > 0; 3015 var currentPage = 1; 3016 var lastRenderedCount = 0; // for "new items" detection 3017 var isShowMoreLoading = false; 3018 3019 // Loader + no results 3020 function ensureNoResults() { 3021 var el = wrapper.querySelector(".rz-no-results"); 3022 if (!el) { 3023 el = document.createElement("div"); 3024 el.className = "rz-no-results w-100 text-center opacity-75"; 3025 el.textContent = "No results found."; 3026 el.style.display = "none"; 3027 wrapper.appendChild(el); 3028 } 3029 return el; 3030 } 3031 3032 function ensureSpinner() { 3033 var el = wrapper.querySelector(".rz-search-spinner"); 3034 if (!el) { 3035 el = document.createElement("div"); 3036 el.className = "rz-search-spinner text-center"; 3037 el.style.cssText = 3038 "display:none; position:absolute; top:5rem; left:50%; transform:translateX(-50%); z-index:10;"; 3039 el.innerHTML = 3040 '<div class="spinner-border text-primary" role="status" style="width:2rem; height:2rem;"><span class="visually-hidden">Searching...</span></div>'; 3041 wrapper.appendChild(el); 3042 } 3043 return el; 3044 } 3045 3046 var noResultsEl = ensureNoResults(); 3047 var spinnerEl = ensureSpinner(); 3048 3049 // show = true/false, fromShowMore = true only when called by "Show more" 3050 function showSpinner(show, fromShowMore) { 3051 if (!spinnerEl) return; 3052 3053 var isShowMoreMode = !!fromShowMore; 3054 3055 // Position: bottom for show-more, top for normal filtering/search 3056 if (isShowMoreMode) { 3057 spinnerEl.style.top = "auto"; 3058 spinnerEl.style.bottom = "5rem"; 3059 spinnerEl.style.transform = "translateX(-50%)"; 3060 } else { 3061 spinnerEl.style.top = "5rem"; 3062 spinnerEl.style.bottom = "auto"; 3063 spinnerEl.style.transform = "translateX(-50%)"; 3064 } 3065 3066 // Clear any previous hide timer 3067 if (spinnerEl._hideTimer) { 3068 clearTimeout(spinnerEl._hideTimer); 3069 spinnerEl._hideTimer = null; 3070 } 3071 3072 if (show) { 3073 spinnerEl.style.display = "block"; 3074 requestAnimationFrame(function () { 3075 spinnerEl.classList.add("show"); 3076 }); 3077 } else { 3078 // Extra delay ONLY for infinite "show more" mode 3079 var extraDelay = isShowMoreMode ? 300 : 0; 3080 3081 spinnerEl._hideTimer = setTimeout(function () { 3082 spinnerEl.classList.remove("show"); 3083 setTimeout(function () { 3084 spinnerEl.style.display = "none"; 3085 }, 400); 3086 }, extraDelay); 3087 } 3088 } 3089 3090 function showNoResults(show) { 3091 if (!noResultsEl) return; 3092 noResultsEl.style.setProperty('display', show ? 'block' : 'none', 'important'); 3093 3094 if (infiniteScrollEnabled) { 3095 var btn = showMoreBtn || ensureShowMoreButton(); 3096 if (btn) { 3097 var wrap = btn.closest('.rz-show-more-wrap'); 3098 if (wrap) { 3099 if (show) { 3100 wrap.classList.remove('d-flex'); 3101 wrap.classList.add('d-none'); 3102 } 3103 } 3104 } 3105 } 3106 } 3107 3108 // Category + date controls (all optional if no search wrapper)
3109 var categoryDropdown = searchWrapper 3110 ? searchWrapper.querySelector(".category-dropdown") 3111 : null; 3112 var categoryMenu = categoryDropdown 3113 ? categoryDropdown.querySelector(".dropdown-menu") 3114 : null; 3115 3116 var sortWrapper = searchWrapper 3117 ? searchWrapper.querySelector(".sort-dropdown") 3118 : null; 3119 3120 var dateFromInput = wrapper.querySelector('[data-search-date="from"]'); 3121 var dateToInput = wrapper.querySelector('[data-search-date="to"]'); 3122 3123 function getSelectedCategories() { 3124 if (!categoryMenu) return []; 3125 var out = []; 3126 categoryMenu.querySelectorAll("input.option.justone:checked").forEach(function (cb) { 3127 var val = (cb.value || "").trim().toLowerCase(); 3128 if (val) out.push(val); 3129 }); 3130 return out; 3131 } 3132 3133 // ------- Category dropdown with count badges -------- 3134 function buildCategoryOptionsOnce() { 3135 if (!categoryMenu || categoryMenu._rzBuilt) return; 3136 categoryMenu._rzBuilt = true; 3137 3138 // Gather all categories + counts from cards' data-category 3139 var allCategories = new Set(); 3140 var categoryCounts = Object.create(null); 3141 3142 allCards.forEach(function (card) { 3143 (card.getAttribute("data-category") || "") 3144 .split(",") 3145 .forEach(function (cat) { 3146 var clean = cat.trim(); 3147 if (!clean) return; 3148 allCategories.add(clean); 3149 categoryCounts[clean] = (categoryCounts[clean] || 0) + 1; 3150 }); 3151 }); 3152 3153 // Remove any existing option-link items so we don't duplicate 3154 Array.prototype.slice 3155 .call(categoryMenu.querySelectorAll(".option-link")) 3156 .forEach(function (e) { e.remove(); }); 3157 3158 // If no categories at all, remove dropdown 3159 if (!allCategories.size) { 3160 if (categoryDropdown) { 3161 categoryDropdown.remove(); 3162 if (searchWrapper) { 3163 var vr = searchWrapper.querySelector(".vr"); 3164 if (vr) vr.remove(); 3165 } 3166 } 3167 return; 3168 } 3169 3170 // Build list items from categories WITH count badges 3171 Array.from(allCategories) 3172 .sort() 3173 .forEach(function (cat) { 3174 var count = categoryCounts[cat] || 0; 3175 var li = document.createElement("li"); 3176 var catEscaped = cat.replace(/"/g, """); 3177 li.innerHTML = 3178 '<div class="dropdown-item option-link" role="menuitemcheckbox" aria-checked="false" tabindex="0">' + 3179 '<label class="w-100 d-flex align-items-center justify-content-between mb-0">' + 3180 '<span class="d-flex align-items-center">' + 3181 '<input id="cat_' + catEscaped.replace(/\s+/g, "_") + '" ' + 3182 'name="options[]" type="checkbox" ' + 3183 'class="option justone form-check-input" ' + 3184 'value="' + catEscaped + '" ' + 3185 'aria-label="Filter by ' + cat + '">' + 3186 '<span class="me-2 form-check-label" for="cat_' + catEscaped.replace(/\s+/g, "_") + '">' + 3187 cat + 3188 "</span>" + 3189 "</span>" + 3190 '<span class="badge text-bg-light" aria-hidden="true">' + count + "</span>" + 3191 "</label>" + 3192 "</div>"; 3193 categoryMenu.appendChild(li); 3194 3195 var anchor = li.querySelector("a"); 3196 if (anchor) { 3197 anchor.addEventListener("click", function (e) { 3198 // prevent closing dropdown when clicking label body 3199 if (e.target === this) e.preventDefault(); 3200 }); 3201 } 3202 }); 3203 3204 var selectAll = categoryMenu.querySelector(".selectall"); 3205 var selectText = categoryMenu.querySelector(".select-text"); 3206 var dropdownTxt = categoryDropdown 3207 ? categoryDropdown.querySelector(".dropdown-text") 3208 : null; 3209 3210 // "Select all" checkbox 3211 if (selectAll) { 3212 selectAll.addEventListener("change", function () { 3213 var checked = this.checked; 3214 categoryMenu 3215 .querySelectorAll("input.option") 3216 .forEach(function (opt) { opt.checked = checked; }); 3217 3218 var totalChecked = categoryMenu.querySelectorAll("input.option:checked").length; 3219 3220 if (dropdownTxt) { 3221 dropdownTxt.textContent = totalChecked 3222 ? "(" + totalChecked + ") Selected" 3223 : "Categories"; 3224 } 3225 if (selectText) { 3226 selectText.textContent = checked ? " Deselect" : " Select"; 3227 } 3228 3229 triggerFiltering(); 3230 }); 3231 } 3232 3233 // Individual category checkboxes 3234 categoryMenu.querySelectorAll("input.option").forEach(function (opt) { 3235 opt.addEventListener("change", function () { 3236 var all = categoryMenu.querySelectorAll("input.option"); 3237 var checked = categoryMenu.querySelectorAll("input.option:checked"); 3238 3239 var parent = this.closest('[role="menuitemcheckbox"]'); 3240 if (parent) parent.setAttribute("aria-checked", this.checked ? "true" : "false"); 3241 3242 if (selectAll) { 3243 selectAll.checked = all.length === checked.length; 3244 } 3245 if (selectText) { 3246 selectText.textContent = 3247 selectAll && selectAll.checked ? " Deselect" : " Select"; 3248 } 3249 if (dropdownTxt) { 3250 dropdownTxt.textContent = checked.length 3251 ? "(" + checked.length + ") Selected" 3252 : "Categories"; 3253 } 3254 3255 triggerFiltering(); 3256 }); 3257 }); 3258 3259 } 3260 3261 // Sort 3262 var currentSort = "default"; // "default" | "newest" | "oldest" 3263 var isFirstRender = true; 3264 3265 function buildSortOnce() { 3266 if (!sortWrapper || sortWrapper._rzBuilt) return; 3267 sortWrapper._rzBuilt = true; 3268 3269 var hasDateCards = allCards.some(function (c) { return !!c.__dateValue; }); 3270 if (!hasDateCards) { 3271 sortWrapper.remove();
3272 if (searchWrapper) { 3273 var vr = searchWrapper.querySelector(".vr"); 3274 if (vr) vr.remove(); 3275 } 3276 return; 3277 } 3278 3279 sortWrapper.querySelectorAll(".dropdown-item").forEach(function (item) { 3280 item.addEventListener("click", function (e) { 3281 e.preventDefault(); 3282 sortWrapper 3283 .querySelectorAll(".dropdown-item") 3284 .forEach(function (el) { el.classList.remove("active"); }); 3285 item.classList.add("active"); 3286 3287 var txt = (item.textContent || "").trim().toLowerCase(); 3288 if (txt === "newest") currentSort = "newest"; 3289 else if (txt === "oldest") currentSort = "oldest"; 3290 else currentSort = "default"; 3291 3292 currentPage = 1; 3293 triggerFiltering(); 3294 }); 3295 }); 3296 } 3297 3298 // Pagination UI + Show more 3299 var paginationNav = null; 3300 var paginationList = null; 3301 var showMoreBtn = null; // for infinite mode 3302 3303 function ensurePaginationUI() { 3304 if (!enablePagination) return null; 3305 if (!paginationNav) { 3306 paginationNav = document.createElement("nav"); 3307 paginationNav.className = "rz-pagination mt-3"; 3308 paginationNav.setAttribute("aria-label", "Search results pagination"); 3309 paginationNav.innerHTML = 3310 '<ul class="pagination justify-content-center mb-0"></ul>'; 3311 wrapper.appendChild(paginationNav); 3312 } 3313 paginationList = paginationNav.querySelector("ul.pagination"); 3314 return paginationNav; 3315 } 3316 3317 function renderPagination(totalMatches) { 3318 if (!enablePagination || cardsPerPage <= 0) { 3319 if (paginationNav) paginationNav.style.display = "none"; 3320 return; 3321 } 3322 3323 const totalPages = Math.max( 3324 1, 3325 Math.ceil(totalMatches / Math.max(1, cardsPerPage)) 3326 ); 3327 if (totalPages <= 1) { 3328 if (paginationNav) paginationNav.style.display = "none"; 3329 return; 3330 } 3331 3332 ensurePaginationUI(); 3333 if (!paginationNav || !paginationList) return; 3334 3335 currentPage = Math.max(1, Math.min(currentPage, totalPages)); 3336 paginationNav.style.display = ""; 3337 paginationList.innerHTML = ""; 3338 3339 function addNavButton(labelHTML, targetPage, disabled) { 3340 const li = document.createElement("li"); 3341 li.className = "page-item"; 3342 if (disabled) li.classList.add("disabled"); 3343 3344 const a = document.createElement("a"); 3345 a.className = "page-link"; 3346 a.href = "#"; 3347 a.innerHTML = labelHTML; 3348 3349 if (!disabled) { 3350 a.addEventListener("click", function (e) { 3351 e.preventDefault(); 3352 if (targetPage === currentPage) return; 3353 3354 currentPage = targetPage; 3355 showSpinner(true); 3356 3357 // Scroll to the wrapper top *immediately*, 3358 // before we rebuild the grid. 3359 var top = 3360 wrapper.getBoundingClientRect().top + 3361 window.pageYOffset - 3362 100; // adjust offset as you like 3363 window.scrollTo(0, top); // instant, no smooth 3364 3365 // Now rebuild the page + Masonry 3366 requestAnimationFrame(function () { 3367 evaluateAndRender(true); // keepPage = true for currentPage 3368 showSpinner(false); 3369 }); 3370 }); 3371 } 3372 3373 li.appendChild(a); 3374 paginationList.appendChild(li); 3375 } 3376 3377 // FIRST 3378 addNavButton('<i class="bi bi-chevron-bar-left small"></i>', 1, currentPage === 1); 3379 3380 // PREV 3381 addNavButton( 3382 '<i class="bi bi-chevron-left small"></i>', 3383 currentPage - 1, 3384 currentPage === 1 3385 ); 3386 3387 // CENTER "3 of 20" 3388 const liCenter = document.createElement("li");
3389 liCenter.className = "page-item disabled"; 3390 const spanCenter = document.createElement("span"); 3391 spanCenter.className = "page-link rz-page-status"; 3392 spanCenter.innerHTML = 3393 '<span class="rz-page-current">' + currentPage + '</span> ' + 3394 '<span class="rz-page-of">of</span> ' + 3395 '<span class="rz-page-total">' + totalPages + '</span>'; 3396 liCenter.appendChild(spanCenter); 3397 paginationList.appendChild(liCenter); 3398 3399 // NEXT 3400 addNavButton( 3401 '<i class="bi bi-chevron-right small"></i>', 3402 currentPage + 1, 3403 currentPage === totalPages 3404 ); 3405 3406 // LAST 3407 addNavButton( 3408 '<i class="bi bi-chevron-bar-right small"></i>', 3409 totalPages, 3410 currentPage === totalPages 3411 ); 3412 } 3413 3414 // Show more button (infinite mode) wrapped in div 3415 function ensureShowMoreButton() { 3416 if (!infiniteScrollEnabled) return null; 3417 if (!showMoreBtn) { 3418 var wrapDiv = document.createElement("div"); 3419 wrapDiv.className = "rz-show-more-wrap d-flex justify-content-center mt-3"; 3420 3421 showMoreBtn = document.createElement("button"); 3422 showMoreBtn.type = "button"; 3423 showMoreBtn.className = "btn btn-link p-3 px-4 fs-base d-flex gap-1 align-items-center rz-show-more-btn"; 3424 showMoreBtn.textContent = "Show more"; 3425 3426 wrapDiv.appendChild(showMoreBtn); 3427 3428 // place the wrapper after the card container 3429 cardContainer.parentNode.insertBefore( 3430 wrapDiv, 3431 cardContainer.nextSibling 3432 ); 3433 } 3434 return showMoreBtn; 3435 } 3436 3437 function updateShowMoreButton(currentPageVal, totalPagesVal, noResults) { 3438 if (!infiniteScrollEnabled) { 3439 if (showMoreBtn) showMoreBtn.classList.replace('d-flex', 'd-none'); 3440 return; 3441 } 3442 3443 var btn = ensureShowMoreButton(); 3444 if (!btn) return; 3445 3446 var wrap = btn.closest('.rz-show-more-wrap'); 3447 if (!wrap) return; 3448 3449 // 1) No results at all â hide and reset button 3450 if (noResults) { 3451 wrap.classList.remove('d-flex'); 3452 wrap.classList.add('d-none'); 3453 3454 btn.disabled = false; 3455 btn.classList.remove('disabled', 'opacity-75'); 3456 btn.innerHTML = 'Show more'; 3457 return; 3458 } 3459 3460 // 2) Only one page of results â hide control 3461 if (totalPagesVal <= 1) { 3462 wrap.classList.remove('d-flex'); 3463 wrap.classList.add('d-none'); 3464 return; 3465 } 3466 3467 // 3) Multiple pages â show 3468 wrap.classList.remove('d-none'); 3469 wrap.classList.add('d-flex'); 3470 3471 if (currentPageVal >= totalPagesVal) { 3472 btn.disabled = true; 3473 btn.classList.add("disabled", "opacity-75"); 3474 btn.innerHTML = 'End of list'; 3475 } else { 3476 btn.disabled = false; 3477 btn.classList.remove('disabled', 'opacity-75'); 3478 btn.innerHTML = 'Show more'; 3479 } 3480 } 3481 3482 // Scroll helper: scroll to bottom of the newly added items 3483 function scrollToNewItems(prevCount) { 3484 try { 3485 if (!cardContainer) return; 3486 var cardsNow = cardContainer.querySelectorAll(".rz-element"); 3487 if (!cardsNow.length) return; 3488 if (prevCount >= cardsNow.length) return; 3489 3490 // Last new card = last child (bottom of the new batch) 3491 var lastNew = cardsNow[cardsNow.length - 1]; 3492 var rect = lastNew.getBoundingClientRect(); 3493 var offset = rect.bottom + window.pageYOffset - (window.innerHeight - 120); 3494 window.scrollTo({ 3495 top: offset, 3496 behavior: "smooth" 3497 }); 3498 } catch (e) { 3499 // fail silently 3500 } 3501 } 3502 3503 // Filtering core 3504 function getActiveFilters() { 3505 var rawQuery = searchInput ? (searchInput.value || "").trim() : ""; 3506 var tokensRaw = rawQuery 3507 ? rawQuery.split(/\s+/).filter(function (t) { return t.trim(); }) 3508 : []; 3509 var tokensNorm = tokensRaw.map(normalize); 3510 3511 var cats = getSelectedCategories(); 3512 var fromDate = getDateFromInput(dateFromInput); 3513 var toDate = getDateFromInput(dateToInput); 3514 3515 return { 3516 rawQuery: rawQuery, 3517 tokensRaw: tokensRaw, 3518 tokensNorm: tokensNorm, 3519 categories: cats, 3520 fromDate: fromDate, 3521 toDate: toDate 3522 }; 3523 } 3524 3525 function cardMatchesFilters(card, f) { 3526 // Text 3527 if (f.tokensNorm.length) { 3528 var txt = card.__searchText || normalize(card.textContent || ""); 3529 var ok = f.tokensNorm.some(function (tok) { 3530 return txt.indexOf(tok) !== -1; 3531 }); 3532 if (!ok) return false; 3533 } 3534 3535 // Categories 3536 if (f.categories.length) { 3537 var attr = (card.getAttribute("data-category") || "") 3538 .split(",") 3539 .map(function (c) { return normalize(c.trim()); }) 3540 .filter(Boolean); 3541 if (!attr.length) return false; 3542 var matchCat = f.categories.some(function (c) { 3543 return attr.indexOf(c) !== -1; 3544 }); 3545 if (!matchCat) return false; 3546 } 3547 3548 // Date range 3549 if (f.fromDate || f.toDate) { 3550 var d = card.__dateValue; 3551 if (!d) return false; 3552 if (f.fromDate && d < f.fromDate) return false; 3553 if (f.toDate && d > f.toDate) return false; 3554 } 3555 3556 return true; 3557 } 3558 3559 function filterAndSortCards() { 3560 var filters = getActiveFilters(); 3561 var filtered = allCards.filter(function (card) { 3562 return cardMatchesFilters(card, filters); 3563 }); 3564 3565 filtered.sort(function (a, b) { 3566 if (currentSort === "newest" || currentSort === "oldest") { 3567 var da = a.__dateValue || new Date("1900-01-01"); 3568 var db = b.__dateValue || new Date("1900-01-01"); 3569 return currentSort === "newest" ? db - da : da - db; 3570 } 3571 return (a.__origIndex || 0) - (b.__origIndex || 0); 3572 }); 3573 3574 return { filters: filters, filtered: filtered }; 3575 } 3576 3577 function evaluateAndRender(keepPage) { 3578 // Capture previous container height for smooth transition 3579 var previousHeight = cardContainer.offsetHeight || 0; 3580 3581 // reset HTML (remove previous highlights etc.) 3582 allCards.forEach(function (c) { 3583 if (c.__origHTML) c.innerHTML = c.__origHTML; 3584 }); 3585 3586 if (!keepPage) { 3587 currentPage = 1; 3588 lastRenderedCount = 0; 3589 } 3590 3591 var result = filterAndSortCards(); 3592 var filters = result.filters; 3593 var list = result.filtered; 3594 var total = list.length; 3595 3596 var pageItems = list; 3597 var newItems = []; // used only in infinite/show-more mode 3598 3599 if (infiniteScrollEnabled && cardsPerPage > 0) { 3600 var totalPages = Math.max( 3601 1, 3602 Math.ceil(total / Math.max(1, cardsPerPage)) 3603 ); 3604 if (currentPage < 1) currentPage = 1; 3605 if (currentPage > totalPages) currentPage = totalPages; 3606 3607 var end = cardsPerPage * currentPage; 3608 pageItems = list.slice(0, end); 3609 3610 var startNew = lastRenderedCount; 3611 if (startNew < 0) startNew = 0; 3612 if (startNew > end) startNew = end; 3613 3614 newItems = list.slice(startNew, Math.min(end, list.length)); 3615 lastRenderedCount = Math.min(end, list.length); 3616 3617 if (paginationNav) paginationNav.style.display = "none"; 3618 updateShowMoreButton(currentPage, totalPages, total === 0); 3619 } else if (enablePagination && cardsPerPage > 0 && total > cardsPerPage) { 3620 var totalPages2 = Math.max( 3621 1, 3622 Math.ceil(total / Math.max(1, cardsPerPage)) 3623 ); 3624 if (currentPage < 1) currentPage = 1; 3625 if (currentPage > totalPages2) currentPage = totalPages2; 3626 3627 var start = (currentPage - 1) * cardsPerPage; 3628 var end2 = start + cardsPerPage; 3629 pageItems = list.slice(start, end2); 3630 3631 renderPagination(total); 3632 if (showMoreBtn) showMoreBtn.style.display = "none"; 3633 } else { 3634 if (paginationNav) paginationNav.style.display = "none"; 3635 if (showMoreBtn) showMoreBtn.style.display = "none"; 3636 lastRenderedCount = pageItems.length; 3637 } 3638 3639 // Rebuild DOM for visible cards only 3640 while (cardContainer.firstChild) { 3641 cardContainer.removeChild(cardContainer.firstChild); 3642 } 3643 var frag = document.createDocumentFragment();
3644 pageItems.forEach(function (card) { 3645 frag.appendChild(card); 3646 }); 3647 cardContainer.appendChild(frag); 3648 3649 // 1) Filtering / searching / sorting (any grid) 3650 // We use the faster animation here. 3651 if (!keepPage && pageItems.length && !isFirstRender) { 3652 pageItems.forEach(function (card) { 3653 card.classList.add("rz-fade-in-fast"); 3654 card.addEventListener("animationend", function handler() { 3655 card.classList.remove("rz-fade-in-fast"); 3656 card.removeEventListener("animationend", handler); 3657 }); 3658 }); 3659 } 3660 3661 // 2) Classic pagination (non-infinite grids): 3662 // when moving between pages, use the main fade-zoom-up (.rz-fade-in). 3663 if (!infiniteScrollEnabled && enablePagination && keepPage && pageItems.length) { 3664 pageItems.forEach(function (card) { 3665 card.classList.add("rz-fade-in"); 3666 card.addEventListener("animationend", function handler() { 3667 card.classList.remove("rz-fade-in"); 3668 card.removeEventListener("animationend", handler); 3669 }); 3670 }); 3671 } 3672 3673 // 3) Infinite "Show more": 3674 // only animate the *newly appended* cards as they come in. 3675 if (infiniteScrollEnabled && keepPage && newItems.length) { 3676 newItems.forEach(function (card) { 3677 card.classList.add("rz-fade-in"); 3678 card.addEventListener("animationend", function handler() { 3679 card.classList.remove("rz-fade-in"); 3680 card.removeEventListener("animationend", handler); 3681 }); 3682 }); 3683 } 3684 3685 3686 // Highlight visible cards 3687 if (filters.tokensRaw.length) { 3688 pageItems.forEach(function (card) { 3689 highlightMatch(card, filters.tokensRaw); 3690 }); 3691 } 3692 3693 showNoResults(total === 0); 3694 3695 if (masonryContainer) { 3696 // If we already have a Masonry instance (either ours or RZ's), 3697 // just ask it to relayout. This keeps the inline height changing 3698 // smoothly so the CSS `transition: height` can animate it. 3699 if (masonryContainer._masonryInstance) { 3700 softMasonryRelayout(masonryContainer); 3701 } else { 3702 // First time (or if RZ hasn't wired it yet), schedule init. 3703 // After this runs once and Masonry attaches, future calls 3704 // will go through softMasonryRelayout above. 3705 scheduleMasonryRestore(masonryContainer); 3706 } 3707 } 3708 3709 // ---- Smooth height transition for this grid (Masonry or not) ---- 3710 if (!isFirstRender) { 3711 var newHeight = cardContainer.offsetHeight || 0; 3712 3713 // Only animate if it actually changed 3714 if (previousHeight && newHeight && previousHeight !== newHeight) { 3715 // Start from the old height 3716 cardContainer.style.height = previousHeight + "px"; 3717 3718 // Force reflow so browser picks up the starting value 3719 void cardContainer.offsetHeight; 3720 3721 // Animate to the new height (CSS handles timing) 3722 cardContainer.style.height = newHeight + "px"; 3723 3724 var onEnd = function (evt) { 3725 if (!evt || evt.propertyName === "height") { 3726 cardContainer.style.height = ""; // back to auto 3727 cardContainer.removeEventListener("transitionend", onEnd); 3728 } 3729 }; 3730 cardContainer.addEventListener("transitionend", onEnd); 3731 } 3732 } 3733 3734 isFirstRender = false; 3735 3736 3737 } 3738 3739 // "Show more" click handler (only infinite mode) 3740 if (infiniteScrollEnabled && cardsPerPage > 0) { 3741 var loadingMore = false; 3742 var btn = ensureShowMoreButton(); 3743 if (btn) { 3744 // Add icon 3745 btn.innerHTML = 'Show more'; 3746 3747 btn.addEventListener("click", function () { 3748 if (loadingMore || btn.disabled) return; 3749 3750 var result = filterAndSortCards(); 3751 var total = result.filtered.length; 3752 if (!total) return; 3753 3754 var maxPage = Math.max( 3755 1, 3756 Math.ceil(total / Math.max(1, cardsPerPage)) 3757 ); 3758 3759 // If already at the end 3760 if (currentPage >= maxPage) { 3761 updateShowMoreButton(currentPage, maxPage, total === 0); 3762 return; 3763 } 3764 3765 currentPage++; 3766 3767 loadingMore = true; 3768 var prevCount = lastRenderedCount; 3769 3770 // Spinner for Show more â at bottom 3771 showSpinner(true, true); 3772 3773 requestAnimationFrame(function () { 3774 evaluateAndRender(true); // keepPage = true (fade/zoom only for new batch) 3775 3776 // Hide spinner (still in Show more context, so same mode) 3777 showSpinner(false, true); 3778 3779 loadingMore = false; 3780 3781 // Delay scroll so layout settles 3782 setTimeout(function () { 3783 scrollToNewItems(prevCount); 3784 3785 // If we've reached the end, disable button and update label 3786 if (currentPage >= maxPage) { 3787 btn.disabled = true; 3788 btn.classList.add("disabled", "opacity-75"); 3789 btn.innerHTML = "End of list"; 3790 } 3791 }, 0); 3792 }); 3793 }); 3794 } 3795 } 3796 3797 // Events 3798 var debounceId; 3799 3800 function triggerFiltering() { 3801 showSpinner(true); 3802 clearTimeout(debounceId); 3803 debounceId = setTimeout(function () { 3804 requestAnimationFrame(function () { 3805 evaluateAndRender(false); 3806 showSpinner(false); 3807 }); 3808 }, 160); 3809 } 3810 3811 buildCategoryOptionsOnce(); 3812 buildSortOnce(); 3813 evaluateAndRender(false); // initial render (no AOS for first batch) 3814 3815 if (searchInput) { 3816 searchInput.addEventListener("input", triggerFiltering); 3817 searchInput.addEventListener("search", triggerFiltering); 3818 searchInput.addEventListener("keydown", function (e) { 3819 if (e.key === "Enter") { 3820 e.preventDefault(); 3821 triggerFiltering(); 3822 return; 3823 } 3824 if (e.key === "Escape") { 3825 this.value = ""; 3826 triggerFiltering(); 3827 } 3828 }); 3829 } 3830 3831 var searchForm = wrapper.querySelector("form.rz-element-search-form"); 3832 if (searchForm) { 3833 searchForm.addEventListener("submit", function (e) { 3834 e.preventDefault(); 3835 triggerFiltering(); 3836 }); 3837 } 3838 3839 if (dateFromInput) dateFromInput.addEventListener("change", triggerFiltering); 3840 if (dateToInput) dateToInput.addEventListener("change", triggerFiltering); 3841 }); 3842 });
3843 } 3844 3845 if (document.readyState === "loading") { 3846 document.addEventListener("DOMContentLoaded", initRZElementSearch); 3847 } else { 3848 initRZElementSearch(); 3849 } 3850})();
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.