resume/docker/resume/one-pager-tools/csv-tool.js

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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* 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();
}
});