more changes
This commit is contained in:
80
index.html
80
index.html
@ -14,15 +14,10 @@
|
||||
<table id="contactsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Formatted Name (FN)</th>
|
||||
<th>Family Name</th>
|
||||
<th>Given Name</th>
|
||||
<th>Phone 1</th>
|
||||
<th>Phone 2</th>
|
||||
<th>Email 1</th>
|
||||
<th>Email 2</th>
|
||||
<th>Full Name</th>
|
||||
<th>Mobile Phone</th>
|
||||
<th>Primary Email</th>
|
||||
<th>Organization</th>
|
||||
<th>Other Properties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="contactsTableBody">
|
||||
@ -33,6 +28,75 @@
|
||||
<!-- <div id="vcardRawContent"> -->
|
||||
<!-- VCard raw content was displayed here -->
|
||||
<!-- </div> -->
|
||||
|
||||
<!-- The Modal -->
|
||||
<div id="contactModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-button">×</span>
|
||||
<h2>Contact Details</h2>
|
||||
|
||||
<div id="modalFullNameContainer">
|
||||
<h3>Name</h3>
|
||||
<p><strong>Formatted Name (FN):</strong> <span id="modalFN">N/A</span></p>
|
||||
<p><strong>Family Name:</strong> <span id="modalFamilyName">N/A</span></p>
|
||||
<p><strong>Given Name:</strong> <span id="modalGivenName">N/A</span></p>
|
||||
<p><strong>Middle Name:</strong> <span id="modalMiddleName">N/A</span></p>
|
||||
<p><strong>Prefix:</strong> <span id="modalPrefix">N/A</span></p>
|
||||
<p><strong>Suffix:</strong> <span id="modalSuffix">N/A</span></p>
|
||||
<p><strong>Nickname:</strong> <span id="modalNickname">N/A</span></p>
|
||||
</div>
|
||||
|
||||
<div id="modalPersonalDetailsContainer">
|
||||
<h3>Personal Details</h3>
|
||||
<p><strong>Birthday:</strong> <span id="modalBirthday">N/A</span></p>
|
||||
<p><strong>Anniversary:</strong> <span id="modalAnniversary">N/A</span></p>
|
||||
<p><strong>Gender:</strong> <span id="modalGender">N/A</span></p>
|
||||
</div>
|
||||
|
||||
<div id="modalOrganizationContainer">
|
||||
<h3>Work/Organization</h3>
|
||||
<p><strong>Organization:</strong> <span id="modalOrg">N/A</span></p>
|
||||
<p><strong>Title:</strong> <span id="modalTitle">N/A</span></p>
|
||||
<p><strong>Role:</strong> <span id="modalRole">N/A</span></p>
|
||||
</div>
|
||||
|
||||
<div id="modalPhonesContainer">
|
||||
<h3>Phone Numbers</h3>
|
||||
<ul id="modalPhonesList"><li>N/A</li></ul>
|
||||
</div>
|
||||
|
||||
<div id="modalEmailsContainer">
|
||||
<h3>Email Addresses</h3>
|
||||
<ul id="modalEmailsList"><li>N/A</li></ul>
|
||||
</div>
|
||||
|
||||
<div id="modalAddressesContainer">
|
||||
<h3>Addresses</h3>
|
||||
<ul id="modalAddressesList"><li>N/A</li></ul>
|
||||
</div>
|
||||
|
||||
<div id="modalWebsitesContainer">
|
||||
<h3>Websites/URLs</h3>
|
||||
<ul id="modalWebsitesList"><li>N/A</li></ul>
|
||||
</div>
|
||||
|
||||
<div id="modalSocialProfilesContainer">
|
||||
<h3>Social Profiles</h3>
|
||||
<ul id="modalSocialProfilesList"><li>N/A</li></ul>
|
||||
</div>
|
||||
|
||||
<div id="modalNotesContainer">
|
||||
<h3>Notes</h3>
|
||||
<p id="modalNote" style="white-space: pre-wrap;">N/A</p>
|
||||
</div>
|
||||
|
||||
<div id="modalOtherPropertiesContainer">
|
||||
<h3>Other Properties</h3>
|
||||
<ul id="modalOtherPropertiesList"><li>N/A</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
334
script.js
334
script.js
@ -67,12 +67,39 @@ 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') {
|
||||
const charset = params['CHARSET'] || 'UTF-8';
|
||||
@ -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';
|
||||
// Full Name
|
||||
row.insertCell().textContent = constructFullName(contact);
|
||||
|
||||
// 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';
|
||||
// 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(`<div><strong>${labels[index]}:</strong> ${part.trim()}</div>`);
|
||||
}
|
||||
});
|
||||
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 = '<li>N/A</li>';
|
||||
}
|
||||
|
||||
// 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 = '<li>N/A</li>';
|
||||
}
|
||||
|
||||
// --- 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 += `<div><strong>Label (${adrType || 'General'}):</strong> ${matchingLabel.value}</div>`;
|
||||
}
|
||||
adrContent += formatAdr(adrEntry.value); // Use helper to format components
|
||||
if (adrEntry.params && adrEntry.params.TYPE) {
|
||||
if(!matchingLabel) adrContent += `<div><small>Type: ${Array.isArray(adrEntry.params.TYPE) ? adrEntry.params.TYPE.join(', ') : adrEntry.params.TYPE}</small></div>`;
|
||||
}
|
||||
li.innerHTML = adrContent;
|
||||
addressesList.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
addressesList.innerHTML = '<li>N/A</li>';
|
||||
}
|
||||
|
||||
// --- 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 = `<a href="${urlEntry.value}" target="_blank">${urlEntry.value}</a>`;
|
||||
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 = '<li>N/A</li>';
|
||||
}
|
||||
|
||||
// --- 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 = `<a href="${profileValue.startsWith('http') ? profileValue : '//' + profileValue}" target="_blank">${profileEntry.value}</a>`;
|
||||
}
|
||||
li.innerHTML = `<strong>${profileType}:</strong> ${profileValue}`;
|
||||
socialProfilesList.appendChild(li);
|
||||
});
|
||||
}
|
||||
if (socialProfilesList.children.length === 0) {
|
||||
socialProfilesList.innerHTML = '<li>N/A</li>';
|
||||
}
|
||||
|
||||
// --- 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 = '<li>N/A</li>';
|
||||
}
|
||||
}
|
||||
|
||||
100
style.css
100
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 */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user