319ee44aae
Ergänzt aussagekräftige Beispiel-Platzhalter im vCard-Formular und zeigt den generierten vCard-Text als Live-Vorschau im vCard-Modus an. Das erleichtert die Eingabe und macht das spätere QR-Ergebnis sofort nachvollziehbar. Made-with: Cursor
1082 lines
40 KiB
JavaScript
1082 lines
40 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
||
|
||
const textInput = document.getElementById('text');
|
||
const clearTextBtn = document.getElementById('clear-text');
|
||
const contentModeSelect = document.getElementById('content-mode');
|
||
const sectionText = document.getElementById('section-text');
|
||
const sectionWifi = document.getElementById('section-wifi');
|
||
const sectionEvent = document.getElementById('section-event');
|
||
const sectionVcard = document.getElementById('section-vcard');
|
||
const wifiSsidInput = document.getElementById('wifi-ssid');
|
||
const wifiPasswordInput = document.getElementById('wifi-password');
|
||
const eventTitleInput = document.getElementById('event-title');
|
||
const eventStartDateInput = document.getElementById('event-start-date');
|
||
const eventStartHourInput = document.getElementById('event-start-hour');
|
||
const eventStartMinuteInput = document.getElementById('event-start-minute');
|
||
const eventEndDateInput = document.getElementById('event-end-date');
|
||
const eventEndHourInput = document.getElementById('event-end-hour');
|
||
const eventEndMinuteInput = document.getElementById('event-end-minute');
|
||
const eventLocationInput = document.getElementById('event-location');
|
||
const eventDescriptionInput = document.getElementById('event-description');
|
||
const vcardFirstNameInput = document.getElementById('vcard-first-name');
|
||
const vcardLastNameInput = document.getElementById('vcard-last-name');
|
||
const vcardOrgInput = document.getElementById('vcard-org');
|
||
const vcardTitleInput = document.getElementById('vcard-title');
|
||
const vcardPhoneInput = document.getElementById('vcard-phone');
|
||
const vcardEmailInput = document.getElementById('vcard-email');
|
||
const vcardWebsiteInput = document.getElementById('vcard-website');
|
||
const vcardAddressInput = document.getElementById('vcard-address');
|
||
const vcardNoteInput = document.getElementById('vcard-note');
|
||
const sizeSelect = document.getElementById('size');
|
||
const errorCorrectionSelect = document.getElementById('errorCorrection');
|
||
const foregroundColor = document.getElementById('foreground');
|
||
const backgroundColor = document.getElementById('background');
|
||
const downloadBtn = document.getElementById('download');
|
||
const qrcodeCanvas = document.getElementById('qrcode');
|
||
const qrcodeImg = document.getElementById('qrcode-img');
|
||
const errorMessage = document.getElementById('error-message');
|
||
const infoButton = document.getElementById('info-button');
|
||
const infoPanel = document.getElementById('info-panel');
|
||
const infoClose = document.getElementById('info-close');
|
||
const titleElement = document.getElementById('title');
|
||
const shareBtn = document.getElementById('share');
|
||
const shareHint = document.getElementById('share-hint');
|
||
const eventBerlinPreview = document.getElementById('event-berlin-preview');
|
||
const vcardPreview = document.getElementById('vcard-preview');
|
||
|
||
const MAX_EVENT_TITLE = 2000;
|
||
const MAX_EVENT_LOCATION = 1000;
|
||
const MAX_EVENT_DESCRIPTION = 8000;
|
||
const MAX_VCARD_FIELD = 500;
|
||
|
||
// Title Easter egg
|
||
let titleClickCount = 0;
|
||
let titleTimeoutId = null;
|
||
|
||
titleElement.addEventListener('click', function() {
|
||
titleClickCount++;
|
||
clearTimeout(titleTimeoutId);
|
||
|
||
if (titleClickCount === 1) {
|
||
this.textContent = "Just a [bleep]ing QR Code";
|
||
|
||
titleTimeoutId = setTimeout(() => {
|
||
this.textContent = "Just a QR Code";
|
||
titleClickCount = 0;
|
||
}, 500);
|
||
}
|
||
});
|
||
|
||
// Info panel toggle
|
||
infoButton.addEventListener('click', function() {
|
||
infoPanel.classList.add('open');
|
||
});
|
||
|
||
infoClose.addEventListener('click', function() {
|
||
infoPanel.classList.remove('open');
|
||
});
|
||
|
||
// Close info panel when clicking outside
|
||
document.addEventListener('click', function(event) {
|
||
if (infoPanel.classList.contains('open') &&
|
||
!infoPanel.contains(event.target) &&
|
||
event.target !== infoButton) {
|
||
infoPanel.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
// Close info panel with Escape key
|
||
document.addEventListener('keydown', function(event) {
|
||
if (event.key === 'Escape' && infoPanel.classList.contains('open')) {
|
||
infoPanel.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
function toggleContentSections() {
|
||
const mode = contentModeSelect.value;
|
||
sectionText.style.display = mode === 'text' ? 'block' : 'none';
|
||
sectionWifi.style.display = mode === 'wifi' ? 'block' : 'none';
|
||
sectionEvent.style.display = mode === 'event' ? 'block' : 'none';
|
||
sectionVcard.style.display = mode === 'vcard' ? 'block' : 'none';
|
||
}
|
||
|
||
contentModeSelect.addEventListener('change', function() {
|
||
toggleContentSections();
|
||
updateBerlinPreview();
|
||
generateQRCode();
|
||
if (this.value === 'text') {
|
||
textInput.focus();
|
||
} else if (this.value === 'wifi') {
|
||
wifiSsidInput.focus();
|
||
} else if (this.value === 'event') {
|
||
eventTitleInput.focus();
|
||
} else if (this.value === 'vcard') {
|
||
vcardFirstNameInput.focus();
|
||
}
|
||
});
|
||
|
||
// Set random compliment as default text and select it
|
||
textInput.value = 'https://medisoftware.de';
|
||
textInput.select();
|
||
|
||
// Show/hide clear button based on input content
|
||
function toggleClearButton() {
|
||
const hasText = textInput.value.length > 0;
|
||
const hasWifi = wifiSsidInput.value.trim().length > 0 || wifiPasswordInput.value.trim().length > 0;
|
||
const hasEvent = eventTitleInput.value.trim().length > 0 ||
|
||
eventStartDateInput.value.length > 0 ||
|
||
eventStartHourInput.value !== '' ||
|
||
eventStartMinuteInput.value !== '' ||
|
||
eventEndDateInput.value.length > 0 ||
|
||
eventEndHourInput.value !== '' ||
|
||
eventEndMinuteInput.value !== '' ||
|
||
eventLocationInput.value.trim().length > 0 ||
|
||
eventDescriptionInput.value.trim().length > 0;
|
||
const hasVCard = vcardFirstNameInput.value.trim().length > 0 ||
|
||
vcardLastNameInput.value.trim().length > 0 ||
|
||
vcardOrgInput.value.trim().length > 0 ||
|
||
vcardTitleInput.value.trim().length > 0 ||
|
||
vcardPhoneInput.value.trim().length > 0 ||
|
||
vcardEmailInput.value.trim().length > 0 ||
|
||
vcardWebsiteInput.value.trim().length > 0 ||
|
||
vcardAddressInput.value.trim().length > 0 ||
|
||
vcardNoteInput.value.trim().length > 0;
|
||
if (hasText || hasWifi || hasEvent || hasVCard) {
|
||
clearTextBtn.style.display = 'block';
|
||
} else {
|
||
clearTextBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Initialize clear button visibility
|
||
toggleClearButton();
|
||
|
||
// Clear text when clear button is clicked
|
||
clearTextBtn.addEventListener('click', function() {
|
||
textInput.value = '';
|
||
wifiSsidInput.value = '';
|
||
wifiPasswordInput.value = '';
|
||
eventTitleInput.value = '';
|
||
eventStartDateInput.value = '';
|
||
eventStartHourInput.value = '';
|
||
eventStartMinuteInput.value = '';
|
||
eventEndDateInput.value = '';
|
||
eventEndHourInput.value = '';
|
||
eventEndMinuteInput.value = '';
|
||
eventLocationInput.value = '';
|
||
eventDescriptionInput.value = '';
|
||
vcardFirstNameInput.value = '';
|
||
vcardLastNameInput.value = '';
|
||
vcardOrgInput.value = '';
|
||
vcardTitleInput.value = '';
|
||
vcardPhoneInput.value = '';
|
||
vcardEmailInput.value = '';
|
||
vcardWebsiteInput.value = '';
|
||
vcardAddressInput.value = '';
|
||
vcardNoteInput.value = '';
|
||
contentModeSelect.value = 'text';
|
||
toggleContentSections();
|
||
updateBerlinPreview();
|
||
toggleClearButton();
|
||
textInput.focus();
|
||
generateQRCode();
|
||
});
|
||
|
||
// Update clear button visibility when text changes
|
||
textInput.addEventListener('input', function() {
|
||
toggleClearButton();
|
||
if (contentModeSelect.value === 'text') {
|
||
generateQRCode();
|
||
}
|
||
});
|
||
|
||
let qr = null;
|
||
|
||
// Generate QR code when settings change
|
||
sizeSelect.addEventListener('change', generateQRCode);
|
||
errorCorrectionSelect.addEventListener('change', generateQRCode);
|
||
foregroundColor.addEventListener('input', generateQRCode);
|
||
backgroundColor.addEventListener('input', generateQRCode);
|
||
|
||
// Generate QR code when wifi settings change
|
||
wifiSsidInput.addEventListener('input', function() {
|
||
if (contentModeSelect.value === 'wifi') {
|
||
updateTextDisplay();
|
||
}
|
||
generateQRCode();
|
||
toggleClearButton();
|
||
});
|
||
wifiPasswordInput.addEventListener('input', function() {
|
||
if (contentModeSelect.value === 'wifi') {
|
||
updateTextDisplay();
|
||
}
|
||
generateQRCode();
|
||
toggleClearButton();
|
||
});
|
||
|
||
eventTitleInput.addEventListener('input', function() {
|
||
updateBerlinPreview();
|
||
generateQRCode();
|
||
toggleClearButton();
|
||
});
|
||
function onEventDateTimeInput() {
|
||
updateBerlinPreview();
|
||
generateQRCode();
|
||
toggleClearButton();
|
||
}
|
||
eventStartDateInput.addEventListener('input', onEventDateTimeInput);
|
||
eventStartDateInput.addEventListener('change', onEventDateTimeInput);
|
||
eventStartHourInput.addEventListener('input', onEventDateTimeInput);
|
||
eventStartHourInput.addEventListener('change', onEventDateTimeInput);
|
||
eventStartMinuteInput.addEventListener('input', onEventDateTimeInput);
|
||
eventStartMinuteInput.addEventListener('change', onEventDateTimeInput);
|
||
eventEndDateInput.addEventListener('input', onEventDateTimeInput);
|
||
eventEndDateInput.addEventListener('change', onEventDateTimeInput);
|
||
eventEndHourInput.addEventListener('input', onEventDateTimeInput);
|
||
eventEndHourInput.addEventListener('change', onEventDateTimeInput);
|
||
eventEndMinuteInput.addEventListener('input', onEventDateTimeInput);
|
||
eventEndMinuteInput.addEventListener('change', onEventDateTimeInput);
|
||
|
||
function clampTimeNumberInput(el, max) {
|
||
if (el.value === '') {
|
||
return;
|
||
}
|
||
var v = parseInt(el.value, 10);
|
||
if (isNaN(v)) {
|
||
return;
|
||
}
|
||
if (v < 0) {
|
||
el.value = '0';
|
||
} else if (v > max) {
|
||
el.value = String(max);
|
||
}
|
||
}
|
||
eventStartHourInput.addEventListener('blur', function() { clampTimeNumberInput(eventStartHourInput, 23); });
|
||
eventStartMinuteInput.addEventListener('blur', function() { clampTimeNumberInput(eventStartMinuteInput, 59); });
|
||
eventEndHourInput.addEventListener('blur', function() { clampTimeNumberInput(eventEndHourInput, 23); });
|
||
eventEndMinuteInput.addEventListener('blur', function() { clampTimeNumberInput(eventEndMinuteInput, 59); });
|
||
eventLocationInput.addEventListener('input', function() {
|
||
generateQRCode();
|
||
toggleClearButton();
|
||
});
|
||
eventDescriptionInput.addEventListener('input', function() {
|
||
generateQRCode();
|
||
toggleClearButton();
|
||
});
|
||
function onVcardInput() {
|
||
generateQRCode();
|
||
toggleClearButton();
|
||
}
|
||
vcardFirstNameInput.addEventListener('input', onVcardInput);
|
||
vcardLastNameInput.addEventListener('input', onVcardInput);
|
||
vcardOrgInput.addEventListener('input', onVcardInput);
|
||
vcardTitleInput.addEventListener('input', onVcardInput);
|
||
vcardPhoneInput.addEventListener('input', onVcardInput);
|
||
vcardEmailInput.addEventListener('input', onVcardInput);
|
||
vcardWebsiteInput.addEventListener('input', onVcardInput);
|
||
vcardAddressInput.addEventListener('input', onVcardInput);
|
||
vcardNoteInput.addEventListener('input', onVcardInput);
|
||
|
||
// Download QR code as image
|
||
downloadBtn.addEventListener('click', downloadQRCode);
|
||
|
||
function sanitizeUrlString(s, maxLen) {
|
||
if (typeof s !== 'string') {
|
||
return '';
|
||
}
|
||
return s.replace(/\0/g, '').slice(0, maxLen);
|
||
}
|
||
|
||
function isValidDatetimeLocal(s) {
|
||
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(s);
|
||
}
|
||
|
||
function combineDateTimeFromParts(dateStr, hourStr, minuteStr) {
|
||
if (!dateStr || hourStr === '' || minuteStr === '') {
|
||
return '';
|
||
}
|
||
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
||
return '';
|
||
}
|
||
var h = parseInt(String(hourStr).trim(), 10);
|
||
var mi = parseInt(String(minuteStr).trim(), 10);
|
||
if (isNaN(h) || isNaN(mi)) {
|
||
return '';
|
||
}
|
||
if (h < 0 || h > 23 || mi < 0 || mi > 59) {
|
||
return '';
|
||
}
|
||
var pad = function(n) { return String(n).padStart(2, '0'); };
|
||
return dateStr + 'T' + pad(h) + ':' + pad(mi);
|
||
}
|
||
|
||
function applyCombinedToDateTimeInputs(combined, dateInput, hourInput, minuteInput) {
|
||
if (!isValidDatetimeLocal(combined)) {
|
||
return;
|
||
}
|
||
var splitT = combined.split('T');
|
||
var d = splitT[0];
|
||
var t = splitT[1];
|
||
var tm = /^(\d{2}):(\d{2})$/.exec(t);
|
||
if (!tm) {
|
||
return;
|
||
}
|
||
dateInput.value = d;
|
||
hourInput.value = String(parseInt(tm[1], 10));
|
||
minuteInput.value = String(parseInt(tm[2], 10));
|
||
}
|
||
|
||
// Or check for URL parameters if present
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
|
||
let explicitModeFromUrl = false;
|
||
if (urlParams.has('mode')) {
|
||
const m = urlParams.get('mode');
|
||
if (m === 'text' || m === 'wifi' || m === 'event' || m === 'vcard') {
|
||
contentModeSelect.value = m;
|
||
explicitModeFromUrl = true;
|
||
}
|
||
}
|
||
|
||
if (urlParams.has('text')) {
|
||
const safeText = urlParams.get('text');
|
||
if (/^[\w\-.:/?&=#%+@!;,]*$/i.test(safeText)) {
|
||
textInput.value = safeText;
|
||
}
|
||
}
|
||
if (urlParams.has('ssid')) {
|
||
const safeSsid = urlParams.get('ssid');
|
||
if (/^[^<>"'`\\]*$/.test(safeSsid)) {
|
||
wifiSsidInput.value = safeSsid;
|
||
}
|
||
}
|
||
if (urlParams.has('password')) {
|
||
const safePassword = urlParams.get('password');
|
||
if (/^[^<>"'`\\]*$/.test(safePassword)) {
|
||
wifiPasswordInput.value = safePassword;
|
||
}
|
||
}
|
||
|
||
const hasEventParams = urlParams.has('eventTitle') || urlParams.has('eventStart');
|
||
if (!explicitModeFromUrl && hasEventParams) {
|
||
contentModeSelect.value = 'event';
|
||
}
|
||
if (urlParams.has('eventTitle')) {
|
||
eventTitleInput.value = sanitizeUrlString(urlParams.get('eventTitle'), MAX_EVENT_TITLE);
|
||
}
|
||
if (urlParams.has('eventStart')) {
|
||
const es = urlParams.get('eventStart');
|
||
if (isValidDatetimeLocal(es)) {
|
||
applyCombinedToDateTimeInputs(es, eventStartDateInput, eventStartHourInput, eventStartMinuteInput);
|
||
}
|
||
}
|
||
if (urlParams.has('eventEnd')) {
|
||
const ee = urlParams.get('eventEnd');
|
||
if (isValidDatetimeLocal(ee)) {
|
||
applyCombinedToDateTimeInputs(ee, eventEndDateInput, eventEndHourInput, eventEndMinuteInput);
|
||
}
|
||
}
|
||
if (urlParams.has('eventLocation')) {
|
||
eventLocationInput.value = sanitizeUrlString(urlParams.get('eventLocation'), MAX_EVENT_LOCATION);
|
||
}
|
||
if (urlParams.has('eventDescription')) {
|
||
eventDescriptionInput.value = sanitizeUrlString(urlParams.get('eventDescription'), MAX_EVENT_DESCRIPTION);
|
||
}
|
||
const hasVcardParams = urlParams.has('vcardFirstName') ||
|
||
urlParams.has('vcardLastName') ||
|
||
urlParams.has('vcardOrg') ||
|
||
urlParams.has('vcardPhone') ||
|
||
urlParams.has('vcardEmail');
|
||
if (!explicitModeFromUrl && !hasEventParams && hasVcardParams) {
|
||
contentModeSelect.value = 'vcard';
|
||
}
|
||
if (urlParams.has('vcardFirstName')) {
|
||
vcardFirstNameInput.value = sanitizeUrlString(urlParams.get('vcardFirstName'), MAX_VCARD_FIELD);
|
||
}
|
||
if (urlParams.has('vcardLastName')) {
|
||
vcardLastNameInput.value = sanitizeUrlString(urlParams.get('vcardLastName'), MAX_VCARD_FIELD);
|
||
}
|
||
if (urlParams.has('vcardOrg')) {
|
||
vcardOrgInput.value = sanitizeUrlString(urlParams.get('vcardOrg'), MAX_VCARD_FIELD);
|
||
}
|
||
if (urlParams.has('vcardTitle')) {
|
||
vcardTitleInput.value = sanitizeUrlString(urlParams.get('vcardTitle'), MAX_VCARD_FIELD);
|
||
}
|
||
if (urlParams.has('vcardPhone')) {
|
||
vcardPhoneInput.value = sanitizeUrlString(urlParams.get('vcardPhone'), MAX_VCARD_FIELD);
|
||
}
|
||
if (urlParams.has('vcardEmail')) {
|
||
vcardEmailInput.value = sanitizeUrlString(urlParams.get('vcardEmail'), MAX_VCARD_FIELD);
|
||
}
|
||
if (urlParams.has('vcardWebsite')) {
|
||
vcardWebsiteInput.value = sanitizeUrlString(urlParams.get('vcardWebsite'), MAX_VCARD_FIELD);
|
||
}
|
||
if (urlParams.has('vcardAddress')) {
|
||
vcardAddressInput.value = sanitizeUrlString(urlParams.get('vcardAddress'), MAX_VCARD_FIELD);
|
||
}
|
||
if (urlParams.has('vcardNote')) {
|
||
vcardNoteInput.value = sanitizeUrlString(urlParams.get('vcardNote'), MAX_VCARD_FIELD);
|
||
}
|
||
|
||
if (urlParams.has('size')) {
|
||
const sizeValue = urlParams.get('size');
|
||
if (["128", "256", "512", "1024"].includes(sizeValue)) {
|
||
sizeSelect.value = sizeValue;
|
||
}
|
||
}
|
||
if (urlParams.has('errorCorrection')) {
|
||
const ecValue = urlParams.get('errorCorrection').toUpperCase();
|
||
if (["L", "M", "Q", "H"].includes(ecValue)) {
|
||
errorCorrectionSelect.value = ecValue;
|
||
}
|
||
}
|
||
if (urlParams.has('foreground')) {
|
||
const fgValue = urlParams.get('foreground');
|
||
if (/^#[0-9A-F]{6}$/i.test(fgValue)) {
|
||
foregroundColor.value = fgValue;
|
||
}
|
||
}
|
||
if (urlParams.has('background')) {
|
||
const bgValue = urlParams.get('background');
|
||
if (/^#[0-9A-F]{6}$/i.test(bgValue)) {
|
||
backgroundColor.value = bgValue;
|
||
}
|
||
}
|
||
|
||
toggleContentSections();
|
||
updateBerlinPreview();
|
||
generateQRCode();
|
||
|
||
function daysInMonth(y, mo) {
|
||
return new Date(y, mo, 0).getDate();
|
||
}
|
||
|
||
function parseDatetimeLocalValue(s) {
|
||
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(s);
|
||
if (!m) {
|
||
return null;
|
||
}
|
||
return {
|
||
y: parseInt(m[1], 10),
|
||
mo: parseInt(m[2], 10),
|
||
d: parseInt(m[3], 10),
|
||
h: parseInt(m[4], 10),
|
||
mi: parseInt(m[5], 10)
|
||
};
|
||
}
|
||
|
||
function addHoursWall(parts, deltaH) {
|
||
let y = parts.y;
|
||
let mo = parts.mo;
|
||
let d = parts.d;
|
||
let h = parts.h + deltaH;
|
||
let mi = parts.mi;
|
||
while (h >= 24) {
|
||
h -= 24;
|
||
d += 1;
|
||
const dim = daysInMonth(y, mo);
|
||
if (d > dim) {
|
||
d = 1;
|
||
mo += 1;
|
||
if (mo > 12) {
|
||
mo = 1;
|
||
y += 1;
|
||
}
|
||
}
|
||
}
|
||
return { y: y, mo: mo, d: d, h: h, mi: mi };
|
||
}
|
||
|
||
function wallTimeToICal(p) {
|
||
const pad = function(n) { return String(n).padStart(2, '0'); };
|
||
return String(p.y) + pad(p.mo) + pad(p.d) + 'T' + pad(p.h) + pad(p.mi) + '00';
|
||
}
|
||
|
||
function compareWallTime(a, b) {
|
||
return (a.y * 1e8 + a.mo * 1e6 + a.d * 1e4 + a.h * 100 + a.mi) -
|
||
(b.y * 1e8 + b.mo * 1e6 + b.d * 1e4 + b.h * 100 + b.mi);
|
||
}
|
||
|
||
function formatSlashDateTime24hBerlin(p) {
|
||
const pad = function(n) { return String(n).padStart(2, '0'); };
|
||
return pad(p.d) + '/' + pad(p.mo) + '/' + p.y + ', ' + pad(p.h) + ':' + pad(p.mi);
|
||
}
|
||
|
||
function updateBerlinPreview() {
|
||
if (!eventBerlinPreview) {
|
||
return;
|
||
}
|
||
if (contentModeSelect.value !== 'event') {
|
||
eventBerlinPreview.textContent = '';
|
||
return;
|
||
}
|
||
const startRaw = combineDateTimeFromParts(
|
||
eventStartDateInput.value,
|
||
eventStartHourInput.value,
|
||
eventStartMinuteInput.value
|
||
);
|
||
const endD = eventEndDateInput.value;
|
||
const endH = eventEndHourInput.value;
|
||
const endM = eventEndMinuteInput.value;
|
||
const endTimeComplete = endH !== '' && endM !== '';
|
||
const endTimeAny = endH !== '' || endM !== '';
|
||
let endRaw = '';
|
||
if (endD && endTimeComplete) {
|
||
endRaw = combineDateTimeFromParts(endD, endH, endM);
|
||
} else if (endD && !endTimeComplete) {
|
||
eventBerlinPreview.textContent =
|
||
'Bitte geben Sie für das Ende Datum sowie Stunde (0–23) und Minute (0–59) an, oder lassen Sie das Ende vollständig leer.';
|
||
return;
|
||
} else if (!endD && endTimeAny) {
|
||
eventBerlinPreview.textContent =
|
||
'Bitte geben Sie für das Ende ein Datum ein, oder entfernen Sie Stunde/Minute.';
|
||
return;
|
||
}
|
||
const sp = parseDatetimeLocalValue(startRaw);
|
||
if (!sp) {
|
||
eventBerlinPreview.textContent = '';
|
||
return;
|
||
}
|
||
let endDisplay;
|
||
if (endRaw) {
|
||
const ep = parseDatetimeLocalValue(endRaw);
|
||
if (!ep) {
|
||
eventBerlinPreview.textContent = 'Beginn: ' + formatSlashDateTime24hBerlin(sp) + ' (Europe/Berlin)';
|
||
return;
|
||
}
|
||
endDisplay = ep;
|
||
} else {
|
||
endDisplay = addHoursWall(sp, 1);
|
||
}
|
||
eventBerlinPreview.textContent =
|
||
'Beginn: ' + formatSlashDateTime24hBerlin(sp) +
|
||
' · Ende: ' + formatSlashDateTime24hBerlin(endDisplay) +
|
||
' (Europe/Berlin)';
|
||
}
|
||
|
||
function escapeICalText(str) {
|
||
return String(str)
|
||
.replace(/\r\n/g, '\n')
|
||
.replace(/\r/g, '')
|
||
.replace(/\\/g, '\\\\')
|
||
.replace(/;/g, '\\;')
|
||
.replace(/,/g, '\\,')
|
||
.replace(/\n/g, '\\n');
|
||
}
|
||
|
||
function formatICalUTCStamp() {
|
||
const d = new Date();
|
||
const pad = function(n) { return String(n).padStart(2, '0'); };
|
||
return (
|
||
d.getUTCFullYear() +
|
||
pad(d.getUTCMonth() + 1) +
|
||
pad(d.getUTCDate()) +
|
||
'T' +
|
||
pad(d.getUTCHours()) +
|
||
pad(d.getUTCMinutes()) +
|
||
pad(d.getUTCSeconds()) +
|
||
'Z'
|
||
);
|
||
}
|
||
|
||
function newEventUid() {
|
||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||
return crypto.randomUUID() + '@qr-generator.local';
|
||
}
|
||
return 'evt-' + String(Date.now()) + '-' + String(Math.random()).slice(2) + '@qr-generator.local';
|
||
}
|
||
|
||
function buildICalEvent(opts) {
|
||
const summary = opts.summary;
|
||
const dtStart = opts.dtStart;
|
||
const dtEnd = opts.dtEnd;
|
||
const location = opts.location;
|
||
const description = opts.description;
|
||
|
||
const vtimezoneBerlin = [
|
||
'BEGIN:VTIMEZONE',
|
||
'TZID:Europe/Berlin',
|
||
'BEGIN:DAYLIGHT',
|
||
'DTSTART:19810329T020000',
|
||
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
|
||
'TZNAME:CEST',
|
||
'TZOFFSETFROM:+0100',
|
||
'TZOFFSETTO:+0200',
|
||
'END:DAYLIGHT',
|
||
'BEGIN:STANDARD',
|
||
'DTSTART:19961027T030000',
|
||
'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
|
||
'TZNAME:CET',
|
||
'TZOFFSETFROM:+0200',
|
||
'TZOFFSETTO:+0100',
|
||
'END:STANDARD',
|
||
'END:VTIMEZONE'
|
||
].join('\r\n');
|
||
|
||
const lines = [
|
||
'BEGIN:VCALENDAR',
|
||
'VERSION:2.0',
|
||
'PRODID:-//qr-generator//DE',
|
||
'CALSCALE:GREGORIAN',
|
||
vtimezoneBerlin,
|
||
'BEGIN:VEVENT',
|
||
'UID:' + opts.uid,
|
||
'DTSTAMP:' + opts.dtStamp,
|
||
'DTSTART;TZID=Europe/Berlin:' + dtStart,
|
||
'DTEND;TZID=Europe/Berlin:' + dtEnd,
|
||
'SUMMARY:' + escapeICalText(summary)
|
||
];
|
||
if (location && location.trim()) {
|
||
lines.push('LOCATION:' + escapeICalText(location.trim()));
|
||
}
|
||
if (description && description.trim()) {
|
||
lines.push('DESCRIPTION:' + escapeICalText(description.trim()));
|
||
}
|
||
lines.push('END:VEVENT', 'END:VCALENDAR');
|
||
return lines.join('\r\n');
|
||
}
|
||
|
||
function escapeVCardText(str) {
|
||
return String(str)
|
||
.replace(/\r\n/g, '\n')
|
||
.replace(/\r/g, '')
|
||
.replace(/\\/g, '\\\\')
|
||
.replace(/;/g, '\\;')
|
||
.replace(/,/g, '\\,')
|
||
.replace(/\n/g, '\\n');
|
||
}
|
||
|
||
function buildVCard(opts) {
|
||
const firstName = opts.firstName.trim();
|
||
const lastName = opts.lastName.trim();
|
||
const org = opts.org.trim();
|
||
const title = opts.title.trim();
|
||
const phone = opts.phone.trim();
|
||
const email = opts.email.trim();
|
||
const website = opts.website.trim();
|
||
const address = opts.address.trim();
|
||
const note = opts.note.trim();
|
||
const displayName = [firstName, lastName].filter(Boolean).join(' ').trim() || org;
|
||
|
||
const lines = [
|
||
'BEGIN:VCARD',
|
||
'VERSION:3.0',
|
||
'N:' + escapeVCardText(lastName) + ';' + escapeVCardText(firstName) + ';;;',
|
||
'FN:' + escapeVCardText(displayName)
|
||
];
|
||
if (org) {
|
||
lines.push('ORG:' + escapeVCardText(org));
|
||
}
|
||
if (title) {
|
||
lines.push('TITLE:' + escapeVCardText(title));
|
||
}
|
||
if (phone) {
|
||
lines.push('TEL;TYPE=CELL:' + escapeVCardText(phone));
|
||
}
|
||
if (email) {
|
||
lines.push('EMAIL;TYPE=INTERNET:' + escapeVCardText(email));
|
||
}
|
||
if (website) {
|
||
lines.push('URL:' + escapeVCardText(website));
|
||
}
|
||
if (address) {
|
||
lines.push('ADR;TYPE=WORK:;;' + escapeVCardText(address) + ';;;;');
|
||
}
|
||
if (note) {
|
||
lines.push('NOTE:' + escapeVCardText(note));
|
||
}
|
||
lines.push('END:VCARD');
|
||
return lines.join('\r\n');
|
||
}
|
||
|
||
function updateVCardPreview(text) {
|
||
if (!vcardPreview) {
|
||
return;
|
||
}
|
||
if (contentModeSelect.value !== 'vcard') {
|
||
vcardPreview.textContent = '';
|
||
return;
|
||
}
|
||
if (text && String(text).trim()) {
|
||
vcardPreview.textContent = String(text);
|
||
return;
|
||
}
|
||
vcardPreview.textContent = 'Bitte mindestens Vorname, Nachname oder Organisation eingeben.';
|
||
}
|
||
|
||
function generateQRCode() {
|
||
const mode = contentModeSelect.value;
|
||
const text = textInput.value.trim();
|
||
const wifiSsid = wifiSsidInput.value.trim();
|
||
const wifiPassword = wifiPasswordInput.value.trim();
|
||
const size = parseInt(sizeSelect.value, 10);
|
||
const errorCorrection = errorCorrectionSelect.value.toLowerCase();
|
||
const fgColor = foregroundColor.value;
|
||
const bgColor = backgroundColor.value;
|
||
|
||
// Update the gabe link color to match foreground color
|
||
const gabeLink = document.querySelector('.credit a');
|
||
if (gabeLink) {
|
||
gabeLink.style.color = fgColor;
|
||
}
|
||
|
||
// Clear previous error
|
||
errorMessage.textContent = '';
|
||
downloadBtn.style.display = 'none';
|
||
if (mode !== 'vcard') {
|
||
updateVCardPreview('');
|
||
}
|
||
|
||
let qrText = '';
|
||
|
||
if (mode === 'event') {
|
||
const title = eventTitleInput.value.trim();
|
||
const startRaw = combineDateTimeFromParts(
|
||
eventStartDateInput.value,
|
||
eventStartHourInput.value,
|
||
eventStartMinuteInput.value
|
||
);
|
||
const endD = eventEndDateInput.value;
|
||
const endH = eventEndHourInput.value;
|
||
const endM = eventEndMinuteInput.value;
|
||
const endTimeComplete = endH !== '' && endM !== '';
|
||
const endTimeAny = endH !== '' || endM !== '';
|
||
const loc = eventLocationInput.value;
|
||
const desc = eventDescriptionInput.value;
|
||
|
||
if (!title) {
|
||
errorMessage.textContent = 'Bitte geben Sie einen Titel für den Termin ein';
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
}
|
||
if (!startRaw) {
|
||
errorMessage.textContent =
|
||
'Bitte geben Sie Datum sowie Stunde (0–23) und Minute (0–59) für den Beginn ein';
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
if (endD && !endTimeComplete) {
|
||
errorMessage.textContent =
|
||
'Für das Ende: bitte Stunde und Minute ergänzen (0–23 / 0–59) oder Ende leer lassen';
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
}
|
||
if (!endD && endTimeAny) {
|
||
errorMessage.textContent = 'Für das Ende bitte zuerst ein Datum wählen oder Zeitfelder leeren';
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const startParts = parseDatetimeLocalValue(startRaw);
|
||
if (!startParts) {
|
||
errorMessage.textContent = 'Ungültige Startzeit (Datum und 24h-Uhrzeit prüfen)';
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const dtStart = wallTimeToICal(startParts);
|
||
|
||
let endParts;
|
||
const endRaw = endD && endTimeComplete ? combineDateTimeFromParts(endD, endH, endM) : '';
|
||
if (endRaw) {
|
||
endParts = parseDatetimeLocalValue(endRaw);
|
||
if (!endParts) {
|
||
errorMessage.textContent = 'Ungültige Endzeit';
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
}
|
||
} else {
|
||
endParts = addHoursWall(startParts, 1);
|
||
}
|
||
|
||
if (compareWallTime(endParts, startParts) <= 0) {
|
||
errorMessage.textContent = 'Die Endzeit muss nach dem Beginn liegen (Ortszeit Europe/Berlin)';
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const dtEndStr = wallTimeToICal(endParts);
|
||
|
||
qrText = buildICalEvent({
|
||
uid: newEventUid(),
|
||
dtStamp: formatICalUTCStamp(),
|
||
summary: title.slice(0, MAX_EVENT_TITLE),
|
||
dtStart: dtStart,
|
||
dtEnd: dtEndStr,
|
||
location: loc.slice(0, MAX_EVENT_LOCATION),
|
||
description: desc.slice(0, MAX_EVENT_DESCRIPTION)
|
||
});
|
||
|
||
textInput.value = qrText;
|
||
toggleClearButton();
|
||
} else if (mode === 'vcard') {
|
||
const firstName = vcardFirstNameInput.value.slice(0, MAX_VCARD_FIELD);
|
||
const lastName = vcardLastNameInput.value.slice(0, MAX_VCARD_FIELD);
|
||
const org = vcardOrgInput.value.slice(0, MAX_VCARD_FIELD);
|
||
const title = vcardTitleInput.value.slice(0, MAX_VCARD_FIELD);
|
||
const phone = vcardPhoneInput.value.slice(0, MAX_VCARD_FIELD);
|
||
const email = vcardEmailInput.value.slice(0, MAX_VCARD_FIELD);
|
||
const website = vcardWebsiteInput.value.slice(0, MAX_VCARD_FIELD);
|
||
const address = vcardAddressInput.value.slice(0, MAX_VCARD_FIELD);
|
||
const note = vcardNoteInput.value.slice(0, MAX_VCARD_FIELD);
|
||
const hasIdentity = firstName.trim() || lastName.trim() || org.trim();
|
||
|
||
if (!hasIdentity) {
|
||
updateVCardPreview('');
|
||
errorMessage.textContent = 'Bitte geben Sie mindestens Vorname, Nachname oder Organisation an';
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
qrText = buildVCard({
|
||
firstName: firstName,
|
||
lastName: lastName,
|
||
org: org,
|
||
title: title,
|
||
phone: phone,
|
||
email: email,
|
||
website: website,
|
||
address: address,
|
||
note: note
|
||
});
|
||
updateVCardPreview(qrText);
|
||
textInput.value = qrText;
|
||
toggleClearButton();
|
||
} else if (mode === 'wifi') {
|
||
if (wifiSsid && wifiPassword) {
|
||
qrText = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:WPA;P:' + escapeSpecialChars(wifiPassword) + ';;';
|
||
} else if (wifiSsid && !wifiPassword) {
|
||
qrText = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:nopass;;';
|
||
} else if (!wifiSsid && wifiPassword) {
|
||
errorMessage.textContent = 'Bitte geben Sie eine SSID ein, wenn Sie ein Passwort angeben';
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
} else {
|
||
qrText = text;
|
||
}
|
||
} else {
|
||
qrText = text;
|
||
}
|
||
|
||
// Validate input
|
||
if (qrText === '') {
|
||
if (mode === 'wifi') {
|
||
errorMessage.textContent = 'Bitte geben Sie Text/URL oder WLAN-Daten ein';
|
||
} else if (mode === 'event') {
|
||
errorMessage.textContent = 'Termin konnte nicht erzeugt werden';
|
||
} else if (mode === 'vcard') {
|
||
errorMessage.textContent = 'vCard konnte nicht erzeugt werden';
|
||
} else {
|
||
errorMessage.textContent = 'Bitte geben Sie Text oder eine URL ein';
|
||
}
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Create new QR code on canvas
|
||
try {
|
||
if (!qr) {
|
||
qr = new QRious({
|
||
element: qrcodeCanvas,
|
||
size: size,
|
||
value: qrText,
|
||
foreground: fgColor,
|
||
background: bgColor,
|
||
level: errorCorrection
|
||
});
|
||
} else {
|
||
qr.set({
|
||
size: size,
|
||
value: qrText,
|
||
foreground: fgColor,
|
||
background: bgColor,
|
||
level: errorCorrection
|
||
});
|
||
}
|
||
|
||
// Convert canvas to image
|
||
const dataURL = qrcodeCanvas.toDataURL('image/png');
|
||
qrcodeImg.src = dataURL;
|
||
|
||
// Show image (hide canvas) and download button
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'block';
|
||
downloadBtn.style.display = 'inline-block';
|
||
} catch (err) {
|
||
errorMessage.textContent = 'Error generating QR code: ' + err.message;
|
||
qrcodeCanvas.style.display = 'none';
|
||
qrcodeImg.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Helper function to escape special characters in wifi strings
|
||
function escapeSpecialChars(str) {
|
||
return str.replace(/[\\;:,]/g, '\\$&');
|
||
}
|
||
|
||
function downloadQRCode() {
|
||
// Use the image source for download
|
||
const link = document.createElement('a');
|
||
link.download = 'qrcode.png';
|
||
link.href = qrcodeImg.src;
|
||
|
||
// Simulate click to trigger download
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
// Show success feedback
|
||
const originalText = downloadBtn.textContent;
|
||
downloadBtn.textContent = 'Downloaded!';
|
||
|
||
// Reset button after a short delay
|
||
setTimeout(function() {
|
||
downloadBtn.textContent = originalText;
|
||
}, 2000);
|
||
}
|
||
|
||
// Function to update the text display with current QR code content (WLAN)
|
||
function updateTextDisplay() {
|
||
const wifiSsid = wifiSsidInput.value.trim();
|
||
const wifiPassword = wifiPasswordInput.value.trim();
|
||
|
||
if (wifiSsid && wifiPassword) {
|
||
textInput.value = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:WPA;P:' + escapeSpecialChars(wifiPassword) + ';;';
|
||
} else if (wifiSsid && !wifiPassword) {
|
||
textInput.value = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:nopass;;';
|
||
}
|
||
|
||
toggleClearButton();
|
||
}
|
||
|
||
function updateShareHint() {
|
||
const mode = contentModeSelect.value;
|
||
const wifiPassword = wifiPasswordInput.value.trim();
|
||
if (mode === 'wifi' && wifiPassword) {
|
||
shareHint.style.display = 'block';
|
||
} else {
|
||
shareHint.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
wifiPasswordInput.addEventListener('input', updateShareHint);
|
||
wifiSsidInput.addEventListener('input', updateShareHint);
|
||
textInput.addEventListener('input', updateShareHint);
|
||
contentModeSelect.addEventListener('change', updateShareHint);
|
||
updateShareHint();
|
||
|
||
shareBtn.addEventListener('click', function() {
|
||
const params = new URLSearchParams();
|
||
const mode = contentModeSelect.value;
|
||
const text = textInput.value.trim();
|
||
const wifiSsid = wifiSsidInput.value.trim();
|
||
const wifiPassword = wifiPasswordInput.value.trim();
|
||
const size = sizeSelect.value;
|
||
const errorCorrection = errorCorrectionSelect.value;
|
||
const fgColor = foregroundColor.value;
|
||
const bgColor = backgroundColor.value;
|
||
|
||
params.set('mode', mode);
|
||
|
||
if (mode === 'event') {
|
||
if (eventTitleInput.value.trim()) {
|
||
params.set('eventTitle', eventTitleInput.value.trim());
|
||
}
|
||
const shareStart = combineDateTimeFromParts(
|
||
eventStartDateInput.value,
|
||
eventStartHourInput.value,
|
||
eventStartMinuteInput.value
|
||
);
|
||
const shareEnd = combineDateTimeFromParts(
|
||
eventEndDateInput.value,
|
||
eventEndHourInput.value,
|
||
eventEndMinuteInput.value
|
||
);
|
||
if (shareStart) {
|
||
params.set('eventStart', shareStart);
|
||
}
|
||
if (shareEnd) {
|
||
params.set('eventEnd', shareEnd);
|
||
}
|
||
if (eventLocationInput.value.trim()) {
|
||
params.set('eventLocation', eventLocationInput.value.trim());
|
||
}
|
||
if (eventDescriptionInput.value.trim()) {
|
||
params.set('eventDescription', eventDescriptionInput.value.trim());
|
||
}
|
||
} else if (mode === 'vcard') {
|
||
if (vcardFirstNameInput.value.trim()) {
|
||
params.set('vcardFirstName', vcardFirstNameInput.value.trim());
|
||
}
|
||
if (vcardLastNameInput.value.trim()) {
|
||
params.set('vcardLastName', vcardLastNameInput.value.trim());
|
||
}
|
||
if (vcardOrgInput.value.trim()) {
|
||
params.set('vcardOrg', vcardOrgInput.value.trim());
|
||
}
|
||
if (vcardTitleInput.value.trim()) {
|
||
params.set('vcardTitle', vcardTitleInput.value.trim());
|
||
}
|
||
if (vcardPhoneInput.value.trim()) {
|
||
params.set('vcardPhone', vcardPhoneInput.value.trim());
|
||
}
|
||
if (vcardEmailInput.value.trim()) {
|
||
params.set('vcardEmail', vcardEmailInput.value.trim());
|
||
}
|
||
if (vcardWebsiteInput.value.trim()) {
|
||
params.set('vcardWebsite', vcardWebsiteInput.value.trim());
|
||
}
|
||
if (vcardAddressInput.value.trim()) {
|
||
params.set('vcardAddress', vcardAddressInput.value.trim());
|
||
}
|
||
if (vcardNoteInput.value.trim()) {
|
||
params.set('vcardNote', vcardNoteInput.value.trim());
|
||
}
|
||
} else if (mode === 'wifi') {
|
||
if (wifiSsid) {
|
||
params.set('ssid', wifiSsid);
|
||
}
|
||
if (wifiPassword) {
|
||
params.set('password', wifiPassword);
|
||
}
|
||
} else {
|
||
if (text) {
|
||
params.set('text', text);
|
||
}
|
||
}
|
||
|
||
if (size !== '256') {
|
||
params.set('size', size);
|
||
}
|
||
if (errorCorrection !== 'M') {
|
||
params.set('errorCorrection', errorCorrection);
|
||
}
|
||
if (fgColor !== '#000000') {
|
||
params.set('foreground', fgColor);
|
||
}
|
||
if (bgColor !== '#ffffff') {
|
||
params.set('background', bgColor);
|
||
}
|
||
|
||
const url = window.location.origin + window.location.pathname + '?' + params.toString();
|
||
navigator.clipboard.writeText(url).then(function() {
|
||
const original = shareBtn.textContent;
|
||
shareBtn.textContent = 'Link kopiert!';
|
||
setTimeout(function() { shareBtn.textContent = original; }, 2000);
|
||
}, function() {
|
||
alert('Konnte Link nicht kopieren.');
|
||
});
|
||
});
|
||
});
|