You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

398 lines
13 KiB

/**
* Kontaktní formulář - JavaScript (script.js)
* ==============================================
* Klientská validace formuláře a UX vylepšení.
*
* DŮLEŽITÉ: Klientská validace (JS) je jen pro pohodlí uživatele!
* Serverová validace (PHP) je nezbytná pro bezpečnost.
* Uživatel může JS vypnout nebo obejít - PHP validaci obejít nemůže.
*
* Obsah:
* 1. Inicializace
* 2. Validace v reálném čase (při psaní)
* 3. Validace při odeslání
* 4. Počítadlo znaků
* 5. Vizuální vylepšení
*/
'use strict';
// ============================================================
// KONFIGURACE PRAVIDEL VALIDACE
// ============================================================
/**
* Objekt s validačními pravidly pro každé pole.
* Každé pole má funkci validate() která vrátí string s chybou nebo null.
*/
const validators = {
firstName: {
validate(value) {
if (!value.trim()) return 'Jméno je povinné.';
if (value.trim().length < 2) return 'Jméno musí mít alespoň 2 znaky.';
if (value.trim().length > 50) return 'Jméno nesmí být delší než 50 znaků.';
return null; // null = validní
}
},
lastName: {
validate(value) {
if (!value.trim()) return 'Příjmení je povinné.';
if (value.trim().length < 2) return 'Příjmení musí mít alespoň 2 znaky.';
if (value.trim().length > 50) return 'Příjmení nesmí být delší než 50 znaků.';
return null;
}
},
email: {
validate(value) {
if (!value.trim()) return 'Email je povinný.';
// Regulární výraz pro validaci emailu
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
if (!emailRegex.test(value.trim())) return 'Zadejte platnou emailovou adresu.';
return null;
}
},
phone: {
validate(value) {
if (!value.trim()) return null; // Telefon je volitelný
// Povolené formáty: +420777123456, 00420777123456, 777 123 456
const phoneRegex = /^(\+|00)?[0-9\s\-]{9,15}$/;
if (!phoneRegex.test(value.trim())) return 'Zadejte platné telefonní číslo.';
return null;
}
},
subject: {
validate(value) {
if (!value) return 'Vyberte předmět zprávy.';
if (value.length > 100) return 'Předmět nesmí být delší než 100 znaků.';
return null;
}
},
message: {
validate(value) {
if (!value.trim()) return 'Zpráva je povinná.';
if (value.trim().length < 10) return 'Zpráva musí mít alespoň 10 znaků.';
if (value.trim().length > 2000) return 'Zpráva nesmí být delší než 2000 znaků.';
return null;
}
},
agreement: {
validate(checked) {
if (!checked) return 'Musíte souhlasit se zpracováním osobních údajů.';
return null;
}
}
};
// ============================================================
// INICIALIZACE
// ============================================================
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('contactForm');
if (!form) return; // Formulář neexistuje (např. na stránce s úspěchem)
// Inicializace všech funkcí
initRealtimeValidation(); // Validace při psaní
initFormSubmit(); // Validace při odeslání
initCharCounter(); // Počítadlo znaků pro textarea
initFloatingLabels(); // Vizuální efekty
});
// ============================================================
// 1. VALIDACE V REÁLNÉM ČASE
// ============================================================
/**
* Přidá posluchače na všechna pole - validuje při odchodu z pole (blur).
* Uživatel dostane zpětnou vazbu ihned po vyplnění každého pole.
*/
function initRealtimeValidation() {
// Textová a email pole - validace při ztrátě focusu (blur)
['firstName', 'lastName', 'email', 'phone', 'subject', 'message'].forEach(function (fieldName) {
const field = document.getElementById(fieldName);
if (!field) return;
// blur = uživatel opustil pole (přesunul focus jinam)
field.addEventListener('blur', function () {
validateField(this);
});
// input = uživatel píše - pokud pole bylo označeno jako nevalidní,
// začneme znovu validovat při každém stiku klávesy (okamžitá oprava)
field.addEventListener('input', function () {
if (this.classList.contains('is-invalid')) {
validateField(this);
}
});
});
// Checkbox - validace při změně
const agreement = document.getElementById('agreement');
if (agreement) {
agreement.addEventListener('change', function () {
validateCheckbox(this);
});
}
}
/**
* Validuje jedno textové/email/select pole.
* Přidá nebo odebere CSS třídy is-valid / is-invalid.
*
* @param {HTMLElement} field - Pole formuláře
* @returns {boolean} - True = validní, False = nevalidní
*/
function validateField(field) {
const name = field.id;
const validator = validators[name];
if (!validator) return true; // Neznámé pole považujeme za validní
// Získání hodnoty
const value = field.value;
// Spuštění validační funkce
const error = validator.validate(value);
if (error) {
// Pole je NEVALIDNÍ
setFieldInvalid(field, error);
return false;
} else {
// Pole je VALIDNÍ
setFieldValid(field);
return true;
}
}
/**
* Validuje checkbox souhlas.
*
* @param {HTMLInputElement} checkbox - Checkbox element
* @returns {boolean}
*/
function validateCheckbox(checkbox) {
const error = validators.agreement.validate(checkbox.checked);
if (error) {
checkbox.classList.add('is-invalid');
// Vložíme nebo aktualizujeme chybovou zprávu
updateFeedback(checkbox, error);
return false;
} else {
checkbox.classList.remove('is-invalid');
checkbox.classList.add('is-valid'); // Zelená barva pro zaškrtnutý checkbox
clearFeedback(checkbox);
return true;
}
}
// ============================================================
// 2. VALIDACE PŘI ODESLÁNÍ
// ============================================================
/**
* Přidá posluchač na odeslání formuláře.
* Zkontroluje všechna pole a zobrazí souhrn chyb.
*/
function initFormSubmit() {
const form = document.getElementById('contactForm');
if (!form) return;
form.addEventListener('submit', function (e) {
// Zastavíme výchozí odeslání - nejprve validujeme JS
// Pokud je vše ok, necháme formulář odeslat na server (PHP validace)
let isFormValid = true;
// Validace všech textových polí
['firstName', 'lastName', 'email', 'phone', 'subject', 'message'].forEach(function (fieldName) {
const field = document.getElementById(fieldName);
if (field && !validateField(field)) {
isFormValid = false;
}
});
// Validace checkboxu
const agreement = document.getElementById('agreement');
if (agreement && !validateCheckbox(agreement)) {
isFormValid = false;
}
if (!isFormValid) {
// CHYBA: Zastavíme odeslání
e.preventDefault();
// Přidáme animaci třesení na chybná pole
document.querySelectorAll('.is-invalid').forEach(function (field) {
field.classList.add('shake');
setTimeout(function () {
field.classList.remove('shake');
}, 300);
});
// Scrollování na první chybné pole
const firstError = document.querySelector('.is-invalid');
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstError.focus();
}
} else {
// VŠE OK: Zobrazíme stav načítání na tlačítku
setButtonLoading(true);
// Formulář se odešle - PHP zpracuje dál
}
});
}
// ============================================================
// 3. POČÍTADLO ZNAKŮ
// ============================================================
/**
* Sleduje počet znaků v textarea a aktualizuje počítadlo.
* Mění barvu počítadla při přiblížení k limitu.
*/
function initCharCounter() {
const textarea = document.getElementById('message');
const counter = document.getElementById('charCount');
if (!textarea || !counter) return;
const maxLength = 2000; // Maximální počet znaků
const warnAt = 1800; // Varování při 1800 znacích (90%)
function updateCounter() {
const current = textarea.value.length;
counter.textContent = `${current}/${maxLength}`;
// Změna barvy podle počtu znaků
counter.classList.remove('near-limit', 'at-limit');
if (current >= maxLength) {
counter.classList.add('at-limit'); // Červená - dosažen limit
} else if (current >= warnAt) {
counter.classList.add('near-limit'); // Oranžová - blíží se limit
}
}
// Okamžitá aktualizace při psaní
textarea.addEventListener('input', updateCounter);
// Inicializace (pro případ, že textarea má předvyplněný text z PHP)
updateCounter();
}
// ============================================================
// 4. VIZUÁLNÍ VYLEPŠENÍ
// ============================================================
/**
* Inicializuje efekty pro vstupní pole:
* - Zvýraznění řádku při focusu
* - Animace ikonek
*/
function initFloatingLabels() {
// Přidání efektu zaměření na skupiny s ikonou
document.querySelectorAll('.input-group').forEach(function (group) {
const input = group.querySelector('.form-control, .form-select');
if (!input) return;
// Focus = zaměření na pole
input.addEventListener('focus', function () {
group.classList.add('focused');
});
// Blur = ztráta focusu
input.addEventListener('blur', function () {
group.classList.remove('focused');
});
});
}
// ============================================================
// POMOCNÉ FUNKCE
// ============================================================
/**
* Označí pole jako NEVALIDNÍ a zobrazí chybovou zprávu.
*
* @param {HTMLElement} field - Vstupní pole
* @param {string} message - Chybová zpráva
*/
function setFieldInvalid(field, message) {
field.classList.remove('is-valid');
field.classList.add('is-invalid');
updateFeedback(field, message);
}
/**
* Označí pole jako VALIDNÍ a skryje chybovou zprávu.
*
* @param {HTMLElement} field - Vstupní pole
*/
function setFieldValid(field) {
field.classList.remove('is-invalid');
field.classList.add('is-valid');
clearFeedback(field);
}
/**
* Aktualizuje nebo vytvoří element s chybovou zprávou pod polem.
*
* @param {HTMLElement} field - Vstupní pole
* @param {string} message - Text chybové zprávy
*/
function updateFeedback(field, message) {
// Hledáme existující zprávu
let feedback = field.parentElement.querySelector('.invalid-feedback');
if (!feedback) {
// Vytvoříme nový element pro zprávu
feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
field.parentElement.appendChild(feedback);
}
feedback.textContent = message;
}
/**
* Odstraní chybovou zprávu pod polem.
*
* @param {HTMLElement} field - Vstupní pole
*/
function clearFeedback(field) {
const feedback = field.parentElement.querySelector('.invalid-feedback');
if (feedback) {
feedback.remove(); // Smazání z DOM
}
}
/**
* Nastaví stav načítání tlačítka Odeslat.
*
* @param {boolean} loading - True = načítání, False = normální stav
*/
function setButtonLoading(loading) {
const btn = document.getElementById('submitBtn');
if (!btn) return;
if (loading) {
btn.disabled = true;
btn.classList.add('loading');
// Spinner (Bootstrap) + text
btn.innerHTML = `
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
Odesílám...
`;
} else {
btn.disabled = false;
btn.classList.remove('loading');
btn.innerHTML = '<i class="bi bi-send me-2"></i>Odeslat zprávu';
}
}