PageSourceSearch

https://www.7bar.pl/assets/components/formit/js/web/formit.js

js 7bar.pl collected 2026-10-08 09:25:29 UTC 9,924 bytes, 296 lines download raw bytes

1/**
2 * FormIt
3 *
4 * Client-side functionality for FormIt forms.
5 *
6 * @package formit
7 */
8(function (window, document) {
9    'use strict';
10
11    /**
12     * Global config. Set by PHP via Object.assign(FormIt, {...}).
13     */
14    window.FormIt = {
15        actionUrl: '/assets/components/formit/action.php',
16        recaptchaDefaultAction: 'submit'
17    };
18
19    /**
20     * @param {HTMLFormElement} form
21     * @constructor
22     */
23    function FormItForm(form) {
24        if (!(form instanceof HTMLFormElement)) {
25            console.error('[FormIt] First argument must be a form element.');
26            return;
27        }
28
29        this.form = form;
30        this._lastSubmitter = null;
31        this.form.addEventListener('click', this._onClickSubmit.bind(this));
32        this.form.addEventListener('submit', this._onSubmit.bind(this));
33    }
34
35    /**
36     * Track which submit button was clicked.
37     * @param {MouseEvent} e
38     * @private
39     */
40    FormItForm.prototype._onClickSubmit = function (e) {
41        var btn = e.target.closest('[type="submit"]');
42        this._lastSubmitter = btn && btn.name ? btn : null;
43    };
44
45    /**
46     * @param {SubmitEvent} e
47     * @private
48     */
49    FormItForm.prototype._onSubmit = function (e) {
50        this.ajaxToken = this.form.getAttribute('data-formit-ajax-token');
51
52        if (!this.ajaxToken && !this.form.querySelector('[name="g-recaptcha-response"]')) {
53            return;
54        }
55
56        e.preventDefault();
57
58        // beforesubmit event (cancelable)
59        var beforeEvent = this._dispatch('formit:beforesubmit', { form: this.form }, true);
60        if (beforeEvent.defaultPrevented) return;
61
62        this.submitter = e.submitter || this._lastSubmitter || this.form.querySelector('[type="submit"]');
63        var formData  = new FormData(this.form);
64
65        if (this.submitter && this.submitter.name) {
66            formData.append(this.submitter.name, this.submitter.value || '');
67        }
68
69        var self = this;
70
71        this._clearMessages();
72        this._setLoading(true);
73
74        // _resolveRecaptcha has its own guards — resolves immediately if not configured
75        this._resolveRecaptcha(formData)
76            .then(function () {
77                self._submit(formData);
78            })
79            .catch(function (error) {
80                console.error('[FormIt] reCAPTCHA failed:', error);
81                self._setLoading(false);
82                self._showMessage('[data-formit-error-message]', error.message || 'Request failed');
83                self._dispatch('formit:error', { data: null, error: error });
84            });
85    };
86
87    /**
88     * Execute reCAPTCHA v3 and fill both formData and the response field.
89     * Resolves immediately when reCAPTCHA is not configured.
90     *
91     * @param {FormData} formData
92     * @returns {Promise<void>}
93     * @private
94     */
95    FormItForm.prototype._resolveRecaptcha = function (formData) {
96        var recaptchaResponseField = this.form.querySelector('[name="g-recaptcha-response"]');
97
98        if (!recaptchaResponseField || typeof grecaptcha === 'undefined') {
99            return Promise.resolve();
100        }
101
102        var actionField     = this.form.querySelector('[name="g-recaptcha-action"]');
103        var recaptchaAction = (actionField && actionField.value) || FormIt.recaptchaDefaultAction;
104
105        return new Promise(function (resolve, reject) {
106            grecaptcha.ready(function () {
107                grecaptcha.execute(FormIt.recaptchaSiteKey, { action: recaptchaAction })
108                    .then(function (token) {
109                        recaptchaResponseField.value = token;
110                        formData.set('g-recaptcha-response', token);
111                        resolve();
112                    })
113                    .catch(reject);
114            });
115        });
116    };
117
118    /**
119     * Submit the form: native submit or AJAX fetch depending on ajaxToken.
120     *
121     * @param {FormData} formData
122     * @private
123     */
124    FormItForm.prototype._injectSubmitter = function () {
125        if (!this.submitter || !this.submitter.name) return;
126        var existing = this.form.querySelector('[name="' + this.submitter.name + '"]');
127        if (existing && existing !== this.submitter) return;
128        var hidden = document.createElement('input');
129        hidden.type = 'hidden';
130        hidden.name = this.submitter.name;
131        hidden.value = this.submitter.value || '';
132        this.form.appendChild(hidden);
133    };
134
135    FormItForm.prototype._submit = function (formData) {
136        if (!this.ajaxToken) {
137            this._injectSubmitter();
138            this.form.submit();
139            return;
140        }
141
142        var self = this;
143        var form = this.form;
144
145        fetch(FormIt.actionUrl, {
146            method: 'POST',
147            headers: {
148                'X-Requested-With': 'XMLHttpRequest',
149                'X-FormIt-Token': this.ajaxToken
150            },
151            body: formData
152        })
153        .then(function (response) {
154            if (!response.ok) {
155                return response.text().then(function (text) {
156                    throw new Error('[FormIt] Server returned HTTP ' + response.status + ': ' + text.substring(0, 200));
157                });
158            }
159
160            var contentType = response.headers.get('Content-Type') || '';
161            if (contentType.indexOf('application/json') === -1) {
162                return response.text().then(function (text) {
163                    throw new Error('[FormIt] Expected JSON but received ' + contentType + ': ' + text.substring(0, 200));
164                });
165            }
166
167            return response.json();
168        })
169        .then(function (data) {
170            self._handleResponse(data);
171        })
172        .catch(function (error) {
173            // AJAX failed — fall back to native submit so the request still goes through.
174            // reCAPTCHA token is already set in the response field value.
175            console.warn('[FormIt] AJAX failed, falling back to native submit:', error);
176            self._injectSubmitter();
177            form.submit();
178        })
179        .finally(function () {
180            self._setLoading(false);
181            self._dispatch('formit:complete', {});
182        });
183    };
184
185    /**
186     * @param {Object} data - Response from processForm()
187     * @private
188     */
189    FormItForm.prototype._handleResponse = function (data) {
190        var placeholders   = data.placeholders || {};
191        var hasFieldErrors = false;
192
193        for (var key in placeholders) {
194            if (placeholders.hasOwnProperty(key) && key.indexOf('error.') === 0) {
195                hasFieldErrors = true;
196                var fieldName = key.substring(6);
197                var el = this.form.querySelector('[data-formit-error="' + fieldName + '"]');
198                if (el) el.innerHTML = placeholders[key];
199            }
200        }
201
202        this._showMessage('[data-formit-success-message]', placeholders.successMessage || '');
203        this._showMessage('[data-formit-validation-error-message]', placeholders.validation_error_message || '');
204        this._showMessage('[data-formit-error-message]', placeholders.error_message || '');
205
206        var isError = !data.success || hasFieldErrors || placeholders.validation_error || placeholders.error_message;
207
208        if (isError) {
209            this._dispatch('formit:error', { data: data });
210        } else {
211            this._dispatch('formit:success', { data: data });
212
213            this.form.reset();
214
215            if (data.redirect_url) {
216                var redirectEvent = this._dispatch('formit:redirect', { url: data.redirect_url }, true);
217                if (redirectEvent.defaultPrevented) return;
218
219                window.location.href = data.redirect_url;
220            }
221        }
222    };
223
224    /**
225     * @param {string} selector
226     * @param {string} message
227     * @private
228     */
229    FormItForm.prototype._showMessage = function (selector, message) {
230        if (!message) return;
231        var el = this.form.querySelector(selector);
232        if (el) {
233            el.innerHTML = message;
234        } else {
235            var stripEl = document.createElement('div');
236            stripEl.innerHTML = message;
237            alert(stripEl.textContent || stripEl.innerText || message);
238        }
239    };
240
241    /**
242     * @private
243     */
244    FormItForm.prototype._clearMessages = function () {
245        this.form.querySelectorAll('[data-formit-error]').forEach(function (el) {
246            el.textContent = '';
247        });
248        this.form.querySelectorAll('[data-formit-success-message], [data-formit-validation-error-message], [data-formit-error-message]')
249            .forEach(function (el) {
250                el.textContent = '';
251            });
252    };
253
254    /**
255     * @param {boolean} loading
256     * @private
257     */
258    FormItForm.prototype._setLoading = function (loading) {
259        if (loading) {
260            this.form.classList.add('formit-loading');
261        } else {
262            this.form.classList.remove('formit-loading');
263        }
264
265        var buttons = this.form.querySelectorAll('[type="submit"]');
266        for (var i = 0; i < buttons.length; i++) {
267            buttons[i].disabled = loading;
268        }
269    };
270
271    /**
272     * @param {string} name
273     * @param {Object} detail
274     * @param {boolean} [cancelable]
275     * @returns {CustomEvent}
276     * @private
277     */
278    FormItForm.prototype._dispatch = function (name, detail, cancelable) {
279        var event = new CustomEvent(name, {
280            detail: detail,
281            bubbles: true,
282            cancelable: !!cancelable
283        });
284        this.form.dispatchEvent(event);
285        return event;
286    };
287
288    // Auto-initialize all forms
289    document.addEventListener('DOMContentLoaded', function () {
290        var forms = document.querySelectorAll('form');
291        for (var i = 0; i < forms.length; i++) {
292            new FormItForm(forms[i]);
293        }
294    });
295
296})(window, document);

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.