1/* ~~~ Determine if SPA ~~~ */ 2function resolveSPA() { 3 const path = window.location.pathname; 4 5 // Explicit PWA always implies SPA 6 if (window.RUNTIME.isPWA) return true; 7 8 // In the future, can add more complex logic here to determine SPA status... 9 10 return false; 11} 12 13window.RUNTIME.isSPA = resolveSPA(); 14console.log("SPA Mode:", window.RUNTIME.isSPA); 15 16function isAdminPath(path) { 17 const memberID = getMemberIDFromPath(path); 18 const adminPathPrefix = `/${memberID}/admin/`; 19 20 return path.startsWith(adminPathPrefix); 21} 22 23function jumpingToFromAdmin(targetPath, currentPath) { 24 if (!isAdminPath(targetPath) && isAdminPath(currentPath) || isAdminPath(targetPath) && !isAdminPath(currentPath)) { 25 return true; 26 } 27 return false; 28} 29 30function navigate(href, opts = {}) { 31 const currentPath = window.location.pathname; 32 const targetPath = new URL(href, window.location.origin).pathname; 33 34 // If not SPA, or Jumping to/from from admin to non-admin, do full reload. 35 if (!window.RUNTIME.isSPA || jumpingToFromAdmin(targetPath, currentPath)) { 36 console.log("Performing full page load for navigation to:", href); 37 window.location.href = href; 38 return; 39 } else if (window.RUNTIME.isPWA) { 40 console.log("Performing PWA SPA navigation to:", href); 41 pwaAjaxLoad(href, opts); 42 } else { 43 console.log("Falling into Non-PWA SPA navigation."); 44 } 45} 46/* ~~~ End Determine if SPA ~~~ */ 47 48//global a tag click handler 49function initAnchorHandler() { 50 $(document).on("click", "a", function (e) { 51 // If not SPA, let browser handle everything 52 if (!window.RUNTIME.isSPA) return; 53 54 const rawHref = $(this).attr("href") || $(this).data("href"); 55 56 // Skip invalid / non-nav links 57 if ( 58 !rawHref || 59 rawHref === "" || 60 rawHref === "#" || 61 rawHref.startsWith("##") || 62 $(this).hasClass("no-ajax") || 63 $(this).attr("data-spa-no-ajax") !== undefined 64 ) { 65 return; 66 } 67 68 if ( 69 e.metaKey || 70 e.ctrlKey || 71 e.shiftKey || 72 this.target === "_blank" 73 ) { 74 return; 75 } 76 77 const targetURL = new URL(rawHref, window.location.origin); 78 79 // External links â let browser handle 80 if (targetURL.origin !== window.location.origin) return; 81 82 e.preventDefault(); 83 84 // Delegate navigation 85 navigate(targetURL.href); 86 }); 87} 88 89function initFormHandler() { 90 $(document).on("submit", "form", function (e) { 91 if (!window.RUNTIME.isSPA) return; 92 93 const $form = $(this); 94 const action = $form.attr("action") || window.location.href; 95 const method = ($form.attr("method") || "GET").toUpperCase(); 96 const data = $form.serialize(); 97 98 console.log("Form submit detected for SPA handling:", $form); 99 100 if ( 101 !$form.is("[data-spa-ajax]") && 102 !$form.is("[data-spa-nav]") && 103 !$form.is("[data-pwa-download]") 104 ) { 105 console.log("Normal form submit (no SPA attributes)."); 106 return; // allow normal submit 107 /*If a normal submit occurs, you can prevent updating the back navigation page stack 108 in the PWA by adding &noHistory to the form action URL or other applicable redirect.*/ 109 } 110 111 e.preventDefault(); 112 113 if ($form.is("[data-spa-replace-state]")) { 114 console.log("SPA replaceState form submit detected."); 115 popPage(); // Remove current page from stack since we're replacing it 116 pushPage(action); // Push new page onto stack 117 } 118 119 if ($form.is("[data-pwa-download]")) { 120 console.log("PWA download form submit detected."); 121 pwaSubmitFormAndDownload(this); 122 return; 123 } 124 125 if ($form.is("[data-spa-nav]")) { 126 navigate(action, { 127 method, 128 data, 129 noBackNavPush: $form.is("[data-no-history]") 130 }); 131 return; 132 } 133 134 if ($form.is("[data-spa-ajax]")) { 135 $.ajax({ 136 url: action, 137 method, 138 data, 139 headers: { "X-PWA-Request": "true" }, 140 success(response) { 141 $form.trigger("spa:success", response); 142 }, 143 error(xhr) { 144 $form.trigger("spa:error", xhr); 145 } 146 }); 147 } 148 }); 149} 150/* ~~~ PWA Navigation Functions ~~~ */ 151 152/* ~ PRIMARY AJAX LOAD FUNCTIONS ~ */ 153function pwaAjaxLoad(href, opts = {}) { 154 const { 155 method = "GET", 156 data = null, 157 noBackNavPush = false, 158 onComplete = null 159 } = opts; 160 161 let oldHref = window.location.href; 162 let newHref = href; 163 164 $("#pwa-side-nav").removeClass("active"); 165 $("#pwa-side-nav-overlay").removeClass("active"); 166 167 // Remove non-PWA DOM 168 $("body").children().each(function () { 169 if (!this.id || !this.id.startsWith("pwa-")) { 170 $(this).remove(); 171 } 172 }); 173 174 $("#pwa-content").fadeOut(100, function () { 175 $("#pwa-content").empty(); 176 177 // AJAX load (GET or POST) 178 $.ajax({ 179 url: href, 180 method, 181 data, 182 headers: { 183 "X-PWA-Request": "true" 184 }, 185 success(response) { 186 console.log("AJAX load successful for " + href); 187 $("#pwa-content").html(response); 188 updateHeaderIcons(oldHref, newHref); 189 setPWAFloatBtns(newHref); 190 changeBNav(newHref); 191 192 applyPWAPageConfig(); 193 194 // Only push back stack if allowed 195 if (!noBackNavPush) { 196 pushPage(href); 197 } 198 199 // Only update history for GET navigations 200 if (method === "GET") { 201 window.history.pushState(null, "", href); 202 } 203 }, 204 error() { 205 $("#pwa-content").html("<p>Error loading content.</p>"); 206 }, 207 complete() { 208 $("#pwa-content").fadeIn(100, function () { 209 if (onComplete && typeof onComplete === "function") { 210 onComplete(); 211 } 212 }); 213 } 214 }); 215 }); 216} 217 218/* ~ Apply PWA Page Config -- we output <div id="pwa-page-config" for specific override settings. Ex: emergency btn in emergency mode. ~ */ 219function applyPWAPageConfig() { 220 const $config = $("#pwa-page-config"); 221 222 // Apply defaults when the loaded page has no configuration. 223 const config = { 224 // can add more default config options here as needed... 225 emergencyBtn: "false" 226 }; 227 228 if ($config.length) { 229 config.emergencyBtn = 230 $config.attr("data-emergency-btn") || "false"; 231 } 232 233 if ($config.length && config.emergencyBtn === "true") { 234 $("#pwa-emergency-btn").show(); 235 } else { 236 $("#pwa-emergency-btn").hide(); 237 } 238} 239/* ~ End Ajax Load Function ~ */ 240 241/* Parsing Tools */ 242function getRouteSegments(pathname = window.location.pathname) {
243 return pathname.split("/").filter(Boolean); 244} 245 246function getMemberIDFromPath(pathname = window.location.pathname) { 247 const segments = getRouteSegments(pathname); 248 const memberID = segments[0] || ""; 249 250 return memberID; 251} 252/* End Parsing Tools */ 253 254/* ~ Bottom Navigation Functions ~ */ 255function changeBNav(href = window.location.href) { 256 const pathname = new URL(href, window.location.origin).pathname; 257 const segments = getRouteSegments(pathname); 258 259 // segments[0] = memberID 260 // segments[1] = section 261 262 let section = ""; 263 264 if (segments[1] === "admin") { 265 section = segments[2] || ""; 266 } else { 267 section = segments[1] || ""; 268 } 269 270 const $tabs = { 271 events: $("#events-btn"), 272 conversations: $("#conversations-btn"), 273 notifications: $("#notifications-btn"), 274 class: $("#class-btn") 275 }; 276 277 // Clear all 278 Object.values($tabs).forEach($el => 279 $el.removeClass("pwa-bottom-nav-active") 280 ); 281 282 switch (section) { 283 case "events": 284 $tabs.events.addClass("pwa-bottom-nav-active"); 285 break; 286 case "forum": 287 $tabs.conversations.addClass("pwa-bottom-nav-active"); 288 break; 289 case "pwa": 290 if (segments[2] === "notifications") { 291 $tabs.notifications.addClass("pwa-bottom-nav-active"); 292 } 293 break; 294 case "class": 295 $tabs.class.addClass("pwa-bottom-nav-active"); 296 break; 297 } 298} 299/* End Bottom Navigation Functions */ 300 301/* ~ PWA Modal Functions ~ */ 302function openModal() { 303 // Reset swipe state 304 touchStartY = 0; 305 touchCurrentY = 0; 306 isDragging = false; 307 308 // Prevent body scroll 309 $("body").css("overflow", "hidden"); 310 311 $("#pwa-modal-wrapper").addClass("active"); 312 requestAnimationFrame(() => { 313 $("#pwa-modal-overlay").toggleClass("active"); 314 $("#pwa-modal").addClass("active"); 315 }); 316} 317 318function closeModal() { 319 // IMPORTANT: Clear inline styles FIRST so CSS transitions can work 320 $("#pwa-modal").css({ 321 "transform": "", 322 "transition": "" 323 }); 324 $("#pwa-modal-overlay").css("opacity", ""); 325 326 requestAnimationFrame(() => { 327 $("#pwa-modal-overlay").removeClass("active"); 328 $("#pwa-modal").removeClass("active"); 329 }); 330 331 // Re-enable body scroll 332 $("body").css("overflow", ""); 333 334 // wait for transform animation to finish 335 setTimeout(() => { 336 $("#pwa-modal-wrapper").removeClass("active"); 337 }, 420); 338} 339 340function setDonePWAModalHandler(opts = {}) { 341 const { 342 formSelector = null, 343 onDone = null, 344 closeOnDone = true 345 } = opts; 346 347 // Clear previous handlers 348 $(document).off("click.pwaModal"); 349 350 // Save handler 351 $(document).on("click.pwaModal", "#pwa-modal-done", function (e) { 352 e.preventDefault(); 353 e.stopImmediatePropagation(); 354 355 let $form = null; 356 let data = null; 357 358 if (formSelector) { 359 $form = $(formSelector); 360 data = $form.length ? $form.serialize() : null; 361 } 362 363 console.log("onDone type:", typeof onDone); 364 console.log("Has form:", !!($form && $form.length)); 365 366 // If consumer supplied save logic, call it 367 if (typeof onDone === "function") { 368 const result = onDone({ 369 $form, 370 data 371 }); 372 373 // Allow async save (Promise support) 374 if (result instanceof Promise) { 375 result.then(() => { 376 if (closeOnDone) { setTimeout(() => closeModal(), 10); } 377 }).catch(err => { 378 console.error("PWA modal save failed:", err); 379 }); 380 return; 381 } 382 } 383 384 // Default behavior: submit form via AJAX 385 if ($form && $form.length) { 386 $.ajax({ 387 url: $form.attr("action"), 388 type: $form.attr("method") || "POST", 389 data, 390 headers: { "X-PWA-Request": "true" }, 391 success() { 392 if (closeOnDone) { setTimeout(() => closeModal(), 10); } 393 }, 394 error() { 395 console.error("Error submitting modal form:", $form.attr("action")); 396 } 397 }); 398 } else { 399 if (closeOnDone) { setTimeout(() => closeModal(), 10); } 400 } 401 }); 402} 403 404function loadPWAModal(pathname, title = null, opts = {}) { 405 $("#pwa-modal-title").text(title || ""); 406 $("#pwa-modal-content").load(pathname, function (response, status) { 407 if (status === "error") { 408 console.error("Error loading PWA modal content:", response); 409 return; 410 } 411 412 setDonePWAModalHandler({ 413 formSelector: "#pwa-modal-content form", 414 ...opts 415 }); 416 417 openModal(); 418 }); 419} 420 421function loadPWAModalFromElement(sourceEl, title = null, opts = {}) { 422 $("#pwa-modal-title").text(title || ""); 423 424 const $sourceEl = $(sourceEl); 425 426 // Clone the element (deep clone, events optional) 427 const $clone = $sourceEl.clone(true, true); 428 429 $clone.find("[id]").each(function () { 430 this.id = this.id + "-clone"; 431 }); 432 433 $clone 434 .show() 435 .css({ 436 visibility: "visible", 437 display: "block", 438 opacity: 1 439 }); 440 441 $("#pwa-modal-content") 442 .empty() 443 .append($clone); 444 445 // Fire callback after clone is in the DOM 446 if (typeof opts.afterClone === "function") { 447 opts.afterClone($clone); 448 } 449 450 setDonePWAModalHandler({ 451 formSelector: "#pwa-modal-content form", 452 ...opts 453 }); 454 openModal(); 455} 456 457function initPWAModalHandlers() { 458 let touchStartY = 0; 459 let touchCurrentY = 0; 460 let isDragging = false;
461 let contentTouchStartY = 0; 462 const modalSwipeThreshold = 150; // pixels to swipe before dismissing 463 464 const $modal = $("#pwa-modal"); 465 const $overlay = $("#pwa-modal-overlay"); 466 467 /* Touch handlers for swipe-to-dismiss */ 468 $(document).on("touchstart", "#pwa-modal-pulltab, #pwa-modal-header", function (e) { 469 if (!$("#pwa-modal").hasClass("active") || $("#pwa-modal-header").hasClass("pwa-force-update")) return; 470 471 touchStartY = e.originalEvent.touches[0].clientY; 472 touchCurrentY = touchStartY; 473 isDragging = true; 474 475 // Disable CSS transitions during drag 476 $modal.css("transition", "none"); 477 }); 478 479 $(document).on("touchmove", function (e) { 480 if (!isDragging) return; 481 482 touchCurrentY = e.originalEvent.touches[0].clientY; 483 const deltaY = Math.max(0, touchCurrentY - touchStartY); // Only allow downward swipes 484 485 // Apply transform on top of the active state transform 486 $modal.css("transform", `translateY(${deltaY}px) rotateX(0deg) scale(1)`); 487 488 // Fade overlay based on swipe distance 489 const opacity = Math.max(0, 0.5 - (deltaY / 300) * 0.5); 490 $overlay.css("opacity", opacity); 491 }); 492 493 $(document).on("touchend touchcancel", function () { 494 if (!isDragging) return; 495 isDragging = false; 496 497 const deltaY = touchCurrentY - touchStartY; 498 499 // Re-enable transitions 500 $modal.css("transition", ""); 501 502 if (deltaY > modalSwipeThreshold) { 503 // Dismiss modal 504 closeModal(); 505 } else { 506 // Snap back to open position 507 $modal.css("transform", "translateY(0) rotateX(0deg) scale(1)"); 508 $overlay.css("opacity", ""); 509 } 510 }); 511 512 /* Handle content scrolling vs swipe-to-dismiss */ 513 $(document).on("touchstart", "#pwa-modal-content", function(e) { 514 contentTouchStartY = e.originalEvent.touches[0].clientY; 515 }); 516 517 $(document).on("touchmove", "#pwa-modal-content", function(e) { 518 const $content = $(this); 519 const scrollTop = $content.scrollTop(); 520 const touchY = e.originalEvent.touches[0].clientY; 521 const swipingDown = touchY > contentTouchStartY; 522 523 // Only allow swipe-to-dismiss if: 524 // 1. Scrolled to the top (scrollTop === 0) 525 // 2. AND swiping downward 526 if (scrollTop === 0 && swipingDown) { 527 // Don't stop propagation - let swipe handler work 528 return; 529 } 530 531 // Otherwise, stop propagation to allow content scrolling 532 e.stopPropagation(); 533 }); 534} 535 536/* ~ End PWA Modal Functions ~ */ 537 538 539/* ~ Header Functions ~ */ 540 541/* Header Icon Functions */ 542function getHeaderIcons(href) { 543 let dir = new URL(href, window.location.origin); 544 let dirPath = dir.pathname; 545 let urlParams = dir.searchParams; 546 let leftIcon = ""; 547 let rightIcon = ""; 548 549 const segments = getRouteSegments(dirPath); 550 551 // segments[0] = memberID 552 // segments[1] = section 553 554 const noMemberPath = segments.slice(1).join("/"); 555 556 if (noMemberPath.endsWith("forum") || noMemberPath.startsWith("forum/?") || 557 noMemberPath.endsWith("pwa/notifications") || noMemberPath.startsWith("pwa/notifications?") || 558 noMemberPath.endsWith("events") || noMemberPath.startsWith("events/?") || 559 noMemberPath.endsWith("class/dashboard") && !urlParams.has("swid") || noMemberPath.startsWith("class/dashboard?") && !urlParams.has("swid") || 560 noMemberPath.endsWith("profile") || noMemberPath.startsWith("profile?") || 561 noMemberPath.endsWith("help_manual") || noMemberPath.startsWith("help_manual?")) { 562 leftIcon = "bars"; 563 } else { 564 leftIcon = "arrow-left"; 565 } 566 567 if (noMemberPath.endsWith("forum") || noMemberPath.startsWith("forum/?")) { 568 rightIcon = "arrow-up-from-dotted-line"; 569 } else if (noMemberPath.endsWith("pwa/notifications") || noMemberPath.startsWith("pwa/notifications?")) { 570 rightIcon = "gear"; 571 } else { 572 rightIcon = null; 573 } 574 575 return { leftIcon, rightIcon }; 576} 577 578function renderLeftHeaderIcon(icon, $par) { 579 requestAnimationFrame(() => { 580 $par.addClass("slide-out"); 581 582 $par.one("transitionend", function (e) { 583 if (e.originalEvent.propertyName !== "transform") return; 584 585 $par.children().hide(); 586 587 if (icon === "bars") { 588 $("#pwa-menu-toggle").show(); 589 } else { 590 $("#pwa-back-button").show(); 591 } 592 593 requestAnimationFrame(() => { 594 $par.removeClass("slide-out"); 595 }); 596 }); 597 }); 598} 599 600function renderRightHeaderIcon(icon, $par, href) { 601 requestAnimationFrame(() => { 602 $par.addClass("slide-out"); 603 }); 604 605 $par.one("transitionend", function (e) { 606 if (e.originalEvent.propertyName !== "transform") return; 607 608 $par.children().hide(); 609 610 const memberID = getMemberIDFromPath(window.location.pathname); 611 612 if (icon === "arrow-up-from-dotted-line") { 613 $("#pwa-ToCategories").show(); 614 $par.attr("href", "/" + memberID + "/forum/category"); 615 $par.removeAttr("data-spa-no-ajax"); 616 } else if (icon === "gear") { 617 $("#pwa-PushSettings").show(); 618 $par.attr("href", "javascript:void(0);"); 619 $par.attr("data-spa-no-ajax", "true"); 620 } else { 621 $par.attr("href", ""); 622 $par.removeAttr("data-spa-no-ajax"); 623 } 624 625 requestAnimationFrame(() => { 626 $par.removeClass("slide-out"); 627 }); 628 }); 629} 630 631 632function updateHeaderIcons(oldHref, newHref) { 633 let $leftIconPar = $("#pwa-LeftHeaderIcon"); 634 let $rightIconPar = $("#pwa-RightHeaderIcon"); 635 if (oldHref === null) { 636 let icons = getHeaderIcons(newHref); 637 renderLeftHeaderIcon(icons.leftIcon, $leftIconPar, newHref); 638 renderRightHeaderIcon(icons.rightIcon, $rightIconPar, newHref); 639 if (icons.leftIcon === "bars") { 640 //clear stack and populate with new top level 641 clearStack(); 642 } 643 } else { 644 let oldIcons = getHeaderIcons(oldHref); 645 let newIcons = getHeaderIcons(newHref); 646 if (newIcons.leftIcon === "bars") { 647 clearStack(); 648 } 649 if (oldIcons.leftIcon !== newIcons.leftIcon) { 650 renderLeftHeaderIcon(newIcons.leftIcon, $leftIconPar, newHref); 651 } 652 if (oldIcons.rightIcon !== newIcons.rightIcon) { 653 renderRightHeaderIcon(newIcons.rightIcon, $rightIconPar, newHref); 654 } 655 } 656} 657/* End Header Icon Functions */ 658
659/* PWA Floating Button Functions */ 660const pwaFloatButtonRules = [ 661 { 662 selector: "#myBarcodeBtn", 663 match: "exact", 664 paths: [ 665 "pwa/notifications" 666 ] 667 } 668]; 669 670 671function routeMatches(route, path, matchType) { 672 switch (matchType) { 673 case "exact": 674 return route === path; 675 676 case "startsWith": 677 return route.startsWith(path); 678 679 case "includes": 680 return route.includes(path); 681 682 default: 683 return false; 684 } 685} 686 687function setPWAFloatBtns(href) { 688 const url = new URL(href, window.location.origin); 689 const segments = getRouteSegments(url.pathname); 690 const noMemberPath = segments.slice(1).join("/"); 691 692 pwaFloatButtonRules.forEach(rule => { 693 const shouldShow = rule.paths.some(path => 694 routeMatches(noMemberPath, path, rule.match) 695 ); 696 697 $(rule.selector).toggle(shouldShow); 698 }); 699} 700/* End PWA Floating Button Functions */ 701 702/* PWA Back Navigation Functionality */ 703window.PWA = window.PWA || {}; 704 705if (!window.PWA.STACK_KEY) { 706 window.PWA.STACK_KEY = "pwa_stack"; 707} 708 709function getStack() { 710 return JSON.parse(sessionStorage.getItem(window.PWA.STACK_KEY) || "[]"); 711} 712 713function saveStack(stack) { 714 sessionStorage.setItem(window.PWA.STACK_KEY, JSON.stringify(stack)); 715} 716 717function clearStack() { 718 sessionStorage.removeItem(window.PWA.STACK_KEY); 719 console.log("PWA navigation stack cleared", getStack()); 720} 721 722function normalizedURL(url = location.href) { 723 return new URL(url, location.origin); 724} 725 726function pushPage(url = location.href) { 727 //To prevent pushing the page on fresh page load add noHistory to urlParams 728 const stack = getStack(); 729 console.log("stack before push:", stack); 730 731 if (stack[stack.length - 1] !== url) { 732 stack.push(url); 733 saveStack(stack); 734 } 735 console.log("stack after push:", stack); 736} 737 738function popPage() { 739 const stack = getStack(); 740 const page = stack.pop(); 741 saveStack(stack); 742} 743 744function goBack() { 745 popPage(); // Remove current page from stack 746 const stack = getStack(); 747 const previous = stack.pop(); // Get previous page 748 console.log("stack previous:", previous); 749 750 saveStack(stack); 751 navigate(previous); 752} 753/* End PWA Back Navigation Functionality */ 754 755/* Other Header Functions */ 756function initPWAHeaderTouchHandlers() { 757 const memberID = getMemberIDFromPath(window.location.pathname); 758 759 $(document).on("click", "#pwa-PushSettings", function() { 760 loadPWAModal(`/${memberID}/pwa/pushpreferences`, "Push Notification Settings"); 761 }); 762 763 //To prevent form resubmission 764 if (window.history.replaceState) { 765 window.history.replaceState(null, null, window.location.href); 766 } 767} 768/* End Other Header Functions */ 769 770/* ~ End Header Functions ~ */ 771 772/* PWA Refresh Handler */ 773function initPWARefreshHandler() { 774 const ua = window.navigator.userAgent; 775 const isIOS = /iP(ad|hone|od)/.test(ua); 776 const isWebkit = /WebKit/.test(ua); 777 const isChrome = /CriOS/.test(ua) || /FxiOS/.test(ua); // Chrome/Firefox on iOS 778 779 // Cache jQuery objects 780 const $pwaHeader = $("#pwa-header"); 781 const $pwaReloadContainer = $("#pwa-reload"); 782 const $pwaReloadIcon = $("#pwa-reload i"); 783 const $pwaBars = $("#pwa-LeftHeaderIcon"); 784 const $pwaRightIcon = $("#pwa-RightHeaderIcon"); 785 const $pwaSideNav = $("#pwa-side-nav"); 786 const $pwaSideNavOverlay = $("#pwa-side-nav-overlay"); 787 const $pwaContent = $("#pwa-content"); 788 const $pwaModal = $("#pwa-modal-wrapper"); 789 790 if (isIOS && isWebkit && !isChrome) { 791 $pwaReloadContainer.addClass("pwa-ios-safari-refresh-bgcolor"); 792 } 793 794 let startY = 0; 795 let isPulling = false; 796 let pullingDistance = 0; 797 const threshold = 50; 798 const maxPull = window.innerHeight * 0.04; 799 800 $(document).on("touchstart", function (e) { 801 if ($pwaSideNav.hasClass("active") || 802 $pwaSideNavOverlay.hasClass("active") || 803 $pwaModal.hasClass("active") || 804 hasScrollableParent(e.target)) return; 805 806 const scrollTop = window.pageYOffset || 807 document.documentElement.scrollTop || 808 document.body.scrollTop || 809 0; 810 811 if (scrollTop === 0) { 812 startY = e.originalEvent.touches[0].clientY; 813 isPulling = true; 814 } 815 }); 816 817 $(document).on("touchmove", function (e) { 818 if ( 819 !isPulling || 820 ($(".ui-dialog").length && $(".ui-dialog").is(":visible")) 821 ) return; 822 823 const currentY = e.originalEvent.touches[0].clientY; 824 pullingDistance = currentY - startY; 825 826 if (pullingDistance > threshold) { 827 $pwaReloadIcon.addClass("spin"); 828 829 $pwaReloadContainer 830 .addClass("refreshTrue") 831 .removeClass("refreshFalse"); 832 833 $pwaHeader 834 .addClass("refreshTrue") 835 .removeClass("refreshFalse"); 836 837 $pwaBars 838 .addClass("slide-out"); 839 840 $pwaRightIcon 841 .addClass("slide-out"); 842 } 843 }); 844 845 $(document).on("touchend", function () { 846 if (!isPulling) return; 847 isPulling = false;
848 849 if (pullingDistance > maxPull) { 850 851 const onContentLoaded = function () { 852 $pwaReloadIcon.removeClass("spin"); 853 854 $pwaReloadContainer 855 .removeClass("refreshTrue") 856 .addClass("refreshFalse"); 857 858 $pwaHeader 859 .removeClass("refreshTrue") 860 .addClass("refreshFalse"); 861 862 $pwaBars 863 .removeClass("slide-out"); 864 865 $pwaRightIcon 866 .removeClass("slide-out") 867 }; 868 869 let href = window.location.href; 870 871 navigate(href, { 872 noBackNavPush: true, // ð important for refresh 873 onComplete: onContentLoaded 874 }); 875 } 876 877 pullingDistance = 0; 878 }); 879} 880/* End PWA Refresh Handler */ 881 882/* PWA Side Navigation Functions */ 883function toggleSideNav() { 884 $('#pwa-side-nav').toggleClass('active'); 885 $('#pwa-side-nav-overlay').toggleClass('active'); 886} 887 888function initPWASideNav() { 889 $(document).on('click', '#pwa-menu-toggle', function(e) { 890 toggleSideNav(); 891 }); 892 893 $(document).on('pointerup', (event) => { 894 if ($('#pwa-side-nav').hasClass('active')) { 895 if (!$('#pwa-side-nav').is(event.target) && !$('#pwa-side-nav').has(event.target).length && !$('#pwa-menu-toggle').is(event.target)) { 896 toggleSideNav(); 897 } 898 } 899 }); 900 901 $(document).on('click', '#pwa-back-button', function(e) { 902 e.preventDefault(); 903 goBack(); 904 }); 905} 906/* End PWA Side Navigation Functions */ 907 908/* ~~~ End PWA Navigation Functions ~~~ */ 909 910/* ~~~ Conversations PWA Navigation JS ~~~ */ 911function initConversationTouchHandler() { 912 const memberID = getMemberIDFromPath(window.location.pathname); 913 914 $(document).on('click.convo', '.threadRow', function(e) { 915 const threadid = $(this).closest('.threadRow').data('threadid'); 916 if ( 917 $('.threadTitle').is(e.target) || 918 $(e.target).closest('.addInfoPopup').length || 919 $(e.target).closest('.bubbleSC, .bubbleGen, .bubbleClass').length 920 ) return; 921 href = `/${memberID}/forum/thread?threadid=${threadid}&daysBack=${$('#daysBack').val()}&cSelect=${$('#cSelect').val()}&keywords=${encodeURIComponent($('#keywords').val())}`; 922 navigate(href); 923 }); 924 925 $(document).on('click.convo', '.categoryRow', function(e) { 926 const categoryid = $(this).data('categoryid'); 927 href = `/${memberID}/forum/?daysBack=9999&cSelect=${categoryid}&keywords=`; 928 navigate(href); 929 }); 930} 931/* ~~~ End conversations PWA Navigation JS ~~~ */ 932 933/* ~~~ notifications.cfm JS ~~~ */ 934// Show "No Notifications" message if there are no notification groups left 935function showNoNotifcationsMessage() { 936 if ($('.notiGroupContainer').length === 0) { 937 $('.noNotifMsg').css({display: 'flex', opacity: 0}).animate({opacity: 1}, 300); 938 } 939} 940 941// Trash (delete) a notification item and move notifications around appropriately 942function trashNotification(item) { 943 const $item = $(item); 944 const $notifGroupContainer = $item.closest('.notiGroupContainer'); 945 const $notiGroup = $notifGroupContainer.find('.notiGroup'); 946 const $notifGroupHeader = $notifGroupContainer.find('.notiGroupHeader'); 947 const $notifGroupTrashIcon = $notifGroupHeader.find('.notiTrashIcon'); 948 const $moreNotifs = $notifGroupContainer.find('.moreNotifs'); 949 const $moreNotifsBtn = $notifGroupContainer.find('.moreNotifsBtn'); 950 let $trash = $item.children('div.notiTrash').first(); 951 let $trashIcon = $trash.find('i.notiTrashIcon').first(); 952 953 $item.css("transition", "transform 0.3s ease"); 954 const $num = $notiGroup.children('.notiContainer').length; 955 const $moreNum = $moreNotifs.find('.notiContainer').length; 956 const notiCount = $num + $moreNum; 957 958 $trashIcon.addClass("shake"); 959 setTimeout(function () { 960 $trash.css("transition", "transform 0.25s ease"); 961 $trash.css("transform", "translateX(6%)"); 962 setTimeout(function () { 963 slideFadeOut($item, 250); 964 setTimeout(function () { 965 if (notiCount <= 1) { 966 $notifGroupTrashIcon.addClass("shake"); 967 slideFadeOut($notifGroupHeader, 250, function() { 968 $item.closest('.notiGroupContainer').remove(); 969 showNoNotifcationsMessage(); 970 }); 971 } else { 972 $item.remove(); 973 } 974 }, 250); 975 }, 250); 976 }, 250); 977 978 if ($item.closest('.moreNotifs').length == 0 && !$moreNotifs.is(':visible')) { 979 // Item was in main list 980 const remainingNotis = $notiGroup.children('.notiContainer'); 981 if (remainingNotis.length < 4 && $moreNotifs.children('.notiContainer').length > 0) { 982 // Move one from moreNotifs to main list 983 const $firstMoreNoti = $moreNotifs.children('.notiContainer').first(); 984 $firstMoreNoti.detach().insertBefore($notiGroup.find('.moreNotifsContainer')).css({ 985 display: 'none', 986 opacity: 0 987 }).slideDown(250).animate({opacity: 1}, 250); 988 989 // Update moreNotifsBtn text 990 const newMoreCount = $moreNotifs.children('.notiContainer').length; 991 if (newMoreCount > 0) { 992 $moreNotifsBtn.html('See <span class="moreNotifsNum">' + newMoreCount + '</span> More <i class="fa-regular fa-angle-down"></i>'); 993 } else { 994 $moreNotifsBtn.remove(); 995 } 996 } 997 } else if ($item.closest('.moreNotifs').length == 0 && $moreNotifs.is(':visible')) { 998 const $firstMoreNoti = $moreNotifs.children('.notiContainer').first(); 999 if ($firstMoreNoti.length > 0) { 1000 $firstMoreNoti.detach().insertBefore($notiGroup.find('.moreNotifsContainer')); 1001 } 1002 } 1003 if ($moreNotifs.children('.notiContainer').length == 0 || ($moreNotifs.children('.notiContainer').length == 1 && $item.closest('.moreNotifs').length == 1)) { 1004 $moreNotifsBtn.remove(); 1005 } 1006} 1007 1008// Set or clear app badge (used in markResponded) 1009function setBadgeCount(count) { 1010 if ('setAppBadge' in navigator) { 1011 if (count > 0) { 1012 navigator.setAppBadge(count).catch((error) => { 1013 console.error('Error setting app badge:', error); 1014 }); 1015 } else { 1016 navigator.clearAppBadge().catch((error) => { 1017 console.error('Error clearing app badge:', error); 1018 }); 1019 } 1020 } 1021} 1022 1023// Mark notifications as responded in backend and update badge count 1024function markResponded(tag, subtractCount) { 1025 let notifCount = $('#notiTray').data('notifcount'); 1026 1027 $.ajax({ 1028 url: window.location.pathname, 1029 type: "POST", 1030 data: { 1031 responded: 1, 1032 tag: tag 1033 } 1034 }); 1035 notifCount -= subtractCount; 1036 setBadgeCount(notifCount); 1037} 1038 1039// Notification Handlers 1040function getNotiSwipeThreshold() { 1041 return Math.round(window.innerWidth * 0.20); 1042} 1043 1044// Clear All Notifications in Group Button Handler 1045function initNotificationTouchHandlers() { 1046 let notifTouch = { 1047 startX: 0, 1048 currentX: 0, 1049 dragging: false, 1050 } 1051 1052 $(document) 1053 .on("touchstart.noti", ".notiContainer", function (e) { 1054 const $tag = $(this).data("tag"); 1055 if ($tag.endsWith("clear")) { 1056 e.preventDefault(); 1057 trashNotification($(this)); 1058 markResponded($tag, 1); 1059 return; 1060 } 1061 const $item = $(this); 1062 const $trash = $item.children('.notiTrash').first(); 1063 1064 notifTouch.startX = e.originalEvent.touches[0].clientX; 1065 notifTouch.dragging = true; 1066 notifTouch.currentX = 0; 1067 1068 $item.css("transition", "none"); 1069 $trash.css("transition", "none"); 1070 }) 1071
1072 .on("touchmove.noti", ".notiContainer", function (e) { 1073 if (!notifTouch.dragging) return; 1074 1075 const $item = $(this); 1076 const $trash = $item.children('.notiTrash').first(); 1077 const moveX = e.originalEvent.touches[0].clientX; 1078 const threshold = getNotiSwipeThreshold(); 1079 notifTouch.currentX = moveX - notifTouch.startX; 1080 1081 console.log("Threshold: " + -threshold + ", CurrentX: " + notifTouch.currentX); 1082 1083 if ( notifTouch.currentX < 0 && notifTouch.currentX > -threshold) { // stop at threshold 1084 console.log("Moving to " + notifTouch.currentX); 1085 $trash.css("transform", "translateX(calc(6% + " + notifTouch.currentX + "px))"); 1086 } else if (notifTouch.currentX < -threshold) { 1087 notifTouch.currentX = -threshold; 1088 } 1089 }) 1090 1091 .on("touchend.noti", ".notiContainer", function () { 1092 const $item = $(this); 1093 const $trash = $item.children('div.notiTrash').first(); 1094 const $notifTag = $item.data("tag"); 1095 const threshold = getNotiSwipeThreshold(); 1096 notifTouch.dragging = false; 1097 if (notifTouch.currentX == -threshold) { 1098 trashNotification($item); 1099 markResponded($notifTag, 1); 1100 } else { 1101 // slide item and trash back smoothly 1102 $trash.css("transition", "transform 0.3s ease"); 1103 $trash.css("transform", "translateX(6%)"); 1104 } 1105 }); 1106 1107 $(document).on("click.noti", ".notiGroupClearBtn", function (event) { 1108 event.preventDefault(); 1109 event.stopPropagation(); 1110 1111 console.log("Clear All Notifications in Group button clicked"); 1112 1113 const $groupContainer = $(this).closest('.notiGroupContainer'); 1114 const $groupHeader = $groupContainer.find('.notiGroupHeader'); 1115 const $notiGroup = $groupContainer.find('.notiGroup'); 1116 const $notifItems = $groupContainer.find('.notiContainer'); 1117 1118 const tags = $notifItems 1119 .map(function () { 1120 const t = $(this).data("tag"); 1121 return typeof t === "string" && t.length ? t : null; 1122 }) 1123 .get(); 1124 1125 const tagList = tags.join(","); 1126 1127 console.log("Tag List for group:", tagList); 1128 1129 // Remove Notification Group 1130 $notiGroup.slideToggle(200, function () { 1131 setTimeout(function () { 1132 const $trashIcons = $groupHeader.find('.notiTrashIcon'); 1133 $trashIcons.addClass('shake'); 1134 setTimeout(function () { 1135 $trashIcons.removeClass('shake'); 1136 $groupContainer.slideToggle(250, function () { 1137 $groupContainer.remove(); 1138 showNoNotifcationsMessage(); 1139 }); 1140 }, 350); 1141 }, 200); 1142 }); 1143 1144 markResponded(tagList, $notifItems.length); 1145 }); 1146 1147 $(document).on('click', '.moreNotifsBtn', function(event) { 1148 event.stopPropagation(); 1149 const $group = $(this).closest('.notiGroupContainer'); 1150 const $moreNotifs = $group.find('.moreNotifs'); 1151 if (!$moreNotifs.is(':visible')) { 1152 $(this).html('Close <i class="fa-regular fa-angle-up"></i>'); 1153 } else { 1154 const num = $moreNotifs.find('.notiContainer').length; 1155 $(this).html('See <span class="moreNotifsNum">' + num + '</span> More <i class="fa-regular fa-angle-down"></i>'); 1156 } 1157 $moreNotifs.slideToggle(200); 1158 }); 1159} 1160/* ~~~ End notifications.cfm JS ~~~ */ 1161 1162/* ~~~ Quill Functions ~~~ */ 1163function updateDispTxt(text, displayContainer) { 1164 const $displayContainer = $(displayContainer); 1165 $displayContainer.html(text); 1166} 1167 1168function toggleQuillAndDisplay(quillContainer, displayContainer, text) { 1169 const $quillContainer = $(quillContainer); 1170 const $displayContainer = $(displayContainer); 1171 if ($quillContainer.is(":visible")) { 1172 // Hide Quill, show display 1173 slideFadeOut($quillContainer, 300, function() { 1174 $quillContainer.hide(); 1175 console.log("text: ", text) 1176 updateDispTxt(text, displayContainer);
1177 $displayContainer.show(); 1178 slideFadeIn($displayContainer, 300); 1179 }); 1180 } else { 1181 // Hide display, show Quill 1182 slideFadeOut($displayContainer, 300, function() { 1183 $displayContainer.hide(); 1184 $quillContainer.show(); 1185 slideFadeIn($quillContainer, 300); 1186 }); 1187 } 1188} 1189 1190(function initEditWithQuillHandler() { 1191 $(document).on("click", ".editWithQuill", function () { 1192 const $parContainer = $(this).closest(".quillParContainer"); 1193 const quillContainer = $parContainer.find(".quillContainer"); 1194 const displayContainer = $parContainer.find(".dispContainer"); 1195 const text = $parContainer.find(".ql-editor").html(); 1196 toggleQuillAndDisplay(quillContainer, displayContainer, text); 1197 }); 1198})(); 1199/* ~~~ End Quill Functions ~~~ */ 1200 1201/* ~~~ Class Functions ~~~ */ 1202function showNewSubmissionForm() { 1203 if ($(".assignUploadForm").is(":visible")) { 1204 $("#newSubmissionBtn").removeClass("invertedAccentBtn").addClass("accentBtn"); 1205 slideFadeOut($(".assignUploadForm"), 300, function() { 1206 $(".assignUploadForm").hide(); 1207 }); 1208 } else { 1209 $("#newSubmissionBtn").removeClass("accentBtn").addClass("invertedAccentBtn"); 1210 $(".assignUploadForm").show(); 1211 slideFadeIn($(".assignUploadForm"), 300); 1212 } 1213} 1214 1215(function() { 1216 if (window.__initClassHandlers) return; 1217 window.__initClassHandlers = true; 1218 1219 $(document).on("click", "#newSubmissionBtn", function() { 1220 showNewSubmissionForm(); 1221 }); 1222})(); 1223/* ~~~ End Class Functions ~~~ */ 1224 1225/* ~~~ Barcode Functions ~~~ */ 1226function initBarcodeModal() { 1227 $(document).on("click", "#myBarcodeBtn", function() { 1228 loadPWAModalFromElement("#pwa-MyBarcodes", "Check-in Barcodes", { 1229 afterClone($clone) { 1230 new Swiper($clone[0], { 1231 slidesPerView: 1, 1232 pagination: { 1233 el: $clone.find(".swiper-pagination")[0], 1234 clickable: true 1235 } 1236 }); 1237 }, 1238 1239 onDone() { 1240 closeModal(); 1241 } 1242 }); 1243 }); 1244} 1245 1246function initBarcodeHandler() { 1247 initBarcodeModal(); 1248} 1249/* ~~~ End Barcode Functions ~~~ */ 1250 1251 1252/* ~~~ PWA initialization ~~~ */ 1253// This function is called once on initial PWA load in the footer 1254 1255function initPWA() { 1256 // Set header icons 1257 updateHeaderIcons(null, window.location.href); 1258 1259 // Override the default theme color for iOS Safari 1260 const rootStyles = getComputedStyle(document.documentElement); 1261 const metaColor2 = rootStyles.getPropertyValue('--theme-color-swap-header').trim(); 1262 setThemeColor(metaColor2); 1263 1264 // Set up PWA floating buttons 1265 setPWAFloatBtns(window.location.href); 1266 1267 // Set up active bottom nav icon 1268 changeBNav(); 1269 1270 // Push the initial page on first load --> unless noHistory is set 1271 if (new URLSearchParams(window.location.search).has("noHistory") === false) { 1272 pushPage(); 1273 } 1274 1275 // Initialize handlers 1276 initAnchorHandler(); 1277 initFormHandler(); 1278 initPWAHeaderTouchHandlers(); 1279 initPWAModalHandlers(); 1280 initPWASideNav(); 1281 initPWARefreshHandler(); 1282 initConversationTouchHandler(); 1283 initNotificationTouchHandlers(); 1284 initBarcodeHandler(); 1285} 1286/* ~~~ End PWA initialization ~~~ */
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.