Login oben sichtbar + Musik laeuft beim Seitenwechsel weiter
Login: - Kleiner "🔒 Login"-Knopf oben rechts in der Navigation, auf jeder Seite sichtbar. Fuehrt direkt zum Postfach mit der Code-Eingabe. Nahtloser Seitenwechsel: - Klicks auf interne Links tauschen nur noch <main> aus, statt die ganze Seite neu zu laden. Kopfzeile, Fussleiste und Musik-Knopf bleiben stehen — der Spotify-Player laeuft dadurch weiter. - Postfach und Zugaenge sind bewusst ausgenommen und laden normal: an Login, Sitzung und Rechten aendert sich nichts. - Seiteneigene Skripte werden der Reihe nach nachgeladen; spaete DOMContentLoaded-Anmeldungen laufen sofort, damit Inhalte wie die Team-Vorschau auch nach einem Wechsel erscheinen. - Faellt bei einem Fehler automatisch auf normales Laden zurueck. - Zurueck-/Vorwaerts-Knopf und geteilte Links funktionieren weiter. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+26
-1
@@ -191,6 +191,27 @@ header.site-header {
|
||||
font-size: .88rem;
|
||||
}
|
||||
|
||||
/* Interner Login — dezent, aber auf jeder Seite oben rechts auffindbar */
|
||||
.nav-login {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .3rem;
|
||||
padding: .5rem .9rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
font-size: .82rem;
|
||||
white-space: nowrap;
|
||||
transition: color .2s ease, border-color .2s ease, background .2s ease;
|
||||
}
|
||||
.nav-login:hover,
|
||||
.nav-login:focus-visible {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
@@ -220,7 +241,8 @@ header.site-header {
|
||||
.nav-flat { display: block; padding: .9rem 0; border-bottom: 1px solid var(--border); }
|
||||
.nav-dropdown { position: static; transform: none; box-shadow: none; width: 100%; }
|
||||
.nav-group.open .nav-dropdown { display: flex; }
|
||||
.nav-cta { align-self: flex-start; margin: 1rem 0; }
|
||||
.nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; }
|
||||
.nav-login { align-self: flex-start; }
|
||||
}
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
@@ -949,6 +971,9 @@ footer.site-footer {
|
||||
}
|
||||
}
|
||||
|
||||
/* Kurzes, dezentes Abblenden während ein Seitenwechsel geholt wird */
|
||||
body.seite-laedt main { opacity: .55; transition: opacity .15s ease; }
|
||||
|
||||
/* ==========================================================================
|
||||
MUSIK-KNOPF — kleiner schwebender Knopf unten rechts, auf jeder Seite
|
||||
|
||||
|
||||
+164
-4
@@ -52,7 +52,8 @@
|
||||
{ label: "Links", href: "links.html" },
|
||||
];
|
||||
|
||||
const currentFile = (location.pathname.split("/").pop() || "index.html");
|
||||
/* Aktuelle Datei — wird beim nahtlosen Seitenwechsel mit aktualisiert. */
|
||||
let currentFile = (location.pathname.split("/").pop() || "index.html");
|
||||
|
||||
function isActive(href) {
|
||||
return href === currentFile;
|
||||
@@ -93,6 +94,11 @@
|
||||
<nav class="nav-links">
|
||||
${NAV.map(renderNavItem).join("")}
|
||||
<a class="nav-cta" href="bewerben.html">Bewerben 🩵</a>
|
||||
<!-- Interner Einstieg: klein, aber auf JEDER Seite oben sichtbar,
|
||||
damit Dogi und das Team nie danach suchen müssen. -->
|
||||
<a class="nav-login" href="postfach.html" title="Interner Login — Postfach & Zugänge">
|
||||
<span aria-hidden="true">🔒</span> Login
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>`;
|
||||
@@ -114,12 +120,17 @@
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/* Klick außerhalb schließt offene Menüs. Bewusst NUR EINMAL angemeldet —
|
||||
renderHeader() läuft bei jedem Seitenwechsel erneut, und ein Zuhörer
|
||||
pro Wechsel würde sich sonst endlos aufsummieren. */
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!el.contains(e.target)) {
|
||||
el.querySelectorAll(".nav-group.open").forEach((g) => g.classList.remove("open"));
|
||||
const kopf = document.getElementById("site-header");
|
||||
if (kopf && !kopf.contains(e.target)) {
|
||||
kopf.querySelectorAll(".nav-group.open").forEach((g) => g.classList.remove("open"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderFooter() {
|
||||
const el = document.getElementById("site-footer");
|
||||
@@ -260,10 +271,159 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Nahtloser Seitenwechsel — damit die Musik nicht abreißt
|
||||
|
||||
Normalerweise lädt der Browser bei jedem Klick die ganze Seite neu,
|
||||
wodurch der Spotify-Player jedes Mal von vorn startet. Stattdessen
|
||||
holen wir die Zielseite im Hintergrund und tauschen nur den
|
||||
Inhaltsbereich <main> aus. Kopfzeile, Fußzeile und der Musik-Knopf
|
||||
bleiben stehen — die Musik läuft einfach weiter.
|
||||
|
||||
Wichtig für die Sicherheit: Die internen Seiten (Postfach, Zugänge)
|
||||
sind bewusst AUSGENOMMEN und laden weiterhin ganz normal. An Login,
|
||||
Sitzung und Rechten ändert sich dadurch nichts.
|
||||
|
||||
Absicherung: Geht beim Holen irgendetwas schief, wird ganz normal
|
||||
zur Zielseite navigiert. Schlimmstenfalls ist es also wie vorher.
|
||||
=================================================================== */
|
||||
const KEIN_NAHTLOSER_WECHSEL = ["postfach.html", "zugaenge.html"];
|
||||
|
||||
/* Seiteneigene Skripte melden sich fast alle über "DOMContentLoaded" an.
|
||||
Nach dem ersten Laden ist dieses Ereignis längst vorbei — ein später
|
||||
nachgeladenes Skript würde also nie starten. Deshalb: Anmeldungen, die
|
||||
NACH dem Laden eintreffen, sofort ausführen. Während des normalen
|
||||
Erstaufbaus (readyState === "loading") bleibt alles wie gehabt. */
|
||||
const anmeldenOriginal = document.addEventListener.bind(document);
|
||||
document.addEventListener = function (typ, fn, optionen) {
|
||||
if (typ === "DOMContentLoaded" && document.readyState !== "loading" && typeof fn === "function") {
|
||||
setTimeout(() => fn(new Event("DOMContentLoaded")), 0);
|
||||
return;
|
||||
}
|
||||
return anmeldenOriginal(typ, fn, optionen);
|
||||
};
|
||||
|
||||
/** Skripte der neuen Seite der Reihe nach ausführen. */
|
||||
async function skripteUebernehmen(neuesDokument) {
|
||||
const schonGeladen = new Set(
|
||||
[...document.querySelectorAll("script[src]")].map((s) => s.getAttribute("src"))
|
||||
);
|
||||
|
||||
for (const alt of [...neuesDokument.querySelectorAll("script")]) {
|
||||
const src = alt.getAttribute("src");
|
||||
|
||||
if (src) {
|
||||
// Schon vorhanden (z.B. main.js oder eine bereits geladene Datendatei):
|
||||
// nicht erneut ausführen — sonst gäbe es doppelte Deklarationen.
|
||||
if (schonGeladen.has(src)) continue;
|
||||
schonGeladen.add(src);
|
||||
await new Promise((fertig) => {
|
||||
const neu = document.createElement("script");
|
||||
neu.src = src;
|
||||
neu.async = false;
|
||||
neu.onload = neu.onerror = fertig; // auch bei Fehler weitermachen
|
||||
document.body.appendChild(neu);
|
||||
});
|
||||
} else {
|
||||
// Seiteneigenes Skript (z.B. Team-Vorschau der Startseite).
|
||||
// Läuft erst, wenn die Datendateien davor fertig geladen sind.
|
||||
const neu = document.createElement("script");
|
||||
neu.textContent = alt.textContent;
|
||||
document.body.appendChild(neu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ladeVorgang = 0;
|
||||
|
||||
async function seiteLaden(url, mitVerlauf) {
|
||||
const lauf = ++ladeVorgang;
|
||||
document.body.classList.add("seite-laedt");
|
||||
|
||||
let dok;
|
||||
try {
|
||||
const antwort = await fetch(url, { credentials: "same-origin" });
|
||||
if (!antwort.ok) throw new Error("HTTP " + antwort.status);
|
||||
dok = new DOMParser().parseFromString(await antwort.text(), "text/html");
|
||||
} catch {
|
||||
location.href = url; // Rückfall: ganz normal navigieren
|
||||
return;
|
||||
}
|
||||
if (lauf !== ladeVorgang) return; // zwischenzeitlich woanders hingeklickt
|
||||
|
||||
const neuerInhalt = dok.querySelector("main");
|
||||
const alterInhalt = document.querySelector("main");
|
||||
if (!neuerInhalt || !alterInhalt) {
|
||||
location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
alterInhalt.replaceWith(neuerInhalt);
|
||||
|
||||
// Kopf der Seite mitziehen: Titel, Farbwelt, Beschreibung
|
||||
document.title = dok.title;
|
||||
const themaNeu = dok.documentElement.getAttribute("data-theme");
|
||||
if (themaNeu) document.documentElement.setAttribute("data-theme", themaNeu);
|
||||
const beschreibungNeu = dok.querySelector('meta[name="description"]');
|
||||
const beschreibungAlt = document.querySelector('meta[name="description"]');
|
||||
if (beschreibungNeu && beschreibungAlt) {
|
||||
beschreibungAlt.setAttribute("content", beschreibungNeu.getAttribute("content"));
|
||||
}
|
||||
|
||||
if (mitVerlauf) history.pushState({ nahtlos: true }, "", url);
|
||||
currentFile = (new URL(url, location.href).pathname.split("/").pop() || "index.html");
|
||||
|
||||
renderHeader(); // markiert den richtigen Menüpunkt als aktiv
|
||||
window.scrollTo(0, 0);
|
||||
document.body.classList.remove("seite-laedt");
|
||||
|
||||
await skripteUebernehmen(dok);
|
||||
}
|
||||
|
||||
function nahtloseNavigationAktivieren() {
|
||||
// Ohne History-API oder auf file:// lieber gar nicht erst versuchen
|
||||
if (!window.history || !window.history.pushState || location.protocol === "file:") return;
|
||||
if (KEIN_NAHTLOSER_WECHSEL.includes(currentFile)) return;
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (e.defaultPrevented || e.button !== 0) return;
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; // neuer Tab o.ä.
|
||||
|
||||
const link = e.target.closest("a");
|
||||
if (!link) return;
|
||||
|
||||
const href = link.getAttribute("href");
|
||||
if (!href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) return;
|
||||
if (link.target && link.target !== "_self") return;
|
||||
if (link.hasAttribute("download")) return;
|
||||
|
||||
const ziel = new URL(href, location.href);
|
||||
if (ziel.origin !== location.origin) return; // fremde Seite
|
||||
if (!ziel.pathname.endsWith(".html")) return; // z.B. Bilddatei
|
||||
const datei = ziel.pathname.split("/").pop();
|
||||
if (KEIN_NAHTLOSER_WECHSEL.includes(datei)) return; // interner Bereich
|
||||
if (ziel.pathname === location.pathname && ziel.search === location.search) return;
|
||||
|
||||
e.preventDefault();
|
||||
seiteLaden(ziel.href, true);
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
// Zurück-/Vorwärts-Knopf im Browser
|
||||
const datei = location.pathname.split("/").pop() || "index.html";
|
||||
if (KEIN_NAHTLOSER_WECHSEL.includes(datei)) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
seiteLaden(location.href, false);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
renderHeader();
|
||||
renderFooter();
|
||||
renderFog();
|
||||
renderMusicWidget();
|
||||
nahtloseNavigationAktivieren();
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user