402 lines
14 KiB
JavaScript
402 lines
14 KiB
JavaScript
/**
|
|
* 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 = '<i class="bi bi-clipboard"></i> 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 = '<i class="bi bi-check"></i> 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';
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|