generated from coulomb/repo-seed
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>
282 lines
8.0 KiB
JavaScript
282 lines
8.0 KiB
JavaScript
// File Editor Module
|
|
window.fileEditor = {
|
|
currentFile: null,
|
|
currentContent: null,
|
|
modifiedFiles: new Set(),
|
|
|
|
init() {
|
|
// Set up edit button handlers
|
|
document.getElementById('editProjectBtn').addEventListener('click', () => {
|
|
this.openEditor('project', 'Project Configuration (project.json)',
|
|
JSON.stringify(window.timelineEngine.config, null, 2));
|
|
});
|
|
|
|
document.getElementById('editSvgBtn').addEventListener('click', () => {
|
|
this.openEditor('svg', 'SVG Template', window.timelineEngine.template);
|
|
});
|
|
|
|
document.getElementById('editCssBtn').addEventListener('click', () => {
|
|
this.openEditor('css', 'Stylesheet', window.timelineEngine.cssData);
|
|
});
|
|
|
|
document.getElementById('editCsvBtn').addEventListener('click', () => {
|
|
this.openEditor('csv', 'CSV Data', window.timelineEngine.csvData);
|
|
});
|
|
|
|
// Set up save changes button
|
|
document.getElementById('saveChanges').addEventListener('click', () => {
|
|
this.saveAllChanges();
|
|
});
|
|
|
|
// Close modal on background click
|
|
document.getElementById('editorModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'editorModal') {
|
|
this.closeEditor();
|
|
}
|
|
});
|
|
|
|
// Keyboard shortcut: Escape to close
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && document.getElementById('editorModal').style.display === 'block') {
|
|
this.closeEditor();
|
|
}
|
|
});
|
|
|
|
console.log('File editor initialized');
|
|
},
|
|
|
|
openEditor(fileType, title, content) {
|
|
if (!content) {
|
|
alert(`No ${title} loaded. Please load a file first.`);
|
|
return;
|
|
}
|
|
|
|
this.currentFile = fileType;
|
|
this.currentContent = content;
|
|
|
|
document.getElementById('editorTitle').textContent = `Edit ${title}`;
|
|
document.getElementById('editorTextarea').value = content;
|
|
document.getElementById('editorModal').style.display = 'block';
|
|
|
|
console.log(`Opened editor for ${fileType}`);
|
|
},
|
|
|
|
closeEditor() {
|
|
document.getElementById('editorModal').style.display = 'none';
|
|
this.currentFile = null;
|
|
this.currentContent = null;
|
|
},
|
|
|
|
applyChanges() {
|
|
const newContent = document.getElementById('editorTextarea').value;
|
|
|
|
// Validate based on file type
|
|
if (this.currentFile === 'project' || this.currentFile === 'json') {
|
|
try {
|
|
JSON.parse(newContent);
|
|
} catch (e) {
|
|
alert(`Invalid JSON: ${e.message}\n\nPlease fix the syntax errors before applying.`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check if content actually changed
|
|
if (newContent === this.currentContent) {
|
|
console.log('No changes detected');
|
|
this.closeEditor();
|
|
return;
|
|
}
|
|
|
|
// Apply changes based on file type
|
|
console.log(`Applying changes to ${this.currentFile}`);
|
|
|
|
try {
|
|
switch (this.currentFile) {
|
|
case 'project':
|
|
const cfg = JSON.parse(newContent);
|
|
window.timelineEngine.config = cfg;
|
|
|
|
// Update UI
|
|
document.getElementById("projectName").textContent = cfg.name || "Timeline";
|
|
document.getElementById("projectSubtitle").textContent =
|
|
cfg.description || "Project configuration updated.";
|
|
|
|
// Show field mappings
|
|
window.timelineEngine.showFieldMappings();
|
|
|
|
// Regenerate timeline if we have CSV data
|
|
if (window.timelineEngine.csvData) {
|
|
window.timelineEngine.processCsv(window.timelineEngine.csvData);
|
|
}
|
|
break;
|
|
|
|
case 'svg':
|
|
window.timelineEngine.template = newContent;
|
|
window.timelineEngine.showTemplateFields();
|
|
|
|
// Regenerate timeline if we have CSV data
|
|
if (window.timelineEngine.csvData) {
|
|
window.timelineEngine.processCsv(window.timelineEngine.csvData);
|
|
} else {
|
|
window.timelineEngine.showTemplatePreview();
|
|
}
|
|
break;
|
|
|
|
case 'css':
|
|
window.timelineEngine.cssData = newContent;
|
|
const blob = new Blob([newContent], { type: "text/css" });
|
|
document.getElementById("dynamicCss").href = URL.createObjectURL(blob);
|
|
break;
|
|
|
|
case 'csv':
|
|
window.timelineEngine.csvData = newContent;
|
|
window.timelineEngine.processCsv(newContent);
|
|
break;
|
|
}
|
|
|
|
// Mark file as modified
|
|
this.modifiedFiles.add(this.currentFile);
|
|
this.updateModifiedBadges();
|
|
this.updateSaveButton();
|
|
|
|
console.log(`✅ Changes applied to ${this.currentFile}`);
|
|
this.closeEditor();
|
|
|
|
} catch (error) {
|
|
console.error('Error applying changes:', error);
|
|
alert(`Error applying changes: ${error.message}`);
|
|
}
|
|
},
|
|
|
|
updateModifiedBadges() {
|
|
// Remove all existing badges
|
|
document.querySelectorAll('.modified-badge').forEach(badge => badge.remove());
|
|
|
|
// Add badges to modified files
|
|
this.modifiedFiles.forEach(fileType => {
|
|
let statusElement;
|
|
switch (fileType) {
|
|
case 'project':
|
|
statusElement = document.getElementById('projectFile');
|
|
break;
|
|
case 'svg':
|
|
statusElement = document.getElementById('svgFile');
|
|
break;
|
|
case 'css':
|
|
statusElement = document.getElementById('cssFile');
|
|
break;
|
|
case 'csv':
|
|
statusElement = document.getElementById('csvFile');
|
|
break;
|
|
}
|
|
|
|
if (statusElement && !statusElement.querySelector('.modified-badge')) {
|
|
const badge = document.createElement('span');
|
|
badge.className = 'modified-badge';
|
|
badge.textContent = 'MODIFIED';
|
|
statusElement.appendChild(badge);
|
|
}
|
|
});
|
|
},
|
|
|
|
updateSaveButton() {
|
|
const saveBtn = document.getElementById('saveChanges');
|
|
if (this.modifiedFiles.size > 0) {
|
|
saveBtn.disabled = false;
|
|
saveBtn.style.opacity = '1';
|
|
} else {
|
|
saveBtn.disabled = true;
|
|
saveBtn.style.opacity = '0.6';
|
|
}
|
|
},
|
|
|
|
saveAllChanges() {
|
|
if (this.modifiedFiles.size === 0) {
|
|
alert('No files have been modified.');
|
|
return;
|
|
}
|
|
|
|
const filesToSave = Array.from(this.modifiedFiles);
|
|
console.log('Saving modified files:', filesToSave);
|
|
|
|
filesToSave.forEach(fileType => {
|
|
let content, filename, mimeType;
|
|
|
|
switch (fileType) {
|
|
case 'project':
|
|
content = JSON.stringify(window.timelineEngine.config, null, 2);
|
|
filename = 'project.json';
|
|
mimeType = 'application/json';
|
|
break;
|
|
|
|
case 'svg':
|
|
content = window.timelineEngine.template;
|
|
filename = 'template-v2.svg';
|
|
mimeType = 'image/svg+xml';
|
|
break;
|
|
|
|
case 'css':
|
|
content = window.timelineEngine.cssData;
|
|
filename = 'style.css';
|
|
mimeType = 'text/css';
|
|
break;
|
|
|
|
case 'csv':
|
|
content = window.timelineEngine.csvData;
|
|
filename = 'sample.csv';
|
|
mimeType = 'text/csv';
|
|
break;
|
|
|
|
default:
|
|
console.warn(`Unknown file type: ${fileType}`);
|
|
return;
|
|
}
|
|
|
|
// Create and trigger download
|
|
const blob = new Blob([content], { type: mimeType });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
|
|
console.log(`✅ Saved ${filename}`);
|
|
});
|
|
|
|
// Clear modified state after saving
|
|
setTimeout(() => {
|
|
if (confirm(`${filesToSave.length} file(s) downloaded.\n\nClear modified state?`)) {
|
|
this.modifiedFiles.clear();
|
|
this.updateModifiedBadges();
|
|
this.updateSaveButton();
|
|
}
|
|
}, 500);
|
|
},
|
|
|
|
enableEditButton(fileType) {
|
|
let btnId;
|
|
switch (fileType) {
|
|
case 'project':
|
|
btnId = 'editProjectBtn';
|
|
break;
|
|
case 'svg':
|
|
btnId = 'editSvgBtn';
|
|
break;
|
|
case 'css':
|
|
btnId = 'editCssBtn';
|
|
break;
|
|
case 'csv':
|
|
btnId = 'editCsvBtn';
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById(btnId);
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
};
|