Files
astrolabe-nvc/src/js/config.js

310 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Global variables and configuration
let editor; // Global editor instance
let renderTimeout; // For debouncing
let currentViewMode = 'draft'; // Track current view mode: 'draft' or 'published'
// Panel resizing variables
let isResizing = false;
let currentHandle = null;
let startX = 0;
let startWidths = [];
// Panel memory for toggle functionality
let panelMemory = {
snippetWidth: '25%',
editorWidth: '50%',
previewWidth: '25%'
};
// URL State Management
const URLState = {
// Parse current hash into state object
parse() {
const hash = window.location.hash.slice(1); // Remove '#'
if (!hash) return { view: 'snippets', snippetId: null, datasetId: null };
const parts = hash.split('/');
// #snippet-123456
if (hash.startsWith('snippet-')) {
return { view: 'snippets', snippetId: hash, datasetId: null };
}
// #datasets
if (parts[0] === 'datasets') {
if (parts.length === 1) {
return { view: 'datasets', snippetId: null, datasetId: null };
}
// #datasets/new
if (parts[1] === 'new') {
return { view: 'datasets', snippetId: null, datasetId: 'new' };
}
// #datasets/edit-dataset-123456 or #datasets/dataset-123456
if (parts[1].startsWith('edit-') || parts[1].startsWith('dataset-')) {
return { view: 'datasets', snippetId: null, datasetId: parts[1] };
}
}
return { view: 'snippets', snippetId: null, datasetId: null };
},
// Update URL hash without triggering hashchange
update(state, replaceState = false) {
let hash = '';
if (state.view === 'datasets') {
if (state.datasetId === 'new') {
hash = '#datasets/new';
} else if (state.datasetId) {
// Add 'dataset-' prefix if not already present
const datasetId = typeof state.datasetId === 'string' && state.datasetId.startsWith('dataset-')
? state.datasetId
: `dataset-${state.datasetId}`;
hash = `#datasets/${datasetId}`;
} else {
hash = '#datasets';
}
} else if (state.snippetId) {
// Add 'snippet-' prefix if not already present
const snippetId = typeof state.snippetId === 'string' && state.snippetId.startsWith('snippet-')
? state.snippetId
: `snippet-${state.snippetId}`;
hash = `#${snippetId}`;
}
if (replaceState) {
window.history.replaceState(null, '', hash || '#');
} else {
window.location.hash = hash;
}
},
// Clear hash
clear() {
window.history.replaceState(null, '', window.location.pathname);
}
};
// Settings storage
const AppSettings = {
STORAGE_KEY: 'astrolabe-settings',
// Default settings
defaults: {
sortBy: 'modified',
sortOrder: 'desc'
},
// Load settings from localStorage
load() {
try {
const stored = localStorage.getItem(this.STORAGE_KEY);
return stored ? { ...this.defaults, ...JSON.parse(stored) } : this.defaults;
} catch (error) {
console.error('Failed to load settings:', error);
return this.defaults;
}
},
// Save settings to localStorage
save(settings) {
try {
const currentSettings = this.load();
const updatedSettings = { ...currentSettings, ...settings };
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(updatedSettings));
return true;
} catch (error) {
console.error('Failed to save settings:', error);
return false;
}
},
// Get specific setting
get(key) {
const settings = this.load();
return settings[key];
},
// Set specific setting
set(key, value) {
const update = {};
update[key] = value;
return this.save(update);
}
};
// Toast Notification System
const Toast = {
// Auto-dismiss duration in milliseconds
DURATION: 4000,
// Toast counter for unique IDs
toastCounter: 0,
// Show toast notification
show(message, type = 'info') {
const container = document.getElementById('toast-container');
if (!container) return;
// Create toast element
const toast = document.createElement('div');
const toastId = `toast-${++this.toastCounter}`;
toast.id = toastId;
toast.className = `toast toast-${type}`;
// Toast icon based on type
const icons = {
error: '❌',
success: '✓',
warning: '⚠️',
info: ''
};
toast.innerHTML = `
<span class="toast-icon">${icons[type] || icons.info}</span>
<span class="toast-message">${message}</span>
<button class="toast-close" onclick="Toast.dismiss('${toastId}')">×</button>
`;
// Add to container
container.appendChild(toast);
// Trigger animation
setTimeout(() => toast.classList.add('toast-show'), 10);
// Auto-dismiss
setTimeout(() => this.dismiss(toastId), this.DURATION);
},
// Dismiss specific toast
dismiss(toastId) {
const toast = document.getElementById(toastId);
if (!toast) return;
toast.classList.remove('toast-show');
toast.classList.add('toast-hide');
// Remove from DOM after animation
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
},
// Convenience methods
error(message) {
this.show(message, 'error');
},
success(message) {
this.show(message, 'success');
},
warning(message) {
this.show(message, 'warning');
},
info(message) {
this.show(message, 'info');
}
};
// Analytics utility: Track events with GoatCounter
const Analytics = {
track(eventName, title) {
// Only track if GoatCounter is loaded
if (window.goatcounter && window.goatcounter.count) {
window.goatcounter.count({
path: eventName,
title: title || eventName,
event: true,
});
}
}
};
// Modal Manager - Generic modal control utility
const ModalManager = {
// Track which events to send to analytics when opening modals
trackingMap: {
'help-modal': ['modal-help', 'Open Help modal'],
'donate-modal': ['modal-donate', 'Open Donate modal'],
'settings-modal': ['modal-settings', 'Open Settings modal'],
'dataset-modal': ['modal-dataset', 'Open Dataset Manager']
},
open(modalId, shouldTrack = true) {
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'flex';
if (shouldTrack && this.trackingMap[modalId]) {
const [event, title] = this.trackingMap[modalId];
Analytics.track(event, title);
}
}
},
close(modalId) {
// Special handling for dataset modal to ensure URL state is updated
if (modalId === 'dataset-modal' && typeof closeDatasetManager === 'function') {
closeDatasetManager();
return;
}
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'none';
}
},
isOpen(modalId) {
const modal = document.getElementById(modalId);
return modal && modal.style.display === 'flex';
},
// Close any open modal (for ESC key handler)
closeAny() {
const modalIds = ['help-modal', 'donate-modal', 'settings-modal', 'dataset-modal', 'extract-modal'];
for (const modalId of modalIds) {
if (this.isOpen(modalId)) {
this.close(modalId);
return true;
}
}
return false;
}
};
// Shared utility: Format bytes for display
function formatBytes(bytes) {
if (bytes === null || bytes === undefined) return 'N/A';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
// Sample Vega-Lite specification
const sampleSpec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "A simple bar chart with embedded data.",
"data": {
"values": [
{ "category": "A", "value": 28 },
{ "category": "B", "value": 55 },
{ "category": "C", "value": 43 },
{ "category": "D", "value": 91 },
{ "category": "E", "value": 81 },
{ "category": "F", "value": 53 },
{ "category": "G", "value": 19 },
{ "category": "H", "value": 87 }
]
},
"mark": "bar",
"encoding": {
"x": { "field": "category", "type": "nominal", "axis": { "labelAngle": 0 } },
"y": { "field": "value", "type": "quantitative" }
}
};