Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
211 lines
8.2 KiB
JavaScript
211 lines
8.2 KiB
JavaScript
// site.js — Interaktivität der Mehrseiten-Website "Operations Research mit
|
|
// Python": Dark-Mode-Toggle, mobile Sidebar, Volltextsuche, Lernfortschritt,
|
|
// Code-Kopieren/Herunterladen. Reines Vanilla-JS, keine Abhaengigkeiten.
|
|
(function () {
|
|
"use strict";
|
|
|
|
var LS_THEME = "or-theme";
|
|
var LS_FORTSCHRITT = "or-fortschritt";
|
|
|
|
function ladeFortschritt() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(LS_FORTSCHRITT) || "{}");
|
|
} catch (e) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function speichereFortschritt(daten) {
|
|
try {
|
|
localStorage.setItem(LS_FORTSCHRITT, JSON.stringify(daten));
|
|
} catch (e) {}
|
|
}
|
|
|
|
// --- Dark-Mode-Toggle -------------------------------------------------
|
|
function initTheme() {
|
|
var btn = document.getElementById("theme-toggle");
|
|
if (!btn) return;
|
|
btn.addEventListener("click", function () {
|
|
var aktuell = document.documentElement.getAttribute("data-theme");
|
|
var dunkelSystem = window.matchMedia &&
|
|
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
var istDunkel = aktuell ? aktuell === "dark" : dunkelSystem;
|
|
var neu = istDunkel ? "light" : "dark";
|
|
document.documentElement.setAttribute("data-theme", neu);
|
|
try { localStorage.setItem(LS_THEME, neu); } catch (e) {}
|
|
});
|
|
}
|
|
|
|
// --- Mobile Sidebar -----------------------------------------------------
|
|
function initSidebar() {
|
|
var sidebar = document.getElementById("sidebar");
|
|
var overlay = document.getElementById("sidebar-overlay");
|
|
var knopf = document.getElementById("sidebar-toggle");
|
|
if (!sidebar || !overlay || !knopf) return;
|
|
|
|
function schliessen() {
|
|
sidebar.classList.remove("offen");
|
|
overlay.classList.remove("offen");
|
|
overlay.hidden = true;
|
|
}
|
|
knopf.addEventListener("click", function () {
|
|
var offen = sidebar.classList.toggle("offen");
|
|
overlay.hidden = !offen;
|
|
overlay.classList.toggle("offen", offen);
|
|
});
|
|
overlay.addEventListener("click", schliessen);
|
|
sidebar.querySelectorAll("a").forEach(function (a) {
|
|
a.addEventListener("click", schliessen);
|
|
});
|
|
|
|
// Aktuelle Kapitelgruppe aufklappen, falls noch nicht offen.
|
|
var aktiv = sidebar.querySelector("li.aktiv");
|
|
if (aktiv) {
|
|
var details = aktiv.closest("details");
|
|
if (details) details.open = true;
|
|
// Nur den Sidebar-eigenen Scrollbereich verschieben, NICHT
|
|
// aktiv.scrollIntoView() verwenden: das wuerde auch das ganze Fenster
|
|
// scrollen und einen #anker-Sprung aus der URL (z.B. von einem
|
|
// Querverweis) wieder zunichtemachen.
|
|
sidebar.scrollTop = Math.max(0, aktiv.offsetTop - sidebar.clientHeight / 2);
|
|
}
|
|
}
|
|
|
|
// --- Lernfortschritt ------------------------------------------------
|
|
function initFortschritt() {
|
|
var daten = ladeFortschritt();
|
|
document.querySelectorAll('.sidebar li[data-kapitel]').forEach(function (li) {
|
|
if (daten[li.dataset.kapitel]) li.dataset.erledigt = "1";
|
|
});
|
|
var knopf = document.querySelector(".fortschritt-knopf");
|
|
if (!knopf) return;
|
|
var kapitel = knopf.dataset.kapitel;
|
|
function aktualisiereKnopf() {
|
|
var erledigt = !!daten[kapitel];
|
|
knopf.classList.toggle("erledigt", erledigt);
|
|
knopf.querySelector("span").textContent =
|
|
erledigt ? "Als gelesen markiert" : "Als gelesen markieren";
|
|
var eintrag = document.querySelector(
|
|
'.sidebar li[data-kapitel="' + kapitel + '"]');
|
|
if (eintrag) eintrag.dataset.erledigt = erledigt ? "1" : "0";
|
|
}
|
|
aktualisiereKnopf();
|
|
knopf.addEventListener("click", function () {
|
|
daten[kapitel] = !daten[kapitel];
|
|
speichereFortschritt(daten);
|
|
aktualisiereKnopf();
|
|
});
|
|
}
|
|
|
|
// --- Suche ------------------------------------------------------------
|
|
function initSuche() {
|
|
var eingabe = document.getElementById("suche-eingabe");
|
|
var ergebnisBox = document.getElementById("suche-ergebnisse");
|
|
if (!eingabe || !ergebnisBox) return;
|
|
// window.OR_SEARCH_INDEX kommt aus assets/search-index.js (als normales
|
|
// <script> geladen, nicht per fetch()) - fetch() auf lokale Dateien
|
|
// scheitert unter file:// (Seite ohne Webserver geoeffnet) an CORS.
|
|
var index = window.OR_SEARCH_INDEX || [];
|
|
|
|
function anzeigen(treffer) {
|
|
if (!treffer.length) {
|
|
ergebnisBox.innerHTML = '<p class="suche-leer">Keine Treffer.</p>';
|
|
ergebnisBox.hidden = false;
|
|
return;
|
|
}
|
|
ergebnisBox.innerHTML = treffer.slice(0, 20).map(function (t) {
|
|
var href = t.seite + (t.anker ? "#" + t.anker : "");
|
|
var snippet = t.text ? "<small>" + t.text + "</small>" : "";
|
|
return '<a href="' + href + '">' + t.titel + snippet + "</a>";
|
|
}).join("");
|
|
ergebnisBox.hidden = false;
|
|
}
|
|
|
|
eingabe.addEventListener("input", function () {
|
|
var q = eingabe.value.trim().toLowerCase();
|
|
if (q.length < 2) { ergebnisBox.hidden = true; return; }
|
|
var treffer = index.filter(function (e) {
|
|
return (e.titel && e.titel.toLowerCase().indexOf(q) !== -1) ||
|
|
(e.text && e.text.toLowerCase().indexOf(q) !== -1);
|
|
});
|
|
anzeigen(treffer);
|
|
});
|
|
document.addEventListener("click", function (ev) {
|
|
if (!ergebnisBox.contains(ev.target) && ev.target !== eingabe) {
|
|
ergebnisBox.hidden = true;
|
|
}
|
|
});
|
|
eingabe.addEventListener("keydown", function (ev) {
|
|
if (ev.key === "Escape") { ergebnisBox.hidden = true; eingabe.blur(); }
|
|
});
|
|
}
|
|
|
|
// --- Code: Kopieren-Button (+ Herunterladen, falls Programmdatei bekannt) --
|
|
function initCodeButtons() {
|
|
// window.OR_PROGRAMME kommt aus assets/programme.js (normales <script>,
|
|
// kein fetch() - siehe initSuche()).
|
|
var programmListe = window.OR_PROGRAMME || [];
|
|
|
|
document.querySelectorAll("div.sourceCode").forEach(function (box) {
|
|
var code = box.querySelector("code");
|
|
if (!code) return;
|
|
var leiste = document.createElement("div");
|
|
leiste.className = "code-btn-leiste";
|
|
|
|
var kopieren = document.createElement("button");
|
|
kopieren.type = "button";
|
|
kopieren.className = "code-btn";
|
|
kopieren.setAttribute("aria-label", "Code kopieren");
|
|
kopieren.innerHTML =
|
|
'<svg class="icon"><use href="#icon-copy"></use></svg><span>Kopieren</span>';
|
|
kopieren.addEventListener("click", function () {
|
|
navigator.clipboard.writeText(code.innerText).then(function () {
|
|
var label = kopieren.querySelector("span");
|
|
var original = label.textContent;
|
|
kopieren.classList.add("copied");
|
|
label.textContent = "Kopiert!";
|
|
setTimeout(function () {
|
|
kopieren.classList.remove("copied");
|
|
label.textContent = original;
|
|
}, 1500);
|
|
});
|
|
});
|
|
leiste.appendChild(kopieren);
|
|
box.appendChild(leiste);
|
|
|
|
// Erste Codezeile "# Name.py" (nach Shebang) -> Download-Button, wenn
|
|
// eine passende Datei unter programme/ existiert (Programme.py werden
|
|
// beim Bau nach OR_HTML/programme/ kopiert, siehe build_version_03.py).
|
|
var text = code.innerText;
|
|
var treffer = text.match(/^#\s*([A-Za-z0-9_]+\.py)\s*$/m);
|
|
if (treffer && programmListe.indexOf(treffer[1]) !== -1) {
|
|
var link = document.createElement("a");
|
|
link.className = "code-btn";
|
|
link.href = "programme/" + treffer[1];
|
|
link.setAttribute("download", "");
|
|
link.innerHTML =
|
|
'<svg class="icon"><use href="#icon-download"></use></svg><span>Herunterladen</span>';
|
|
leiste.appendChild(link);
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
initTheme();
|
|
initSidebar();
|
|
initFortschritt();
|
|
initSuche();
|
|
initCodeButtons();
|
|
});
|
|
|
|
// Bilder ohne width/height-Attribut (siehe Kapitelseiten) reflowen den
|
|
// Text erst, wenn sie fertig geladen sind - das passiert erst NACH
|
|
// DOMContentLoaded. Ein #anker-Sprung aus der URL (Querverweis, Such-
|
|
// treffer, Stichwortverzeichnis) kann dadurch zu weit oben landen. Nach
|
|
// vollstaendigem Laden (inkl. Bilder) hier zur Sicherheit erneut hinspringen.
|
|
window.addEventListener("load", function () {
|
|
if (!location.hash) return;
|
|
var ziel = document.getElementById(decodeURIComponent(location.hash.slice(1)));
|
|
if (ziel) ziel.scrollIntoView({ behavior: "instant", block: "start" });
|
|
});
|
|
})();
|