window.timelineEngine = { config: null, template: null, csvOverride: false, cssOverride: false, projectBasePath: '', showTemplatePreview() { if (!this.template) return; console.log("Showing template preview"); // Detect template theme for appropriate colors const isEnhanced = this.template.includes('Enhanced'); const isBlueTheme = this.template.includes('My Project'); const isGreenTheme = this.template.includes('Example'); // Color scheme selection let primaryColor = '#0A4D8C'; // default let secondaryColor = '#5C6B7A'; // default if (isEnhanced) { if (isBlueTheme) { primaryColor = '#3b82f6'; secondaryColor = '#60a5fa'; } else if (isGreenTheme) { primaryColor = '#2d8659'; secondaryColor = '#4a9b6b'; } } // Create sample month graphics matching the template style const sampleMonths = isEnhanced ? ` Jan 25 Feb 25 Mar 25 Apr 25 ` : ` Jan 25 Feb 25 Mar 25 Apr 25 `; // Create sample lane content matching the template style const sampleLanes = isEnhanced ? ` Example Epic EX-1: Sample Task Preview Another Epic EX-2: Template Preview ` : ` Example Epic EX-1: Sample Task Preview Another Epic EX-2: Template Preview `; // Replace placeholders with sample data let previewSvg = this.template .replace("{{MONTHS}}", sampleMonths) .replace("{{LANES}}", sampleLanes); // Add dimensions for proper preview display (using reasonable default size) const previewWidth = 720; const previewHeight = 360; previewSvg = previewSvg.replace( /]*?)>/, `` ); document.getElementById("viewer").innerHTML = previewSvg; // Initialize zoom functionality for the preview if (window.svgViewer) { window.svgViewer.resetZoom(); window.svgViewer.updateZoomControls(); } console.log("Template preview displayed with theme:", { isEnhanced, isBlueTheme, isGreenTheme }); }, updateFileStatus(fileType, filename, status = 'loaded') { const statusElement = document.getElementById(`${fileType}File`); if (statusElement) { if (filename && status === 'loaded') { statusElement.textContent = filename; statusElement.style.color = '#28a745'; statusElement.title = `Loaded: ${filename}`; } else if (filename && status === 'error') { statusElement.textContent = `Error: ${filename}`; statusElement.style.color = '#dc3545'; statusElement.title = `Failed to load: ${filename}`; } else { statusElement.textContent = 'Not loaded'; statusElement.style.color = '#6c757d'; statusElement.title = 'No file loaded'; } } // Add visual feedback with brief highlight animation if (statusElement && filename) { statusElement.style.transform = 'scale(1.05)'; setTimeout(() => { statusElement.style.transform = 'scale(1)'; }, 200); } }, resolveProjectPath(relativePath) { if (!relativePath) return relativePath; // If path is already absolute (starts with http/https) or has no base path, return as-is if (relativePath.startsWith('http') || !this.projectBasePath) { return relativePath; } const resolvedPath = this.projectBasePath + relativePath; console.log("Resolved path:", relativePath, "->", resolvedPath); return resolvedPath; }, async autoLoadDefaultProject() { console.log("Starting autoLoadDefaultProject"); // Try projects in order: binect, my-project, example const candidates = [ { path: "binect/project.json", basePath: "binect/" }, { path: "my-project/project.json", basePath: "my-project/" }, { path: "example/project.json", basePath: "example/" } ]; for (const candidate of candidates) { try { const res = await fetch(candidate.path); if (!res.ok) continue; const cfg = await res.json(); this.projectBasePath = candidate.basePath; await this.loadProjectConfigObject(cfg); console.log("Project loaded successfully from:", candidate.path); return; } catch (e) { continue; } } console.log("No project could be auto-loaded"); }, async loadProjectConfigObject(cfg) { this.config = cfg; const name = cfg.name || "Timeline"; document.getElementById("projectName").innerText = name; document.getElementById("projectSubtitle").innerText = cfg.description || "Projektkonfiguration geladen."; // Update project status this.updateFileStatus('project', name, 'loaded'); // Track loading errors for user feedback const loadingErrors = []; // Stylesheet if (cfg.stylesheet && !this.cssOverride) { const stylesheetPath = this.resolveProjectPath(cfg.stylesheet); const linkElement = document.getElementById("dynamicCss"); // Set up load/error event handlers before setting href const handleLoad = () => { console.log("Stylesheet loaded successfully:", stylesheetPath); this.updateFileStatus('css', cfg.stylesheet + ' ✨', 'loaded'); linkElement.removeEventListener('load', handleLoad); linkElement.removeEventListener('error', handleError); }; const handleError = () => { console.error("Stylesheet could not be loaded:", stylesheetPath); this.updateFileStatus('css', cfg.stylesheet, 'error'); loadingErrors.push(`Stylesheet: ${cfg.stylesheet}`); linkElement.removeEventListener('load', handleLoad); linkElement.removeEventListener('error', handleError); }; linkElement.addEventListener('load', handleLoad); linkElement.addEventListener('error', handleError); linkElement.href = stylesheetPath; } // SVG template if (cfg.svgTemplate) { try { const templatePath = this.resolveProjectPath(cfg.svgTemplate); console.log("Attempting to fetch SVG template from:", templatePath); const response = await fetch(templatePath); console.log("SVG template fetch response:", response.status, response.statusText); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } this.template = await response.text(); console.log("SVG template loaded, length:", this.template.length); this.updateFileStatus('svg', cfg.svgTemplate, 'loaded'); // Show template preview if no CSV data is loaded yet if (!this.csvOverride && document.querySelector("#viewer").innerHTML.includes("Keine Timeline verfügbar")) { this.showTemplatePreview(); } } catch (e) { console.error("SVG template could not be loaded:", e); console.error("Failed SVG template path was:", this.resolveProjectPath(cfg.svgTemplate)); this.updateFileStatus('svg', cfg.svgTemplate, 'error'); loadingErrors.push(`SVG template: ${cfg.svgTemplate}`); } } // CSV data if (cfg.dataSource && !this.csvOverride) { try { const csvPath = this.resolveProjectPath(cfg.dataSource); console.log("Attempting to fetch CSV from:", csvPath); const response = await fetch(csvPath); console.log("CSV fetch response:", response.status, response.statusText); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const csvText = await response.text(); console.log("CSV text loaded, length:", csvText.length); console.log("CSV preview:", csvText.substring(0, 200)); this.updateFileStatus('csv', cfg.dataSource, 'loaded'); this.processCsv(csvText); } catch (e) { console.error("CSV could not be loaded:", e); console.error("Failed CSV path was:", this.resolveProjectPath(cfg.dataSource)); this.updateFileStatus('csv', cfg.dataSource, 'error'); loadingErrors.push(`CSV data: ${cfg.dataSource}`); } } // Show user feedback if there were loading errors if (loadingErrors.length > 0) { setTimeout(() => { const viewer = document.getElementById("viewer"); if (viewer && !viewer.innerHTML.includes('
⚠️

Projektdateien konnten nicht geladen werden

Fehlgeschlagene Dateien:
${loadingErrors.map(err => `• ${err}`).join('
')}

💡 Tipp: Lade die Dateien manuell mit den Load-Buttons oben oder verwende einen lokalen Server (make serve).

`; } }, 500); // Small delay to let other operations complete } }, processCsv(text) { console.log("processCsv called with text length:", text?.length); if (!this.config || !this.config.fieldMapping) { console.error("No config or fieldMapping found."); return; } if (typeof Papa === 'undefined') { console.error("Papa Parse library not available"); document.getElementById("viewer").innerHTML = "Fehler: CSV Parser nicht verfügbar."; return; } const m = this.config.fieldMapping; const self = this; // Capture 'this' context Papa.parse(text, { header: true, skipEmptyLines: true, complete: (res) => { console.log("Papa.parse complete, found", res.data.length, "rows"); const rows = res.data; const items = rows.map((r) => { const dueField = (m.due || []).find(f => r[f]); const item = { 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: self.parseDate(dueField ? r[dueField] : null) }; return item; }).filter(i => i.title && i.due); console.log("Filtered to", items.length, "valid items"); if (!items.length) { document.getElementById("viewer").innerHTML = "Keine gültigen Items gefunden."; return; } // Ensure generator is available if (!window.timelineGenerator) { console.error("Timeline generator not available"); document.getElementById("viewer").innerHTML = "Fehler: Timeline Generator nicht verfügbar."; return; } try { console.log("Generating timeline with:", items, self.config, self.template ? "template loaded" : "no template"); const svg = window.timelineGenerator.generate(items, self.config, self.template); document.getElementById("viewer").innerHTML = svg; const dlBtn = document.getElementById("downloadSvg"); dlBtn.disabled = false; dlBtn.style.opacity = 1; // Initialize zoom functionality for the new SVG if (window.svgViewer) { window.svgViewer.resetZoom(); window.svgViewer.updateZoomControls(); } console.log("Timeline generated successfully"); } catch (error) { console.error("Error generating timeline:", error); document.getElementById("viewer").innerHTML = "Fehler beim Generieren der Timeline."; } } }); }, 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; } }; // --------- SVG Viewer with Zoom functionality --------- window.svgViewer = { currentZoom: 1, minZoom: 0.25, maxZoom: 3, zoomStep: 0.25, initializeZoom() { const viewer = document.getElementById("viewer"); const zoomControls = document.getElementById("zoomControls"); const zoomIn = document.getElementById("zoomIn"); const zoomOut = document.getElementById("zoomOut"); const zoomReset = document.getElementById("zoomReset"); if (!viewer || !zoomControls || !zoomIn || !zoomOut || !zoomReset) { console.warn("Zoom controls not found in DOM"); return; } // Show zoom controls when SVG is present this.updateZoomControls(); // Zoom in zoomIn.addEventListener("click", () => { this.zoomIn(); }); // Zoom out zoomOut.addEventListener("click", () => { this.zoomOut(); }); // Reset zoom zoomReset.addEventListener("click", () => { this.resetZoom(); }); // Mouse wheel zoom (with Ctrl key) viewer.addEventListener("wheel", (e) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); if (e.deltaY < 0) { this.zoomIn(); } else { this.zoomOut(); } } }); console.log("SVG zoom functionality initialized"); }, showZoomControls() { const zoomControls = document.getElementById("zoomControls"); if (zoomControls) { zoomControls.style.display = "block"; } }, hideZoomControls() { const zoomControls = document.getElementById("zoomControls"); if (zoomControls) { zoomControls.style.display = "none"; } }, zoomIn() { if (this.currentZoom < this.maxZoom) { this.currentZoom = Math.min(this.maxZoom, this.currentZoom + this.zoomStep); this.applyZoom(); } }, zoomOut() { if (this.currentZoom > this.minZoom) { this.currentZoom = Math.max(this.minZoom, this.currentZoom - this.zoomStep); this.applyZoom(); } }, resetZoom() { this.currentZoom = 1; this.applyZoom(); }, applyZoom() { const viewer = document.getElementById("viewer"); const container = document.getElementById("viewerContainer"); if (viewer && container) { viewer.style.transform = `scale(${this.currentZoom})`; // Adjust container overflow behavior based on zoom level if (this.currentZoom !== 1) { viewer.classList.add("zoomed"); container.style.overflow = "auto"; } else { viewer.classList.remove("zoomed"); container.style.overflow = "hidden"; } this.updateZoomControls(); } }, updateZoomControls() { const zoomIn = document.getElementById("zoomIn"); const zoomOut = document.getElementById("zoomOut"); const zoomReset = document.getElementById("zoomReset"); if (zoomIn && zoomOut && zoomReset) { // Update button states zoomIn.disabled = this.currentZoom >= this.maxZoom; zoomOut.disabled = this.currentZoom <= this.minZoom; // Update zoom percentage display const percentage = Math.round(this.currentZoom * 100); zoomReset.textContent = `${percentage}%`; // Show/hide controls based on whether SVG is present const hasSvg = document.querySelector("#viewer svg") !== null; if (hasSvg) { this.showZoomControls(); } else { this.hideZoomControls(); } } } }; // --------- UI event handlers --------- function setupEventHandlers() { const projectInput = document.getElementById("projectInput"); if (projectInput) { projectInput.addEventListener("change", async (ev) => { const file = ev.target.files[0]; if (!file) return; try { const text = await file.text(); const cfg = JSON.parse(text); // For manually loaded projects, try to infer base path from filename // If it's example/project.json or binect/project.json, set appropriate base path const filename = file.name; if (filename === 'project.json') { // Try to detect if this is a known project by checking the config if (cfg.name && cfg.name.includes('Example')) { window.timelineEngine.projectBasePath = 'example/'; console.log("Detected example project, setting base path to example/"); } else if (cfg.name && (cfg.name.includes('My Project') || cfg.name.includes('Roadmap'))) { window.timelineEngine.projectBasePath = 'my-project/'; console.log("Detected my-project, setting base path to my-project/"); } else { window.timelineEngine.projectBasePath = ''; console.log("Unknown project, clearing base path"); } } else { window.timelineEngine.projectBasePath = ''; } window.timelineEngine.updateFileStatus('project', file.name, 'loaded'); await window.timelineEngine.loadProjectConfigObject(cfg); // Show message about relative paths if project has data sources if (cfg.dataSource || cfg.stylesheet || cfg.svgTemplate) { const viewer = document.getElementById("viewer"); if (viewer && (viewer.innerHTML.includes("could not be loaded") || viewer.innerHTML.includes("Keine gültigen Items"))) { viewer.innerHTML += "

💡 Hinweis: Stelle sicher, dass sich die referenzierten Dateien (CSV, CSS, SVG) im gleichen Verzeichnis wie die HTML-Datei befinden."; } } } catch (error) { console.error("Error loading project:", error); window.timelineEngine.updateFileStatus('project', file.name, 'error'); } }); } const csvInput = document.getElementById("csvInput"); if (csvInput) { 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.updateFileStatus('csv', file.name, 'loaded'); window.timelineEngine.processCsv(text); }); } const cssInput = document.getElementById("cssInput"); if (cssInput) { 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); window.timelineEngine.updateFileStatus('css', file.name, 'loaded'); }); } const svgInput = document.getElementById("svgInput"); if (svgInput) { svgInput.addEventListener("change", async (ev) => { const file = ev.target.files[0]; if (!file) return; window.timelineEngine.template = await file.text(); window.timelineEngine.updateFileStatus('svg', file.name, 'loaded'); // Show template preview immediately when manually loaded window.timelineEngine.showTemplatePreview(); }); } const downloadSvg = document.getElementById("downloadSvg"); if (downloadSvg) { 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); }); } const toggleView = document.getElementById("toggleView"); if (toggleView) { toggleView.addEventListener("click", () => { const body = document.body; const isInternal = body.classList.contains("internal-mode"); if (isInternal) { body.classList.remove("internal-mode"); body.classList.add("external-mode"); toggleView.textContent = "Switch to Internal View"; // Hide IDs in external mode document.querySelectorAll(".item-id").forEach(el => el.style.display = "none"); } else { body.classList.remove("external-mode"); body.classList.add("internal-mode"); toggleView.textContent = "Switch to External View"; // Show IDs in internal mode document.querySelectorAll(".item-id").forEach(el => el.style.display = "inline"); } }); } }