diff --git a/src/public/index.html b/src/public/index.html index 68b6f2014..33c4beacf 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -149,10 +149,9 @@ display: none; } - /* Charts toggle button styling */ - .charts-mode #toggle-charts-button { - background: rgba(59, 130, 246, 0.1); - color: #3b82f6; + /* Charts mode indicator */ + .charts-mode { + /* Chart mode styles handled by checkbox state */ } /* Enhanced metric cell layouts */ @@ -201,117 +200,6 @@ background: rgba(255, 255, 255, 0.3); } - /* ALL first cells are sticky on mobile */ - #main-table td:first-child, - #main-table th:first-child { - position: sticky !important; - left: -2px; /* Shift left to cover gap */ - z-index: 1; - padding-left: 10px; /* Add extra padding to compensate */ - } - - /* Guest row first cells */ - #main-table tbody tr:not(.node-header) td:first-child { - background: white; - background-clip: padding-box; - box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.05); - } - .dark #main-table tbody tr:not(.node-header) td:first-child { - background: rgb(31 41 55); - background-clip: padding-box; - box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.2); - } - #main-table tbody tr:not(.node-header):hover td:first-child { - background: rgb(249 250 251); - } - .dark #main-table tbody tr:not(.node-header):hover td:first-child { - background: rgb(55 65 81); - } - - /* Table header first cell */ - #main-table thead th:first-child { - background: rgb(243 244 246); - background-clip: padding-box; - z-index: 11; - box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.05); - } - .dark #main-table thead th:first-child { - background: rgb(55 65 81); - background-clip: padding-box; - box-shadow: 2px 0 2px -1px rgba(0, 0, 0, 0.2); - } - - /* Node header first cell styling */ - #main-table tr.node-header td:first-child { - z-index: 2; - background: rgb(229 231 235) !important; - background-clip: padding-box; - box-shadow: 2px 0 4px -2px rgba(0, 0, 0, 0.1); - } - .dark #main-table tr.node-header td:first-child { - background: rgb(75 85 99) !important; - background-clip: padding-box; - box-shadow: 2px 0 4px -2px rgba(0, 0, 0, 0.3); - } - - /* Ensure full coverage on sticky cells */ - #main-table td:first-child, - #main-table th:first-child { - min-width: 140px; - position: relative; - } - - /* Solid edge on sticky columns to prevent bleed-through */ - #main-table td:first-child::before, - #main-table th:first-child::before { - content: ''; - position: absolute; - top: -1px; - left: -20px; /* Extended further left */ - right: -1px; - bottom: -1px; - background: inherit; - z-index: -1; - } - - /* Additional left coverage with after element */ - #main-table td:first-child::after, - #main-table th:first-child::after { - content: ''; - position: absolute; - top: 0; - left: -5px; - bottom: 0; - width: 5px; - background: inherit; - z-index: 1; - } - - /* Additional left edge coverage */ - #main-table td:first-child, - #main-table th:first-child { - border-left: 2px solid; - } - #main-table tbody tr:not(.node-header) td:first-child { - border-left-color: white; - } - .dark #main-table tbody tr:not(.node-header) td:first-child { - border-left-color: rgb(31 41 55); - } - #main-table tr.node-header td:first-child { - border-left-color: rgb(229 231 235); - } - .dark #main-table tr.node-header td:first-child { - border-left-color: rgb(75 85 99); - } - #main-table thead th:first-child { - border-left-color: rgb(243 244 246); - } - .dark #main-table thead th:first-child { - border-left-color: rgb(55 65 81); - } - - /* Remove the old node header mobile handling since we're using individual cells now */ } @@ -344,7 +232,7 @@ - + @@ -382,84 +270,93 @@ Loading node summary... - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + View: + + + + + + + + - 0 - - + Charts + + + + + + + + + + + + + + + + + Thresholds + 0 + + + - - - - + + + Display: + - + Grouped - + List - - + + - + All - + VM - + LXC - - + + - + All - + Running - + Stopped @@ -489,7 +386,7 @@ - Name + Name Type ID Up @@ -593,26 +490,26 @@ Loading backup status overview... - - + + - + - + All All - + VM - + LXC @@ -621,32 +518,32 @@ - + All All - + OK - + Stale - + Old - + Failed - + None @@ -662,7 +559,7 @@ - Guest Name + Guest Name ID Type Node diff --git a/src/public/js/ui/backups.js b/src/public/js/ui/backups.js index 17879e967..7589a3a3e 100644 --- a/src/public/js/ui/backups.js +++ b/src/public/js/ui/backups.js @@ -254,7 +254,7 @@ PulseApp.ui.backups = (() => { sevenDayDots += ''; row.innerHTML = ` - ${guestStatus.guestName} + ${guestStatus.guestName} ${guestStatus.guestId} ${typeIcon} ${guestStatus.node} @@ -299,6 +299,11 @@ PulseApp.ui.backups = (() => { console.error("UI elements for Backups tab not found!"); return; } + + // Find the scrollable container + const scrollableContainer = PulseApp.utils.getScrollableParent(tableBody) || + tableContainer.closest('.overflow-x-auto') || + tableContainer; const { allGuests, initialDataReceived, tasksByGuest, snapshotsByGuest, dayBoundaries, threeDaysAgo, sevenDaysAgo } = _getInitialBackupData(); @@ -364,15 +369,16 @@ PulseApp.ui.backups = (() => { const sortStateBackups = PulseApp.state.getSortState('backups'); const sortedBackupStatus = PulseApp.utils.sortData(filteredBackupStatus, sortStateBackups.column, sortStateBackups.direction, 'backups'); - tableBody.innerHTML = ''; - if (sortedBackupStatus.length > 0) { - sortedBackupStatus.forEach(guestStatus => { - const row = _renderBackupTableRow(guestStatus); - tableBody.appendChild(row); - }); - noDataMsg.classList.add('hidden'); - tableContainer.classList.remove('hidden'); - } else { + PulseApp.utils.preserveScrollPosition(scrollableContainer, () => { + tableBody.innerHTML = ''; + if (sortedBackupStatus.length > 0) { + sortedBackupStatus.forEach(guestStatus => { + const row = _renderBackupTableRow(guestStatus); + tableBody.appendChild(row); + }); + noDataMsg.classList.add('hidden'); + tableContainer.classList.remove('hidden'); + } else { tableContainer.classList.add('hidden'); let emptyMessage = "No backup information found for any guests."; if (backupStatusByGuest.length > 0 && filteredBackupStatus.length === 0) { // Data exists, but filters hide all @@ -393,6 +399,7 @@ PulseApp.ui.backups = (() => { noDataMsg.textContent = emptyMessage; noDataMsg.classList.remove('hidden'); } + }); // End of preserveScrollPosition const backupsSortColumn = sortStateBackups.column; const backupsHeader = document.querySelector(`#backups-overview-table th[data-sort="${backupsSortColumn}"]`); diff --git a/src/public/js/ui/common.js b/src/public/js/ui/common.js index 8607e7b4a..d119ae184 100644 --- a/src/public/js/ui/common.js +++ b/src/public/js/ui/common.js @@ -312,18 +312,13 @@ PulseApp.ui.common = (() => { function generateNodeGroupHeaderCellHTML(text, colspan, cellTag = 'td') { const baseClasses = 'py-0.5 px-2 text-left font-medium text-xs sm:text-sm text-gray-700 dark:text-gray-300'; - // On mobile, create individual cells so first one can be sticky - if (window.innerWidth < 768) { - let html = `<${cellTag} class="${baseClasses} bg-gray-200 dark:bg-gray-700">${text}${cellTag}>`; - // Add empty cells for remaining columns - for (let i = 1; i < colspan; i++) { - html += `<${cellTag} class="bg-gray-200 dark:bg-gray-700">${cellTag}>`; - } - return html; + // Always create individual cells so first one can be sticky + let html = `<${cellTag} class="sticky left-0 bg-gray-200 dark:bg-gray-700 z-10 ${baseClasses}">${text}${cellTag}>`; + // Add empty cells for remaining columns + for (let i = 1; i < colspan; i++) { + html += `<${cellTag} class="bg-gray-200 dark:bg-gray-700">${cellTag}>`; } - - // Desktop: use colspan - return `<${cellTag} colspan="${colspan}" class="${baseClasses} bg-gray-200 dark:bg-gray-700 node-header-cell">${text}${cellTag}>`; + return html; } return { diff --git a/src/public/js/ui/dashboard.js b/src/public/js/ui/dashboard.js index 520f02acd..eb1be64dc 100644 --- a/src/public/js/ui/dashboard.js +++ b/src/public/js/ui/dashboard.js @@ -67,9 +67,9 @@ PulseApp.ui.dashboard = (() => { } // Initialize charts toggle - const chartsToggleButton = document.getElementById('toggle-charts-button'); - if (chartsToggleButton) { - chartsToggleButton.addEventListener('click', toggleChartsMode); + const chartsToggleCheckbox = document.getElementById('toggle-charts-checkbox'); + if (chartsToggleCheckbox) { + chartsToggleCheckbox.addEventListener('change', toggleChartsMode); } // Initialize mobile scroll indicators @@ -524,18 +524,21 @@ PulseApp.ui.dashboard = (() => { } else { row.className = 'border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'; } - + // Update specific cells that might have changed const cells = row.querySelectorAll('td'); + + // Ensure name cell keeps sticky styling even after row class updates + if (cells[0]) { + cells[0].className = 'sticky left-0 bg-white dark:bg-gray-800 z-10 p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0'; + } if (cells.length >= 10) { // Cell order: name(0), type(1), id(2), uptime(3), cpu(4), memory(5), disk(6), diskread(7), diskwrite(8), netin(9), netout(10) - // Update name (cell 0) - const nameCell = cells[0]; - - if (nameCell.textContent !== guest.name) { - nameCell.textContent = guest.name; - nameCell.title = guest.name; + // Update name (cell 0) content only (styling handled above) + if (cells[0].textContent !== guest.name) { + cells[0].textContent = guest.name; + cells[0].title = guest.name; } // Ensure ID cell (2) has proper classes @@ -682,6 +685,11 @@ PulseApp.ui.dashboard = (() => { return; } + // Find the scrollable container + const scrollableContainer = PulseApp.utils.getScrollableParent(tableBodyEl) || + document.querySelector('.table-container') || + tableBodyEl.closest('.overflow-x-auto'); + // Show loading skeleton if no data yet const currentData = PulseApp.state.get('dashboardData'); if (!currentData || currentData.length === 0) { @@ -746,50 +754,56 @@ PulseApp.ui.dashboard = (() => { visibleCount = sortedData.length; sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase())); } else if (needsFullRebuild) { - // Full rebuild for normal rendering - if (groupByNode) { - const groupRenderResult = _renderGroupedByNode(tableBodyEl, sortedData, createGuestRow); - visibleCount = groupRenderResult.visibleCount; - visibleNodes = groupRenderResult.visibleNodes; - } else { - PulseApp.utils.renderTableBody(tableBodyEl, sortedData, createGuestRow, "No matching guests found.", 11); - visibleCount = sortedData.length; - sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase())); - } + // Full rebuild for normal rendering with scroll preservation + PulseApp.utils.preserveScrollPosition(scrollableContainer, () => { + if (groupByNode) { + const groupRenderResult = _renderGroupedByNode(tableBodyEl, sortedData, createGuestRow); + visibleCount = groupRenderResult.visibleCount; + visibleNodes = groupRenderResult.visibleNodes; + } else { + PulseApp.utils.renderTableBody(tableBodyEl, sortedData, createGuestRow, "No matching guests found.", 11); + visibleCount = sortedData.length; + sortedData.forEach(guest => visibleNodes.add((guest.node || 'Unknown Node').toLowerCase())); + } + }); previousGroupByNode = groupByNode; } else { - // Incremental update using DOM diffing - const result = _updateTableIncremental(tableBodyEl, sortedData, createGuestRow, groupByNode); - visibleCount = result.visibleCount; - visibleNodes = result.visibleNodes; + // Incremental update using DOM diffing with scroll preservation + PulseApp.utils.preserveScrollPosition(scrollableContainer, () => { + const result = _updateTableIncremental(tableBodyEl, sortedData, createGuestRow, groupByNode); + visibleCount = result.visibleCount; + visibleNodes = result.visibleNodes; + }); } previousTableData = sortedData; if (visibleCount === 0 && tableBodyEl) { - const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : []; - const activeThresholds = Object.entries(thresholdState).filter(([_, state]) => state.value > 0); - const thresholdTexts = activeThresholds.map(([key, state]) => { - return `${PulseApp.utils.getReadableThresholdName(key)}>=${PulseApp.utils.formatThresholdValue(key, state.value)}`; - }); - - const hasFilters = filterGuestType !== FILTER_ALL || filterStatus !== FILTER_ALL || textSearchTerms.length > 0 || activeThresholds.length > 0; - - if (PulseApp.ui.emptyStates) { - const context = { - filterType: filterGuestType, - filterStatus: filterStatus, - searchTerms: textSearchTerms, - thresholds: thresholdTexts - }; + PulseApp.utils.preserveScrollPosition(scrollableContainer, () => { + const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : []; + const activeThresholds = Object.entries(thresholdState).filter(([_, state]) => state.value > 0); + const thresholdTexts = activeThresholds.map(([key, state]) => { + return `${PulseApp.utils.getReadableThresholdName(key)}>=${PulseApp.utils.formatThresholdValue(key, state.value)}`; + }); - const emptyType = hasFilters ? 'no-results' : 'no-guests'; - tableBodyEl.innerHTML = PulseApp.ui.emptyStates.createTableEmptyState(emptyType, context, 11); - } else { - // Fallback to simple message - let message = hasFilters ? "No guests match the current filters." : "No guests found."; - tableBodyEl.innerHTML = `${message}`; - } + const hasFilters = filterGuestType !== FILTER_ALL || filterStatus !== FILTER_ALL || textSearchTerms.length > 0 || activeThresholds.length > 0; + + if (PulseApp.ui.emptyStates) { + const context = { + filterType: filterGuestType, + filterStatus: filterStatus, + searchTerms: textSearchTerms, + thresholds: thresholdTexts + }; + + const emptyType = hasFilters ? 'no-results' : 'no-guests'; + tableBodyEl.innerHTML = PulseApp.ui.emptyStates.createTableEmptyState(emptyType, context, 11); + } else { + // Fallback to simple message + let message = hasFilters ? "No guests match the current filters." : "No guests found."; + tableBodyEl.innerHTML = `${message}`; + } + }); } _updateDashboardStatusMessage(statusElementEl, visibleCount, visibleNodes, groupByNode, filterGuestType, filterStatus, searchInput, thresholdState); @@ -943,7 +957,7 @@ PulseApp.ui.dashboard = (() => { } row.innerHTML = ` - ${guest.name} + ${guest.name} ${typeIcon} ${guest.id} ${uptimeDisplay} @@ -980,16 +994,13 @@ PulseApp.ui.dashboard = (() => { function toggleChartsMode() { const mainContainer = document.getElementById('main'); - const button = document.getElementById('toggle-charts-button'); + const checkbox = document.getElementById('toggle-charts-checkbox'); + const label = checkbox ? checkbox.parentElement : null; - if (mainContainer.classList.contains('charts-mode')) { - // Switch to metrics mode - mainContainer.classList.remove('charts-mode'); - button.title = 'Toggle Charts View'; - } else { + if (checkbox && checkbox.checked) { // Switch to charts mode mainContainer.classList.add('charts-mode'); - button.title = 'Toggle Metrics View'; + if (label) label.title = 'Toggle Metrics View'; // Immediately render charts when switching to charts mode if (PulseApp.charts) { @@ -997,6 +1008,10 @@ PulseApp.ui.dashboard = (() => { PulseApp.charts.updateAllCharts(); }); } + } else { + // Switch to metrics mode + mainContainer.classList.remove('charts-mode'); + if (label) label.title = 'Toggle Charts View'; } } diff --git a/src/public/js/ui/nodes.js b/src/public/js/ui/nodes.js index f4d6d4743..b6fc1e3a5 100644 --- a/src/public/js/ui/nodes.js +++ b/src/public/js/ui/nodes.js @@ -64,7 +64,7 @@ PulseApp.ui.nodes = (() => { ${statusText} - ${node.node || 'N/A'} + ${node.node || 'N/A'} ${cpuBarHTML} ${memoryBarHTML} ${diskBarHTML} @@ -147,7 +147,13 @@ PulseApp.ui.nodes = (() => { return; } - container.innerHTML = ''; // Clear previous content + // Find the scrollable container + const scrollableContainer = PulseApp.utils.getScrollableParent(container) || + container.closest('.overflow-x-auto') || + container.parentElement; + + PulseApp.utils.preserveScrollPosition(scrollableContainer, () => { + container.innerHTML = ''; // Clear previous content const numNodes = nodes.length; const isMobile = window.innerWidth < 640; // sm breakpoint @@ -197,6 +203,7 @@ PulseApp.ui.nodes = (() => { }); container.appendChild(gridDiv); } + }); // End of preserveScrollPosition } function createCondensedNodeCard(node) { @@ -246,7 +253,14 @@ PulseApp.ui.nodes = (() => { console.log('[Nodes] Node table not found - using summary cards display instead'); return; } - tbody.innerHTML = ''; + + // Find the scrollable container + const scrollableContainer = PulseApp.utils.getScrollableParent(tbody) || + tbody.closest('.overflow-x-auto') || + tbody.parentElement; + + PulseApp.utils.preserveScrollPosition(scrollableContainer, () => { + tbody.innerHTML = ''; if (!nodes || nodes.length === 0) { tbody.innerHTML = 'No nodes found or data unavailable'; @@ -290,6 +304,7 @@ PulseApp.ui.nodes = (() => { }); } } + }); // End of preserveScrollPosition } function init() { diff --git a/src/public/js/ui/pbs.js b/src/public/js/ui/pbs.js index c53109538..f92f33fb3 100644 --- a/src/public/js/ui/pbs.js +++ b/src/public/js/ui/pbs.js @@ -367,9 +367,15 @@ PulseApp.ui.pbs = (() => { const table = tableBody.closest('table'); const tableId = table?.id; const isShowMoreExpanded = tableId ? expandedShowMoreState.has(tableId) : false; + + // Find the scrollable container + const scrollableContainer = PulseApp.utils.getScrollableParent(tableBody) || + parentSectionElement.closest('.overflow-x-auto') || + parentSectionElement; // Use global expanded state instead of scanning DOM - tableBody.innerHTML = ''; + PulseApp.utils.preserveScrollPosition(scrollableContainer, () => { + tableBody.innerHTML = ''; const tasks = fullTasksArray || []; @@ -478,11 +484,19 @@ PulseApp.ui.pbs = (() => { } } } + }); // End of preserveScrollPosition } const _populateDsTableBody = (dsTableBody, datastores, statusText, showDetails) => { if (!dsTableBody) return; - dsTableBody.innerHTML = ''; + + // Find the scrollable container + const scrollableContainer = PulseApp.utils.getScrollableParent(dsTableBody) || + dsTableBody.closest('.overflow-x-auto') || + dsTableBody.parentElement; + + PulseApp.utils.preserveScrollPosition(scrollableContainer, () => { + dsTableBody.innerHTML = ''; if (showDetails && datastores) { if (datastores.length === 0) { @@ -566,6 +580,7 @@ PulseApp.ui.pbs = (() => { cell.className = `px-4 py-4 ${CSS_CLASSES.TEXT_SM} ${CSS_CLASSES.TEXT_GRAY_400} text-center`; cell.textContent = statusText; } + }); // End of preserveScrollPosition }; const _populateInstanceTaskSections = (detailsContainer, instanceId, pbsInstance, statusText, showDetails) => { diff --git a/src/public/js/ui/storage.js b/src/public/js/ui/storage.js index 313cbcf73..7f596f28b 100644 --- a/src/public/js/ui/storage.js +++ b/src/public/js/ui/storage.js @@ -115,8 +115,16 @@ PulseApp.ui.storage = (() => { function updateStorageInfo() { const contentDiv = document.getElementById('storage-info-content'); if (!contentDiv) return; - contentDiv.innerHTML = ''; - contentDiv.className = ''; + + // Find the scrollable container + const scrollableContainer = PulseApp.utils.getScrollableParent(contentDiv) || + contentDiv.closest('.overflow-x-auto') || + contentDiv.parentElement; + + // Preserve scroll position while updating + PulseApp.utils.preserveScrollPosition(scrollableContainer, () => { + contentDiv.innerHTML = ''; + contentDiv.className = ''; const nodes = PulseApp.state.get('nodesData') || []; @@ -200,6 +208,7 @@ PulseApp.ui.storage = (() => { table.appendChild(thead); table.appendChild(tbody); contentDiv.appendChild(table); + }); // End of preserveScrollPosition } function _createStorageRow(store) { @@ -222,7 +231,7 @@ PulseApp.ui.storage = (() => { const contentBadges = getContentBadgesHTML(store.content); row.innerHTML = ` - ${store.storage || 'N/A'} + ${store.storage || 'N/A'} ${contentBadges} ${store.type || 'N/A'} ${sharedIcon} diff --git a/src/public/js/ui/thresholds.js b/src/public/js/ui/thresholds.js index a722dcdaa..deda3a559 100644 --- a/src/public/js/ui/thresholds.js +++ b/src/public/js/ui/thresholds.js @@ -10,7 +10,7 @@ PulseApp.ui.thresholds = (() => { function init() { thresholdRow = document.getElementById('threshold-slider-row'); - toggleThresholdsButton = document.getElementById('toggle-thresholds-button'); + toggleThresholdsButton = document.getElementById('toggle-thresholds-checkbox'); thresholdBadge = document.getElementById('threshold-count-badge'); sliders = { @@ -30,12 +30,12 @@ PulseApp.ui.thresholds = (() => { updateThresholdRowVisibility(); if (toggleThresholdsButton) { - toggleThresholdsButton.addEventListener('click', () => { - PulseApp.state.set('isThresholdRowVisible', !PulseApp.state.get('isThresholdRowVisible')); + toggleThresholdsButton.addEventListener('change', () => { + PulseApp.state.set('isThresholdRowVisible', toggleThresholdsButton.checked); updateThresholdRowVisibility(); }); } else { - console.warn('#toggle-thresholds-button not found.'); + console.warn('#toggle-thresholds-checkbox not found.'); } _setupSliderListeners(); @@ -126,10 +126,8 @@ PulseApp.ui.thresholds = (() => { if (thresholdRow) { thresholdRow.classList.toggle('hidden', !isVisible); if (toggleThresholdsButton) { - toggleThresholdsButton.classList.toggle('bg-blue-100', isVisible); - toggleThresholdsButton.classList.toggle('dark:bg-blue-800/50', isVisible); - toggleThresholdsButton.classList.toggle('text-blue-700', isVisible); - toggleThresholdsButton.classList.toggle('dark:text-blue-300', isVisible); + // Update checkbox state to match visibility + toggleThresholdsButton.checked = isVisible; } } } diff --git a/src/public/js/utils.js b/src/public/js/utils.js index be422752f..8d4300047 100644 --- a/src/public/js/utils.js +++ b/src/public/js/utils.js @@ -285,6 +285,43 @@ PulseApp.utils = (() => { // Debounced version for resize events const updateProgressBarTextsDebounced = debounce(updateProgressBarTexts, 100); + // Scroll position preservation for table updates + function preserveScrollPosition(element, updateFn) { + if (!element) { + updateFn(); + return; + } + + // Save current scroll positions + const scrollLeft = element.scrollLeft; + const scrollTop = element.scrollTop; + + // Execute the update function + updateFn(); + + // Restore scroll positions after a microtask to ensure DOM is updated + Promise.resolve().then(() => { + element.scrollLeft = scrollLeft; + element.scrollTop = scrollTop; + }); + } + + // Get the scrollable parent of a table + function getScrollableParent(element) { + if (!element) return null; + + let parent = element.parentElement; + while (parent) { + const style = window.getComputedStyle(parent); + if (style.overflowX === 'auto' || style.overflowX === 'scroll' || + style.overflowY === 'auto' || style.overflowY === 'scroll') { + return parent; + } + parent = parent.parentElement; + } + return null; + } + // Return the public API for this module return { sanitizeForId: (str) => str.replace(/[^a-zA-Z0-9-]/g, '-'), @@ -303,6 +340,8 @@ PulseApp.utils = (() => { renderTableBody, debounce, updateProgressBarTexts, - updateProgressBarTextsDebounced + updateProgressBarTextsDebounced, + preserveScrollPosition, + getScrollableParent }; })(); \ No newline at end of file
Loading node summary...
Loading backup status overview...