document.getElementById('vcardFile').addEventListener('change', function(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(e) { const rawContent = e.target.result; const contacts = parseVCard(rawContent); displayContactsInTable(contacts); }; reader.readAsText(file); // Defaulting to UTF-8, VCard CHARSET can override } }); function decodeQuotedPrintable(str, charset = 'UTF-8') { // Handle soft line breaks (= at the end of a line) str = str.replace(/=\r?\n/g, ''); // Decode =XX hex sequences try { const bytes = []; for (let i = 0; i < str.length; i++) { if (str[i] === '=' && str[i+1] && str[i+2]) { const hex = str.substring(i+1, i+3); bytes.push(parseInt(hex, 16)); i += 2; } else { bytes.push(str.charCodeAt(i)); } } const byteArray = new Uint8Array(bytes); // Standard charsets like UTF-8, ISO-8859-1 are well supported // For others, TextDecoder might not work or might need a polyfill return new TextDecoder(charset).decode(byteArray); } catch (e) { console.error("Error decoding quoted-printable string:", e); // Fallback to a simpler replacement if TextDecoder fails or for basic cases return str.replace(/=([A-F0-9]{2})/g, (match, p1) => String.fromCharCode(parseInt(p1, 16))); } } function parseVCard(rawContent) { const contacts = []; // Handle line folding: join lines that start with a space or tab const foldedLines = rawContent.replace(/\r?\n[ \t]/g, ''); const lines = foldedLines.split(/\r?\n|\r|\n/); let currentContact = null; lines.forEach(line => { if (line.trim() === '') return; if (line.toUpperCase() === 'BEGIN:VCARD') { currentContact = { tel: [], email: [], adr: [], other: {} // For all other properties }; } else if (line.toUpperCase() === 'END:VCARD') { if (currentContact) { contacts.push(currentContact); currentContact = null; } } else if (currentContact) { let [fullKey, ...valueParts] = line.split(':'); let value = valueParts.join(':'); const keyParts = fullKey.split(';'); const mainKey = keyParts[0].toUpperCase(); const params = { TYPE: [] }; // Initialize TYPE as an array keyParts.slice(1).forEach(p => { const [paramName, ...paramValueParts] = p.split('='); const paramValue = paramValueParts.join('='); if (paramValueParts.length > 0) { // It's a key=value pair if (paramName.toUpperCase() === 'TYPE') { // TYPE can be comma-separated, e.g., TYPE=HOME,VOICE paramValue.split(',').forEach(typeVal => { if (!params.TYPE.includes(typeVal.toUpperCase())) { params.TYPE.push(typeVal.toUpperCase()); } }); } else { params[paramName.toUpperCase()] = paramValue; } } else { // No '=', so it's a V2.1 style type or other valueless param // e.g., TEL;CELL or TEL;PREF (though PREF usually has =1 in V3) // We'll assume valueless parameters are types for now, common in V2.1 for TEL/ADR/EMAIL if (!params.TYPE.includes(paramName.toUpperCase())) { params.TYPE.push(paramName.toUpperCase()); } } }); // If params.TYPE is empty after processing, remove it if (params.TYPE.length === 0) { delete params.TYPE; } // Check for Quoted-Printable encoding if (params['ENCODING'] === 'QUOTED-PRINTABLE') { const charset = params['CHARSET'] || 'UTF-8'; value = decodeQuotedPrintable(value, charset); } switch (mainKey) { case 'FN': currentContact.fn = value; break; case 'N': // N:Family;Given;Middle;Prefix;Suffix const nameParts = value.split(';'); currentContact.n = { family: nameParts[0] || '', given: nameParts[1] || '', middle: nameParts[2] || '', prefix: nameParts[3] || '', suffix: nameParts[4] || '' }; break; case 'TEL': currentContact.tel.push({ value: value, params: params }); break; case 'EMAIL': currentContact.email.push({ value: value, params: params }); break; case 'ORG': currentContact.org = value.split(';')[0]; // Organization name, ignore department for now break; case 'ADR': currentContact.adr.push({ value: value, params: params }); break; // Add more common cases if needed, or they fall into 'other' default: if (currentContact.other[mainKey]) { if (!Array.isArray(currentContact.other[mainKey])) { currentContact.other[mainKey] = [currentContact.other[mainKey]]; } currentContact.other[mainKey].push({ value: value, params: params }); } else { currentContact.other[mainKey] = { value: value, params: params }; } break; } } }); return contacts; } function displayContactsInTable(contacts) { const tableBody = document.getElementById('contactsTableBody'); if (!tableBody) { console.error('Table body not found!'); return; } tableBody.innerHTML = ''; // Clear previous entries if (!contacts || contacts.length === 0) { const row = tableBody.insertRow(); const cell = row.insertCell(); cell.colSpan = 4; // Updated to match new table structure (Full Name, Mobile, Email, Org) cell.textContent = 'No contacts found or VCard is empty/invalid.'; return; } contacts.forEach(contact => { const row = tableBody.insertRow(); // Full Name row.insertCell().textContent = constructFullName(contact); // Mobile Phone let mobilePhone = 'N/A'; if (contact.tel && contact.tel.length > 0) { const mobileEntry = contact.tel.find(t => t.params && t.params.TYPE && t.params.TYPE.toUpperCase().includes('CELL')); if (mobileEntry) { mobilePhone = mobileEntry.value; } } row.insertCell().textContent = mobilePhone; // Primary Email let primaryEmail = 'N/A'; if (contact.email && contact.email.length > 0) { // Prefer email with PREF=1 if available const prefEmail = contact.email.find(em => em.params && em.params.PREF === '1'); if (prefEmail) { primaryEmail = prefEmail.value; } else { primaryEmail = contact.email[0].value; // Fallback to the first email } } row.insertCell().textContent = primaryEmail; // Organization row.insertCell().textContent = contact.org || 'N/A'; // Add event listener to row for modal opening row.addEventListener('click', () => { populateModal(contact); modal.style.display = 'block'; }); }); } // --- Modal Logic --- const modal = document.getElementById('contactModal'); const closeButton = document.querySelector('.close-button'); if (modal && closeButton) { closeButton.onclick = function() { modal.style.display = 'none'; } window.onclick = function(event) { if (event.target == modal) { modal.style.display = 'none'; } } } else { console.error("Modal or close button not found. Modal functionality will be affected."); } function constructFullName(contact) { if (contact.fn) { // Formatted Name usually preferred if available return contact.fn; } if (contact.n) { // Structured Name const parts = [contact.n.prefix, contact.n.given, contact.n.middle, contact.n.family, contact.n.suffix]; return parts.filter(Boolean).join(' ').trim(); // Filter out empty parts and join } return 'N/A'; // Fallback } function populateModal(contact) { // Name Details // Helper to get property value, handling potential array structure function getPropertyValue(prop) { if (!prop) return 'N/A'; return Array.isArray(prop) ? prop[0].value : prop.value; } function getPropertyParams(prop) { if (!prop) return {}; return Array.isArray(prop) ? prop[0].params : prop.params; } // Helper to format YYYYMMDD to YYYY-MM-DD function formatDate(yyyymmdd) { if (!yyyymmdd || yyyymmdd.length !== 8) return 'N/A'; return `${yyyymmdd.substring(0, 4)}-${yyyymmdd.substring(4, 6)}-${yyyymmdd.substring(6, 8)}`; } // Helper to format ADR components function formatAdr(adrValue) { if (!adrValue) return 'N/A'; const parts = adrValue.split(';'); // Order: PO Box; Extended Addr; Street; Locality (City); Region (State); Postal Code; Country const labels = ["PO Box", "Extended Address", "Street", "City", "Region/State", "Postal Code", "Country"]; let formatted = []; parts.forEach((part, index) => { if (part.trim() && labels[index]) { formatted.push(`