feat: add inline file editing with modification tracking and save functionality

Add comprehensive inline editing capabilities for all project files with visual modification tracking and batch save functionality.

Features:
- Edit buttons next to each file load button (Project, SVG, CSS, CSV)
- Modal editor with syntax validation for JSON files
- Real-time preview updates after applying changes
- Visual "MODIFIED" badges on edited files
- "Save Changes" button to download all modified files
- Keyboard shortcuts (Escape to close editor)

Implementation:
- file-editor.js: New module handling all editor functionality
  - Editor modal with textarea for content editing
  - Modification tracking using Set data structure
  - File-specific validation (JSON syntax checking)
  - Batch download of all modified files
  - Visual badge updates for modified state

- index.html: UI updates
  - Edit buttons added to all file items
  - Modal HTML structure for editor
  - Save Changes button in controls area
  - CSS styling for editor modal and modified badges

- engine.js: Integration and data storage
  - Store CSV and CSS content when loaded (for editing)
  - Enable edit buttons when files are successfully loaded
  - Works with all loading methods (folder picker, individual files, auto-load)
  - CSS now loaded via fetch to store content (changed from link href)

- Makefile: Include file-editor.js in distribution build

User workflow:
1. Load project files (folder picker or individual files)
2. Click "✏️ Edit" button next to any file
3. Make changes in modal editor
4. Click "Apply Changes" to update and see immediate preview
5. Modified files show orange "MODIFIED" badge
6. Click "💾 Save Changes" to download all modified files at once

Technical details:
- Edit buttons start disabled, enabled when file loads
- JSON validation prevents saving invalid configurations
- Changes apply immediately to in-memory data
- Timeline regenerates automatically after edits
- Modified state persists until files are saved
- Optional clearing of modified state after download

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 17:15:36 +01:00
parent cefbf96a82
commit 4576d066b3
4 changed files with 521 additions and 36 deletions

110
engine.js
View File

@@ -3,6 +3,8 @@ window.timelineEngine = {
template: null,
csvOverride: false,
cssOverride: false,
csvData: null, // Store current CSV text
cssData: null, // Store current CSS text
projectBasePath: '',
@@ -190,6 +192,11 @@ window.timelineEngine = {
// Update project status
this.updateFileStatus('project', name, 'loaded');
// Enable project edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('project');
}
// Show field mappings in debug panel
this.showFieldMappings();
@@ -199,27 +206,35 @@ window.timelineEngine = {
// 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);
};
// 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
const handleError = () => {
console.error("Stylesheet could not be loaded:", stylesheetPath);
// 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);
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
@@ -238,6 +253,11 @@ window.timelineEngine = {
console.log("SVG template loaded, length:", this.template.length);
this.updateFileStatus('svg', cfg.svgTemplate, 'loaded');
// Enable SVG edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('svg');
}
// Show template fields in debug panel
this.showTemplateFields();
@@ -270,6 +290,12 @@ window.timelineEngine = {
console.log("CSV preview:", csvText.substring(0, 200));
this.updateFileStatus('csv', cfg.dataSource, 'loaded');
// Enable CSV edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('csv');
}
this.processCsv(csvText);
} catch (e) {
console.error("CSV could not be loaded:", e);
@@ -306,6 +332,9 @@ window.timelineEngine = {
processCsv(text) {
console.log("processCsv called with text length:", text?.length);
// Store CSV data for editing
this.csvData = text;
if (!this.config || !this.config.fieldMapping) {
console.error("No config or fieldMapping found.");
return;
@@ -696,6 +725,11 @@ window.setupEventHandlers = function() {
// Update project status
window.timelineEngine.updateFileStatus('project', projectFile.name, 'loaded');
// Enable project edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('project');
}
// Load referenced files from the folder
const errors = [];
@@ -704,10 +738,17 @@ window.setupEventHandlers = function() {
const cssFile = files.find(f => f.name === cfg.stylesheet || f.webkitRelativePath.endsWith(cfg.stylesheet));
if (cssFile) {
const cssText = await cssFile.text();
window.timelineEngine.cssData = cssText; // Store for editing
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');
// Enable CSS edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('css');
}
console.log("Loaded stylesheet:", cssFile.name);
} else {
errors.push(`Stylesheet: ${cfg.stylesheet}`);
@@ -722,6 +763,12 @@ window.setupEventHandlers = function() {
window.timelineEngine.template = await svgFile.text();
window.timelineEngine.updateFileStatus('svg', svgFile.name, 'loaded');
window.timelineEngine.showTemplateFields();
// Enable SVG edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('svg');
}
console.log("Loaded SVG template:", svgFile.name);
} else {
errors.push(`SVG template: ${cfg.svgTemplate}`);
@@ -736,6 +783,12 @@ window.setupEventHandlers = function() {
const csvText = await csvFile.text();
window.timelineEngine.csvOverride = true;
window.timelineEngine.updateFileStatus('csv', csvFile.name, 'loaded');
// Enable CSV edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('csv');
}
console.log("Loaded CSV data:", csvFile.name);
// Set config and process CSV
@@ -800,6 +853,12 @@ window.setupEventHandlers = function() {
}
window.timelineEngine.updateFileStatus('project', file.name, 'loaded');
// Enable project edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('project');
}
await window.timelineEngine.loadProjectConfigObject(cfg);
// Show message about relative paths if project has data sources
@@ -824,6 +883,12 @@ window.setupEventHandlers = function() {
const text = await file.text();
window.timelineEngine.csvOverride = true;
window.timelineEngine.updateFileStatus('csv', file.name, 'loaded');
// Enable CSV edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('csv');
}
window.timelineEngine.processCsv(text);
});
}
@@ -834,10 +899,16 @@ window.setupEventHandlers = function() {
const file = ev.target.files[0];
if (!file) return;
const cssText = await file.text();
window.timelineEngine.cssData = cssText; // Store for editing
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');
// Enable CSS edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('css');
}
});
}
@@ -849,6 +920,11 @@ window.setupEventHandlers = function() {
window.timelineEngine.template = await file.text();
window.timelineEngine.updateFileStatus('svg', file.name, 'loaded');
// Enable SVG edit button
if (window.fileEditor) {
window.fileEditor.enableEditButton('svg');
}
// Show template fields in debug panel
window.timelineEngine.showTemplateFields();