From 9dd681d4f775b181ae85b801c19dc2866fe72eef Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Sat, 18 Oct 2025 01:49:14 +0300 Subject: [PATCH] feat: abstract common code to another file --- src/js/generic-storage-ui.js | 264 +++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 src/js/generic-storage-ui.js diff --git a/src/js/generic-storage-ui.js b/src/js/generic-storage-ui.js new file mode 100644 index 0000000..656b499 --- /dev/null +++ b/src/js/generic-storage-ui.js @@ -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 = `
${emptyMessage}
`; + 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); + } +}