Files
ETPlot/assets/js/metadata-popup.js
T
2025-07-08 13:19:54 +02:00

213 lines
7.5 KiB
JavaScript

/**
* Metadata Popup functionality for Gallery
*
* Handles showing metadata in small popups overlaid on plot thumbnails
*/
class MetadataPopup {
constructor() {
this.activePopup = null;
this.init();
}
init() {
// Close popup when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.metadata-btn') && !e.target.closest('.metadata-popup')) {
this.hidePopup();
}
});
// Close popup on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.hidePopup();
}
});
}
showPopup(button, plotName, metadata) {
// Hide any existing popup
this.hidePopup();
// Create popup element
const popup = document.createElement('div');
popup.className = 'metadata-popup';
popup.innerHTML = this.formatMetadata(plotName, metadata);
// Position popup relative to button
const rect = button.getBoundingClientRect();
popup.style.position = 'fixed';
popup.style.left = rect.left + 'px';
popup.style.top = (rect.bottom + 5) + 'px';
popup.style.zIndex = '1000';
// Add to DOM
document.body.appendChild(popup);
this.activePopup = popup;
// Adjust position if popup goes off screen
setTimeout(() => {
const popupRect = popup.getBoundingClientRect();
// Adjust horizontal position
if (popupRect.right > window.innerWidth) {
popup.style.left = (rect.right - popupRect.width) + 'px';
}
// Adjust vertical position
if (popupRect.bottom > window.innerHeight) {
popup.style.top = (rect.top - popupRect.height - 5) + 'px';
}
}, 0);
// Animate in
requestAnimationFrame(() => {
popup.classList.add('show');
});
}
hidePopup() {
if (this.activePopup) {
this.activePopup.classList.remove('show');
setTimeout(() => {
if (this.activePopup && this.activePopup.parentNode) {
this.activePopup.parentNode.removeChild(this.activePopup);
}
this.activePopup = null;
}, 200);
}
}
formatMetadata(plotName, metadata) {
if (!metadata || Object.keys(metadata).length === 0) {
return `
<div class="metadata-popup-header">
<h4>${plotName}</h4>
</div>
<div class="metadata-popup-content">
<p class="no-metadata">No metadata available</p>
</div>
`;
}
let html = `
<div class="metadata-popup-header">
<h4>${plotName}</h4>
</div>
<div class="metadata-popup-content">
`;
// Show priority fields first
const priorityFields = ['title', 'description', 'plot_type', 'experiment'];
const processedKeys = new Set();
// Display priority fields first
for (const key of priorityFields) {
if (metadata[key] !== undefined) {
html += this.formatMetadataField(key, metadata[key]);
processedKeys.add(key);
}
}
// Show file info if available
if (metadata.file_info) {
html += `<div class="metadata-section-title">File Information</div>`;
html += this.formatMetadataField('File Size', metadata.file_info.size);
if (metadata.file_info.extension) {
html += this.formatMetadataField('Format', metadata.file_info.extension);
}
processedKeys.add('file_info');
}
// Show timestamps if available
if (metadata.timestamps) {
html += `<div class="metadata-section-title">Timestamps</div>`;
if (metadata.timestamps.created_human) {
html += this.formatMetadataField('Created', metadata.timestamps.created_human);
}
if (metadata.timestamps.modified_human) {
html += this.formatMetadataField('Modified', metadata.timestamps.modified_human);
}
processedKeys.add('timestamps');
}
// Show extracted info if available
if (metadata.extracted_info && Object.keys(metadata.extracted_info).length > 0) {
html += `<div class="metadata-section-title">Plot Details</div>`;
for (const [key, value] of Object.entries(metadata.extracted_info)) {
html += this.formatMetadataField(key, value);
}
processedKeys.add('extracted_info');
}
// Display other fields (excluding generation info unless it's the only data)
const otherKeys = Object.keys(metadata).filter(key =>
!processedKeys.has(key) && key !== 'generation'
);
if (otherKeys.length > 0) {
html += `<div class="metadata-section-title">Additional Information</div>`;
for (const key of otherKeys) {
html += this.formatMetadataField(key, metadata[key]);
}
}
// Show generation info last if there's no other meaningful data
if (processedKeys.size <= 2 && metadata.generation) {
html += `<div class="metadata-section-title">Generation Info</div>`;
if (metadata.generation.generation_time) {
const genDate = new Date(metadata.generation.generation_time);
html += this.formatMetadataField('Generated', genDate.toLocaleString());
}
}
html += '</div>';
return html;
}
formatMetadataField(key, value) {
const displayKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
let formattedValue;
if (value === null || value === undefined) {
formattedValue = '<em>null</em>';
} else if (typeof value === 'object') {
if (Array.isArray(value)) {
if (value.length <= 3) {
formattedValue = value.map(item => `<span class="metadata-tag">${item}</span>`).join(' ');
} else {
formattedValue = `${value.slice(0, 3).map(item => `<span class="metadata-tag">${item}</span>`).join(' ')} <span class="metadata-more">+${value.length - 3} more</span>`;
}
} else {
// Show object as compact JSON for small objects, or just key count for large ones
const keys = Object.keys(value);
if (keys.length <= 3) {
formattedValue = '<code>' + JSON.stringify(value) + '</code>';
} else {
formattedValue = `<em>Object with ${keys.length} properties</em>`;
}
}
} else {
// Truncate long strings
const str = String(value);
formattedValue = str.length > 50 ? str.substring(0, 47) + '...' : str;
}
return `
<div class="metadata-field">
<span class="metadata-key">${displayKey}:</span>
<span class="metadata-value">${formattedValue}</span>
</div>
`;
}
}
// Global instance
window.metadataPopup = new MetadataPopup();
// Global function for template usage
window.showMetadataPopup = function(button, plotName, metadata) {
window.metadataPopup.showPopup(button, plotName, metadata);
};