/** * SCADA vs DWG Manifest Comparison Tool * Main JavaScript functionality */ document.addEventListener('DOMContentLoaded', function() { // Initialize Bootstrap components initializeBootstrapComponents(); // Add search/filter functionality to tables initializeTableFilters(); // Add copy to clipboard functionality initializeClipboardCopy(); // Handle form submissions to prevent resubmission prompts initializeFormHandlers(); }); /** * Initialize form handlers to prevent resubmission prompts */ function initializeFormHandlers() { // Handle main comparison form const comparisonForm = document.querySelector('form[action*="compare"]'); if (comparisonForm) { comparisonForm.addEventListener('submit', function(e) { e.preventDefault(); const formData = new FormData(this); // Show loading indicator const submitBtn = this.querySelector('button[type="submit"]'); const originalBtnText = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = 'Processing...'; fetch(this.action, { method: 'POST', body: formData, redirect: 'manual' }) .then(response => { if (response.type === 'opaqueredirect') { // Server redirected, follow the redirect window.location.href = response.url || '/'; return; } return response.text(); }) .then(html => { if (html) { // If we got HTML back and not a redirect, handle it // This is a fallback in case redirect didn't work window.location.reload(); } }) .catch(error => { console.error('Error:', error); alert('An error occurred during form submission. Please try again.'); submitBtn.disabled = false; submitBtn.innerHTML = originalBtnText; }); }); } // Handle update files form const updateForm = document.querySelector('form[action*="update_files"]'); if (updateForm) { updateForm.addEventListener('submit', function(e) { e.preventDefault(); const formData = new FormData(this); // Show loading indicator const submitBtn = this.querySelector('button[type="submit"]'); const originalBtnText = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = 'Updating...'; fetch(this.action, { method: 'POST', body: formData, redirect: 'manual' }) .then(response => { if (response.type === 'opaqueredirect') { // Server redirected, follow the redirect window.location.href = response.url || '/'; return; } return response.text(); }) .then(html => { if (html) { // If we got HTML back and not a redirect, handle it window.location.reload(); } }) .catch(error => { console.error('Error:', error); alert('An error occurred during update. Please try again.'); submitBtn.disabled = false; submitBtn.innerHTML = originalBtnText; }); }); } // Handle delete comparison form (the one created dynamically in the results page) document.addEventListener('submit', function(e) { if (e.target.action && e.target.action.includes('delete_comparison')) { e.preventDefault(); fetch(e.target.action, { method: 'POST', redirect: 'manual' }) .then(response => { if (response.type === 'opaqueredirect') { window.location.href = response.url || '/'; return; } return response.text(); }) .then(html => { if (html) { window.location.href = '/'; } }) .catch(error => { console.error('Error:', error); alert('An error occurred while deleting the comparison. Please try again.'); }); } }); } /** * Initialize Bootstrap components like tabs */ function initializeBootstrapComponents() { // Initialize tabs if they exist const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]'); if (tabEls.length > 0) { tabEls.forEach(tabEl => { new bootstrap.Tab(tabEl); }); // Add event listener for tab change to persist active tab tabEls.forEach(tab => { tab.addEventListener('shown.bs.tab', function (event) { // Store the currently active tab in local storage localStorage.setItem('activeTab', event.target.id); }); }); // Restore active tab from localStorage if available const activeTabId = localStorage.getItem('activeTab'); if (activeTabId) { const activeTab = document.getElementById(activeTabId); if (activeTab) { new bootstrap.Tab(activeTab).show(); } } } } /** * Add search/filter functionality to tables */ function initializeTableFilters() { // Create filter inputs for each table that needs filtering const tableContainers = document.querySelectorAll('.table-container'); tableContainers.forEach((container, index) => { const table = container.querySelector('table'); if (!table) return; // Make the table the first element in a new structure const originalHTML = container.innerHTML; container.innerHTML = ''; // Create a wrapper for the sticky elements const stickyHeader = document.createElement('div'); stickyHeader.className = 'sticky-table-header'; // Create filter input const filterDiv = document.createElement('div'); filterDiv.className = 'table-filter-container d-flex justify-content-between align-items-center'; // Create the left side with search input const searchGroup = document.createElement('div'); searchGroup.className = 'input-group'; searchGroup.style.flexBasis = '80%'; const filterInput = document.createElement('input'); filterInput.type = 'text'; filterInput.className = 'form-control table-filter'; filterInput.placeholder = 'Filter items...'; filterInput.setAttribute('data-table-index', index); const filterClearBtn = document.createElement('button'); filterClearBtn.className = 'btn btn-outline-secondary'; filterClearBtn.innerHTML = '×'; filterClearBtn.type = 'button'; searchGroup.appendChild(filterInput); searchGroup.appendChild(filterClearBtn); // Add the search group to the filter container filterDiv.appendChild(searchGroup); // The copy button will be added by initializeClipboardCopy() stickyHeader.appendChild(filterDiv); // Create a scrollable content div const scrollableContent = document.createElement('div'); scrollableContent.className = 'scrollable-table-content'; scrollableContent.innerHTML = originalHTML; // Add both elements to the container container.appendChild(stickyHeader); container.appendChild(scrollableContent); // Fix for header width issue - ensure the table has a consistent width // and the header cells match the body cells window.addEventListener('load', function() { const tableHeader = scrollableContent.querySelector('thead'); const tableBody = scrollableContent.querySelector('tbody'); if (tableHeader && tableBody) { // Ensure equal cell widths const headerCells = tableHeader.querySelectorAll('th'); const firstRowCells = tableBody.querySelector('tr') ? tableBody.querySelector('tr').querySelectorAll('td') : null; if (headerCells.length > 0 && firstRowCells && firstRowCells.length > 0) { // Set explicit widths on header cells based on first row for (let i = 0; i < headerCells.length && i < firstRowCells.length; i++) { const width = firstRowCells[i].offsetWidth; headerCells[i].style.minWidth = width + 'px'; } } } }); // Add event listeners filterInput.addEventListener('input', function() { // Get the table from the scrollable content const tableInContent = scrollableContent.querySelector('table'); filterTable(tableInContent, this.value); }); filterClearBtn.addEventListener('click', function() { filterInput.value = ''; const tableInContent = scrollableContent.querySelector('table'); filterTable(tableInContent, ''); }); }); } /** * Filter table rows based on input */ function filterTable(table, filterText) { const tbody = table.querySelector('tbody'); if (!tbody) return; const rows = tbody.querySelectorAll('tr'); const lowerFilterText = filterText.toLowerCase(); rows.forEach(row => { const text = row.textContent.toLowerCase(); if (text.includes(lowerFilterText)) { row.style.display = ''; } else { row.style.display = 'none'; } }); // Update counter if there is one const cardHeader = table.closest('.card').querySelector('.card-header h5'); if (cardHeader) { const originalText = cardHeader.getAttribute('data-original-text'); if (!originalText) { cardHeader.setAttribute('data-original-text', cardHeader.textContent); } if (filterText) { const visibleRowCount = [...rows].filter(row => row.style.display !== 'none').length; const matches = cardHeader.textContent.match(/\((\d+)\)/); if (matches && matches[1]) { cardHeader.textContent = cardHeader.textContent.replace( `(${matches[1]})`, `(${visibleRowCount} of ${matches[1]})` ); } } else if (originalText) { cardHeader.textContent = originalText; } } } /** * Add ability to copy table contents to clipboard */ function initializeClipboardCopy() { const tableContainers = document.querySelectorAll('.table-container'); tableContainers.forEach(container => { // Find the filter container in the sticky header const filterContainer = container.querySelector('.table-filter-container'); if (!filterContainer) return; // Create copy button const copyBtn = document.createElement('button'); copyBtn.className = 'btn btn-primary ms-2 copy-btn'; copyBtn.innerHTML = ' Copy'; // Insert button into the filter container filterContainer.appendChild(copyBtn); // Add event listener copyBtn.addEventListener('click', function() { // Find the table in the scrollable content const scrollableContent = container.querySelector('.scrollable-table-content'); const table = scrollableContent ? scrollableContent.querySelector('table') : container.querySelector('table'); if (!table) return; let text = ''; const rows = table.querySelectorAll('tbody tr'); rows.forEach(row => { if (row.style.display !== 'none') { const cells = row.querySelectorAll('td'); cells.forEach(cell => { text += cell.textContent.trim() + '\t'; }); text += '\n'; } }); copyToClipboard(text); // Visual feedback const originalText = copyBtn.innerHTML; copyBtn.innerHTML = ' Copied!'; copyBtn.classList.add('btn-success'); copyBtn.classList.remove('btn-primary'); setTimeout(() => { copyBtn.innerHTML = originalText; copyBtn.classList.remove('btn-success'); copyBtn.classList.add('btn-primary'); }, 1500); }); }); } /** * Helper function to copy text to clipboard */ function copyToClipboard(text) { // Create a temporary textarea const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; document.body.appendChild(textarea); // Select and copy textarea.select(); document.execCommand('copy'); // Clean up document.body.removeChild(textarea); } /** * Handle file upload previews and validation */ document.querySelectorAll('input[type="file"]').forEach(fileInput => { fileInput.addEventListener('change', function(e) { const fileName = e.target.files[0]?.name; const fileLabel = fileInput.nextElementSibling; if (fileName) { // Show the filename if (fileLabel) { fileLabel.textContent = fileName; } // Basic validation for Excel files if (!fileName.endsWith('.xlsx') && !fileName.endsWith('.xls')) { alert('Please upload a valid Excel file (.xlsx or .xls)'); fileInput.value = ''; if (fileLabel) { fileLabel.textContent = 'Choose file'; } } } }); });