PageSourceSearch

https://www.rsatidewater.org/assets/js/events.js?v=1.77

js rsatidewater.org collected 2026-10-02 01:07:27 UTC 40,607 bytes, 1,286 lines download raw bytes

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.