1class W360_Form { 2 constructor(form, options = {}) { 3 this.form = form; 4 this.statusEl = form.querySelector('.form-status') || this._createStatusElement(); 5 this.appId = form.dataset.appId; 6 this.endpoint = `https://next360-api-dev.agencewebcom.com/plugin/ContactForm/${this.appId}`; 7 this.endpointToken = `https://next360-api-dev.agencewebcom.com/plugin/ContactForm/token`; 8 this.recaptchaKey = '6LdvphkrAAAAAD_tKtpf42rJzYo1b-WHEvURtLWz'; 9 this.to = form.dataset.to || "email"; 10 this.subject = form.dataset.subject || 'Message du site'; 11 this.customLoader = options.customLoader || null; 12 this.template = form.dataset.template || 'default'; 13 this.message = { 14 "success": `â ${form.dataset.success || 'Message envoyé avec succès !'}`, 15 "error": `â ${form.dataset.error || 'Erreur lors de l\'envoi du message.'}` 16 } 17 this.callback = options.callback || (() => {}); 18 19 this._bindSubmit(); 20 } 21 22 _createStatusElement() { 23 const div = document.createElement('div'); 24 div.className = 'form-status'; 25 this.form.appendChild(div); 26 return div; 27 } 28 29 _bindSubmit() { 30 this.form.addEventListener('submit', async (e) => { 31 e.preventDefault(); 32 33 if (this.isLoading) return; 34 35 const fields = this._collectFields(); 36 const files = this._collectFiles(); 37 await this._submit(fields, files); 38 }); 39 } 40 41 _collectFields() { 42 const elements = this.form.querySelectorAll('input, textarea, select'); 43 const fields = {}; 44 elements.forEach(el => { 45 if ((el.type === 'radio' || el.type === 'checkbox') && !el.checked) return; 46 if (el.type === 'file') return; 47 if (el.name) fields[el.name] = el.value; 48 }); 49 return fields; 50 } 51 52 _collectFiles() { 53 const elements = this.form.querySelectorAll('input[type="file"]'); 54 const files = []; 55 56 for (const el of elements) { 57 for (const file of el.files) { 58 files.push(file); 59 } 60 } 61 62 return files; 63 } 64 65 async _loadCaptchaToken() { 66 const res = await fetch(this.endpointToken); 67 const { token } = await res.json(); 68 69 return token 70 } 71 72 _showLoader() { 73 this.isLoading = true; 74 75 if (this.customLoader) { 76 this.customLoader.style.display = 'block'; 77 } else { 78 const btn = this.form.querySelector('button[type="submit"]'); 79 if (btn) { 80 btn.disabled = true; 81 btn.classList.add('loading'); 82 83 if (!this._spinnerEl) { 84 const spinner = document.createElement('div'); 85 spinner.className = 'form-spinner'; 86 spinner.innerHTML = ` 87 <div style=" 88 width: 24px; 89 height: 24px; 90 border: 3px solid #ccc; 91 border-top-color: #333; 92 border-radius: 50%; 93 animation: spin 1s linear infinite; 94 margin: 10px auto; 95 "></div> 96 <style> 97 @keyframes spin { 98 to { transform: rotate(360deg); } 99 } 100 </style> 101 `; 102 btn.after(spinner); 103 this._spinnerEl = spinner; 104 } 105 } 106 } 107 } 108 109 _hideLoader() { 110 this.isLoading = false; 111 112 if (this.customLoader) { 113 this.customLoader.style.display = 'none'; 114 } else { 115 const btn = this.form.querySelector('button[type="submit"]'); 116 if (btn) { 117 btn.disabled = false; 118 btn.classList.remove('loading'); 119 } 120 121 if (this._spinnerEl) { 122 this._spinnerEl.remove(); 123 this._spinnerEl = null; 124 } 125 } 126 } 127 128 async _submit(fields, files) { 129 try { 130 this._showLoader(); 131 132 // multipart/form-data : les fichiers ne sont pas comptes par la limite 133 // ModSecurity "no files data" (1 Mo), contrairement au base64 en JSON. 134 const formData = new FormData(); 135 formData.append('captchaToken', await this._loadCaptchaToken()); 136 formData.append('fields', JSON.stringify(fields)); 137 formData.append('to', this.to); 138 formData.append('subject', this.subject); 139 formData.append('template', this.template); 140 files.forEach(file => formData.append('attachments', file, file.name)); 141 142 // Pas de Content-Type manuel : le navigateur doit poser le boundary. 143 const res = await fetch(this.endpoint, { 144 method: 'POST', 145 body: formData 146 }); 147 148 // Une erreur bloquee en amont (413 ModSecurity, page d'erreur d'un proxy) 149 // renvoie du HTML, pas du JSON. res.json() leverait alors 150 // "Unexpected token '<'" et ce message remplacerait la vraie cause a 151 // l'ecran. On lit en texte, puis on tente le JSON. 152 const raw = await res.text(); 153 let data = {}; 154
155 try { 156 data = raw ? JSON.parse(raw) : {}; 157 } catch (err) { 158 console.error(`W360_Form: reponse non-JSON (HTTP ${res.status})`, raw.slice(0, 500)); 159 } 160 161 this.statusEl.style.display = 'block'; 162 163 if (res.ok) { 164 this.statusEl.innerText = this.message.success; 165 this.statusEl.style.color = 'green'; 166 this.form.reset(); 167 this.callback(); 168 } else { 169 // this.message.error porte deja son â : ne pas en rajouter un. 170 this.statusEl.innerText = data.message ? `â ${data.message}` : this.message.error; 171 this.statusEl.style.color = 'red'; 172 } 173 } catch (err) { 174 this.statusEl.innerText = `â Erreur : ${err.message}`; 175 this.statusEl.style.color = 'red'; 176 } finally { 177 this._hideLoader(); 178 } 179 } 180}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.