generated from coulomb/repo-seed
- Add interactive zoom functionality (25%-300% with Ctrl+scroll wheel) - Fix SVG width constraints by injecting calculated dimensions into templates - Implement template preview with theme-aware sample data - Enhance UI layout with file reordering (Project → Template → CSS → Data) - Add blue theme support for my-project alongside existing green theme - Fix my-project field mapping from empty 'Übergeordnet' to 'Status' - Improve SVG viewport handling with proper scrolling and container management - Add visual zoom controls with percentage display and smart visibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
668 lines
25 KiB
JavaScript
668 lines
25 KiB
JavaScript
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").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('<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;
|
|
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;
|
|
}
|
|
};
|
|
|
|
// --------- 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 += "<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 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");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|