generated from coulomb/repo-seed
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:
4
Makefile
4
Makefile
@@ -152,8 +152,8 @@ dist:
|
|||||||
@echo "📦 Created dist/ directory"
|
@echo "📦 Created dist/ directory"
|
||||||
|
|
||||||
@# Copy core application files
|
@# Copy core application files
|
||||||
@cp index.html engine.js generator.js dist/
|
@cp index.html engine.js generator.js file-editor.js dist/
|
||||||
@echo "✅ Copied core files (index.html, engine.js, generator.js)"
|
@echo "✅ Copied core files (index.html, engine.js, generator.js, file-editor.js)"
|
||||||
|
|
||||||
@# Copy project directories
|
@# Copy project directories
|
||||||
@cp -r example example-1 my-project dist/
|
@cp -r example example-1 my-project dist/
|
||||||
|
|||||||
110
engine.js
110
engine.js
@@ -3,6 +3,8 @@ window.timelineEngine = {
|
|||||||
template: null,
|
template: null,
|
||||||
csvOverride: false,
|
csvOverride: false,
|
||||||
cssOverride: false,
|
cssOverride: false,
|
||||||
|
csvData: null, // Store current CSV text
|
||||||
|
cssData: null, // Store current CSS text
|
||||||
|
|
||||||
projectBasePath: '',
|
projectBasePath: '',
|
||||||
|
|
||||||
@@ -190,6 +192,11 @@ window.timelineEngine = {
|
|||||||
// Update project status
|
// Update project status
|
||||||
this.updateFileStatus('project', name, 'loaded');
|
this.updateFileStatus('project', name, 'loaded');
|
||||||
|
|
||||||
|
// Enable project edit button
|
||||||
|
if (window.fileEditor) {
|
||||||
|
window.fileEditor.enableEditButton('project');
|
||||||
|
}
|
||||||
|
|
||||||
// Show field mappings in debug panel
|
// Show field mappings in debug panel
|
||||||
this.showFieldMappings();
|
this.showFieldMappings();
|
||||||
|
|
||||||
@@ -199,27 +206,35 @@ window.timelineEngine = {
|
|||||||
// Stylesheet
|
// Stylesheet
|
||||||
if (cfg.stylesheet && !this.cssOverride) {
|
if (cfg.stylesheet && !this.cssOverride) {
|
||||||
const stylesheetPath = this.resolveProjectPath(cfg.stylesheet);
|
const stylesheetPath = this.resolveProjectPath(cfg.stylesheet);
|
||||||
const linkElement = document.getElementById("dynamicCss");
|
|
||||||
|
|
||||||
// Set up load/error event handlers before setting href
|
// Load CSS via fetch to store content for editing
|
||||||
const handleLoad = () => {
|
try {
|
||||||
console.log("Stylesheet loaded successfully:", stylesheetPath);
|
const response = await fetch(stylesheetPath);
|
||||||
this.updateFileStatus('css', cfg.stylesheet + ' ✨', 'loaded');
|
if (response.ok) {
|
||||||
linkElement.removeEventListener('load', handleLoad);
|
const cssText = await response.text();
|
||||||
linkElement.removeEventListener('error', handleError);
|
this.cssData = cssText; // Store for editing
|
||||||
};
|
|
||||||
|
|
||||||
const handleError = () => {
|
// Apply CSS
|
||||||
console.error("Stylesheet could not be loaded:", stylesheetPath);
|
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');
|
this.updateFileStatus('css', cfg.stylesheet, 'error');
|
||||||
loadingErrors.push(`Stylesheet: ${cfg.stylesheet}`);
|
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
|
// SVG template
|
||||||
@@ -238,6 +253,11 @@ window.timelineEngine = {
|
|||||||
console.log("SVG template loaded, length:", this.template.length);
|
console.log("SVG template loaded, length:", this.template.length);
|
||||||
this.updateFileStatus('svg', cfg.svgTemplate, 'loaded');
|
this.updateFileStatus('svg', cfg.svgTemplate, 'loaded');
|
||||||
|
|
||||||
|
// Enable SVG edit button
|
||||||
|
if (window.fileEditor) {
|
||||||
|
window.fileEditor.enableEditButton('svg');
|
||||||
|
}
|
||||||
|
|
||||||
// Show template fields in debug panel
|
// Show template fields in debug panel
|
||||||
this.showTemplateFields();
|
this.showTemplateFields();
|
||||||
|
|
||||||
@@ -270,6 +290,12 @@ window.timelineEngine = {
|
|||||||
console.log("CSV preview:", csvText.substring(0, 200));
|
console.log("CSV preview:", csvText.substring(0, 200));
|
||||||
|
|
||||||
this.updateFileStatus('csv', cfg.dataSource, 'loaded');
|
this.updateFileStatus('csv', cfg.dataSource, 'loaded');
|
||||||
|
|
||||||
|
// Enable CSV edit button
|
||||||
|
if (window.fileEditor) {
|
||||||
|
window.fileEditor.enableEditButton('csv');
|
||||||
|
}
|
||||||
|
|
||||||
this.processCsv(csvText);
|
this.processCsv(csvText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("CSV could not be loaded:", e);
|
console.error("CSV could not be loaded:", e);
|
||||||
@@ -306,6 +332,9 @@ window.timelineEngine = {
|
|||||||
processCsv(text) {
|
processCsv(text) {
|
||||||
console.log("processCsv called with text length:", text?.length);
|
console.log("processCsv called with text length:", text?.length);
|
||||||
|
|
||||||
|
// Store CSV data for editing
|
||||||
|
this.csvData = text;
|
||||||
|
|
||||||
if (!this.config || !this.config.fieldMapping) {
|
if (!this.config || !this.config.fieldMapping) {
|
||||||
console.error("No config or fieldMapping found.");
|
console.error("No config or fieldMapping found.");
|
||||||
return;
|
return;
|
||||||
@@ -696,6 +725,11 @@ window.setupEventHandlers = function() {
|
|||||||
// Update project status
|
// Update project status
|
||||||
window.timelineEngine.updateFileStatus('project', projectFile.name, 'loaded');
|
window.timelineEngine.updateFileStatus('project', projectFile.name, 'loaded');
|
||||||
|
|
||||||
|
// Enable project edit button
|
||||||
|
if (window.fileEditor) {
|
||||||
|
window.fileEditor.enableEditButton('project');
|
||||||
|
}
|
||||||
|
|
||||||
// Load referenced files from the folder
|
// Load referenced files from the folder
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
@@ -704,10 +738,17 @@ window.setupEventHandlers = function() {
|
|||||||
const cssFile = files.find(f => f.name === cfg.stylesheet || f.webkitRelativePath.endsWith(cfg.stylesheet));
|
const cssFile = files.find(f => f.name === cfg.stylesheet || f.webkitRelativePath.endsWith(cfg.stylesheet));
|
||||||
if (cssFile) {
|
if (cssFile) {
|
||||||
const cssText = await cssFile.text();
|
const cssText = await cssFile.text();
|
||||||
|
window.timelineEngine.cssData = cssText; // Store for editing
|
||||||
window.timelineEngine.cssOverride = true;
|
window.timelineEngine.cssOverride = true;
|
||||||
const blob = new Blob([cssText], { type: "text/css" });
|
const blob = new Blob([cssText], { type: "text/css" });
|
||||||
document.getElementById("dynamicCss").href = URL.createObjectURL(blob);
|
document.getElementById("dynamicCss").href = URL.createObjectURL(blob);
|
||||||
window.timelineEngine.updateFileStatus('css', cssFile.name, 'loaded');
|
window.timelineEngine.updateFileStatus('css', cssFile.name, 'loaded');
|
||||||
|
|
||||||
|
// Enable CSS edit button
|
||||||
|
if (window.fileEditor) {
|
||||||
|
window.fileEditor.enableEditButton('css');
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Loaded stylesheet:", cssFile.name);
|
console.log("Loaded stylesheet:", cssFile.name);
|
||||||
} else {
|
} else {
|
||||||
errors.push(`Stylesheet: ${cfg.stylesheet}`);
|
errors.push(`Stylesheet: ${cfg.stylesheet}`);
|
||||||
@@ -722,6 +763,12 @@ window.setupEventHandlers = function() {
|
|||||||
window.timelineEngine.template = await svgFile.text();
|
window.timelineEngine.template = await svgFile.text();
|
||||||
window.timelineEngine.updateFileStatus('svg', svgFile.name, 'loaded');
|
window.timelineEngine.updateFileStatus('svg', svgFile.name, 'loaded');
|
||||||
window.timelineEngine.showTemplateFields();
|
window.timelineEngine.showTemplateFields();
|
||||||
|
|
||||||
|
// Enable SVG edit button
|
||||||
|
if (window.fileEditor) {
|
||||||
|
window.fileEditor.enableEditButton('svg');
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Loaded SVG template:", svgFile.name);
|
console.log("Loaded SVG template:", svgFile.name);
|
||||||
} else {
|
} else {
|
||||||
errors.push(`SVG template: ${cfg.svgTemplate}`);
|
errors.push(`SVG template: ${cfg.svgTemplate}`);
|
||||||
@@ -736,6 +783,12 @@ window.setupEventHandlers = function() {
|
|||||||
const csvText = await csvFile.text();
|
const csvText = await csvFile.text();
|
||||||
window.timelineEngine.csvOverride = true;
|
window.timelineEngine.csvOverride = true;
|
||||||
window.timelineEngine.updateFileStatus('csv', csvFile.name, 'loaded');
|
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);
|
console.log("Loaded CSV data:", csvFile.name);
|
||||||
|
|
||||||
// Set config and process CSV
|
// Set config and process CSV
|
||||||
@@ -800,6 +853,12 @@ window.setupEventHandlers = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.timelineEngine.updateFileStatus('project', file.name, 'loaded');
|
window.timelineEngine.updateFileStatus('project', file.name, 'loaded');
|
||||||
|
|
||||||
|
// Enable project edit button
|
||||||
|
if (window.fileEditor) {
|
||||||
|
window.fileEditor.enableEditButton('project');
|
||||||
|
}
|
||||||
|
|
||||||
await window.timelineEngine.loadProjectConfigObject(cfg);
|
await window.timelineEngine.loadProjectConfigObject(cfg);
|
||||||
|
|
||||||
// Show message about relative paths if project has data sources
|
// Show message about relative paths if project has data sources
|
||||||
@@ -824,6 +883,12 @@ window.setupEventHandlers = function() {
|
|||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
window.timelineEngine.csvOverride = true;
|
window.timelineEngine.csvOverride = true;
|
||||||
window.timelineEngine.updateFileStatus('csv', file.name, 'loaded');
|
window.timelineEngine.updateFileStatus('csv', file.name, 'loaded');
|
||||||
|
|
||||||
|
// Enable CSV edit button
|
||||||
|
if (window.fileEditor) {
|
||||||
|
window.fileEditor.enableEditButton('csv');
|
||||||
|
}
|
||||||
|
|
||||||
window.timelineEngine.processCsv(text);
|
window.timelineEngine.processCsv(text);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -834,10 +899,16 @@ window.setupEventHandlers = function() {
|
|||||||
const file = ev.target.files[0];
|
const file = ev.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const cssText = await file.text();
|
const cssText = await file.text();
|
||||||
|
window.timelineEngine.cssData = cssText; // Store for editing
|
||||||
window.timelineEngine.cssOverride = true;
|
window.timelineEngine.cssOverride = true;
|
||||||
const blob = new Blob([cssText], { type: "text/css" });
|
const blob = new Blob([cssText], { type: "text/css" });
|
||||||
document.getElementById("dynamicCss").href = URL.createObjectURL(blob);
|
document.getElementById("dynamicCss").href = URL.createObjectURL(blob);
|
||||||
window.timelineEngine.updateFileStatus('css', file.name, 'loaded');
|
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.template = await file.text();
|
||||||
window.timelineEngine.updateFileStatus('svg', file.name, 'loaded');
|
window.timelineEngine.updateFileStatus('svg', file.name, 'loaded');
|
||||||
|
|
||||||
|
// Enable SVG edit button
|
||||||
|
if (window.fileEditor) {
|
||||||
|
window.fileEditor.enableEditButton('svg');
|
||||||
|
}
|
||||||
|
|
||||||
// Show template fields in debug panel
|
// Show template fields in debug panel
|
||||||
window.timelineEngine.showTemplateFields();
|
window.timelineEngine.showTemplateFields();
|
||||||
|
|
||||||
|
|||||||
281
file-editor.js
Normal file
281
file-editor.js
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
162
index.html
162
index.html
@@ -49,6 +49,96 @@
|
|||||||
background: #343a40;
|
background: #343a40;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
border: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover:not(:disabled) {
|
||||||
|
background: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:disabled {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #adb5bd;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modified-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #fd7e14;
|
||||||
|
color: white;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
z-index: 10000;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
background: white;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-body {
|
||||||
|
padding: 20px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-footer {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.file-status {
|
.file-status {
|
||||||
border-top: 1px solid #f1f3f4;
|
border-top: 1px solid #f1f3f4;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
@@ -226,6 +316,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<script src="generator.js"></script>
|
<script src="generator.js"></script>
|
||||||
<script src="engine.js"></script>
|
<script src="engine.js"></script>
|
||||||
|
<script src="file-editor.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="internal-mode" style="font-family: Inter, Arial, sans-serif; background:#f5f7fa; margin:20px;">
|
<body class="internal-mode" style="font-family: Inter, Arial, sans-serif; background:#f5f7fa; margin:20px;">
|
||||||
|
|
||||||
@@ -260,10 +351,13 @@
|
|||||||
<div class="file-item">
|
<div class="file-item">
|
||||||
<div class="file-header">
|
<div class="file-header">
|
||||||
<span class="file-label">Project Configuration</span>
|
<span class="file-label">Project Configuration</span>
|
||||||
<label class="upload-btn">
|
<div style="display:flex; gap:4px;">
|
||||||
<input type="file" id="projectInput" accept=".json" style="display:none;" />
|
<label class="upload-btn">
|
||||||
📁 Load
|
<input type="file" id="projectInput" accept=".json" style="display:none;" />
|
||||||
</label>
|
📁 Load
|
||||||
|
</label>
|
||||||
|
<button class="edit-btn" id="editProjectBtn" disabled>✏️ Edit</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-status">
|
<div class="file-status">
|
||||||
<span id="projectFile" class="file-name">Not loaded</span>
|
<span id="projectFile" class="file-name">Not loaded</span>
|
||||||
@@ -273,10 +367,13 @@
|
|||||||
<div class="file-item">
|
<div class="file-item">
|
||||||
<div class="file-header">
|
<div class="file-header">
|
||||||
<span class="file-label">SVG Template</span>
|
<span class="file-label">SVG Template</span>
|
||||||
<label class="upload-btn">
|
<div style="display:flex; gap:4px;">
|
||||||
<input type="file" id="svgInput" accept=".svg" style="display:none;" />
|
<label class="upload-btn">
|
||||||
🖼️ Load
|
<input type="file" id="svgInput" accept=".svg" style="display:none;" />
|
||||||
</label>
|
🖼️ Load
|
||||||
|
</label>
|
||||||
|
<button class="edit-btn" id="editSvgBtn" disabled>✏️ Edit</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-status">
|
<div class="file-status">
|
||||||
<span id="svgFile" class="file-name">Not loaded</span>
|
<span id="svgFile" class="file-name">Not loaded</span>
|
||||||
@@ -286,10 +383,13 @@
|
|||||||
<div class="file-item">
|
<div class="file-item">
|
||||||
<div class="file-header">
|
<div class="file-header">
|
||||||
<span class="file-label">Stylesheet</span>
|
<span class="file-label">Stylesheet</span>
|
||||||
<label class="upload-btn">
|
<div style="display:flex; gap:4px;">
|
||||||
<input type="file" id="cssInput" accept=".css" style="display:none;" />
|
<label class="upload-btn">
|
||||||
🎨 Load
|
<input type="file" id="cssInput" accept=".css" style="display:none;" />
|
||||||
</label>
|
🎨 Load
|
||||||
|
</label>
|
||||||
|
<button class="edit-btn" id="editCssBtn" disabled>✏️ Edit</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-status">
|
<div class="file-status">
|
||||||
<span id="cssFile" class="file-name">Not loaded</span>
|
<span id="cssFile" class="file-name">Not loaded</span>
|
||||||
@@ -299,10 +399,13 @@
|
|||||||
<div class="file-item">
|
<div class="file-item">
|
||||||
<div class="file-header">
|
<div class="file-header">
|
||||||
<span class="file-label">CSV Data</span>
|
<span class="file-label">CSV Data</span>
|
||||||
<label class="upload-btn">
|
<div style="display:flex; gap:4px;">
|
||||||
<input type="file" id="csvInput" accept=".csv" style="display:none;" />
|
<label class="upload-btn">
|
||||||
📊 Load
|
<input type="file" id="csvInput" accept=".csv" style="display:none;" />
|
||||||
</label>
|
📊 Load
|
||||||
|
</label>
|
||||||
|
<button class="edit-btn" id="editCsvBtn" disabled>✏️ Edit</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-status">
|
<div class="file-status">
|
||||||
<span id="csvFile" class="file-name">Not loaded</span>
|
<span id="csvFile" class="file-name">Not loaded</span>
|
||||||
@@ -314,9 +417,13 @@
|
|||||||
<button id="toggleView" style="padding:8px 16px; background:#495057; color:white; border:none; border-radius:6px; cursor:pointer; font-size:12px;">
|
<button id="toggleView" style="padding:8px 16px; background:#495057; color:white; border:none; border-radius:6px; cursor:pointer; font-size:12px;">
|
||||||
🔄 Switch View (Internal / External)
|
🔄 Switch View (Internal / External)
|
||||||
</button>
|
</button>
|
||||||
|
<button id="saveChanges" disabled
|
||||||
|
style="padding:8px 16px; background:#28a745; color:white; border:none; border-radius:6px; cursor:pointer; opacity:0.6; font-size:12px;">
|
||||||
|
💾 Save Changes
|
||||||
|
</button>
|
||||||
<button id="downloadSvg" disabled
|
<button id="downloadSvg" disabled
|
||||||
style="padding:8px 16px; background:#495057; color:white; border:none; border-radius:6px; cursor:pointer; opacity:0.6; font-size:12px;">
|
style="padding:8px 16px; background:#495057; color:white; border:none; border-radius:6px; cursor:pointer; opacity:0.6; font-size:12px;">
|
||||||
💾 Download SVG
|
📥 Download SVG
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -409,5 +516,26 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Editor Modal -->
|
||||||
|
<div id="editorModal" class="editor-modal">
|
||||||
|
<div class="editor-content">
|
||||||
|
<div class="editor-header">
|
||||||
|
<h3 id="editorTitle" style="margin:0; font-size:16px; font-weight:600;">Edit File</h3>
|
||||||
|
<button onclick="window.fileEditor.closeEditor()" style="background:none; border:none; font-size:24px; cursor:pointer; color:#6c757d;">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="editor-body">
|
||||||
|
<textarea id="editorTextarea" class="editor-textarea" spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="editor-footer">
|
||||||
|
<button onclick="window.fileEditor.closeEditor()" style="padding:8px 16px; background:#6c757d; color:white; border:none; border-radius:4px; cursor:pointer;">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button onclick="window.fileEditor.applyChanges()" style="padding:8px 16px; background:#007bff; color:white; border:none; border-radius:4px; cursor:pointer;">
|
||||||
|
Apply Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user