mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
feat: abstract common code to another file
This commit is contained in:
264
src/js/generic-storage-ui.js
Normal file
264
src/js/generic-storage-ui.js
Normal file
@@ -0,0 +1,264 @@
|
||||
// Generic Storage UI Utilities
|
||||
// Provides reusable patterns for list management, item selection, and linked item display
|
||||
// Used by both snippet-manager.js and dataset-manager.js
|
||||
|
||||
/**
|
||||
* Generic list rendering function
|
||||
* @param {string} containerId - ID of container to render list into
|
||||
* @param {Array} items - Array of items to render
|
||||
* @param {Function} formatItem - Function that takes item and returns HTML string
|
||||
* @param {Function} onSelectItem - Callback when item is selected, receives item ID
|
||||
* @param {string} emptyMessage - Message to show when list is empty
|
||||
*/
|
||||
function renderGenericList(containerId, items, formatItem, onSelectItem, emptyMessage = 'No items found') {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = `<div class="list-empty">${emptyMessage}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = items.map(formatItem).join('');
|
||||
container.innerHTML = html;
|
||||
|
||||
// Attach click handlers to items
|
||||
container.querySelectorAll('[data-item-id]').forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
const itemId = this.dataset.itemId;
|
||||
onSelectItem(itemId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic item selector function
|
||||
* Updates visual selection and populates details panel
|
||||
* @param {string|number} itemId - ID of item to select
|
||||
* @param {string} itemContainerSelector - CSS selector for item elements (e.g., '.snippet-item')
|
||||
* @param {string} dataAttributeName - Name of data attribute holding ID (e.g., 'snippetId', 'datasetId')
|
||||
*/
|
||||
function selectGenericItem(itemId, itemContainerSelector, dataAttributeName) {
|
||||
// Clear previous selection
|
||||
document.querySelectorAll(itemContainerSelector).forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Set new selection
|
||||
const selector = `${itemContainerSelector}[data-${dataAttributeName}="${itemId}"]`;
|
||||
const selectedItem = document.querySelector(selector);
|
||||
if (selectedItem) {
|
||||
selectedItem.classList.add('selected');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic linked items viewer
|
||||
* Displays items that reference the current item
|
||||
* @param {Array} linkedItems - Array of items to display
|
||||
* @param {string} containerId - ID of container for the list
|
||||
* @param {string} sectionId - ID of section wrapper (shown/hidden based on count)
|
||||
* @param {Function} formatLinkedItem - Function that returns HTML for each linked item
|
||||
* @param {Function} onLinkedItemClick - Callback when a linked item is clicked
|
||||
*/
|
||||
function updateGenericLinkedItems(linkedItems, containerId, sectionId, formatLinkedItem, onLinkedItemClick) {
|
||||
const section = document.getElementById(sectionId);
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (!section || !container) return;
|
||||
|
||||
if (linkedItems.length === 0) {
|
||||
section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
section.style.display = 'block';
|
||||
const html = linkedItems.map(formatLinkedItem).join('');
|
||||
container.innerHTML = html;
|
||||
|
||||
// Attach click handlers
|
||||
container.querySelectorAll('[data-linked-item-id]').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const linkedItemId = this.dataset.linkedItemId;
|
||||
onLinkedItemClick(linkedItemId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic modal helper
|
||||
* @param {string} modalId - ID of modal element
|
||||
* @param {boolean} show - Whether to show (true) or hide (false)
|
||||
* @param {Function} onShow - Optional callback when showing
|
||||
* @param {Function} onHide - Optional callback when hiding
|
||||
*/
|
||||
function toggleGenericModal(modalId, show, onShow, onHide) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (!modal) return;
|
||||
|
||||
if (show) {
|
||||
modal.style.display = 'flex';
|
||||
if (onShow) onShow();
|
||||
} else {
|
||||
modal.style.display = 'none';
|
||||
if (onHide) onHide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic form show/hide helper
|
||||
* @param {string} viewId - ID of view/form container
|
||||
* @param {boolean} show - Whether to show (true) or hide (false)
|
||||
* @param {Function} onShow - Optional callback when showing
|
||||
* @param {Function} onHide - Optional callback when hiding
|
||||
*/
|
||||
function toggleGenericForm(viewId, show, onShow, onHide) {
|
||||
const view = document.getElementById(viewId);
|
||||
if (!view) return;
|
||||
|
||||
if (show) {
|
||||
view.style.display = show === 'block' ? 'block' : 'flex';
|
||||
if (onShow) onShow();
|
||||
} else {
|
||||
view.style.display = 'none';
|
||||
if (onHide) onHide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic element visibility toggler
|
||||
* @param {string} elementId - ID of element to toggle
|
||||
* @param {boolean} show - Whether to show or hide
|
||||
*/
|
||||
function setElementVisibility(elementId, show) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic field populator for details panel
|
||||
* @param {Object} data - Object with field data
|
||||
* @param {Array} fieldMappings - Array of {fieldId, value} objects
|
||||
*/
|
||||
function populateDetailsPanel(data, fieldMappings) {
|
||||
fieldMappings.forEach(({ fieldId, getValue }) => {
|
||||
const element = document.getElementById(fieldId);
|
||||
if (element) {
|
||||
element.value = getValue ? getValue(data) : '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic item deletion with confirmation
|
||||
* @param {string} itemName - Name of item for confirmation message
|
||||
* @param {string} warningMessage - Optional warning if item is in use
|
||||
* @param {Function} onConfirm - Callback if user confirms deletion
|
||||
* @returns {boolean} - Whether deletion was confirmed
|
||||
*/
|
||||
function confirmGenericDeletion(itemName, warningMessage = null, onConfirm) {
|
||||
let message = `Delete "${itemName}"?`;
|
||||
|
||||
if (warningMessage) {
|
||||
message = `⚠️ ${warningMessage}\n\nAre you sure you want to delete it?`;
|
||||
} else {
|
||||
message += ' This action cannot be undone.';
|
||||
}
|
||||
|
||||
if (confirm(message)) {
|
||||
if (onConfirm) onConfirm();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic file download helper
|
||||
* @param {string|Blob} content - Content to download
|
||||
* @param {string} filename - Filename for download
|
||||
* @param {string} mimeType - MIME type (default: 'text/plain')
|
||||
*/
|
||||
function downloadFile(content, filename, mimeType = 'text/plain') {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear file input helper
|
||||
* @param {string} inputId - ID of file input element
|
||||
*/
|
||||
function clearFileInput(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic text field update helper with debounce
|
||||
* @param {string} fieldId - ID of input field
|
||||
* @param {Function} onUpdate - Callback with new value
|
||||
* @param {number} debounceMs - Debounce delay in milliseconds
|
||||
*/
|
||||
function setupDebouncedFieldUpdate(fieldId, onUpdate, debounceMs = 1000) {
|
||||
const field = document.getElementById(fieldId);
|
||||
if (!field) return;
|
||||
|
||||
let timeout;
|
||||
field.addEventListener('input', function() {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
onUpdate(this.value);
|
||||
}, debounceMs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic toggle button state updater
|
||||
* @param {Array} buttons - Array of {id, isActive} objects
|
||||
*/
|
||||
function updateToggleButtons(buttons) {
|
||||
buttons.forEach(({ id, isActive }) => {
|
||||
const button = document.getElementById(id);
|
||||
if (button) {
|
||||
if (isActive) {
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic URL state updater with fallback
|
||||
* Only updates if URLState global is available
|
||||
* @param {Object} state - State object to pass to URLState.update
|
||||
*/
|
||||
function updateURLStateIfAvailable(state) {
|
||||
if (typeof URLState !== 'undefined' && URLState.update) {
|
||||
URLState.update(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic analytics tracker with fallback
|
||||
* Only tracks if Analytics global is available
|
||||
* @param {string} event - Event name
|
||||
* @param {string} label - Event label
|
||||
*/
|
||||
function trackEventIfAvailable(event, label) {
|
||||
if (typeof Analytics !== 'undefined' && Analytics.track) {
|
||||
Analytics.track(event, label);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user