330 lines
11 KiB
JavaScript
330 lines
11 KiB
JavaScript
/**
|
|
* CSV Viewer functionality
|
|
* Automatically processes and displays CSV data when pasted
|
|
* Using Papa Parse for robust CSV handling
|
|
*/
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// DOM Elements
|
|
const csvInput = document.getElementById('csvInput');
|
|
const delimiterSelect = document.getElementById('delimiter');
|
|
const hasHeaderCheckbox = document.getElementById('hasHeader');
|
|
const outputDiv = document.getElementById('output');
|
|
|
|
// Variables to store data
|
|
let csvData = [];
|
|
let headers = [];
|
|
let currentSortColumn = null;
|
|
let sortDirection = 1; // 1 for ascending, -1 for descending
|
|
|
|
// Add input event listener with debounce to process CSV when pasted
|
|
let debounceTimer;
|
|
csvInput.addEventListener('input', function() {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(function() {
|
|
if (csvInput.value.trim() !== '') {
|
|
processCSV();
|
|
}
|
|
}, 300); // 300ms debounce delay
|
|
});
|
|
|
|
// Add paste event listener to format CSV data on paste
|
|
csvInput.addEventListener('paste', function(e) {
|
|
// Let the paste happen naturally, then process after a brief delay
|
|
setTimeout(function() {
|
|
const text = csvInput.value;
|
|
if (text && text.length > 0) {
|
|
// Auto-detect delimiter
|
|
autoDetectDelimiter(text);
|
|
// Process immediately after paste
|
|
processCSV();
|
|
}
|
|
}, 50); // Slightly longer delay to ensure paste completes
|
|
});
|
|
|
|
// Add change listeners to delimiter and header options to reprocess data
|
|
delimiterSelect.addEventListener('change', function() {
|
|
if (csvInput.value.trim() !== '') {
|
|
processCSV();
|
|
}
|
|
});
|
|
|
|
hasHeaderCheckbox.addEventListener('change', function() {
|
|
if (csvInput.value.trim() !== '') {
|
|
processCSV();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Auto-detect the delimiter in pasted CSV data
|
|
*/
|
|
function autoDetectDelimiter(text) {
|
|
// Count occurrences of common delimiters
|
|
const firstFewLines = text.split('\n').slice(0, 5).join('\n');
|
|
const counts = {
|
|
',': (firstFewLines.match(/,/g) || []).length,
|
|
';': (firstFewLines.match(/;/g) || []).length,
|
|
'\t': (firstFewLines.match(/\t/g) || []).length,
|
|
'|': (firstFewLines.match(/\|/g) || []).length
|
|
};
|
|
|
|
// Find the most common delimiter
|
|
let maxCount = 0;
|
|
let detectedDelimiter = ','; // default
|
|
|
|
for (const [delimiter, count] of Object.entries(counts)) {
|
|
if (count > maxCount) {
|
|
maxCount = count;
|
|
detectedDelimiter = delimiter;
|
|
}
|
|
}
|
|
|
|
// Set the delimiter dropdown
|
|
if (maxCount > 0) {
|
|
delimiterSelect.value = detectedDelimiter === '\t' ? '\\t' : detectedDelimiter;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process the CSV data based on selected options
|
|
*/
|
|
function processCSV() {
|
|
const csvText = csvInput.value.trim();
|
|
if (!csvText) {
|
|
outputDiv.innerHTML = '<h3>Output</h3><p class="alert alert-info">Paste CSV data above to view it as a table.</p>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Show processing message
|
|
outputDiv.innerHTML = '<h3>Output</h3><p class="alert alert-info">Processing data...</p>';
|
|
|
|
// Parse CSV using Papa Parse
|
|
const delimiter = delimiterSelect.value;
|
|
const hasHeader = hasHeaderCheckbox.checked;
|
|
|
|
// Enhanced parsing options
|
|
Papa.parse(csvText, {
|
|
delimiter: delimiter,
|
|
header: hasHeader,
|
|
skipEmptyLines: 'greedy', // Skip truly empty lines
|
|
dynamicTyping: true, // Automatically convert numeric values
|
|
trimHeaders: true, // Trim whitespace from headers
|
|
complete: function(results) {
|
|
if (results.errors.length > 0) {
|
|
showError('Error parsing CSV: ' + results.errors[0].message);
|
|
return;
|
|
}
|
|
|
|
if (results.data.length === 0 || (results.data.length === 1 && Object.keys(results.data[0]).length === 0)) {
|
|
showError('No valid data found. Please check your CSV format and delimiter.');
|
|
return;
|
|
}
|
|
|
|
csvData = results.data;
|
|
|
|
// Handle headers
|
|
if (hasHeader) {
|
|
if (results.meta.fields && results.meta.fields.length > 0) {
|
|
headers = results.meta.fields.map(h => h.trim());
|
|
} else {
|
|
// Fallback if no headers detected
|
|
headers = Object.keys(results.data[0] || {}).map((_, i) => `Column${i + 1}`);
|
|
}
|
|
} else {
|
|
headers = Object.keys(results.data[0] || {}).map((_, i) => `Column${i + 1}`);
|
|
}
|
|
|
|
// Preview the data
|
|
previewData();
|
|
},
|
|
error: function(error) {
|
|
showError('Error processing CSV: ' + error.message);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
showError('Error processing CSV: ' + error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Preview the CSV data in a table with sortable columns
|
|
*/
|
|
function previewData() {
|
|
if (csvData.length === 0) {
|
|
showError('No data to preview.');
|
|
return;
|
|
}
|
|
|
|
// Limit preview to first 500 rows
|
|
const previewData = csvData.slice(0, 500);
|
|
|
|
// Generate table HTML
|
|
let tableHtml = '<h3>Data Preview</h3>';
|
|
tableHtml += `<p>Showing ${previewData.length} of ${csvData.length} rows</p>`;
|
|
tableHtml += '<div class="table-responsive"><table class="tool-table">';
|
|
|
|
// Table headers with sort functionality
|
|
tableHtml += '<thead><tr>';
|
|
headers.forEach(header => {
|
|
const isSorted = header === currentSortColumn;
|
|
const sortClass = isSorted ? (sortDirection > 0 ? 'sort-asc' : 'sort-desc') : '';
|
|
tableHtml += `<th class="${sortClass}" data-column="${header}">${header} ${isSorted ? (sortDirection > 0 ? '↑' : '↓') : ''}</th>`;
|
|
});
|
|
tableHtml += '</tr></thead>';
|
|
|
|
// Table body with improved cell formatting
|
|
tableHtml += '<tbody>';
|
|
previewData.forEach(row => {
|
|
tableHtml += '<tr>';
|
|
headers.forEach(header => {
|
|
const cellValue = row[header];
|
|
// Format cell value based on type
|
|
let formattedValue = '';
|
|
|
|
if (cellValue === null || cellValue === undefined) {
|
|
formattedValue = '<span class="empty-cell">(empty)</span>';
|
|
} else if (typeof cellValue === 'string') {
|
|
formattedValue = escapeHtml(cellValue);
|
|
} else {
|
|
formattedValue = String(cellValue);
|
|
}
|
|
|
|
tableHtml += `<td>${formattedValue}</td>`;
|
|
});
|
|
tableHtml += '</tr>';
|
|
});
|
|
tableHtml += '</tbody></table></div>';
|
|
|
|
// Add stats summary
|
|
const totalRows = csvData.length;
|
|
const totalColumns = headers.length;
|
|
tableHtml += `<div class="data-stats">
|
|
<div class="stat-item">
|
|
<span class="stat-label">Total Rows:</span>
|
|
<span class="stat-value">${totalRows}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Total Columns:</span>
|
|
<span class="stat-value">${totalColumns}</span>
|
|
</div>
|
|
</div>`;
|
|
|
|
// Display in output div
|
|
outputDiv.innerHTML = tableHtml;
|
|
|
|
// Add click event listeners to table headers for sorting
|
|
const tableHeaders = outputDiv.querySelectorAll('th');
|
|
tableHeaders.forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const column = th.getAttribute('data-column');
|
|
sortData(column);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Escape HTML special characters to prevent XSS
|
|
*/
|
|
function escapeHtml(unsafe) {
|
|
return unsafe
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* Sort data by column
|
|
*/
|
|
function sortData(column) {
|
|
// Toggle sort direction if clicking the same column
|
|
if (column === currentSortColumn) {
|
|
sortDirection *= -1;
|
|
} else {
|
|
currentSortColumn = column;
|
|
sortDirection = 1;
|
|
}
|
|
|
|
// Sort the data
|
|
csvData.sort((a, b) => {
|
|
const valueA = a[column] !== undefined ? a[column] : '';
|
|
const valueB = b[column] !== undefined ? b[column] : '';
|
|
|
|
// Try to sort numerically if possible
|
|
if (typeof valueA === 'number' && typeof valueB === 'number') {
|
|
return (valueA - valueB) * sortDirection;
|
|
}
|
|
|
|
// Handle dates
|
|
const dateA = new Date(valueA);
|
|
const dateB = new Date(valueB);
|
|
if (!isNaN(dateA) && !isNaN(dateB)) {
|
|
return (dateA - dateB) * sortDirection;
|
|
}
|
|
|
|
// Otherwise sort alphabetically
|
|
return String(valueA).localeCompare(String(valueB)) * sortDirection;
|
|
});
|
|
|
|
// Update the preview
|
|
previewData();
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
*/
|
|
function showError(message) {
|
|
// Clear any existing alerts
|
|
clearAlerts();
|
|
|
|
const alert = document.createElement('div');
|
|
alert.className = 'alert alert-error';
|
|
alert.textContent = message;
|
|
|
|
// Insert at the top of the output div
|
|
const firstChild = outputDiv.querySelector('h3') ?
|
|
outputDiv.querySelector('h3').nextSibling :
|
|
outputDiv.firstChild;
|
|
|
|
outputDiv.insertBefore(alert, firstChild);
|
|
}
|
|
|
|
/**
|
|
* Show success message
|
|
*/
|
|
function showSuccess(message) {
|
|
const alert = document.createElement('div');
|
|
alert.className = 'alert alert-success';
|
|
alert.textContent = message;
|
|
|
|
// Insert after the heading
|
|
const firstChild = outputDiv.querySelector('h3') ?
|
|
outputDiv.querySelector('h3').nextSibling :
|
|
outputDiv.firstChild;
|
|
|
|
outputDiv.insertBefore(alert, firstChild);
|
|
|
|
// Auto-hide success message after 3 seconds
|
|
setTimeout(() => {
|
|
if (alert.parentNode === outputDiv) {
|
|
alert.remove();
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
/**
|
|
* Clear all alert messages
|
|
*/
|
|
function clearAlerts() {
|
|
const alerts = outputDiv.querySelectorAll('.alert');
|
|
alerts.forEach(alert => alert.remove());
|
|
}
|
|
|
|
// Check if there's already content in the textarea on page load
|
|
if (csvInput.value.trim() !== '') {
|
|
processCSV();
|
|
}
|
|
});
|