class FormRenderer { constructor(options) { this.container = typeof options.container === 'string' ? document.querySelector(options.container) : options.container; this.formDefinition = options.formDefinition; this.onSubmit = options.onSubmit; this.onChange = options.onChange; this.values = {}; // form değerlerini tutacak obje // reCAPTCHA v3 ayarları this.recaptcha = { siteKey: options.recaptchaSiteKey || '', // reCAPTCHA site key enabled: options.enableRecaptcha !== false, // varsayılan olarak aktif action: options.recaptchaAction || 'form_submit' // varsayılan action }; this.init(); } init() { // Container kontrolü 31.03.2026-11:06:1 ıüğişçöIÜĞİŞÇÖ if (!this.container) throw new Error('Form container bulunamadı'); // reCAPTCHA script'ini yükle if (this.recaptcha.enabled && this.recaptcha.siteKey) { this.loadRecaptchaScript(); } // Form oluştur this.form = document.createElement('form'); this.form.className = 'needs-validation'; this.form.noValidate = true; this.form.method = 'POST'; this.container.appendChild(this.form); // Form elemanlarını render et this.renderFormFields(); // Submit butonu ekle this.addSubmitButton(); // Form events this.bindEvents(); } // reCAPTCHA script'ini dinamik olarak yükle loadRecaptchaScript() { if (document.querySelector('script[src*="recaptcha"]')) { return; // Zaten yüklenmişse tekrar yükleme } const script = document.createElement('script'); script.src = `https://www.google.com/recaptcha/api.js?render=${this.recaptcha.siteKey}`; script.async = true; script.defer = true; document.head.appendChild(script); } // reCAPTCHA token'ı al async getRecaptchaToken(action = null) { if (!this.recaptcha.enabled || !this.recaptcha.siteKey) { return null; } try { // grecaptcha'nın yüklenmesini bekle await this.waitForRecaptcha(); const token = await grecaptcha.execute(this.recaptcha.siteKey, { action: action || this.recaptcha.action }); return token; } catch (error) { console.error('reCAPTCHA token alınırken hata:', error); return null; } } // grecaptcha'nın yüklenmesini bekle waitForRecaptcha(timeout = 10000) { return new Promise((resolve, reject) => { const startTime = Date.now(); const checkRecaptcha = () => { if (typeof grecaptcha !== 'undefined' && grecaptcha.execute) { resolve(); } else if (Date.now() - startTime > timeout) { reject(new Error('reCAPTCHA yüklenemedi')); } else { setTimeout(checkRecaptcha, 100); } }; checkRecaptcha(); }); } renderFormFields() { const gridContainer = document.createElement('div'); gridContainer.className = 'row'; this.form.appendChild(gridContainer); this.formDefinition.fields.forEach(field => { const fieldContainer = document.createElement('div'); fieldContainer.className = (field.containerClass || 'col-12') + ' mb-4' + ' form-item-box'; // Field HTML'ini oluştur const fieldHtml = this.renderField(field); if (fieldHtml) { fieldContainer.innerHTML = fieldHtml; gridContainer.appendChild(fieldContainer); // HTML eklendikten sonra davranışları uygula this.applyFieldBehaviors(field, fieldContainer); } }); } renderField(field) { const labelPosition = field.labelPosition || 'top'; const labelHtml = field.label && labelPosition !== 'none' ? `` : ''; let inputHtml = ''; switch (field.type) { case 'text': let inputType = 'text'; if (field.maskType === 'email') inputType = 'email'; if (field.maskType === 'number') inputType = 'text'; // IMask ile text daha iyi çalışır inputHtml = ` `; break; case 'textarea': inputHtml = ` `; break; case 'select': const options = field.options.split('\n').map(line => { const [text, value] = line.split('|'); return ``; }).join(''); inputHtml = ` `; break; case 'checkboxGroup': inputHtml = `
`; field.options.split('\n').forEach((line, index) => { const [text, value] = line.split('|'); inputHtml += `
`; }); inputHtml += `
`; break; case 'radioGroup': inputHtml = `
`; field.options.split('\n').forEach((line, index) => { const [text, value] = line.split('|'); inputHtml += `
`; }); inputHtml += `
`; break; case 'heading': return `

${this.escapeHtml(field.text)}

`; case 'paragraph': return `

${this.escapeHtml(field.text)}

`; case 'fileUpload': inputHtml = ` `; break; case 'hierarchicalSelect': // Hierarchical select için özel konteyner inputHtml = `
`; break; } if (labelPosition === 'left') { return `
${labelHtml}
${inputHtml}
`; } return `
${labelPosition === 'top' ? labelHtml : ''} ${inputHtml}
`; } applyFieldBehaviors(field, container) { // Mask uygula if (field.type === 'text' && field.maskType) { const input = container.querySelector('input'); if (input && typeof FIELD_TYPES !== 'undefined' && FIELD_TYPES.text.masks[field.maskType]) { const mask = IMask(input, FIELD_TYPES.text.masks[field.maskType]); input.dataset.masked = "true"; input._mask = mask; // Referans sakla } } // Events if (field.events) { try { const events = new Function(`return ${field.events}`)(); // Container içindeki tüm form elemanlarını bul const formElements = container.querySelectorAll('input, select, textarea'); formElements.forEach(element => { // Her event'ı her elemana ekle Object.entries(events).forEach(([eventName, handler]) => { // on prefix'ini kaldır eventName = eventName.toLowerCase().replace('on', ''); element.addEventListener(eventName, handler); }); }); } catch (error) { console.error('Event parsing error:', error); } } // Hierarchical select için özel işlem if (field.type === 'hierarchicalSelect') { const selectContainer = document.getElementById(`${field.id}_container`); if (selectContainer) { if (field.sourceType === 'json') { try { const jsonData = JSON.parse(field.jsonData); this.initHierarchicalSelect(selectContainer, { data: jsonData, field: field }); } catch (error) { console.error('JSON parse error:', error); } } else if (field.sourceType === 'function') { this.initHierarchicalSelect(selectContainer, { dataFunction: new Function('level', 'selectedValue', field.dataFunction), field: field }); } } } } addSubmitButton() { const submitContainer = document.createElement('div'); submitContainer.className = 'col-12 mt-3 text-center'; submitContainer.innerHTML = ` Gönder `; this.form.appendChild(submitContainer); } bindEvents() { // Form submit event'i this.form.addEventListener('submit', async (e) => { e.preventDefault(); await this.handleSubmit(); }); // Submit butonu click event'i (çünkü elementi kullanılıyor) const submitBtn = this.form.querySelector('.sayfa-btn3'); if (submitBtn) { submitBtn.addEventListener('click', async (e) => { e.preventDefault(); await this.handleSubmit(); }); } } async handleSubmit() { // Form validasyonu if (!this.form.checkValidity()) { this.form.classList.add('was-validated'); return; } // Maske doluluk ve format kontrolü const maskInputs = this.form.querySelectorAll('input[data-masked="true"]'); for (const input of maskInputs) { const mask = input._mask; if (mask) { const hasValue = mask.value.length > 0; const isRequired = input.hasAttribute('required'); // Eğer zorunluysa veya veri girilmişse format tam olmalı if ((isRequired || hasValue) && !mask.masked.isComplete) { input.setCustomValidity('Lütfen istenen formatta tam olarak doldurun'); this.form.classList.add('was-validated'); input.reportValidity(); return; } else { input.setCustomValidity(''); } } } try { // reCAPTCHA token'ı al const recaptchaToken = await this.getRecaptchaToken('form_submit'); if (this.onSubmit) { const formData = this.getFormData(); // reCAPTCHA token'ı form data'sına ekle if (recaptchaToken) { formData.recaptcha_token = recaptchaToken; } const result = await this.onSubmit(formData); // Eğer kullanıcı fonksiyondan özellikle false dönerse sıfırlama yapma // (Hata durumunda alert verip return false yapan senaryolar için) if (result !== false) { this.resetForm(); } } } catch (error) { console.error('Form submit error:', error); alert('Form gönderilirken hata oluştu.'); } } resetForm() { // Standart HTML form reset this.form.reset(); // Validasyon sınıflarını temizle this.form.classList.remove('was-validated'); // Maskeleri temizle const maskInputs = this.form.querySelectorAll('input[data-masked="true"]'); maskInputs.forEach(input => { if (input._mask) { input._mask.value = ''; input.setCustomValidity(''); } }); // Hierarchical select'leri temizle (ilk seviye hariç sil, ilkini seçiniz yap) const hContainers = this.form.querySelectorAll('.hierarchical-select-container'); hContainers.forEach(container => { const selects = container.querySelectorAll('select'); selects.forEach((select, index) => { if (index === 0) { select.value = ''; } else { select.remove(); } }); // Hidden input'u temizle const hiddenInput = container.querySelector('input[type="text"]'); if (hiddenInput) hiddenInput.value = ''; }); // Checkbox ve Radio gruplarını temizle (zaten form.reset() yapar ama emin olalım) const customGroups = this.form.querySelectorAll('.checkbox-group, .radio-group'); customGroups.forEach(group => { const inputs = group.querySelectorAll('input'); inputs.forEach(input => { input.checked = false; }); }); } getFormData() { const formData = new FormData(this.form); const data = {}; for (let [key, value] of formData.entries()) { if (data[key]) { if (!Array.isArray(data[key])) { data[key] = [data[key]]; } data[key].push(value); } else { data[key] = value; } } return data; } getFieldValue(field) { switch (field.type) { case 'checkbox': if (field.name in this.values) { return [...this.values[field.name]]; } return Array.from( this.form.querySelectorAll(`[name="${field.name}"]:checked`) ).map(cb => cb.value); default: return field.value; } } initHierarchicalSelect(container, options) { const { field, data, dataFunction } = options; let currentLevel = 0; const selectedValues = []; if (field.inline) { container.className += ' d-flex align-items-center flex-wrap'; } const createSelect = async (level, items) => { const select = document.createElement('select'); select.className = 'form-select'; // Inline durumunda margin ayarı if (field.inline) { select.className += ' me-2 d-inline-block'; select.style.width = 'auto'; // veya sabit bir genişlik: select.style.width = '200px'; } else { select.className += ' mb-2'; } select.name = `${field.id}_level_${level}`; select.dataset.level = level; select.innerHTML = ` ${items.map(item => ` `).join('')} `; select.addEventListener('change', async function () { const selectedOption = this.selectedOptions[0]; const hasChildren = selectedOption.dataset.hasChildren === 'true'; const selectedValue = this.value; // Bu seviyeden sonraki tüm select'leri temizle removeSelectsAfterLevel(level); // Seçilen değeri kaydet selectedValues[level] = selectedValue; selectedValues.splice(level + 1); if (hasChildren && selectedValue) { let nextLevelItems; if (dataFunction) { nextLevelItems = await dataFunction(level + 1, selectedValue); } else { nextLevelItems = getItemsFromJson(data, selectedValue); } if (nextLevelItems?.length > 0) { await createSelect(level + 1, nextLevelItems); } } // Hidden input'u güncelle updateHiddenInput(); }); container.appendChild(select); }; const removeSelectsAfterLevel = (level) => { const selects = container.querySelectorAll('select'); selects.forEach(select => { if (parseInt(select.dataset.level) > level) { select.remove(); } }); }; const getItemsFromJson = (data, parentId = null) => { const createId = (parentId, text, index) => { // Parent ID varsa parent-index, yoksa sadece index return parentId ? `${parentId}-${index + 1}` : `${index + 1}`; }; if (!parentId) { return data.map((item, index) => ({ id: item.v || createId(null, item.n, index), text: item.n, hasChildren: !!item.alt })); } let items = []; const findItems = (arr, currentParentId = '') => { for (let i = 0; i < arr.length; i++) { const item = arr[i]; const itemId = item.v || createId(currentParentId, item.n, i); if (itemId === parentId && item.alt) { items = item.alt.map((child, childIndex) => ({ id: child.v || createId(itemId, child.n, childIndex), text: child.n, hasChildren: !!child.alt })); return true; } if (item.alt && findItems(item.alt, itemId)) return true; } return false; }; findItems(data); return items; } const updateHiddenInput = () => { let hiddenInput = container.querySelector(`input[name="${field.id}"]`); if (!hiddenInput) { hiddenInput = document.createElement('input'); hiddenInput.type = 'text'; // validasyon için text, stil ile gizlenecek hiddenInput.style.position = 'absolute'; hiddenInput.style.width = '0'; hiddenInput.style.height = '0'; hiddenInput.style.opacity = '0'; hiddenInput.style.pointerEvents = 'none'; hiddenInput.name = field.id; if (field.required) hiddenInput.required = true; container.appendChild(hiddenInput); } // JSON verisinde "v" key'i var mı kontrol et const hasVKey = data ? checkForVKey(data) : false; // Seçili değerleri topla const selectedTexts = selectedValues.map((value, index) => { if (!value) return null; const select = container.querySelector(`select[data-level="${index}"]`); const option = select?.querySelector(`option[value="${value}"]`); return hasVKey ? value : option?.dataset.text; }).filter(Boolean); hiddenInput.value = selectedTexts.join(','); }; const checkForVKey = (data) => { // İlk elemanı kontrol et if (data && data.length > 0) { return 'v' in data[0]; } return false; }; // İlk select'i oluştur if (dataFunction) { dataFunction(0, null) .then(items => createSelect(0, items)) .catch(error => console.error('Data function error:', error)); } else if (data) { const items = getItemsFromJson(data); createSelect(0, items); } } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async startFileUpload(uploadUrl, completeEvent) { // Submit butonunu disable et const submitBtn = this.form.querySelector('.sayfa-btn3'); const originalSubmitState = submitBtn ? submitBtn.style.pointerEvents : ''; const originalSubmitText = submitBtn ? submitBtn.textContent : ''; try { if (submitBtn) { submitBtn.style.pointerEvents = 'none'; submitBtn.style.opacity = '0.6'; submitBtn.textContent = 'Dosyalar yükleniyor...'; } // Dosya upload'u için reCAPTCHA token'ı al const recaptchaToken = await this.getRecaptchaToken('file_upload'); // Tüm file input'ları bul const fileInputs = this.form.querySelectorAll('input[type="file"]'); if (fileInputs.length === 0) { console.log('Form içinde dosya input\'u bulunamadı'); return; } // Her input için upload promise'lerini oluştur const uploadPromises = []; for (const input of fileInputs) { const files = input.files; // Required kontrolü if (input.hasAttribute('required') && files.length === 0) { throw new Error(`${this.getFieldLabel(input)} alanı zorunludur`); } // Her dosya için ayrı promise oluştur for (let i = 0; i < files.length; i++) { const file = files[i]; uploadPromises.push(this.uploadSingleFile(uploadUrl, input, file, completeEvent, recaptchaToken)); } } // Tüm upload'ları paralel olarak başlat const results = await Promise.allSettled(uploadPromises); // Sonuçları kontrol et const failures = results.filter(result => result.status === 'rejected'); if (failures.length > 0) { console.error('Bazı dosyalar yüklenemedi:', failures); } console.log(`${results.length} dosya upload işlemi tamamlandı. Başarılı: ${results.length - failures.length}, Başarısız: ${failures.length}`); } catch (error) { console.error('File upload error:', error); alert('Dosya yükleme hatası: ' + error.message); throw error; } finally { // Submit butonunu eski haline getir const submitBtn = this.form.querySelector('.sayfa-btn3'); if (submitBtn) { submitBtn.style.pointerEvents = originalSubmitState; submitBtn.style.opacity = ''; submitBtn.textContent = originalSubmitText; } } } async uploadSingleFile(uploadUrl, inputElement, file, completeEvent, recaptchaToken = null) { try { // Dosya validasyonu this.validateFile(inputElement, file); // Binary dosya gönderimi için çözüm seçenekleri: // SEÇENEK 1: Header'da reCAPTCHA token'ı gönder (önerilen) const headers = { 'Content-Type': file.type || 'application/octet-stream' }; if (recaptchaToken) { headers['X-Recaptcha-Token'] = recaptchaToken; } const response = await fetch(uploadUrl, { method: 'POST', body: file, // Binary olarak gönder headers: headers }); /* SEÇENEK 2: Query parameter olarak gönder const url = new URL(uploadUrl); if (recaptchaToken) { url.searchParams.append('recaptcha_token', recaptchaToken); } const response = await fetch(url.toString(), { method: 'POST', body: file, headers: { 'Content-Type': file.type || 'application/octet-stream' } }); */ /* SEÇENEK 3: FormData kullan (binary format'ı korumaz ama reCAPTCHA kolay entegre olur) const formData = new FormData(); formData.append('file', file); if (recaptchaToken) { formData.append('recaptcha_token', recaptchaToken); } const response = await fetch(uploadUrl, { method: 'POST', body: formData }); */ if (!response.ok) { const errorText = await response.text(); throw new Error(`Upload failed: ${response.status} - ${errorText}`); } const result = await response.json(); // Complete event'i tetikle if (completeEvent && typeof completeEvent === 'function') { await completeEvent(inputElement, result); } return result; } catch (error) { console.error(`File upload failed for ${file.name}:`, error); // Error event'i tetikle (opsiyonel) if (completeEvent && typeof completeEvent === 'function') { await completeEvent(inputElement, { error: error.message, fileName: file.name }); } throw error; } } validateFile(inputElement, file) { // Dosya boyutu kontrolü (5MB = 5 * 1024 * 1024 bytes) const maxSize = 5 * 1024 * 1024; if (file.size > maxSize) { throw new Error(`${file.name} dosyası çok büyük. Maximum 5MB olmalıdır.`); } // Boş dosya kontrolü if (file.size === 0) { throw new Error(`${file.name} dosyası boş.`); } // Accept attribute kontrolü const acceptAttr = inputElement.getAttribute('accept'); if (acceptAttr) { const acceptedTypes = acceptAttr.split(',').map(type => type.trim().toLowerCase()); const fileType = file.type.toLowerCase(); const fileExtension = '.' + file.name.split('.').pop().toLowerCase(); const isAccepted = acceptedTypes.some(acceptType => { if (acceptType.startsWith('.')) { // Dosya uzantısı kontrolü return acceptType === fileExtension; } else if (acceptType.includes('*')) { // MIME type wildcard kontrolü (image/*, application/*) const baseType = acceptType.split('/')[0]; return fileType.startsWith(baseType + '/'); } else { // Tam MIME type kontrolü return acceptType === fileType; } }); if (!isAccepted) { throw new Error(`${file.name} dosya tipi desteklenmiyor. İzin verilen tipler: ${acceptAttr}`); } } } getFieldLabel(inputElement) { // Input'un label'ını bulmaya çalış const fieldId = inputElement.id; if (fieldId) { const label = this.form.querySelector(`label[for="${fieldId}"]`); if (label) { return label.textContent.trim(); } } return inputElement.name || 'Dosya'; } } // Kullanım örneği: /* const formRenderer = new FormRenderer({ container: '#form-container', formDefinition: formConfig, recaptchaSiteKey: 'YOUR_RECAPTCHA_SITE_KEY', // reCAPTCHA site key enableRecaptcha: true, // reCAPTCHA'yı aktif et recaptchaAction: 'form_submit', // varsayılan action onSubmit: async (formData) => { // formData.recaptcha_token içerisinde reCAPTCHA token'ı bulunur console.log('Form data with reCAPTCHA:', formData); // Backend'e gönder const response = await fetch('/api/submit-form', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); } }); // Dosya upload başlat await formRenderer.startFileUpload('/api/upload', (inputElement, result) => { console.log('Upload completed:', result); }); */