/** * 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 = '

Output

Paste CSV data above to view it as a table.

'; return; } try { // Show processing message outputDiv.innerHTML = '

Output

Processing data...

'; // 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 = '

Data Preview

'; tableHtml += `

Showing ${previewData.length} of ${csvData.length} rows

`; tableHtml += '
'; // Table headers with sort functionality tableHtml += ''; headers.forEach(header => { const isSorted = header === currentSortColumn; const sortClass = isSorted ? (sortDirection > 0 ? 'sort-asc' : 'sort-desc') : ''; tableHtml += ``; }); tableHtml += ''; // Table body with improved cell formatting tableHtml += ''; previewData.forEach(row => { tableHtml += ''; headers.forEach(header => { const cellValue = row[header]; // Format cell value based on type let formattedValue = ''; if (cellValue === null || cellValue === undefined) { formattedValue = '(empty)'; } else if (typeof cellValue === 'string') { formattedValue = escapeHtml(cellValue); } else { formattedValue = String(cellValue); } tableHtml += ``; }); tableHtml += ''; }); tableHtml += '
${header} ${isSorted ? (sortDirection > 0 ? '↑' : '↓') : ''}
${formattedValue}
'; // Add stats summary const totalRows = csvData.length; const totalColumns = headers.length; tableHtml += `
Total Rows: ${totalRows}
Total Columns: ${totalColumns}
`; // 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, "'"); } /** * 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(); } });