vCard Reader
Testet doch einfach mal eine vCard Datei (.vcf), dieser Reader unterstützt ein Großteil der Eigenschaften über alle bis jetzt erschienen Versionen hinweg.
Alles ist komplett über JavaScript geregelt, es erfolgt keine Übertragung der Daten!
Für interessierte Entwickler steht der Quellcode natürlich wieder auf der Seite zur Verfügung.
HTML:
<!DOCTYPE html >
<html lang="en">
<head>
<meta charset="utf-8" />
<title>vCard</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width, minimum-scale=1, maximum-scale=1" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:" />
<style>
html, body {height: 100%; margin: 0; font-family: Arial, Helvetica, sans-serif; background-color: #f6f6f6;}
#header, #content, #footer {position: absolute; left: 0; right:0;}
#header {height: 50px; top: 0;}
fieldset {margin: 0; background-color: #fff;}
#content {top: 51px; bottom: 51px; margin:5px;}
#footer {height: 50px; bottom: 0;}
pre {position: absolute; top: 0; bottom: 0; width: 50%; outline: 1px solid #ccc; margin: 0; overflow: scroll; background-color: #fff;}
#vcard {left: 0;}
#json {right: 0;}
#json .string {color: brown;}
#json .number {color: green;}
#json .boolean {color: blue;}
#json .null {color: red;}
#json .key {color: navy;}
.overlay {position:fixed; top:0; right:0; bottom:0; left:0; z-index:9200; background-color:#222;}
.popup-content {position:absolute; top:50%; left:50%; max-width:100%; transform:translate(-50%, -50%);}
.popup-close {position:absolute; cursor:pointer; background:none; border:none; color:#fff;}
.popup-close::before {display:inline-block; text-shadow:1px 1px 2px black, 0 0 1em black, 0 0 0.2em black;}
.popup-close {top:0; right:20px;}
.popup-close::before {content:"\00d7"; font-size:50px; font-weight:normal;}
.popup-content .card {margin-bottom: 5px; padding: 5px; background-color: #fff;}
.popup-content .card-content {display: flex; padding-top: 5px;}
.popup-content div.wrapper {display: inline-block; margin-right: 10px;}
.popup-content div.wrapper.photo {margin-right:80px;}
.popup-content div.wrapper.name {display: block;}
.popup-content div.wrapper.home:before {content: "\1F3E0";}
.popup-content div.wrapper.work:before {content: "\1F3E2";}
.popup-content p {margin: 0;}
.popup-content p:before {padding-right: 5px;}
.popup-content p.tel:before,
.popup-content p.voice:before {content: "\1F4DE";}
.popup-content p.fax:before {content: "\1F5B7";}
.popup-content p.cell:before {content: "\1F4F1";}
.popup-content p.mailto:before {content: "\1F582";}
.popup-content p.https:before {content: "\1F310";}
</style>
</head>
<body>
<div id="header">
<form id="vcardFile" name="vcardFile">
<fieldset><label for="fileinput">vCard File (.vcf):</label><input type="file" id="fileinput" /><!--input type="button" id="btnLoad" value="add" /--><input type="button" id="btnPreview" value="preview" /><input type="button" id="btnCleanUp" value="reset" /></fieldset>
</form>
</div>
<div id="content">
<pre id="vcard"></pre>
<pre id="json"></pre>
</div>
<div id="footer">
<p>© Udo Schmal</p>
</div>
<script src="vCard.js"></script>
</body>
</html>
JavaScript:
// coding: utf-8
/** Created by: Udo Schmal | https://www.gocher.me/ */
(function () {
'use strict';
let vCardFields = {
"ADR": {
"method": addressValue,
"property": "address"
},
"BDAY": {
"method": dateValue,
"property": "birthday"
},
"BEGIN": {
"method": noValue,
"property": "begin" // not used
},
"CATEGORIES": {
"method": listValue,
"property": "categories"
},
"EMAIL": {
"method": typedValue,
"property": "email"
},
"END": {
"method": endCard,
"property": "begin" // not used
},
"FN": {
"method": stringValue,
"property": "displayName"
},
"GENDER": {
"method": genderValue,
"property": "gender"
},
"KIND": {
"method": stringValue,
"property": "kind"
},
"LABEL": {
"method": labelValue,
"property": "label"
},
"LANG": {
"method": stringValue,
"property": "language"
},
"N": {
"method": structuredValue(['surname', 'name', 'additionalName', 'prefix', 'suffix']),
"property": "name"
},
"NICKNAME": {
"method": stringValue,
"property": "nickname"
},
"NOTE": {
"method": stringValue,
"property": "notes"
},
"ORG": {
"method": stringValue,
"property": "organization"
},
"PHOTO": {
"method": mediaValue,
"property": "photo"
},
"REV": {
"method": dateValue,
"property": "revision"
},
"ROLE": {
"method": stringValue,
"property": "role"
},
"SOUND": {
"method": mediaValue,
"property": "sound"
},
"SOURCE": {
"method": stringValue,
"property": "source"
},
"TEL": {
"method": typedValue,
"property": "telephone"
},
"TITLE": {
"method": stringValue,
"property": "title"
},
"TZ": {
"method": stringValue,
"property": "timezone"
},
"UID": {
"method": stringValue,
"property": "uid"
},
"URL": {
"method": typedValue,
"property": "url"
},
"VERSION": {
"method": stringValue,
"property": "version"
}
},
vCardData = '',
vCard = {},
vCards = [];
function decodeQuotedPrintable(str) {
str = (str || '')
.toString()
// remove invalid whitespace from the end of lines
.replace(/[\t ]+$/gm, '')
// remove soft line breaks
.replace(/\=(?:\r?\n|$)/g, '');
let decoded = '';
for (let i = 0, len = str.length; i < len; i++) {
let chr = str.charAt(i), hex;
if (chr === '=' && (hex = str.substr(i + 1, 2)) && /[\da-fA-F]{2}/.test(hex)) {
decoded += String.fromCharCode(parseInt(hex, 16));
i += 2;
} else {
decoded += chr;
}
}
return decoded;
}
function noValue() {
}
function stringValue(fieldValue, fieldName) {
// convert escaped new lines to real new lines.
fieldValue = fieldValue.replace(/\\n/g, '\n');
fieldValue = fieldValue.replace(/\\,/g, ',');
fieldValue = fieldValue.replace(/\\;/g, ';');
// convert quoted-printable encoded data
fieldValue = decodeQuotedPrintable(fieldValue);
fieldValue = fieldValue.replaceAll('\r\n', '\n');
// append value if previously specified
if (vCard[fieldName]) {
vCard[fieldName] += '\n' + fieldValue;
} else {
vCard[fieldName] = fieldValue;
}
}
function genderValue(fieldValue, fieldName) {
switch (fieldValue.toUpperCase()) {
case 'F': vCard[fieldName] = 'female'; break;
case 'M': vCard[fieldName] = 'male'; break;
case 'D': vCard[fieldName] = 'diverse'; break;
}
}
function listValue(fieldValue, fieldName) {
vCard[fieldName] = fieldValue.split(',');
}
function dateValue(fieldValue, fieldName) {
let dateValue;
if (fieldValue.length === 16) {
// long format "19680628T105900Z"
dateValue = new Date(fieldValue.substr(0, 4) + '-' + fieldValue.substr(4, 2) + '-' + fieldValue.substr(6, 2) +
'T' + fieldValue.substr(9, 2) + ':' + fieldValue.substr(11, 2) + ':' + fieldValue.substr(13, 2));
} else if (fieldValue.length === 8) {
// short format "19680628"
dateValue = new Date(fieldValue.substr(0, 4) + '-' + fieldValue.substr(4, 2) + '-' + fieldValue.substr(6, 2));
} else {
// date format "1968-06-28" / "2025-08-17T18:25:00.000Z"
dateValue = new Date(fieldValue);
}
if (!dateValue || isNaN(dateValue.getDate())) {
dateValue = null;
alert('invalid date format ' + fieldValue);
}
vCard[fieldName] = dateValue && dateValue.toJSON(); // ISO date format
}
function structuredValue(fields) {
return function (fieldValue, fieldName) {
var values = fieldValue.split(';');
vCard[fieldName] = fields.reduce(function (p, c, i) {
p[c] = values[i] || '';
return p;
}, {});
}
}
function typedValue(fieldValue, fieldName, typeInfo, valueFormatter) {
var isDefault = false, fieldSubpart = '';
// find out if it is that preferred value and prepare type info
typeInfo = typeInfo.reduce(function (p, c) {
if (c.value === undefined) {
c.value = c.name;
c.name = 'type';
}
if (c.name === 'type') {
let values = c.value.toLowerCase().split(',');
if (values.indexOf('pref') > -1) {
values = values.filter(e => e !== 'pref');
isDefault = true;
}
c.value = values.join(',');
}
if (c.value !== '') {
if (p[c.name]) {
p[c.name] += ',' + c.value;
} else {
p[c.name] = c.value;
}
}
return p;
}, {});
let backupFieldName = '';
if (fieldName === 'label') {
backupFieldName = 'label';
fieldName = 'address';
}
vCard[fieldName] = vCard[fieldName] || [];
// merge label and adddress
if (fieldName === 'address') {
// find match
let match = vCard[fieldName]?.find((element) => element.valueInfo.type == typeInfo.type);
if (match) {
if (backupFieldName === 'label') {
// store lable value into valueInfo.label
match.valueInfo.label = valueFormatter ? valueFormatter(fieldValue, typeInfo) : fieldValue;
} else {
match.isDefault = isDefault;
// store old value into typeinfo.label
match.valueInfo['label'] = match.value;
for (let key in typeInfo) {
match.valueInfo[key] = typeInfo[key];
}
match.value = valueFormatter ? valueFormatter(fieldValue, typeInfo) : fieldValue;
}
return ;
}
}
vCard[fieldName].push({
isDefault: isDefault,
valueInfo: typeInfo,
value: valueFormatter ? valueFormatter(fieldValue, typeInfo) : fieldValue
});
}
function addressValue(fieldValue, fieldName, typeInfo) {
typedValue(fieldValue, fieldName, typeInfo, function (value) {
var names = value.split(';');
return {
// ADR field sequence
postOfficeBox: names[0],
street: names[2] || '',
number: names[1],
postalCode: names[5] || '',
city: names[3] || '',
region: names[4] || '',
country: names[6] || ''
};
});
}
function mediaValue(fieldValue, fieldName, typeInfo) {
typedValue(fieldValue, fieldName, typeInfo, function (value, typeInfo) {
let prefix = '';
if (typeInfo && typeInfo.encoding) {
if (typeInfo.type) {
prefix += (fieldName === 'sound' ? 'data:audio/' : 'data:image/') + typeInfo.type + ';';
}
if ((typeInfo.encoding == 'b') || (typeInfo.encoding.toLowerCase() == 'base64')) {
prefix += 'base64,';
}
}
return prefix + value;
});
}
function labelValue(fieldValue, fieldName, typeInfo) {
typedValue(fieldValue, fieldName, typeInfo, function (value, typeInfo) {
// convert escaped new lines to real new lines.
let str = value.replaceAll('\\n', '\n');
str = str.replace(/\\n/g, '\n');
if (typeInfo && typeInfo.encoding && typeInfo.encoding.toLowerCase() == 'quoted-printable') {
// convert quoted-printable encoded data
str = decodeQuotedPrintable(str);
str = str.replaceAll('\r\n', '\n');
}
return str;
});
}
// store vCard in vCards and reset vCard.
function endCard() {
vCards.push(vCard);
vCard = {};
}
// parse data, add to vCards
function parse(data) {
if (!data) {
return ;
}
vCardData += '\n' + data;
let lines = data
// replace escaped new lines
.replace(/\r\n\s{1}/g, '')
// split if a character is directly after a newline
.split(/\r\n(?=\S)|\r(?=\S)|\n(?=\S)/);
for (var i = 0; i < lines.length; i++) {
let line = lines[i];
// sometimes lines are prefixed by "item" keyword like "item1.ADR;type=WORK:....."
if (line.substring(0, 4) === "item") {
line = line.match(/item\d\.(.*)/)[1];
}
if ((line.substring(0, 5) === "home.") || (line.substring(0, 5) === "work.")) {
if (line.substring(5, 11) === "label:") {
line = line.substring(5).replace(":", ";TYPE=" + line.substring(0, 4) + ":");
} else {
line = line.substring(5).replace("type=", "TYPE=" + line.substring(0, 4) + ",");
}
console.log(line);
}
var pairs = line.split(':'),
fieldName = pairs[0],
fieldTypeInfo = [],
fieldValue = pairs.slice(1).join(':');
// is additional type info provided ?
if (fieldName.indexOf(';') >= 0 && line.indexOf(';') < line.indexOf(':')) {
var typeInfo = fieldName.split(';');
fieldName = typeInfo[0];
fieldTypeInfo = typeInfo.slice(1).map(function (type) {
let info = type.split('=');
return {
name: info[0]?.toLowerCase(),
value: info[1]?.replace(/"(.*)"/, '$1')
}
});
}
// ensure fieldType is in upper case
let ucFieldName = fieldName.toUpperCase();
if (ucFieldName.substring(0, 2) === 'X-') {
// ignore X- prefixed extension fields.
alert('ignore field "' + fieldName + '" with value "' + fieldValue + '"');
} else {
if (vCardFields[ucFieldName]) {
vCardFields[ucFieldName].method(fieldValue, vCardFields[ucFieldName].property, fieldTypeInfo);
} else {
alert('unknown field "' + fieldName + '" with value "' + fieldValue + '"');
}
}
}
return vCards;
}
// convert object to json
function toJson(cards) {
if (!cards) {
cards = vCards;
}
return JSON.stringify(cards, null, 2); // spacing level = 2
}
// reset object
function reset() {
vCardData = '';
vCard = {};
vCards = [];
}
// if form is present
function syntaxHighlight(json) {
if (typeof json != 'string') {
json = JSON.stringify(json, undefined, 2);
}
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
let syn = /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g;
return json.replace(syn, function (match) {
var cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
var input = document.getElementById('fileinput');
if (input) {
var btnLoad = document.getElementById('btnLoad');
var btnPreview = document.getElementById('btnPreview');
var btnCleanUp = document.getElementById('btnCleanUp');
if (typeof window.FileReader !== 'function') {
alert("The file API isn't supported on this browser yet.");
}
if (!input.files) {
alert("This browser doesn't seem to support the `files` property of file inputs.");
}
function checkStatus() {
if (btnLoad) {
if (!input.files[0]) {
btnLoad.disabled = true;
} else {
btnLoad.disabled = false;
}
} else {
if (input.files[0]) {
loadFile();
}
}
if (vCards.length > 0) {
if (btnCleanUp) {
btnCleanUp.disabled = false;
}
if (btnPreview) {
btnPreview.disabled = false;
}
} else {
if (btnCleanUp) {
btnCleanUp.disabled = true;
}
if (btnPreview) {
btnPreview.disabled = true;
}
}
}
checkStatus();
input.addEventListener('change', checkStatus);
function loadFile() {
var file, fr;
if (input.files[0]) {
file = input.files[0];
fr = new FileReader();
fr.addEventListener('load', receivedText);
fr.readAsText(file);
}
function receivedText(e) {
let data = e.target.result;
// convert vCard to object
let cards = parse(data);
// show data vCard
let preVcard = document.getElementById('vcard');
if (preVcard) {
preVcard.innerHTML = vCardData;
}
// convert object to json
let jsonCards = toJson(cards);
// show json with syntax highlight
let json = syntaxHighlight(jsonCards);
let preJson = document.getElementById('json');
if (preJson) {
preJson.innerHTML = json;
}
input.value = null;
checkStatus();
}
}
if (btnLoad) {
btnLoad.addEventListener('click', loadFile);
}
if (btnPreview) {
function preview() {
var overlay = document.createElement('div');
overlay.className = 'overlay';
document.body.appendChild(overlay);
var content = document.createElement('div');
content.className = 'popup-content';
overlay.appendChild(content);
var close = document.createElement('span');
close.className = 'popup-close';
overlay.appendChild(close);
close.addEventListener('click', function (event) {
document.body.removeChild(overlay);
});
let divCard = null, divCardContent = null, home = null, work = null;
function getElement(el) {
switch (el) {
case 'home':
if (!home) {
home = document.createElement('div');
home.className = 'wrapper home';
divCardContent.appendChild(home);
}
return home;
break;
case 'work':
if (!work) {
work = document.createElement('div');
work.className = 'wrapper work';
divCardContent.appendChild(work);
}
return work;
break;
}
}
function name(name, displayName) {
let wrapper = document.createElement('div');
wrapper.className = 'wrapper name';
divCard.appendChild(wrapper);
let p = document.createElement('p');
wrapper.appendChild(p);
let strong = document.createElement('strong');
p.appendChild(strong);
if (name) {
strong.appendChild(document.createTextNode(name.prefix + ' ' + name.name + ' ' + name.additionalName + ' ' + name.surname));
p.appendChild(document.createElement('br'));
p.appendChild(document.createTextNode(name.suffix));
} else if (vCard.displayName) {
strong.appendChild(document.createTextNode(displayName));
}
}
function photo(arr) {
if (arr && arr[0]) {
let wrapper = document.createElement('div');
wrapper.className = 'wrapper photo';
divCardContent.appendChild(wrapper);
let img = document.createElement('img');
img.src = arr[0].value;
img.className = 'photo';
wrapper.appendChild(img);
}
}
function addresses(arr) {
if (arr) {
for (let i=0; i<arr.length; i++) {
let item = arr[i];
let value = item.value;
if (value) {
let parent = item.valueInfo.type?.indexOf('work') > -1 ? getElement('work') : getElement('home');
let p = document.createElement('p');
p.className = 'address ' + item.valueInfo.type;
parent.appendChild(p);
if (value instanceof Object) {
p.appendChild(document.createTextNode(value.street + ' ' + value.number));
p.appendChild(document.createElement('br'));
p.appendChild(document.createTextNode(value.postalCode + ' ' + value.city));
p.appendChild(document.createElement('br'));
p.appendChild(document.createTextNode(value.region));
p.appendChild(document.createElement('br'));
p.appendChild(document.createTextNode(value.country));
} else {
value = value.split('\n');
for(let u=0; u<value.length; u++) {
p.appendChild(document.createTextNode(value[u]));
if (u < value.length-1) {
p.appendChild(document.createElement('br'));
}
}
}
}
}
}
}
function communications(name, arr) {
if (arr) {
for (let i=0; i<arr.length; i++) {
let item = arr[i];
let parent = item.valueInfo.type?.indexOf('work') > -1 ? getElement('work') : getElement('home');
let p = document.createElement('p');
p.className = name + ' ' + item.valueInfo.type?.replaceAll(',', ' ');
parent.appendChild(p);
let a = document.createElement('a');
let url = item.value;
if (!item.valueInfo.value || item.valueInfo.value !== 'uri') {
url = name + ':' + (name === 'https' ? '////' : '') + url;
}
a.href = url;
a.target = '_blank';
let caption = item.value;
if (item.valueInfo.value && item.valueInfo.value === 'uri') {
caption = caption.replace(/^\/\/|^.*?:(\/\/)?/, '');
}
a.appendChild(document.createTextNode(caption));
p.appendChild(a);
}
}
}
for (let o=0; o<vCards.length; o++) {
let card = vCards[o];
divCard = document.createElement('div');
divCard.className = 'card';
content.appendChild(divCard);
name(card.name, card.displayName);
divCardContent = document.createElement('div');
divCardContent.className = 'card-content';
divCard.appendChild(divCardContent);
photo(card.photo);
addresses(card.address);
communications('tel', card.telephone);
communications('mailto', card.email);
communications('https', card.url);
// next card ?
divCard = null;
divCardContent = null;
home = null;
work = null;
}
}
btnPreview.addEventListener('click', preview);
}
if (btnCleanUp) {
function cleanUp() {
let preVcard = document.getElementById('vcard');
if (preVcard) {
preVcard.innerHTML = '';
}
let preJson = document.getElementById('json');
if (preJson) {
preJson.innerHTML = '';
}
reset();
checkStatus();
}
btnCleanUp.addEventListener('click', cleanUp);
}
}
// expose globally
window.vCards = {
parse: parse,
toJson: toJson,
reset: reset
};
})();