PageSourceSearch

https://www.hotelhabituel.fr/res/js/W360_Form.js

js hotelhabituel.fr collected 2026-10-08 22:36:29 UTC 5,479 bytes, 180 lines download raw bytes

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.