generated from coulomb/repo-seed
init: first commit of chatgpt prebuild
This commit is contained in:
175
engine.js
Normal file
175
engine.js
Normal file
@@ -0,0 +1,175 @@
|
||||
window.timelineEngine = {
|
||||
config: null,
|
||||
template: null,
|
||||
csvOverride: false,
|
||||
cssOverride: false,
|
||||
|
||||
async autoLoadDefaultProject() {
|
||||
// Versuche zuerst Binect-Projekt, sonst Example
|
||||
const candidates = ["binect/project.json", "example/project.json"];
|
||||
for (const path of candidates) {
|
||||
try {
|
||||
const res = await fetch(path);
|
||||
if (!res.ok) continue;
|
||||
const cfg = await res.json();
|
||||
await this.loadProjectConfigObject(cfg);
|
||||
return;
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async loadProjectConfigObject(cfg) {
|
||||
this.config = cfg;
|
||||
const name = cfg.name || "Timeline";
|
||||
document.getElementById("projectName").innerText = name;
|
||||
document.getElementById("projectSubtitle").innerText =
|
||||
cfg.description || "Projektkonfiguration geladen.";
|
||||
|
||||
// Stylesheet
|
||||
if (cfg.stylesheet && !this.cssOverride) {
|
||||
document.getElementById("dynamicCss").href = cfg.stylesheet;
|
||||
}
|
||||
|
||||
// SVG template
|
||||
if (cfg.svgTemplate) {
|
||||
try {
|
||||
this.template = await fetch(cfg.svgTemplate).then(r => r.text());
|
||||
} catch (e) {
|
||||
console.warn("SVG template could not be loaded:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// CSV data
|
||||
if (cfg.dataSource && !this.csvOverride) {
|
||||
try {
|
||||
const csvText = await fetch(cfg.dataSource).then(r => r.text());
|
||||
this.processCsv(csvText);
|
||||
} catch (e) {
|
||||
console.warn("CSV could not be loaded:", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
processCsv(text) {
|
||||
if (!this.config || !this.config.fieldMapping) {
|
||||
console.error("No config or fieldMapping found.");
|
||||
return;
|
||||
}
|
||||
const m = this.config.fieldMapping;
|
||||
|
||||
Papa.parse(text, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
complete: (res) => {
|
||||
const rows = res.data;
|
||||
const items = rows.map((r) => {
|
||||
const dueField = (m.due || []).find(f => r[f]);
|
||||
return {
|
||||
id: m.id ? r[m.id] : (r["ID"] || ""),
|
||||
title: m.title ? r[m.title] : (r["Title"] || ""),
|
||||
lane: m.lane ? (r[m.lane] || "Ohne Epic") : (r["Lane"] || "Default"),
|
||||
due: this.parseDate(dueField ? r[dueField] : null)
|
||||
};
|
||||
}).filter(i => i.title && i.due);
|
||||
|
||||
if (!items.length) {
|
||||
document.getElementById("viewer").innerHTML =
|
||||
"<em style='color:#999;'>Keine gültigen Items gefunden.</em>";
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = window.timelineGenerator.generate(items, this.config, this.template);
|
||||
document.getElementById("viewer").innerHTML = svg;
|
||||
const dlBtn = document.getElementById("downloadSvg");
|
||||
dlBtn.disabled = false;
|
||||
dlBtn.style.opacity = 1;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
parseDate(str) {
|
||||
if (!str) return null;
|
||||
str = String(str).trim();
|
||||
let d = null;
|
||||
|
||||
// YYYY/MM/DD oder YYYY-MM-DD
|
||||
let m = str.match(/^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})$/);
|
||||
if (m) {
|
||||
const [_, yy, mm, dd] = m;
|
||||
d = new Date(Number(yy), Number(mm) - 1, Number(dd));
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
// DD.MM.YYYY
|
||||
m = str.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||||
if (m) {
|
||||
const [_, dd, mm, yy] = m;
|
||||
d = new Date(Number(yy), Number(mm) - 1, Number(dd));
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
d = new Date(str);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
};
|
||||
|
||||
// --------- UI event handlers ---------
|
||||
|
||||
document.getElementById("projectInput").addEventListener("change", async (ev) => {
|
||||
const file = ev.target.files[0];
|
||||
if (!file) return;
|
||||
const text = await file.text();
|
||||
const cfg = JSON.parse(text);
|
||||
await window.timelineEngine.loadProjectConfigObject(cfg);
|
||||
});
|
||||
|
||||
document.getElementById("csvInput").addEventListener("change", async (ev) => {
|
||||
const file = ev.target.files[0];
|
||||
if (!file) return;
|
||||
const text = await file.text();
|
||||
window.timelineEngine.csvOverride = true;
|
||||
window.timelineEngine.processCsv(text);
|
||||
});
|
||||
|
||||
document.getElementById("cssInput").addEventListener("change", async (ev) => {
|
||||
const file = ev.target.files[0];
|
||||
if (!file) return;
|
||||
const cssText = await file.text();
|
||||
window.timelineEngine.cssOverride = true;
|
||||
const blob = new Blob([cssText], { type: "text/css" });
|
||||
document.getElementById("dynamicCss").href = URL.createObjectURL(blob);
|
||||
});
|
||||
|
||||
document.getElementById("svgInput").addEventListener("change", async (ev) => {
|
||||
const file = ev.target.files[0];
|
||||
if (!file) return;
|
||||
window.timelineEngine.template = await file.text();
|
||||
});
|
||||
|
||||
document.getElementById("downloadSvg").addEventListener("click", () => {
|
||||
const svg = document.querySelector("#viewer svg");
|
||||
if (!svg) return;
|
||||
|
||||
// Always external view for export: IDs ausblenden
|
||||
svg.querySelectorAll(".item-id").forEach(el => el.style.display = "none");
|
||||
|
||||
const now = new Date();
|
||||
const ts = String(now.getFullYear()).slice(2)
|
||||
+ String(now.getMonth() + 1).padStart(2, "0")
|
||||
+ String(now.getDate()).padStart(2, "0")
|
||||
+ "T"
|
||||
+ String(now.getHours()).padStart(2, "0")
|
||||
+ String(now.getMinutes()).padStart(2, "0");
|
||||
|
||||
const blob = new Blob([svg.outerHTML], { type: "image/svg+xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${ts}-timeline.svg`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user