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