mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2025-12-21 21:22:25 +00:00
feat: add StorageManager, VisualizationManager, UIManager, EditorManager, and update PanelResizer for improved snippet handling and visualization
This commit is contained in:
67
src/EditorManager.js
Normal file
67
src/EditorManager.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
export class EditorManager {
|
||||||
|
constructor(snippetManager) {
|
||||||
|
this.snippetManager = snippetManager;
|
||||||
|
this.editor = null;
|
||||||
|
this.timeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditor(editor) {
|
||||||
|
this.editor = editor;
|
||||||
|
this.setupEditorEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEditorEvents() {
|
||||||
|
this.editor.onDidChangeModelContent(() => {
|
||||||
|
// Skip event handling if in read-only mode
|
||||||
|
if (this.snippetManager.readOnlyMode) return;
|
||||||
|
|
||||||
|
this.snippetManager.hasUnsavedChanges = true;
|
||||||
|
this.snippetManager.uiManager.updateSaveButton(true);
|
||||||
|
|
||||||
|
// Auto-save to draft
|
||||||
|
if (this.timeoutId) clearTimeout(this.timeoutId);
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
this.snippetManager.saveDraft();
|
||||||
|
this.updateVisualization();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
revertChanges() {
|
||||||
|
const snippet = this.snippetManager.snippets.find(
|
||||||
|
s => s.id === this.snippetManager.currentSnippetId
|
||||||
|
);
|
||||||
|
this.editor.setValue(JSON.stringify(snippet.content, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVisualization() {
|
||||||
|
try {
|
||||||
|
const value = this.editor.getValue();
|
||||||
|
const content = JSON.parse(value);
|
||||||
|
this.snippetManager.visualizationManager.updateVisualization(content);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid JSON:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateReadOnlyState(readOnly) {
|
||||||
|
if (!this.editor) {
|
||||||
|
throw new Error('Editor not initialized');
|
||||||
|
}
|
||||||
|
this.editor.updateOptions({ readOnly });
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue() {
|
||||||
|
if (!this.editor) {
|
||||||
|
throw new Error('Editor not initialized');
|
||||||
|
}
|
||||||
|
return this.editor.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(content) {
|
||||||
|
if (!this.editor) {
|
||||||
|
throw new Error('Editor not initialized');
|
||||||
|
}
|
||||||
|
this.editor.setValue(JSON.stringify(content, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,15 +102,16 @@ export class PanelResizer {
|
|||||||
document.removeEventListener('mouseup', this.handleDragEnd);
|
document.removeEventListener('mouseup', this.handleDragEnd);
|
||||||
|
|
||||||
// Trigger Monaco editor resize
|
// Trigger Monaco editor resize
|
||||||
if (window.editor) {
|
if (this.snippetManager.editorManager.editor) {
|
||||||
window.editor.layout();
|
this.snippetManager.editorManager.editor.layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update visualization after resize is complete
|
// Update visualization after resize is complete
|
||||||
if (this.snippetManager && typeof this.snippetManager.updateVisualization === 'function') {
|
try {
|
||||||
// Get the current editor value and update visualization
|
const editorValue = this.snippetManager.editorManager.getValue();
|
||||||
const editorValue = this.snippetManager.editor.getValue();
|
this.snippetManager.visualizationManager.updateVisualization(editorValue);
|
||||||
this.snippetManager.updateVisualization(editorValue);
|
} catch (e) {
|
||||||
|
console.error('Error updating visualization after resize:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import { defaultSnippets } from './config.js';
|
import { StorageManager } from './StorageManager.js';
|
||||||
|
import { UIManager } from './UIManager.js';
|
||||||
|
import { VisualizationManager } from './VisualizationManager.js';
|
||||||
|
import { EditorManager } from './EditorManager.js';
|
||||||
|
|
||||||
export class SnippetManager {
|
export class SnippetManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.storageManager = new StorageManager();
|
||||||
|
this.uiManager = new UIManager(this);
|
||||||
|
this.visualizationManager = new VisualizationManager();
|
||||||
|
this.editorManager = new EditorManager(this);
|
||||||
|
|
||||||
this.currentSnippetId = null;
|
this.currentSnippetId = null;
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
this.isDraftVersion = false;
|
this.isDraftVersion = false;
|
||||||
this.readOnlyMode = false;
|
this.readOnlyMode = false;
|
||||||
this.loadSnippets();
|
|
||||||
this.setupUI();
|
this.snippets = this.storageManager.loadSnippets();
|
||||||
|
this.uiManager.renderSnippetList(this.snippets, this.currentSnippetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditor(editor) {
|
||||||
|
this.editorManager.setEditor(editor);
|
||||||
|
if (this.snippets.length > 0) {
|
||||||
|
this.loadSnippet(this.snippets[0].id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasDraftChanges(id) {
|
hasDraftChanges(id) {
|
||||||
@@ -15,33 +31,6 @@ export class SnippetManager {
|
|||||||
return snippet && snippet.draft !== undefined;
|
return snippet && snippet.draft !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSnippets() {
|
|
||||||
const stored = localStorage.getItem('vegaSnippets');
|
|
||||||
this.snippets = stored ? JSON.parse(stored) : defaultSnippets;
|
|
||||||
if (!stored) {
|
|
||||||
this.saveToStorage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveToStorage() {
|
|
||||||
localStorage.setItem('vegaSnippets', JSON.stringify(this.snippets));
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSnippetList() {
|
|
||||||
const container = document.getElementById('snippet-list');
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
this.snippets.forEach(snippet => {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = `snippet-item ${snippet.id === this.currentSnippetId ? 'active' : ''}`;
|
|
||||||
const hasChanges = this.hasDraftChanges(snippet.id);
|
|
||||||
const indicator = hasChanges ? '🟡' : '🟢';
|
|
||||||
div.textContent = `${indicator} ${snippet.name}`;
|
|
||||||
div.onclick = () => this.loadSnippet(snippet.id);
|
|
||||||
container.appendChild(div);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSnippet(id, forceDraft = null) {
|
loadSnippet(id, forceDraft = null) {
|
||||||
const snippet = this.snippets.find(s => s.id === id);
|
const snippet = this.snippets.find(s => s.id === id);
|
||||||
if (snippet) {
|
if (snippet) {
|
||||||
@@ -53,12 +42,13 @@ export class SnippetManager {
|
|||||||
snippet.draft :
|
snippet.draft :
|
||||||
snippet.content;
|
snippet.content;
|
||||||
|
|
||||||
this.editor.setValue(JSON.stringify(content, null, 2));
|
this.editorManager.setValue(content);
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
this.updateReadOnlyState();
|
this.updateReadOnlyState();
|
||||||
this.updateSaveButton();
|
this.uiManager.updateSaveButton(this.hasUnsavedChanges);
|
||||||
this.updateVersionSwitch();
|
this.uiManager.updateVersionSwitch(this.currentSnippetId, this.isDraftVersion, this.hasDraftChanges(this.currentSnippetId));
|
||||||
this.renderSnippetList();
|
this.uiManager.renderSnippetList(this.snippets, this.currentSnippetId);
|
||||||
|
this.visualizationManager.updateVisualization(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +68,7 @@ export class SnippetManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.snippets.push(newSnippet);
|
this.snippets.push(newSnippet);
|
||||||
this.saveToStorage();
|
this.storageManager.saveSnippets(this.snippets);
|
||||||
this.loadSnippet(id);
|
this.loadSnippet(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +76,7 @@ export class SnippetManager {
|
|||||||
if (!this.currentSnippetId) return;
|
if (!this.currentSnippetId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = JSON.parse(this.editor.getValue());
|
const content = JSON.parse(this.editorManager.getValue());
|
||||||
const snippetIndex = this.snippets.findIndex(s => s.id === this.currentSnippetId);
|
const snippetIndex = this.snippets.findIndex(s => s.id === this.currentSnippetId);
|
||||||
|
|
||||||
if (snippetIndex !== -1) {
|
if (snippetIndex !== -1) {
|
||||||
@@ -94,9 +84,10 @@ export class SnippetManager {
|
|||||||
if (JSON.stringify(content) !== JSON.stringify(currentSnippet.content)) {
|
if (JSON.stringify(content) !== JSON.stringify(currentSnippet.content)) {
|
||||||
this.snippets[snippetIndex].draft = content;
|
this.snippets[snippetIndex].draft = content;
|
||||||
this.isDraftVersion = true;
|
this.isDraftVersion = true;
|
||||||
this.saveToStorage();
|
this.storageManager.saveSnippets(this.snippets);
|
||||||
this.renderSnippetList();
|
this.uiManager.renderSnippetList(this.snippets, this.currentSnippetId);
|
||||||
this.updateVersionSwitch();
|
this.uiManager.updateVersionSwitch(this.currentSnippetId, this.isDraftVersion, this.hasDraftChanges(this.currentSnippetId));
|
||||||
|
this.visualizationManager.updateVisualization(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -108,124 +99,49 @@ export class SnippetManager {
|
|||||||
if (!this.currentSnippetId) return;
|
if (!this.currentSnippetId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = JSON.parse(this.editor.getValue());
|
const content = JSON.parse(this.editorManager.getValue());
|
||||||
const snippetIndex = this.snippets.findIndex(s => s.id === this.currentSnippetId);
|
const snippetIndex = this.snippets.findIndex(s => s.id === this.currentSnippetId);
|
||||||
|
|
||||||
if (snippetIndex !== -1) {
|
if (snippetIndex !== -1) {
|
||||||
this.snippets[snippetIndex].content = content;
|
this.snippets[snippetIndex].content = content;
|
||||||
delete this.snippets[snippetIndex].draft; // Remove draft after saving
|
delete this.snippets[snippetIndex].draft; // Remove draft after saving
|
||||||
this.saveToStorage();
|
this.storageManager.saveSnippets(this.snippets);
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
this.isDraftVersion = false;
|
this.isDraftVersion = false;
|
||||||
this.updateSaveButton();
|
this.uiManager.updateSaveButton(this.hasUnsavedChanges);
|
||||||
this.updateVersionSwitch();
|
this.uiManager.updateVersionSwitch(this.currentSnippetId, this.isDraftVersion, this.hasDraftChanges(this.currentSnippetId));
|
||||||
this.renderSnippetList();
|
this.uiManager.renderSnippetList(this.snippets, this.currentSnippetId);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Invalid JSON in editor');
|
alert('Invalid JSON in editor');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSaveButton() {
|
handleReadOnlyOverride() {
|
||||||
const saveButton = document.getElementById('save-snippet');
|
|
||||||
saveButton.disabled = !this.hasUnsavedChanges;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVersionSwitch() {
|
|
||||||
const versionSwitch = document.getElementById('version-switch');
|
|
||||||
if (!versionSwitch) return;
|
|
||||||
|
|
||||||
const hasChanges = this.hasDraftChanges(this.currentSnippetId);
|
|
||||||
versionSwitch.style.display = hasChanges ? 'block' : 'none';
|
|
||||||
|
|
||||||
const buttonText = this.isDraftVersion ?
|
|
||||||
'View Saved Version (Read-only)' :
|
|
||||||
'Switch to Draft Version (Editable)';
|
|
||||||
versionSwitch.textContent = buttonText;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupUI() {
|
|
||||||
// New snippet button
|
|
||||||
document.getElementById('new-snippet').onclick = () => this.createNewSnippet();
|
|
||||||
|
|
||||||
// Save button
|
|
||||||
document.getElementById('save-snippet').onclick = () => this.saveCurrentSnippet();
|
|
||||||
|
|
||||||
// Version switch button
|
|
||||||
const versionSwitch = document.getElementById('version-switch');
|
|
||||||
versionSwitch.onclick = () => {
|
|
||||||
this.loadSnippet(this.currentSnippetId, !this.isDraftVersion);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial render
|
|
||||||
this.renderSnippetList();
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditor(editor) {
|
|
||||||
this.editor = editor;
|
|
||||||
|
|
||||||
let timeoutId = null;
|
|
||||||
editor.onDidChangeModelContent((e) => {
|
|
||||||
// Only show warning if we're in read-only mode AND this is a user edit
|
|
||||||
// (not a programmatic change from loadSnippet)
|
|
||||||
if (this.readOnlyMode && e.isUndoRedo === false) {
|
|
||||||
if (confirm('Editing the saved version will overwrite your draft. Continue?')) {
|
|
||||||
this.readOnlyMode = false;
|
this.readOnlyMode = false;
|
||||||
this.isDraftVersion = true;
|
this.isDraftVersion = true;
|
||||||
delete this.snippets.find(s => s.id === this.currentSnippetId).draft;
|
delete this.snippets.find(s => s.id === this.currentSnippetId).draft;
|
||||||
this.saveToStorage();
|
this.storageManager.saveSnippets(this.snippets);
|
||||||
this.updateVersionSwitch();
|
this.uiManager.updateVersionSwitch(this.currentSnippetId, this.isDraftVersion, this.hasDraftChanges(this.currentSnippetId));
|
||||||
this.renderSnippetList();
|
this.uiManager.renderSnippetList(this.snippets, this.currentSnippetId);
|
||||||
} else {
|
|
||||||
// Revert the change
|
|
||||||
const snippet = this.snippets.find(s => s.id === this.currentSnippetId);
|
|
||||||
this.editor.setValue(JSON.stringify(snippet.content, null, 2));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hasUnsavedChanges = true;
|
// Remove these methods as they're now in UIManager
|
||||||
this.updateSaveButton();
|
updateSaveButton() {
|
||||||
|
this.uiManager.updateSaveButton(this.hasUnsavedChanges);
|
||||||
// Auto-save to draft
|
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
this.saveDraft();
|
|
||||||
try {
|
|
||||||
const value = editor.getValue();
|
|
||||||
this.updateVisualization(value);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Invalid JSON:', e);
|
|
||||||
}
|
}
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load first snippet if available
|
updateVersionSwitch() {
|
||||||
if (this.snippets.length > 0) {
|
this.uiManager.updateVersionSwitch(
|
||||||
this.loadSnippet(this.snippets[0].id);
|
this.currentSnippetId,
|
||||||
}
|
this.isDraftVersion,
|
||||||
|
this.hasDraftChanges(this.currentSnippetId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateReadOnlyState() {
|
updateReadOnlyState() {
|
||||||
const hasChanges = this.hasDraftChanges(this.currentSnippetId);
|
const hasChanges = this.hasDraftChanges(this.currentSnippetId);
|
||||||
this.readOnlyMode = hasChanges && !this.isDraftVersion;
|
this.readOnlyMode = hasChanges && !this.isDraftVersion;
|
||||||
this.editor.updateOptions({ readOnly: this.readOnlyMode });
|
this.editorManager.updateReadOnlyState(this.readOnlyMode);
|
||||||
}
|
|
||||||
|
|
||||||
async updateVisualization(spec) {
|
|
||||||
try {
|
|
||||||
const parsedSpec = typeof spec === 'string' ? JSON.parse(spec) : spec;
|
|
||||||
parsedSpec.width = parsedSpec.width? parsedSpec.width : 'container';
|
|
||||||
parsedSpec.height = parsedSpec.height? parsedSpec.height : 'container';
|
|
||||||
await vegaEmbed('#vis', parsedSpec, {
|
|
||||||
actions: true, // This adds the export/view source buttons
|
|
||||||
theme: 'light'
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error rendering visualization:', err);
|
|
||||||
// Optionally show error in the preview panel
|
|
||||||
document.getElementById('vis').innerHTML =
|
|
||||||
`<div style="color: red; padding: 1rem;">Error rendering visualization: ${err.message}</div>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16
src/StorageManager.js
Normal file
16
src/StorageManager.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defaultSnippets } from './config.js';
|
||||||
|
|
||||||
|
export class StorageManager {
|
||||||
|
constructor() {
|
||||||
|
this.SNIPPETS_KEY = 'vegaSnippets';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSnippets() {
|
||||||
|
const stored = localStorage.getItem(this.SNIPPETS_KEY);
|
||||||
|
return stored ? JSON.parse(stored) : defaultSnippets;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSnippets(snippets) {
|
||||||
|
localStorage.setItem(this.SNIPPETS_KEY, JSON.stringify(snippets));
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/UIManager.js
Normal file
44
src/UIManager.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export class UIManager {
|
||||||
|
constructor(snippetManager) {
|
||||||
|
this.snippetManager = snippetManager;
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
document.getElementById('new-snippet').onclick = () => this.snippetManager.createNewSnippet();
|
||||||
|
document.getElementById('save-snippet').onclick = () => this.snippetManager.saveCurrentSnippet();
|
||||||
|
document.getElementById('version-switch').onclick = () => {
|
||||||
|
this.snippetManager.loadSnippet(this.snippetManager.currentSnippetId, !this.snippetManager.isDraftVersion);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSnippetList(snippets, currentSnippetId) {
|
||||||
|
const container = document.getElementById('snippet-list');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
snippets.forEach(snippet => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `snippet-item ${snippet.id === currentSnippetId ? 'active' : ''}`;
|
||||||
|
const hasChanges = this.snippetManager.hasDraftChanges(snippet.id);
|
||||||
|
const indicator = hasChanges ? '🟡' : '🟢';
|
||||||
|
div.textContent = `${indicator} ${snippet.name}`;
|
||||||
|
div.onclick = () => this.snippetManager.loadSnippet(snippet.id);
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSaveButton(hasUnsavedChanges) {
|
||||||
|
const saveButton = document.getElementById('save-snippet');
|
||||||
|
saveButton.disabled = !hasUnsavedChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVersionSwitch(currentSnippetId, isDraftVersion, hasDraftChanges) {
|
||||||
|
const versionSwitch = document.getElementById('version-switch');
|
||||||
|
if (!versionSwitch) return;
|
||||||
|
|
||||||
|
versionSwitch.style.display = hasDraftChanges ? 'block' : 'none';
|
||||||
|
versionSwitch.textContent = isDraftVersion ?
|
||||||
|
'View Saved Version (Read-only)' :
|
||||||
|
'Switch to Draft Version (Editable)';
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/VisualizationManager.js
Normal file
22
src/VisualizationManager.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export class VisualizationManager {
|
||||||
|
constructor(containerId = 'vis') {
|
||||||
|
this.containerId = containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateVisualization(spec) {
|
||||||
|
try {
|
||||||
|
const parsedSpec = typeof spec === 'string' ? JSON.parse(spec) : spec;
|
||||||
|
parsedSpec.width = parsedSpec.width || 'container';
|
||||||
|
parsedSpec.height = parsedSpec.height || 'container';
|
||||||
|
|
||||||
|
await vegaEmbed(`#${this.containerId}`, parsedSpec, {
|
||||||
|
actions: true,
|
||||||
|
theme: 'light'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error rendering visualization:', err);
|
||||||
|
document.getElementById(this.containerId).innerHTML =
|
||||||
|
`<div style="color: red; padding: 1rem;">Error rendering visualization: ${err.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user