Files
timeline-svg/engine.js
tegwick dd3ba4df58 feat: add debug information panel for data loading transparency
Added comprehensive debug panel that displays:
- Field mappings (CSV columns → item properties)
- Template placeholders (required template fields)
- CSV data preview (headers, sample data, validation warnings)

The panel appears below the timeline viewer and helps troubleshoot:
- Missing or mismatched CSV columns
- Template structure issues
- Data format problems

Shows warnings when no valid items are found with actionable
troubleshooting steps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 23:09:35 +01:00

807 lines
30 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ? `
<line x1="240" y1="75" x2="240" y2="300" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />
<rect x="210" y="70" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />
<text x="244" y="90" fill="${primaryColor}" font-size="13" font-weight="600">Jan 25</text>
<line x1="360" y1="75" x2="360" y2="300" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />
<rect x="330" y="70" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />
<text x="364" y="90" fill="${primaryColor}" font-size="13" font-weight="600">Feb 25</text>
<line x1="480" y1="75" x2="480" y2="300" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />
<rect x="450" y="70" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />
<text x="484" y="90" fill="${primaryColor}" font-size="13" font-weight="600">Mar 25</text>
<line x1="600" y1="75" x2="600" y2="300" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />
<rect x="570" y="70" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />
<text x="604" y="90" fill="${primaryColor}" font-size="13" font-weight="600">Apr 25</text>
` : `
<line x1="240" y1="75" x2="240" y2="300" stroke="#E3E8EF" />
<text x="244" y="90" fill="#5C6B7A" font-size="12">Jan 25</text>
<line x1="360" y1="75" x2="360" y2="300" stroke="#E3E8EF" />
<text x="364" y="90" fill="#5C6B7A" font-size="12">Feb 25</text>
<line x1="480" y1="75" x2="480" y2="300" stroke="#E3E8EF" />
<text x="484" y="90" fill="#5C6B7A" font-size="12">Mar 25</text>
<line x1="600" y1="75" x2="600" y2="300" stroke="#E3E8EF" />
<text x="604" y="90" fill="#5C6B7A" font-size="12">Apr 25</text>
`;
// Create sample lane content matching the template style
const sampleLanes = isEnhanced ? `
<rect x="40" y="116" width="640" height="80" fill="rgba(255,255,255,0.7)" stroke="${secondaryColor}" stroke-width="1" opacity="0.5" rx="8" />
<text x="56" y="136" fill="${primaryColor}" font-size="14" font-weight="700">Example Epic</text>
<circle cx="270" cy="156" r="6" fill="${primaryColor}" stroke="${secondaryColor}" stroke-width="2" />
<text x="282" y="160" font-size="12" fill="${primaryColor}" font-weight="500">
<tspan class="item-id">EX-1: </tspan>
<tspan class="item-title" fill="${primaryColor === '#3b82f6' ? '#1e40af' : '#1e5a3d'}">Sample Task Preview</tspan>
</text>
<rect x="40" y="212" width="640" height="80" fill="rgba(255,255,255,0.7)" stroke="${secondaryColor}" stroke-width="1" opacity="0.5" rx="8" />
<text x="56" y="232" fill="${primaryColor}" font-size="14" font-weight="700">Another Epic</text>
<circle cx="390" cy="252" r="6" fill="${primaryColor}" stroke="${secondaryColor}" stroke-width="2" />
<text x="402" y="256" font-size="12" fill="${primaryColor}" font-weight="500">
<tspan class="item-id">EX-2: </tspan>
<tspan class="item-title" fill="${primaryColor === '#3b82f6' ? '#1e40af' : '#1e5a3d'}">Template Preview</tspan>
</text>
` : `
<rect x="40" y="116" width="640" height="80" fill="#FFFFFF" stroke="#E3E8EF" rx="10" />
<text x="56" y="136" fill="#0B1F3B" font-size="14" font-weight="600">Example Epic</text>
<circle cx="270" cy="156" r="5" fill="#0A4D8C" />
<text x="282" y="160" font-size="12" fill="#0B1F3B">
<tspan class="item-id">EX-1: </tspan>
<tspan class="item-title">Sample Task Preview</tspan>
</text>
<rect x="40" y="212" width="640" height="80" fill="#FFFFFF" stroke="#E3E8EF" rx="10" />
<text x="56" y="232" fill="#0B1F3B" font-size="14" font-weight="600">Another Epic</text>
<circle cx="390" cy="252" r="5" fill="#0A4D8C" />
<text x="402" y="256" font-size="12" fill="#0B1F3B">
<tspan class="item-id">EX-2: </tspan>
<tspan class="item-title">Template Preview</tspan>
</text>
`;
// 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(
/<svg([^>]*?)>/,
`<svg$1 width="${previewWidth}" height="${previewHeight}" viewBox="0 0 ${previewWidth} ${previewHeight}">`
);
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").textContent = name;
document.getElementById("projectSubtitle").textContent =
cfg.description || "Projektkonfiguration geladen.";
// Update project status
this.updateFileStatus('project', name, 'loaded');
// Show field mappings in debug panel
this.showFieldMappings();
// 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 fields in debug panel
this.showTemplateFields();
// 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('<svg')) {
viewer.innerHTML = `
<div style="text-align:center; padding:40px 20px; color:#6c757d;">
<div style="font-size:48px; margin-bottom:16px;">⚠️</div>
<h4 style="margin:0 0 8px 0; color:#dc3545;">Projektdateien konnten nicht geladen werden</h4>
<p style="margin:0 0 12px 0; font-size:14px;">
<strong>Fehlgeschlagene Dateien:</strong><br>
${loadingErrors.map(err => `${err}`).join('<br>')}
</p>
<p style="margin:0; font-size:12px; color:#6c757d;">
💡 <strong>Tipp:</strong> Lade die Dateien manuell mit den Load-Buttons oben oder
verwende einen lokalen Server (<code>make serve</code>).
</p>
</div>
`;
}
}, 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 =
"<em style='color:#dc3545;'>Fehler: CSV Parser nicht verfügbar.</em>";
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;
// Show CSV preview in debug panel
self.showCSVPreview(text, rows);
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 =
"<em style='color:#999;'>Keine gültigen Items gefunden.</em>";
return;
}
// Ensure generator is available
if (!window.timelineGenerator) {
console.error("Timeline generator not available");
document.getElementById("viewer").innerHTML =
"<em style='color:#dc3545;'>Fehler: Timeline Generator nicht verfügbar.</em>";
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 =
"<em style='color:#dc3545;'>Fehler beim Generieren der Timeline.</em>";
}
}
});
},
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;
},
// --------- Debug Display Functions ---------
showFieldMappings() {
if (!this.config || !this.config.fieldMapping) return;
const debugInfo = document.getElementById("debugInfo");
const fieldMappingInfo = document.getElementById("fieldMappingInfo");
const fieldMappingDisplay = document.getElementById("fieldMappingDisplay");
if (debugInfo && fieldMappingInfo && fieldMappingDisplay) {
debugInfo.style.display = "block";
fieldMappingInfo.style.display = "block";
const mapping = this.config.fieldMapping;
let display = "CSV Column → Timeline Field\n";
display += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
display += `ID: ${JSON.stringify(mapping.id)}\n`;
display += `Title: ${JSON.stringify(mapping.title)}\n`;
display += `Lane: ${JSON.stringify(mapping.lane)}\n`;
display += `Due: ${JSON.stringify(mapping.due)}\n`;
if (mapping.epic) display += `Epic: ${JSON.stringify(mapping.epic)}\n`;
if (mapping.type) display += `Type: ${JSON.stringify(mapping.type)}\n`;
if (mapping.color) display += `Color: ${JSON.stringify(mapping.color)}\n`;
fieldMappingDisplay.textContent = display;
}
},
showTemplateFields() {
if (!this.template) return;
const debugInfo = document.getElementById("debugInfo");
const templateFieldsInfo = document.getElementById("templateFieldsInfo");
const templateFieldsDisplay = document.getElementById("templateFieldsDisplay");
if (debugInfo && templateFieldsInfo && templateFieldsDisplay) {
debugInfo.style.display = "block";
templateFieldsInfo.style.display = "block";
// Extract placeholders from template
const placeholders = new Set();
const placeholderRegex = /\{\{([A-Z_]+)\}\}/g;
let match;
while ((match = placeholderRegex.exec(this.template)) !== null) {
placeholders.add(match[1]);
}
let display = "Required Template Placeholders:\n";
display += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
// Group by category
const monthFields = Array.from(placeholders).filter(p => p.includes('MONTH'));
const laneFields = Array.from(placeholders).filter(p => p.includes('LANE'));
const taskFields = Array.from(placeholders).filter(p => p.includes('TASK') || p.includes('TEXT'));
const otherFields = Array.from(placeholders).filter(p =>
!monthFields.includes(p) && !laneFields.includes(p) && !taskFields.includes(p)
);
if (monthFields.length > 0) {
display += "\nMonth Fields:\n";
monthFields.forEach(f => display += `${f}\n`);
}
if (laneFields.length > 0) {
display += "\nLane Fields:\n";
laneFields.forEach(f => display += `${f}\n`);
}
if (taskFields.length > 0) {
display += "\nTask Fields:\n";
taskFields.forEach(f => display += `${f}\n`);
}
if (otherFields.length > 0) {
display += "\nOther Fields:\n";
otherFields.forEach(f => display += `${f}\n`);
}
templateFieldsDisplay.textContent = display;
}
},
showCSVPreview(csvText, parsedData) {
const debugInfo = document.getElementById("debugInfo");
const csvDataInfo = document.getElementById("csvDataInfo");
const csvDataDisplay = document.getElementById("csvDataDisplay");
if (debugInfo && csvDataInfo && csvDataDisplay) {
debugInfo.style.display = "block";
csvDataInfo.style.display = "block";
const lines = csvText.trim().split('\n');
const headers = lines[0] ? lines[0].split(',') : [];
const firstDataLine = lines[1] ? lines[1].split(',') : [];
let display = "CSV Structure Preview:\n";
display += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
display += `Headers: ${headers.join(', ')}\n\n`;
if (firstDataLine.length > 0) {
display += "First Data Row:\n";
headers.forEach((header, i) => {
display += ` ${header}: "${firstDataLine[i] || ''}"\n`;
});
}
if (parsedData) {
display += `\nParsed Rows: ${parsedData.length}\n`;
const validCount = parsedData.filter(r => {
const mapping = this.config?.fieldMapping || {};
const titleField = mapping.title;
const dueField = Array.isArray(mapping.due) ? mapping.due.find(f => r[f]) : mapping.due;
return r[titleField] && r[dueField];
}).length;
display += `Valid Items: ${validCount}\n`;
if (validCount === 0 && parsedData.length > 0) {
display += "\n⚠ WARNING: No valid items found!\n";
display += "Check that:\n";
display += " • CSV headers match field mappings\n";
display += " • Title and Due fields have values\n";
display += " • Date format is parseable (e.g., YYYY-MM-DD)\n";
}
}
csvDataDisplay.textContent = display;
}
}
};
// --------- 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 ---------
window.setupEventHandlers = function() {
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 += "<br><br><em style='color:#6c757d; font-size:12px;'>💡 Hinweis: Stelle sicher, dass sich die referenzierten Dateien (CSV, CSS, SVG) im gleichen Verzeichnis wie die HTML-Datei befinden.</em>";
}
}
} 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 fields in debug panel
window.timelineEngine.showTemplateFields();
// 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");
}
});
}
}