PageSourceSearch

https://physicsguy.com/star-trek/components/page-footer/page-footer.js

js physicsguy.com collected 2026-10-02 13:21:34 UTC 6,756 bytes, 165 lines download raw bytes

1// /star-trek/components/page-footer/page-footer.js
2
3// Start of the IIFE
4(function () {
5    "use strict";
6
7    const TEMPLATE_URL = new URL(
8        "./page-footer.html",
9        import.meta.url
10    ).pathname;
11
12    const templatePromise = fetch(TEMPLATE_URL)
13        .then(response => {
14            if (!response.ok) {
15                throw new Error(`Failed to load template: ${response.status}`);
16            }
17            return response.text();
18        })
19        .then(htmlString => {
20            const template = document.createElement("template");
21            template.innerHTML = htmlString.trim();
22            // Remove all dev-only elements
23            template.content.querySelectorAll('[data-dev-only="true"]')
24                .forEach(el => el.remove());
25            return template;
26        })
27        .catch(err => {
28            console.error("Error loading page-footer template:", err);
29        });
30
31    class PageFooter extends HTMLElement {
32        constructor() {
33            super();
34            this.attachShadow({ mode: "open" });
35            this.bannerElement = null;
36            this.majorLinks = [];
37
38            templatePromise.then(template => {
39                this.shadowRoot.appendChild(template.content.cloneNode(true));
40            
41                // Set element references once template is loaded
42                this.bannerElement = this.shadowRoot.querySelector("#bottom-banner");
43                this.majorLinks = this.shadowRoot.querySelectorAll(".major-link");
44            
45                // Apply initial state
46                this._applyColorStyle();
47                this._highlightCurrentPage();
48                this._applyRootPath();
49                this._applyStarTrekPath();
50            });
51        }
52
53        // Observe both required attributes
54        static get observedAttributes() {
55            return ["color-style", "current-page", "root-path", "star-trek-path"];
56        }
57
58        attributeChangedCallback(name, oldValue, newValue) {
59            // Only run logic if the template is loaded
60            if (this.bannerElement) {
61                if (name === "color-style") {
62                    this._applyColorStyle();
63                } else if (name === "current-page") {
64                    this._highlightCurrentPage();
65                } else if (name === "root-path") {
66                    this._applyRootPath();
67                    this._applyStarTrekPath();
68                } else if (name === "star-trek-path") {
69                    this._applyStarTrekPath();
70                }
71            }
72        }
73
74        // Logic to set the color style (Replaces original JS for colorStyle)
75        _applyColorStyle() {
76            const newStyle = this.getAttribute("color-style") || "blue"; // Default to blue
77            const newStyleClass = `color-style-${newStyle}`;
78        
79            // Remove all known style classes before adding the new one
80            this.bannerElement.classList.remove("color-style-blue", "color-style-yellow");
81            this.bannerElement.classList.add(newStyleClass);
82        }
83
84        // Logic to highlight the current major link (Replaces original JS for currentPage)
85        _highlightCurrentPage() {
86            const currentPageId = this.getAttribute("current-page");
87
88            this.majorLinks.forEach(link => {
89                link.classList.remove("current-page-link");
90            
91                // Check the data-link-id attribute against the current-page attribute
92                if (link.getAttribute("data-link-id") === currentPageId) {
93                    link.classList.add("current-page-link");
94                }
95            });
96        }
97
98        _getRootUrlPath(rootPath) {
99            const ensureTrailingSlash = (str) => str.endsWith('/') ? str : `${str}/`;
100            try {
101                // 1. Check if rootPath is already a complete URL
102                const url = new URL(rootPath);
103                return ensureTrailingSlash(url.toString());
104            } catch {
105                // 2. Not a complete URL, treat it as a relative path
106                try {
107                    // This handles cases like "/testing" or "testing"
108                    const baseUrl = new URL(rootPath, window.location.origin);
109                    return ensureTrailingSlash(baseUrl.toString());
110                } catch (error) {
111                    console.error(`Failed to parse root path: ${rootPath}`, error);
112                    // Fallback to origin + /
113                    return new URL("/", window.location.origin).toString();
114                }
115            }
116        }
117
118        /**
119         * Joins a base URL/path with a sub-path, ensuring exactly one slash between them.
120         * @param {string} base - The base URL (e.g., "http://site.com/root" or "/root")
121         * @param {string} extraPath - The path to append (e.g., "/news" or "news")
122         * @returns {string} The combined URL string.
123         */
124        _joinUrlPaths(base, extraPath) {
125            // 1. Remove trailing slashes from base
126            const cleanBase = base.replace(/\/+$/, "");
127            // 2. Remove leading slashes from extraPath
128            const cleanPath = extraPath.replace(/^\/+/, "");
129            // 3. Join with a single slash
130            // If cleanPath is empty (e.g. extraPath was "/"), it just returns the base
131            return cleanPath ? `${cleanBase}/${cleanPath}` : cleanBase;
132        }
133
134        _applyRootPath() {
135            const rootPath = this.getAttribute("root-path") || "/";
136            const rootUrlPath = this._getRootUrlPath(rootPath);
137
138            this.shadowRoot
139                .querySelectorAll("a[data-path-from-root]")
140                .forEach(anchor => {
141                    const path = anchor.dataset.pathFromRoot || "";
142                    anchor.href = this._joinUrlPaths(rootUrlPath, path);
143                });
144        }
145
146        _applyStarTrekPath() {
147            const rootPath = this.getAttribute("root-path") || "/";
148            // Note: starTrekPath should be relative to rootPath
149            const starTrekPath = this.getAttribute("star-trek-path") || "star-trek/";
150            const rootUrlPath = this._getRootUrlPath(rootPath);
151            // Combine rootPath + starTrekPath first, then add path
152            const fullStarTrekPath = this._joinUrlPaths(rootUrlPath, starTrekPath);
153
154            this.shadowRoot
155                .querySelectorAll("a[data-path-from-star-trek]")
156                .forEach(anchor => {
157                    const path = anchor.dataset.pathFromStarTrek || "";
158                    anchor.href = new URL(path, fullStarTrekPath).toString();
159                });
160        }
161    }
162
163    // Register the custom tag
164    customElements.define("page-footer", PageFooter);
165})(); // End of the IIFE

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.