2025-11-18 21:36:59 +01:00
window . timelineEngine = {
config : null ,
template : null ,
csvOverride : false ,
cssOverride : false ,
2026-01-23 17:15:36 +01:00
csvData : null , // Store current CSV text
cssData : null , // Store current CSS text
2025-11-18 21:36:59 +01:00
2025-11-19 00:20:22 +01:00
projectBasePath : '' ,
2025-11-19 00:46:58 +01:00
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 < / t e x t >
< 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 < / t e x t >
< 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 < / t e x t >
< 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 < / t e x t >
` : `
< line x1 = "240" y1 = "75" x2 = "240" y2 = "300" stroke = "#E3E8EF" / >
< text x = "244" y = "90" fill = "#5C6B7A" font - size = "12" > Jan 25 < / t e x t >
< line x1 = "360" y1 = "75" x2 = "360" y2 = "300" stroke = "#E3E8EF" / >
< text x = "364" y = "90" fill = "#5C6B7A" font - size = "12" > Feb 25 < / t e x t >
< line x1 = "480" y1 = "75" x2 = "480" y2 = "300" stroke = "#E3E8EF" / >
< text x = "484" y = "90" fill = "#5C6B7A" font - size = "12" > Mar 25 < / t e x t >
< line x1 = "600" y1 = "75" x2 = "600" y2 = "300" stroke = "#E3E8EF" / >
< text x = "604" y = "90" fill = "#5C6B7A" font - size = "12" > Apr 25 < / t e x t >
` ;
// 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 < / t e x t >
< 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 : < / t s p a n >
< tspan class = "item-title" fill = "${primaryColor === '#3b82f6' ? '#1e40af' : '#1e5a3d'}" > Sample Task Preview < / t s p a n >
< / t e x t >
< 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 < / t e x t >
< 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 : < / t s p a n >
< tspan class = "item-title" fill = "${primaryColor === '#3b82f6' ? '#1e40af' : '#1e5a3d'}" > Template Preview < / t s p a n >
< / t e x t >
` : `
< 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 < / t e x t >
< 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 : < / t s p a n >
< tspan class = "item-title" > Sample Task Preview < / t s p a n >
< / t e x t >
< 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 < / t e x t >
< 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 : < / t s p a n >
< tspan class = "item-title" > Template Preview < / t s p a n >
< / t e x t >
` ;
// 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 } ) ;
} ,
2025-11-19 00:20:22 +01:00
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 ;
} ,
2025-11-18 21:36:59 +01:00
async autoLoadDefaultProject ( ) {
2025-11-19 00:20:22 +01:00
console . log ( "Starting autoLoadDefaultProject" ) ;
2025-11-19 00:46:58 +01:00
// Try projects in order: binect, my-project, example
2025-11-19 00:20:22 +01:00
const candidates = [
{ path : "binect/project.json" , basePath : "binect/" } ,
2025-11-19 00:46:58 +01:00
{ path : "my-project/project.json" , basePath : "my-project/" } ,
2025-11-19 00:20:22 +01:00
{ path : "example/project.json" , basePath : "example/" }
] ;
for ( const candidate of candidates ) {
2025-11-18 21:36:59 +01:00
try {
2025-11-19 00:20:22 +01:00
const res = await fetch ( candidate . path ) ;
2025-11-18 21:36:59 +01:00
if ( ! res . ok ) continue ;
const cfg = await res . json ( ) ;
2025-11-19 00:20:22 +01:00
this . projectBasePath = candidate . basePath ;
2025-11-18 21:36:59 +01:00
await this . loadProjectConfigObject ( cfg ) ;
2025-11-19 00:20:22 +01:00
console . log ( "Project loaded successfully from:" , candidate . path ) ;
2025-11-18 21:36:59 +01:00
return ;
} catch ( e ) {
continue ;
}
}
2025-11-19 00:20:22 +01:00
console . log ( "No project could be auto-loaded" ) ;
2025-11-18 21:36:59 +01:00
} ,
async loadProjectConfigObject ( cfg ) {
this . config = cfg ;
const name = cfg . name || "Timeline" ;
2025-11-27 08:59:23 +01:00
document . getElementById ( "projectName" ) . textContent = name ;
document . getElementById ( "projectSubtitle" ) . textContent =
2025-11-18 21:36:59 +01:00
cfg . description || "Projektkonfiguration geladen." ;
2025-11-19 00:20:22 +01:00
// Update project status
this . updateFileStatus ( 'project' , name , 'loaded' ) ;
2026-01-23 17:15:36 +01:00
// Enable project edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'project' ) ;
}
2025-11-27 23:09:35 +01:00
// Show field mappings in debug panel
this . showFieldMappings ( ) ;
2025-11-19 00:20:22 +01:00
// Track loading errors for user feedback
const loadingErrors = [ ] ;
2025-11-18 21:36:59 +01:00
// Stylesheet
if ( cfg . stylesheet && ! this . cssOverride ) {
2025-11-19 00:20:22 +01:00
const stylesheetPath = this . resolveProjectPath ( cfg . stylesheet ) ;
2026-01-23 17:15:36 +01:00
// Load CSS via fetch to store content for editing
try {
const response = await fetch ( stylesheetPath ) ;
if ( response . ok ) {
const cssText = await response . text ( ) ;
this . cssData = cssText ; // Store for editing
// Apply CSS
const blob = new Blob ( [ cssText ] , { type : "text/css" } ) ;
const linkElement = document . getElementById ( "dynamicCss" ) ;
linkElement . href = URL . createObjectURL ( blob ) ;
this . updateFileStatus ( 'css' , cfg . stylesheet + ' ✨' , 'loaded' ) ;
// Enable CSS edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'css' ) ;
}
console . log ( "Stylesheet loaded successfully:" , stylesheetPath ) ;
} else {
throw new Error ( ` HTTP ${ response . status } ` ) ;
}
} catch ( e ) {
console . error ( "Stylesheet could not be loaded:" , e ) ;
2025-11-19 00:20:22 +01:00
this . updateFileStatus ( 'css' , cfg . stylesheet , 'error' ) ;
loadingErrors . push ( ` Stylesheet: ${ cfg . stylesheet } ` ) ;
2026-01-23 17:15:36 +01:00
}
2025-11-18 21:36:59 +01:00
}
// SVG template
if ( cfg . svgTemplate ) {
try {
2025-11-19 00:20:22 +01:00
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' ) ;
2025-11-19 00:46:58 +01:00
2026-01-23 17:15:36 +01:00
// Enable SVG edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'svg' ) ;
}
2025-11-27 23:09:35 +01:00
// Show template fields in debug panel
this . showTemplateFields ( ) ;
2025-11-19 00:46:58 +01:00
// Show template preview if no CSV data is loaded yet
if ( ! this . csvOverride && document . querySelector ( "#viewer" ) . innerHTML . includes ( "Keine Timeline verfügbar" ) ) {
this . showTemplatePreview ( ) ;
}
2025-11-18 21:36:59 +01:00
} catch ( e ) {
2025-11-19 00:20:22 +01:00
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 } ` ) ;
2025-11-18 21:36:59 +01:00
}
}
// CSV data
if ( cfg . dataSource && ! this . csvOverride ) {
try {
2025-11-19 00:20:22 +01:00
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' ) ;
2026-01-23 17:15:36 +01:00
// Enable CSV edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'csv' ) ;
}
2025-11-18 21:36:59 +01:00
this . processCsv ( csvText ) ;
} catch ( e ) {
2025-11-19 00:20:22 +01:00
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 } ` ) ;
2025-11-18 21:36:59 +01:00
}
}
2025-11-19 00:20:22 +01:00
// 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;" > ⚠ ️ < / d i v >
< h4 style = "margin:0 0 8px 0; color:#dc3545;" > Projektdateien konnten nicht geladen werden < / h 4 >
< p style = "margin:0 0 12px 0; font-size:14px;" >
< strong > Fehlgeschlagene Dateien : < / s t r o n g > < b r >
$ { loadingErrors . map ( err => ` • ${ err } ` ) . join ( '<br>' ) }
< / p >
< p style = "margin:0; font-size:12px; color:#6c757d;" >
💡 < strong > Tipp : < / s t r o n g > L a d e d i e D a t e i e n m a n u e l l m i t d e n L o a d - B u t t o n s o b e n o d e r
verwende einen lokalen Server ( < code > make serve < / c o d e > ) .
< / p >
< / d i v >
` ;
}
} , 500 ) ; // Small delay to let other operations complete
}
2025-11-18 21:36:59 +01:00
} ,
processCsv ( text ) {
2025-11-19 00:20:22 +01:00
console . log ( "processCsv called with text length:" , text ? . length ) ;
2026-01-23 17:15:36 +01:00
// Store CSV data for editing
this . csvData = text ;
2025-11-18 21:36:59 +01:00
if ( ! this . config || ! this . config . fieldMapping ) {
console . error ( "No config or fieldMapping found." ) ;
return ;
}
2025-11-19 00:20:22 +01:00
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 ;
}
2025-11-18 21:36:59 +01:00
const m = this . config . fieldMapping ;
2025-11-19 00:20:22 +01:00
const self = this ; // Capture 'this' context
2025-11-18 21:36:59 +01:00
Papa . parse ( text , {
header : true ,
skipEmptyLines : true ,
complete : ( res ) => {
2025-11-19 00:20:22 +01:00
console . log ( "Papa.parse complete, found" , res . data . length , "rows" ) ;
2025-11-18 21:36:59 +01:00
const rows = res . data ;
2025-11-27 23:09:35 +01:00
// Show CSV preview in debug panel
self . showCSVPreview ( text , rows ) ;
2025-11-18 21:36:59 +01:00
const items = rows . map ( ( r ) => {
const dueField = ( m . due || [ ] ) . find ( f => r [ f ] ) ;
2025-11-19 00:20:22 +01:00
const item = {
2025-11-18 21:36:59 +01:00
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" ) ,
2025-11-19 00:20:22 +01:00
due : self . parseDate ( dueField ? r [ dueField ] : null )
2025-11-18 21:36:59 +01:00
} ;
2025-11-19 00:20:22 +01:00
return item ;
2025-11-18 21:36:59 +01:00
} ) . filter ( i => i . title && i . due ) ;
2025-11-19 00:20:22 +01:00
console . log ( "Filtered to" , items . length , "valid items" ) ;
2025-11-18 21:36:59 +01:00
if ( ! items . length ) {
document . getElementById ( "viewer" ) . innerHTML =
"<em style='color:#999;'>Keine gültigen Items gefunden.</em>" ;
return ;
}
2025-11-19 00:20:22 +01:00
// 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 ;
2025-11-19 00:46:58 +01:00
// Initialize zoom functionality for the new SVG
if ( window . svgViewer ) {
window . svgViewer . resetZoom ( ) ;
window . svgViewer . updateZoomControls ( ) ;
}
2025-11-19 00:20:22 +01:00
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>" ;
}
2025-11-18 21:36:59 +01:00
}
} ) ;
} ,
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 ;
2025-11-27 23:09:35 +01:00
} ,
// --------- 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 += ` \n Parsed 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 ;
}
2025-11-18 21:36:59 +01:00
}
} ;
2025-11-19 00:46:58 +01:00
// --------- 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 ( ) ;
}
}
}
} ;
2025-11-18 21:36:59 +01:00
// --------- UI event handlers ---------
2025-11-27 08:59:23 +01:00
window . setupEventHandlers = function ( ) {
2026-01-23 16:34:35 +01:00
// Handler for loading entire project folder
const folderInput = document . getElementById ( "folderInput" ) ;
if ( folderInput ) {
folderInput . addEventListener ( "change" , async ( ev ) => {
const files = Array . from ( ev . target . files ) ;
if ( ! files . length ) return ;
console . log ( "Folder selected with" , files . length , "files" ) ;
try {
// Find project.json in the uploaded files
const projectFile = files . find ( f => f . name === 'project.json' ) ;
if ( ! projectFile ) {
alert ( 'No project.json found in selected folder. Please select a folder containing a project.json file.' ) ;
return ;
}
// Parse project.json
const projectText = await projectFile . text ( ) ;
const cfg = JSON . parse ( projectText ) ;
console . log ( "Loaded project configuration:" , cfg ) ;
// Clear projectBasePath since we're loading from uploaded files
window . timelineEngine . projectBasePath = '' ;
// Update project status
window . timelineEngine . updateFileStatus ( 'project' , projectFile . name , 'loaded' ) ;
2026-01-23 17:15:36 +01:00
// Enable project edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'project' ) ;
}
2026-01-23 16:34:35 +01:00
// Load referenced files from the folder
const errors = [ ] ;
// Load stylesheet
if ( cfg . stylesheet ) {
const cssFile = files . find ( f => f . name === cfg . stylesheet || f . webkitRelativePath . endsWith ( cfg . stylesheet ) ) ;
if ( cssFile ) {
const cssText = await cssFile . text ( ) ;
2026-01-23 17:15:36 +01:00
window . timelineEngine . cssData = cssText ; // Store for editing
2026-01-23 16:34:35 +01:00
window . timelineEngine . cssOverride = true ;
const blob = new Blob ( [ cssText ] , { type : "text/css" } ) ;
document . getElementById ( "dynamicCss" ) . href = URL . createObjectURL ( blob ) ;
window . timelineEngine . updateFileStatus ( 'css' , cssFile . name , 'loaded' ) ;
2026-01-23 17:15:36 +01:00
// Enable CSS edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'css' ) ;
}
2026-01-23 16:34:35 +01:00
console . log ( "Loaded stylesheet:" , cssFile . name ) ;
} else {
errors . push ( ` Stylesheet: ${ cfg . stylesheet } ` ) ;
window . timelineEngine . updateFileStatus ( 'css' , cfg . stylesheet , 'error' ) ;
}
}
// Load SVG template
if ( cfg . svgTemplate ) {
const svgFile = files . find ( f => f . name === cfg . svgTemplate || f . webkitRelativePath . endsWith ( cfg . svgTemplate ) ) ;
if ( svgFile ) {
window . timelineEngine . template = await svgFile . text ( ) ;
window . timelineEngine . updateFileStatus ( 'svg' , svgFile . name , 'loaded' ) ;
window . timelineEngine . showTemplateFields ( ) ;
2026-01-23 17:15:36 +01:00
// Enable SVG edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'svg' ) ;
}
2026-01-23 16:34:35 +01:00
console . log ( "Loaded SVG template:" , svgFile . name ) ;
} else {
errors . push ( ` SVG template: ${ cfg . svgTemplate } ` ) ;
window . timelineEngine . updateFileStatus ( 'svg' , cfg . svgTemplate , 'error' ) ;
}
}
// Load CSV data
if ( cfg . dataSource ) {
const csvFile = files . find ( f => f . name === cfg . dataSource || f . webkitRelativePath . endsWith ( cfg . dataSource ) ) ;
if ( csvFile ) {
const csvText = await csvFile . text ( ) ;
window . timelineEngine . csvOverride = true ;
window . timelineEngine . updateFileStatus ( 'csv' , csvFile . name , 'loaded' ) ;
2026-01-23 17:15:36 +01:00
// Enable CSV edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'csv' ) ;
}
2026-01-23 16:34:35 +01:00
console . log ( "Loaded CSV data:" , csvFile . name ) ;
// Set config and process CSV
window . timelineEngine . config = cfg ;
window . timelineEngine . processCsv ( csvText ) ;
} else {
errors . push ( ` CSV data: ${ cfg . dataSource } ` ) ;
window . timelineEngine . updateFileStatus ( 'csv' , cfg . dataSource , 'error' ) ;
}
}
// Update project name and description
const name = cfg . name || "Timeline" ;
document . getElementById ( "projectName" ) . textContent = name ;
document . getElementById ( "projectSubtitle" ) . textContent = cfg . description || "Project loaded from folder." ;
// Show field mappings
window . timelineEngine . showFieldMappings ( ) ;
// Show any errors
if ( errors . length > 0 ) {
setTimeout ( ( ) => {
alert ( ` ⚠️ Some files could not be loaded: \n \n ${ errors . join ( '\n' ) } \n \n Make sure all referenced files are in the selected folder. ` ) ;
} , 500 ) ;
} else {
console . log ( "✅ All project files loaded successfully from folder" ) ;
}
} catch ( error ) {
console . error ( "Error loading project folder:" , error ) ;
alert ( ` Error loading project: ${ error . message } ` ) ;
}
} ) ;
}
2025-11-19 00:20:22 +01:00
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/" ) ;
2025-11-19 00:46:58 +01:00
} 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/" ) ;
2025-11-19 00:20:22 +01:00
} else {
window . timelineEngine . projectBasePath = '' ;
console . log ( "Unknown project, clearing base path" ) ;
}
} else {
window . timelineEngine . projectBasePath = '' ;
}
window . timelineEngine . updateFileStatus ( 'project' , file . name , 'loaded' ) ;
2026-01-23 17:15:36 +01:00
// Enable project edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'project' ) ;
}
2025-11-19 00:20:22 +01:00
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' ) ;
2026-01-23 17:15:36 +01:00
// Enable CSV edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'csv' ) ;
}
2025-11-19 00:20:22 +01:00
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 ( ) ;
2026-01-23 17:15:36 +01:00
window . timelineEngine . cssData = cssText ; // Store for editing
2025-11-19 00:20:22 +01:00
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' ) ;
2026-01-23 17:15:36 +01:00
// Enable CSS edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'css' ) ;
}
2025-11-19 00:20:22 +01:00
} ) ;
}
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' ) ;
2025-11-19 00:46:58 +01:00
2026-01-23 17:15:36 +01:00
// Enable SVG edit button
if ( window . fileEditor ) {
window . fileEditor . enableEditButton ( 'svg' ) ;
}
2025-11-27 23:09:35 +01:00
// Show template fields in debug panel
window . timelineEngine . showTemplateFields ( ) ;
2025-11-19 00:46:58 +01:00
// Show template preview immediately when manually loaded
window . timelineEngine . showTemplatePreview ( ) ;
2025-11-19 00:20:22 +01:00
} ) ;
}
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" ) ;
}
} ) ;
}
}
2025-11-18 21:36:59 +01:00