From b1c2f18edd06f31b7c761cbb4afcfa94cca54898 Mon Sep 17 00:00:00 2001 From: GuilhermeStrice <15857393+GuilhermeStrice@users.noreply.github.com> Date: Thu, 26 Jun 2025 03:11:02 +0100 Subject: [PATCH] more changes --- index.html | 80 +++++++++++-- script.js | 338 ++++++++++++++++++++++++++++++++++++++++++++++------- style.css | 100 ++++++++++++++++ 3 files changed, 465 insertions(+), 53 deletions(-) diff --git a/index.html b/index.html index dadccff..fda4f57 100644 --- a/index.html +++ b/index.html @@ -14,15 +14,10 @@ - - - - - - - + + + - @@ -33,6 +28,75 @@ + + + + diff --git a/script.js b/script.js index 1568c54..2d53a8b 100644 --- a/script.js +++ b/script.js @@ -67,11 +67,38 @@ function parseVCard(rawContent) { const keyParts = fullKey.split(';'); const mainKey = keyParts[0].toUpperCase(); - const params = {}; + const params = { TYPE: [] }; // Initialize TYPE as an array + keyParts.slice(1).forEach(p => { - const [paramName, paramValue] = p.split('='); - params[paramName.toUpperCase()] = paramValue; + 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') { @@ -135,62 +162,283 @@ function displayContactsInTable(contacts) { if (!contacts || contacts.length === 0) { const row = tableBody.insertRow(); const cell = row.insertCell(); - // Adjust colSpan when table structure is finalized in index.html - cell.colSpan = 9; // Placeholder, update with actual number of columns + 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(); - row.insertCell().textContent = contact.fn || 'N/A'; - - // Structured Name (N) - row.insertCell().textContent = contact.n ? contact.n.family : 'N/A'; - row.insertCell().textContent = contact.n ? contact.n.given : 'N/A'; - // Phones - display first two, with types if available - for (let i = 0; i < 2; i++) { - const telCell = row.insertCell(); - if (contact.tel && contact.tel[i]) { - let telStr = contact.tel[i].value; - const type = contact.tel[i].params['TYPE']; - if (type) telStr += ` (${type.split(',').join('/')})`; // Display multiple types e.g. (HOME/VOICE) - telCell.textContent = telStr; - } else { - telCell.textContent = 'N/A'; + // 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; - // Emails - display first two, with types if available - for (let i = 0; i < 2; i++) { - const emailCell = row.insertCell(); - if (contact.email && contact.email[i]) { - let emailStr = contact.email[i].value; - const type = contact.email[i].params['TYPE']; - if (type) emailStr += ` (${type.split(',').join('/')})`; - emailCell.textContent = emailStr; + // 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 { - emailCell.textContent = 'N/A'; + primaryEmail = contact.email[0].value; // Fallback to the first email } } + row.insertCell().textContent = primaryEmail; + // Organization row.insertCell().textContent = contact.org || 'N/A'; - - // Other Properties - const otherCell = row.insertCell(); - let otherText = ''; - for (const key in contact.other) { - const prop = contact.other[key]; - if (Array.isArray(prop)) { - prop.forEach(pItem => { - otherText += `${key}: ${pItem.value} (${JSON.stringify(pItem.params)})\n`; - }); - } else { - otherText += `${key}: ${prop.value} (${JSON.stringify(prop.params)})\n`; - } - } - otherCell.textContent = otherText.trim() || 'N/A'; - otherCell.style.whiteSpace = 'pre-wrap'; // To respect newlines + + // 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(`
${labels[index]}: ${part.trim()}
`); + } + }); + return formatted.length > 0 ? formatted.join('') : adrValue; // Fallback to raw if empty after parse + } + + + // --- Populate Name Details --- + document.getElementById('modalFN').textContent = contact.fn || 'N/A'; + document.getElementById('modalFamilyName').textContent = contact.n && contact.n.family ? contact.n.family : 'N/A'; + document.getElementById('modalGivenName').textContent = contact.n && contact.n.given ? contact.n.given : 'N/A'; + document.getElementById('modalMiddleName').textContent = contact.n && contact.n.middle ? contact.n.middle : 'N/A'; + document.getElementById('modalPrefix').textContent = contact.n && contact.n.prefix ? contact.n.prefix : 'N/A'; + document.getElementById('modalSuffix').textContent = contact.n && contact.n.suffix ? contact.n.suffix : 'N/A'; + // NICKNAME + const nicknameProp = contact.other && contact.other['NICKNAME']; + document.getElementById('modalNickname').textContent = nicknameProp ? getPropertyValue(nicknameProp) : 'N/A'; + + // --- Populate Personal Details --- + const bdayProp = contact.other && contact.other['BDAY']; + document.getElementById('modalBirthday').textContent = bdayProp ? formatDate(getPropertyValue(bdayProp)) : 'N/A'; + const anniversaryProp = contact.other && contact.other['ANNIVERSARY']; + document.getElementById('modalAnniversary').textContent = anniversaryProp ? formatDate(getPropertyValue(anniversaryProp)) : 'N/A'; + const genderProp = contact.other && contact.other['GENDER']; + document.getElementById('modalGender').textContent = genderProp ? getPropertyValue(genderProp) : 'N/A'; + + // --- Populate Work/Organization --- + document.getElementById('modalOrg').textContent = contact.org || 'N/A'; + const titleProp = contact.other && contact.other['TITLE']; + document.getElementById('modalTitle').textContent = titleProp ? getPropertyValue(titleProp) : 'N/A'; + const roleProp = contact.other && contact.other['ROLE']; + document.getElementById('modalRole').textContent = roleProp ? getPropertyValue(roleProp) : 'N/A'; + + // --- Populate Phone Numbers --- + const phonesList = document.getElementById('modalPhonesList'); + phonesList.innerHTML = ''; // Clear previous + if (contact.tel && contact.tel.length > 0) { + contact.tel.forEach(tel => { + const li = document.createElement('li'); + let telDesc = tel.value; + if (tel.params && tel.params.TYPE) { + telDesc += ` (${tel.params.TYPE.split(',').join('/')})`; + } + li.textContent = telDesc; + phonesList.appendChild(li); + }); + } else { + phonesList.innerHTML = '
  • N/A
  • '; + } + + // Email Addresses + const emailsList = document.getElementById('modalEmailsList'); + emailsList.innerHTML = ''; // Clear previous + if (contact.email && contact.email.length > 0) { + contact.email.forEach(email => { + const li = document.createElement('li'); + let emailDesc = email.value; + if (email.params && email.params.TYPE) { + emailDesc += ` (${email.params.TYPE.split(',').join('/')})`; + } + li.textContent = emailDesc; + emailsList.appendChild(li); + }); + } else { + emailsList.innerHTML = '
  • N/A
  • '; + } + + // --- Populate Addresses --- + const addressesList = document.getElementById('modalAddressesList'); + addressesList.innerHTML = ''; + if (contact.adr && contact.adr.length > 0) { + contact.adr.forEach(adrEntry => { + const li = document.createElement('li'); + let adrContent = ''; + + // Try to find a LABEL that matches this ADR's type + const adrType = adrEntry.params && adrEntry.params.TYPE ? (Array.isArray(adrEntry.params.TYPE) ? adrEntry.params.TYPE.join(',') : adrEntry.params.TYPE) : ''; + const labelProp = contact.other && contact.other['LABEL']; + let matchingLabel = null; + if (labelProp) { + const labels = Array.isArray(labelProp) ? labelProp : [labelProp]; + matchingLabel = labels.find(lbl => { + const lblType = lbl.params && lbl.params.TYPE ? (Array.isArray(lbl.params.TYPE) ? lbl.params.TYPE.join(',') : lbl.params.TYPE) : ''; + return lblType === adrType; + }); + } + + if (matchingLabel) { + adrContent += `
    Label (${adrType || 'General'}): ${matchingLabel.value}
    `; + } + adrContent += formatAdr(adrEntry.value); // Use helper to format components + if (adrEntry.params && adrEntry.params.TYPE) { + if(!matchingLabel) adrContent += `
    Type: ${Array.isArray(adrEntry.params.TYPE) ? adrEntry.params.TYPE.join(', ') : adrEntry.params.TYPE}
    `; + } + li.innerHTML = adrContent; + addressesList.appendChild(li); + }); + } else { + addressesList.innerHTML = '
  • N/A
  • '; + } + + // --- Populate Websites/URLs --- + const websitesList = document.getElementById('modalWebsitesList'); + websitesList.innerHTML = ''; + const urlProps = contact.other && contact.other['URL']; + if (urlProps) { + const urls = Array.isArray(urlProps) ? urlProps : [urlProps]; + urls.forEach(urlEntry => { + const li = document.createElement('li'); + let urlDesc = `${urlEntry.value}`; + const urlType = urlEntry.params && urlEntry.params.TYPE ? (Array.isArray(urlEntry.params.TYPE) ? urlEntry.params.TYPE.join('/') : urlEntry.params.TYPE) : null; + if (urlType) { + urlDesc += ` (${urlType})`; + } + li.innerHTML = urlDesc; + websitesList.appendChild(li); + }); + } + if (websitesList.children.length === 0) { + websitesList.innerHTML = '
  • N/A
  • '; + } + + // --- Populate Social Profiles --- + const socialProfilesList = document.getElementById('modalSocialProfilesList'); + socialProfilesList.innerHTML = ''; + const socialProps = contact.other && contact.other['X-SOCIALPROFILE']; + if (socialProps) { + const profiles = Array.isArray(socialProps) ? socialProps : [socialProps]; + profiles.forEach(profileEntry => { + const li = document.createElement('li'); + const profileType = profileEntry.params && profileEntry.params.TYPE ? (Array.isArray(profileEntry.params.TYPE) ? profileEntry.params.TYPE[0] : profileEntry.params.TYPE) : 'Profile'; // Take first type if array + // Some X-SOCIALPROFILE values include full URLs, others might be just usernames. + // For simplicity, link if it looks like a URL, otherwise just display. + let profileValue = profileEntry.value; + if (profileValue.toLowerCase().startsWith('http') || profileValue.includes('.com') || profileValue.includes('.net') || profileValue.includes('/')) { + profileValue = `${profileEntry.value}`; + } + li.innerHTML = `${profileType}: ${profileValue}`; + socialProfilesList.appendChild(li); + }); + } + if (socialProfilesList.children.length === 0) { + socialProfilesList.innerHTML = '
  • N/A
  • '; + } + + // --- Populate Notes --- + const noteProp = contact.other && contact.other['NOTE']; + document.getElementById('modalNote').textContent = noteProp ? getPropertyValue(noteProp) : 'N/A'; + document.getElementById('modalNote').style.whiteSpace = 'pre-wrap'; + + + // --- Populate Other Properties --- + const otherPropertiesList = document.getElementById('modalOtherPropertiesList'); + otherPropertiesList.innerHTML = ''; // Clear previous + let hasOther = false; + const handledKeys = ['NICKNAME', 'BDAY', 'ANNIVERSARY', 'GENDER', 'TITLE', 'ROLE', 'LABEL', 'URL', 'NOTE', 'X-SOCIALPROFILE']; + for (const key in contact.other) { + if (handledKeys.includes(key.toUpperCase())) continue; + + const prop = contact.other[key]; + if (Array.isArray(prop)) { + prop.forEach(pItem => { + const li = document.createElement('li'); + li.textContent = `${key}: ${pItem.value} (${JSON.stringify(pItem.params)})`; + otherPropertiesList.appendChild(li); + hasOther = true; + }); + } else { + const li = document.createElement('li'); + li.textContent = `${key}: ${prop.value} (${JSON.stringify(prop.params)})`; + otherPropertiesList.appendChild(li); + hasOther = true; + } + } + if (!hasOther) { + otherPropertiesList.innerHTML = '
  • N/A
  • '; + } +} diff --git a/style.css b/style.css index 2dde92a..ec67b59 100644 --- a/style.css +++ b/style.css @@ -27,14 +27,22 @@ input[type="file"] { width: 100%; border-collapse: collapse; margin-top: 20px; + table-layout: fixed; /* Helps with column width control */ } #contactsTable th, #contactsTable td { border: 1px solid #ddd; padding: 8px; text-align: left; + word-wrap: break-word; /* Break long words to prevent overflow */ } +/* Style for the "Other Properties" column to ensure pre-wrap is effective */ +#contactsTable td:last-child { + white-space: pre-wrap; +} + + #contactsTable th { background-color: #e9e9e9; color: #333; @@ -43,3 +51,95 @@ input[type="file"] { #contactsTable tr:nth-child(even) { background-color: #f9f9f9; } + +#contactsTable tr:hover { + background-color: #f1f1f1; /* Highlight row on hover */ + cursor: pointer; /* Indicate clickable */ +} + + +/* Modal Styles */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1000; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ + padding-top: 60px; +} + +.modal-content { + background-color: #fefefe; + margin: 5% auto; /* 5% from the top and centered */ + padding: 20px; + border: 1px solid #888; + width: 80%; /* Could be more or less, depending on screen size */ + max-width: 700px; + border-radius: 8px; + position: relative; +} + +.close-button { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close-button:hover, +.close-button:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +.modal h2, .modal h3 { + margin-top: 0; + color: #333; +} + +.modal h3 { + margin-top: 15px; + border-bottom: 1px solid #eee; + padding-bottom: 5px; +} + +.modal ul { + list-style-type: none; + padding-left: 0; +} + +.modal ul li { + background-color: #f9f9f9; + border: 1px solid #eee; + padding: 8px; + margin-bottom: 5px; + border-radius: 4px; +} + +.modal p { + margin-bottom: 5px; +} + +.modal ul li div { /* For nested divs within li, like formatted ADR */ + padding: 2px 0; +} +.modal ul li div strong { /* For labels within formatted ADR */ + display: inline-block; + min-width: 120px; /* Adjust as needed for alignment */ + margin-right: 5px; +} + +#modalNote { /* Ensure note paragraph respects newlines from VCard */ + white-space: pre-wrap; + background-color: #f9f9f9; + border: 1px solid #eee; + padding: 8px; + border-radius: 4px; + min-height: 50px; /* Give it some space */ +}
    Formatted Name (FN)Family NameGiven NamePhone 1Phone 2Email 1Email 2Full NameMobile PhonePrimary Email OrganizationOther Properties