/** * 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 = ` Odesílám... `; } else { btn.disabled = false; btn.classList.remove('loading'); btn.innerHTML = 'Odeslat zprávu'; } }