PluginProbe
Contact Forms by Cimatti / 1.9.2
Contact Forms by Cimatti v1.9.2
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
← All changes | AccuaForm.php +92 -1212 2.2.321.9.2 View file →
@@ -1,8 +1,5 @@
1 1 <?php
2 -if ( ! defined( 'ABSPATH' ) ) exit;
3 -
4 -// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped, PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.PHP.DevelopmentFunctions.error_log_trigger_error, WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.WP.AlternativeFunctions.rename_rename -- PFBC Form extension class with controlled HTML output, deprecation notices, debug logging, file operations
5 2 class AccuaForm extends Form {
6 3 protected static $submitted = null;
7 4 protected static $valid = null;
8 5 protected static $submittedID = null;
@@ -10,15 +7,9 @@
10 7 protected static $submittedForm = null;
11 8 protected static $submittedFormUsed = false;
12 9 protected static $submittedData = null;
13 10 protected static $rawData = null;
14 - /**
15 - * Per-form submitted messages array.
16 - * Keys are form IDs, values are message strings.
17 - * This allows multiple forms on the same page to have separate messages.
18 - * @var array<string, string>
19 - */
20 - protected static $submittedMessages = array();
11 + protected static $submittedMessages = '';
21 12
22 13 protected $formID = null;
23 14 protected $buildID = null;
24 15 protected $validate_functions = array();
@@ -29,9 +20,8 @@
29 20 protected $locale = null;
30 21 protected $language = null;
31 22 protected $files = array();
32 23 protected $ga_track = array();
33 - protected $gads_conversion_tracking_code = '';
34 24
35 25 protected $original_locale = null;
36 26 protected $original_language = null;
37 27 protected $original_l10n = null;
@@ -51,12 +41,10 @@
51 41 unset($GLOBALS['wp_locale']);
52 42 unset($GLOBALS['l10n']);
53 43 $GLOBALS['l10n'] = array();
54 44
55 - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- qTranslate plugin integration
56 45 $GLOBALS['q_config']['language'] = $this->language;
57 46 load_default_textdomain();
58 - // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Intentionally forces language at runtime for qTranslate email delivery
59 47 load_plugin_textdomain( 'contact-forms', false, ACCUA_FORM_API_PLUGIN_TEXTDOMAIN_PATH);
60 48 require_once( ABSPATH . WPINC . '/locale.php' );
61 49 $GLOBALS['wp_locale'] = new WP_Locale();
62 50 $GLOBALS['wp_locale']->register_globals();
@@ -70,9 +58,8 @@
70 58 if ($this->forced_language) {
71 59 unset($GLOBALS['wp_locale']);
72 60 unset($GLOBALS['l10n']);
73 61
74 - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- qTranslate plugin integration
75 62 $GLOBALS['q_config']['language'] = $this->original_language;
76 63 $GLOBALS['l10n'] =& $this->original_l10n;
77 64 $GLOBALS['wp_locale'] =& $this->original_locale;
78 65 if ($GLOBALS['wp_locale']) {
@@ -146,10 +133,8 @@
146 133 error_log("headers already sent at {$file}:{$line}");
147 134 }
148 135 }
149 136 $sessionid = session_id();
150 - // Close the session immediately after getting the ID to prevent REST API interference
151 - session_write_close();
152 137 }
153 138 return $sessionid;
154 139 }
155 140
@@ -155,17 +140,13 @@
155 140
156 141 public static function getBaseURL() {
157 142 static $ret = null;
158 143 if ($ret === null) {
159 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
160 144 $s = empty($_SERVER["HTTPS"]) ? '' : (($_SERVER["HTTPS"] == "on") ? "s" : "");
161 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
162 - $sp = isset($_SERVER["SERVER_PROTOCOL"]) ? strtolower(sanitize_text_field(wp_unslash($_SERVER["SERVER_PROTOCOL"]))) : 'http/1.1';
145 + $sp = strtolower($_SERVER["SERVER_PROTOCOL"]);
163 146 $protocol = substr($sp, 0, strpos($sp, "/")) . $s;
164 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
165 - $port = (!isset($_SERVER["SERVER_PORT"]) || $_SERVER["SERVER_PORT"] == "80") ? "" : (":" . absint($_SERVER["SERVER_PORT"]));
166 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
167 - $ret = $protocol . "://" . (isset($_SERVER['SERVER_NAME']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_NAME'])) : 'localhost') . $port;
147 + $port = ($_SERVER["SERVER_PORT"] == "80") ? "" : (":".$_SERVER["SERVER_PORT"]);
148 + $ret = $protocol . "://" . $_SERVER['SERVER_NAME'] . $port;
168 149 }
169 150 return $ret;
170 151 }
171 152
@@ -204,9 +185,8 @@
204 185 'layout' => 'sidebyside',
205 186 'title' => '',
206 187 'track_submit' => false,
207 188 'track_fields' => false,
208 - 'gads_conversion_tracking_code' => '',
209 189 );
210 190 if (is_array($params)) {
211 191 $params += $predefined_params;
212 192 $width = $params['width'];
@@ -214,11 +194,8 @@
214 194 $width = $params;
215 195 $params = $predefined_params;
216 196 $params['width'] = $width;
217 197 }
218 - // Always initialize the property to avoid undefined property warnings
219 - $this->gads_conversion_tracking_code = $params['gads_conversion_tracking_code'];
220 -
221 198 $class = 'accua-form ' . $id;
222 199 switch ($params['layout']) {
223 200 case 'toplabel':
224 201 $this->view = new AccuaForm_View_Standard();
@@ -223,14 +200,8 @@
223 200 case 'toplabel':
224 201 $this->view = new AccuaForm_View_Standard();
225 202 $class .= ' accua-form-view-standard';
226 203 break;
227 - case 'inlinelabel':
228 - $this->view = new AccuaForm_View_InlineLabel();
229 - $class .= ' accua-form-view-inlinelabel';
230 - // Prevent PFBC from adding inline width styles for inline label layout
231 - $width = '';
232 - break;
233 204 case 'sidebyside':
234 205 default:
235 206 $this->view = new AccuaForm_View_SideBySide(
236 207 '19',
@@ -241,11 +212,9 @@
241 212 $this->error = new AccuaForm_Error_Standard(array(
242 213 'errorfound' => '',
243 214 'errorsfound' => '',
244 215 ));
245 - // For inline label view, we don't set default width
246 - // The CSS handles all widths with width: 100%
247 - if ($width === "" && $params['layout'] !== 'inlinelabel'){
216 + if ($width === ""){
248 217 $width = "100%";
249 218 }
250 219 $this->attributes = array(
251 220 'class' => $class,
@@ -277,10 +246,11 @@
277 246 * - Quando ricevi il form, decripta _AccuaForm_serialized e controlla che sia valido
278 247 *
279 248 * */
280 249
250 + //$this->addElement(new Element_Hidden('PHPSESSID', self::sessionID()));
281 251 $this->prevent = array('jQuery', 'jQueryUI', 'jQueryUIButtons', 'focus', 'style');
282 - $this->configure(array('action' => '#'));
252 + $this->configure(array('action' => ''));
283 253
284 254 if (function_exists('qtrans_getLanguage')) {
285 255 $this->language = qtrans_getLanguage();
286 256 $this->locale = $GLOBALS['q_config']['locale'][$this->language];
@@ -292,26 +262,18 @@
292 262
293 263 global $post;
294 264 $pid = empty($post->ID) ? 0 : $post->ID;
295 265
296 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- URL stored for logging, escaped on output
297 - $uri = isset($GLOBALS['q_config']['url_info']['original_url']) ? $GLOBALS['q_config']['url_info']['original_url'] : (isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '');
266 + $uri = isset($GLOBALS['q_config']['url_info']['original_url']) ? $GLOBALS['q_config']['url_info']['original_url'] : $_SERVER['REQUEST_URI'];
298 267
299 268 $url = self::getBaseURL() . $uri ;
300 - // Set the form action to the current page URL so non-AJAX forms submit cleanly
301 - // (AJAX forms override this later in the JS to admin-ajax.php)
302 - $this->configure(array('action' => $url));
303 269
304 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Referrer stored for logging, escaped on output
305 - $referrer = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
270 + $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
306 271
307 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- IP stored for logging, escaped on output
308 - $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
309 -
310 272 $this->stats = array(
311 273 'pid' => $pid,
312 - 'ip' => $remote_addr, //updated after submit
313 - 'original_ip' => $remote_addr, //unreliable if using a static page caching system
274 + 'ip' => $_SERVER['REMOTE_ADDR'], //updated after submit
275 + 'original_ip' => $_SERVER['REMOTE_ADDR'], //unreliable if using a static page caching system
314 276 'uri' => $uri,
315 277 'url' => $url,
316 278 'referrer' => $referrer, //updated after submit using javascript
317 279 'original_referrer' => $referrer, //unreliable if using a static page caching system
@@ -335,44 +297,41 @@
335 297 }
336 298 self::$submitted = false;
337 299 $sessid = self::sessionID();
338 300
339 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Checking request method for form handling
340 - $request_method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : 'GET';
341 - if ($request_method === 'POST') {
301 + if($_SERVER['REQUEST_METHOD'] == 'POST') {
342 302 self::$rawData = stripslashes_deep($_POST);
343 303 } else {
344 304 self::$rawData = stripslashes_deep($_GET);
345 305 }
346 306
347 - // Ensure basic fields for nonce verification are present.
348 - if (empty(self::$rawData['_AccuaForm_buildID']) || empty(self::$rawData['_AccuaForm_wpnonce'])) {
307 + if (empty(self::$rawData['_AccuaForm_ID']) || empty(self::$rawData['_AccuaForm_buildID']) || empty(self::$rawData['_AccuaForm_wpnonce']) /* || empty(self::$rawData['PHPSESSID']) */
308 + || empty(self::$rawData['_AccuaForm_hash']) || empty(self::$rawData['_AccuaForm_iv']) || empty(self::$rawData['_AccuaForm_data'])) {
349 309 return false;
350 310 }
351 311
352 - // Verify nonce for CSRF protection (moved earlier).
353 - // The $id variable (action for nonce) is self::$rawData['_AccuaForm_buildID'].
354 - $requested_buildID = self::$rawData['_AccuaForm_buildID'];
355 - if (!wp_verify_nonce(self::$rawData['_AccuaForm_wpnonce'], $requested_buildID)) {
312 + /*
313 + if (self::$rawData['PHPSESSID'] !== $sessid) {
356 314 return false;
357 315 }
316 + */
358 317
359 - // Now check for fields required for form recovery and other validations.
360 - if (empty(self::$rawData['_AccuaForm_ID'])
361 - || empty(self::$rawData['_AccuaForm_hash']) || empty(self::$rawData['_AccuaForm_iv']) || empty(self::$rawData['_AccuaForm_data'])) {
318 + $id = self::$rawData['_AccuaForm_buildID'];
319 + $form = self::wp_recover(self::$rawData['_AccuaForm_hash'], self::$rawData['_AccuaForm_iv'], self::$rawData['_AccuaForm_data']);
320 +
321 + if (empty($form)) {
362 322 return false;
363 323 }
364 324
365 - // $id was self::$rawData['_AccuaForm_buildID'], now using $requested_buildID for clarity.
366 - $form = self::wp_recover(self::$rawData['_AccuaForm_hash'], self::$rawData['_AccuaForm_iv'], self::$rawData['_AccuaForm_data']);
367 -
368 - if (empty($form)) {
325 + if ($form->formID !== self::$rawData['_AccuaForm_ID'] || $form->buildID !== $id ) {
369 326 return false;
370 327 }
371 328
372 - if ($form->formID !== self::$rawData['_AccuaForm_ID'] || $form->buildID !== $requested_buildID ) {
329 + /*
330 + if (!wp_verify_nonce(self::$rawData['_AccuaForm_wpnonce'], $id)) {
373 331 return false;
374 332 }
333 + */
375 334
376 335 /* check if already submitted from uuid and other parameters */
377 336 $already_submitted = false;
378 337 if (!empty(self::$rawData['_AccuaForm_jsuuid'])) {
@@ -377,20 +336,13 @@
377 336 $already_submitted = false;
378 337 if (!empty(self::$rawData['_AccuaForm_jsuuid'])) {
379 338 if (preg_match("/^[a-z0-9]{25}$/", self::$rawData['_AccuaForm_jsuuid'])) {
380 339 $already_submitted_data = get_transient('accuaformsub_'.self::$rawData['_AccuaForm_jsuuid']);
381 - if ($already_submitted_data && $already_submitted_data['buildID'] == $form->buildID) {
382 - // Only use cached result for successful submissions to prevent duplicate processing
383 - if (!empty($already_submitted_data['valid'])) {
384 - $form = $already_submitted_data['form'];
385 - self::$submittedMessages = $already_submitted_data['submittedMessages'];
386 - self::$valid = true;
387 - $already_submitted = true;
388 - } else {
389 - // For failed validations, delete the transient so form can be re-validated
390 - // This fixes the bug where correcting a field and resubmitting caused form to disappear
391 - delete_transient('accuaformsub_'.self::$rawData['_AccuaForm_jsuuid']);
392 - }
340 + if ($already_submitted_data && $already_submitted_data['buildID'] == $form->buildID) {
341 + $form = $already_submitted_data['form'];
342 + self::$submittedMessages = $already_submitted_data['submittedMessages'];
343 + self::$valid = true;
344 + $already_submitted = true;
393 345 }
394 346 } else {
395 347 return false;
396 348 }
@@ -397,10 +349,9 @@
397 349 }
398 350
399 351 if (!$already_submitted) {
400 352 $form->stats['submitted'] = time();
401 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- IP stored for logging, escaped on output
402 - $form->stats['ip'] = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
353 + $form->stats['ip'] = $_SERVER['REMOTE_ADDR'];
403 354 foreach (array('referrer', 'jsuuid', 'user_agent', 'platform', 'tentatives', 'submit_method') as $i){
404 355 $form->stats[$i] = isset(self::$rawData["_AccuaForm_{$i}"]) ? ((string)self::$rawData["_AccuaForm_{$i}"]) : '';
405 356 }
406 357 }
@@ -447,16 +398,14 @@
447 398 not $_GET or $_POST.*/
448 399 if($element instanceof AccuaForm_Element_File) {
449 400 if (!empty($form->files[$name]['name'])) {
450 401 $value = $form->files[$name]['name'];
451 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- File handling uses $_FILES, nonce verified earlier
452 - } else if ((!empty($_FILES[$name])) && (isset($_FILES[$name]['error']) && $_FILES[$name]['error'] != UPLOAD_ERR_NO_FILE)) {
402 + } else if ((!empty($_FILES[$name])) && ($_FILES[$name]['error'] != UPLOAD_ERR_NO_FILE)) {
453 403 //$file = $_FILES[$name];
454 404 //include_once( ABSPATH . '/wp-admin/includes/file.php' );
455 405 //$overrides = array( 'test_form' => false );
456 406 //$file = wp_handle_upload( $file, $overrides );
457 407
458 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File array passed to WordPress handler
459 408 $file = $element->handle_upload($_FILES[$name]);
460 409 $value = $file['name'];
461 410
462 411 if (empty($file['errors'])) {
@@ -469,20 +418,17 @@
469 418 } else {
470 419 $value = null;
471 420 }
472 421 } else if ($element instanceof Element_File){
473 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Legacy file handling
474 - $value = isset($_FILES[$name]["name"]) ? sanitize_file_name($_FILES[$name]["name"]) : '';
422 + $value = $_FILES[$name]["name"];
475 423 } else if (isset(self::$rawData[$name])) {
476 424 $value = self::$rawData[$name];
477 425 if(is_array($value)) {
478 426 foreach($value as $key => $value_i) {
479 - // Sanitize null bytes to prevent mail() ValueError in PHP 8+
480 - $value[$key] = str_replace("\0", '', (string) $value_i);
427 + $value[$key] = (string) $value_i;
481 428 }
482 429 } else {
483 - // Sanitize null bytes to prevent mail() ValueError in PHP 8+
484 - $value = str_replace("\0", '', (string) $value);
430 + $value = (string) $value;
485 431 }
486 432 } else {
487 433 $value = null;
488 434 }
@@ -499,11 +445,12 @@
499 445 }
500 446 }
501 447
502 448 if($invalidFile) {
503 - // File cleanup is handled in handle_upload() when validation fails
504 - // The uploaded tmp file is already deleted by PHP after request completes
505 - unset($form->files[$name]);
449 + if (isset($file['tmp_name'])) {
450 + unlink($element->getDestPath . $file['tmp_name']);
451 + unset($form->files[$name]);
452 + }
506 453 } else if($name !== '') {
507 454 self::$submittedData[$name] = $value;
508 455 }
509 456 }
@@ -535,38 +482,28 @@
535 482 }
536 483 }
537 484 }
538 485
539 - /*Apply errors from session to form elements for accessibility (ARIA attributes).
540 - This must be done BEFORE session_write_close() is called, so errors persist in element objects.*/
541 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form error handling
542 - if (!$valid && !empty($_SESSION["pfbc"][$id]["errors"])) {
543 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form error handling
544 - $sessionErrors = $_SESSION["pfbc"][$id]["errors"];
545 - $elements = $form->getElements();
546 - if (!empty($elements)) {
547 - foreach ($elements as $element) {
548 - $name = $element->getName();
549 - if (substr($name, -2) == "[]")
550 - $name = substr($name, 0, -2);
551 -
552 - if (isset($sessionErrors[$name]) && is_array($sessionErrors[$name])) {
553 - $element->setErrors($sessionErrors[$name]);
554 - }
555 - }
556 - }
486 + /*If no validation errors were found, the form's session values are cleared.*/
487 + /*
488 + if($valid) {
489 + if($clearValues)
490 + self::clearValues($id);
491 + self::clearErrors($id);
557 492 }
493 + */
558 494
495 + //TODO: Should i save here?
496 + //$form->save();
497 + //$_SESSION["pfbc"][$id]["form"] = serialize($form);
498 +
559 499 $form->restore_language();
560 500
561 - /*Store form with errors in transient for accessibility on redirect.
562 - Both successful and failed submissions are stored so the form can be properly re-rendered with ARIA attributes.*/
563 - if (self::$rawData['_AccuaForm_jsuuid'] ) {
501 + if ($valid && self::$rawData['_AccuaForm_jsuuid'] ) {
564 502 set_transient('accuaformsub_'.self::$rawData['_AccuaForm_jsuuid'], array(
565 503 'buildID' => self::$submittedBuildID,
566 504 'form' => self::$submittedForm,
567 505 'submittedMessages' => self::$submittedMessages,
568 - 'valid' => $valid,
569 506 ), 86400);
570 507 }
571 508
572 509 return self::$valid = (bool)$valid;
@@ -685,11 +622,9 @@
685 622 '_AccuaForm_hash' => base64_encode($hash),
686 623 '_AccuaForm_iv' => base64_encode($iv),
687 624 '_AccuaForm_data' => base64_encode($data),
688 625 );
689 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form values
690 626 if (isset($_SESSION["pfbc"][$this->buildID]["values"])) {
691 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form values
692 627 $_SESSION["pfbc"][$this->buildID]["values"] = $ret + $_SESSION["pfbc"][$this->buildID]["values"];
693 628 }
694 629 $this->setValues($ret);
695 630 return $ret;
@@ -710,53 +645,18 @@
710 645 public function addSubmitFunction($function_name, $priority = 0) {
711 646 $this->submit_functions[$function_name] = $priority;
712 647 }
713 648
714 - /**
715 - * Get submitted messages for a specific form or all forms.
716 - * @param string|null $formId Optional form ID to get message for specific form
717 - * @return string|array If $formId provided, returns string message (or empty string). Otherwise returns all messages as array.
718 - */
719 - public static function getSubmittedMessages($formId = null){
720 - if ($formId !== null) {
721 - return isset(self::$submittedMessages[$formId]) ? self::$submittedMessages[$formId] : '';
722 - }
723 - // Backwards compatibility: if old code expects a string, join all messages
724 - if (is_array(self::$submittedMessages)) {
725 - return implode('', self::$submittedMessages);
726 - }
649 + public static function getSubmittedMessages(){
727 650 return self::$submittedMessages;
728 651 }
729 652
730 - /**
731 - * Set submitted message for a specific form.
732 - * @param string $msg The message content
733 - * @param string|null $formId Optional form ID. If null, uses currently submitted form ID.
734 - * @return string The message that was set
735 - */
736 - public static function setSubmittedMessages($msg, $formId = null){
737 - if ($formId === null) {
738 - $formId = self::$submittedID ?: '__default__';
739 - }
740 - self::$submittedMessages[$formId] = $msg;
741 - return $msg;
653 + public static function setSubmittedMessages($msg){
654 + return self::$submittedMessages = $msg;
742 655 }
743 656
744 - /**
745 - * Append to submitted message for a specific form.
746 - * @param string $msg The message content to append
747 - * @param string|null $formId Optional form ID. If null, uses currently submitted form ID.
748 - * @return string The full message after appending
749 - */
750 - public static function appendSubmittedmessages($msg, $formId = null){
751 - if ($formId === null) {
752 - $formId = self::$submittedID ?: '__default__';
753 - }
754 - if (!isset(self::$submittedMessages[$formId])) {
755 - self::$submittedMessages[$formId] = '';
756 - }
757 - self::$submittedMessages[$formId] .= $msg;
758 - return self::$submittedMessages[$formId];
657 + public static function appendSubmittedmessages($msg){
658 + return self::$submittedMessages .= $msg;
759 659 }
760 660
761 661 protected static function get_anchor_id($id) {
762 662 static $used_anchor_id = array();
@@ -771,38 +671,23 @@
771 671 $id = $id . '-' . $used_anchor_id[$id];
772 672 }
773 673 return $id;
774 674 }
675 +
775 676 public function render($returnHTML = false) {
776 677 $this->wp_save();
777 678 if($returnHTML) {
778 679 ob_start();
779 680 }
780 -
781 681 parent::render(false);
782 682 if (!empty($this->accua_ajax)) {
783 683 $ajax_url = _accua_forms_json_encode(admin_url('admin-ajax.php?action=accua_form_submit'));
784 - $submit_fail_message = _accua_forms_json_encode('<li>'.__('Form submission failed. Please try again.', 'contact-forms').'</li>');
785 -
786 - /* translators: %s is the field name/label */
787 - $required_message_template = _accua_forms_json_encode(__('%s is a required field', 'contact-forms'));
788 - /* translators: %s is the field name/label */
789 - $valid_mail_message_template = _accua_forms_json_encode(__('%s: please enter a valid email address', 'contact-forms'));
790 - /* translators: %s is the field name/label */
791 - $valid_phone_message_template = _accua_forms_json_encode(__('%s: please enter a valid phone number (e.g. +39 333 1234567)', 'contact-forms'));
792 - $error_summary_header = _accua_forms_json_encode(__('Check the following fields to continue:', 'contact-forms'));
793 - $error_type_required = _accua_forms_json_encode(__('required field', 'contact-forms'));
794 - $error_type_invalid_email = _accua_forms_json_encode(__('invalid email address', 'contact-forms'));
795 - $error_type_invalid_phone = _accua_forms_json_encode(__('invalid phone number', 'contact-forms'));
796 - $success_message = _accua_forms_json_encode(__('All fields are valid. Ready to submit!', 'contact-forms'));
797 - $sending_message = _accua_forms_json_encode(__('Sending...', 'contact-forms'));
798 - $loading_summary_message = _accua_forms_json_encode(__('Submitting your form, please wait...', 'contact-forms'));
684 + $submit_fail_message = _accua_forms_json_encode('<li>'.__('Unable to submit the form, please retry', 'contact-forms').'</li>');
685 + $required_message = _accua_forms_json_encode(__('Please fill in all required fields', 'contact-forms'));
686 + $valid_mail_message = _accua_forms_json_encode(__('You have to enter a valid email address where required', 'contact-forms'));
799 687 $js_buildid = preg_replace('/[^a-zA-Z0-9_]/m','_',$this->buildID);
800 688 $formid = $this->getId();
801 - // $hostname = _accua_forms_json_encode($_SERVER['SERVER_NAME']); - rimosso perché non affidabile con proxy reversi e CDN
802 - // Extract hostname from ajax_url for proper cross-domain detection
803 - $ajax_url_parsed = wp_parse_url(admin_url('admin-ajax.php?action=accua_form_submit'));
804 - $ajax_hostname = _accua_forms_json_encode($ajax_url_parsed['host']);
689 + $hostname = _accua_forms_json_encode($_SERVER['SERVER_NAME']);
805 690
806 691 $post_url = _accua_forms_json_encode($this->stats['url']);
807 692 $js_ga_track = _accua_forms_json_encode($this->ga_track);
808 693
@@ -818,28 +703,18 @@
818 703 var _handle_ajax_submit_response_{$js_buildid} = function() {}
819 704
820 705 jQuery(function($) {
821 706 var thisform = $("#{$this->buildID}");
822 - var ajax_enabled = {$ajax_hostname} == location.hostname ;
707 + var ajax_enabled = {$hostname} == location.hostname ;
823 708 var anchor_id = $anchor_id ;
824 709
825 710 var response_messages = $("#_response_messages_{$this->buildID}");
826 711 if (! response_messages.length) {
827 - // Create all three anchors for success, invalid, and error states
828 - thisform.before('\\x3Ca id="formSubmitSuccess-'+anchor_id+'" name="formSubmitSuccess-'+anchor_id+'" class="accua-form-anchor" /\\x3E');
829 - thisform.before('\\x3Ca id="formSubmitInvalid-'+anchor_id+'" name="formSubmitInvalid-'+anchor_id+'" class="accua-form-anchor" /\\x3E');
830 - thisform.before('\\x3Ca id="formSubmitError-'+anchor_id+'" name="formSubmitError-'+anchor_id+'" class="accua-form-anchor" /\\x3E');
712 + thisform.before('\\x3Ca id="formSubmitSuccess-'+anchor_id+'" name="formSubmitSuccess-'+anchor_id+'" /\\x3E');
831 713 response_messages = $('\\x3Cdiv id="_response_messages_{$this->buildID}" class="accua-form-messages"\\x3E\\x3C/div\\x3E');
832 714 thisform.before(response_messages);
833 715 }
834 -
835 - // Smooth scroll helper function
836 - var smoothScrollToElement = function(elementId) {
837 - var target = document.getElementById(elementId);
838 - if (target) {
839 - target.scrollIntoView({ behavior: 'smooth', block: 'start' });
840 - }
841 - };
716 + var throbbler = $("\\x3Cspan class='accua_form_api_throbbler'\\x3E\\x3C/span\\x3E");
842 717
843 718 var _ajax_submitting_{$js_buildid} = false;
844 719 var timeout_handler = false;
845 720 var timeout_count = 0;
@@ -844,11 +719,8 @@
844 719 var timeout_handler = false;
845 720 var timeout_count = 0;
846 721 var fail_count = 0;
847 722 var disabled_fields = false;
848 - var submitBtn = $('button[type="submit"]', thisform);
849 - var submitBtnOriginalText = submitBtn.text();
850 - var submitBtnSendingText = $sending_message;
851 723
852 724 var jsuuid_field = $('input[name="_AccuaForm_jsuuid"]', thisform);
853 725 var jsuuid = jsuuid_field.val();
854 726 if (jsuuid == '') {
@@ -862,13 +734,8 @@
862 734
863 735 var ga_track = {$js_ga_track} ;
864 736 var ga_event, ga_submit_event, ga_field_event, ga_field_events_fired = {};
865 737 ga_event = function(eventCategory, eventAction){
866 - /* matomo */
867 - if (window._mtm && (typeof window._mtm.push == 'function')) {
868 - window._mtm.push({'event': 'ContactForms', 'eventAction': eventAction, 'eventCategory': eventCategory, 'eventLabel': ga_track.title});
869 - }
870 -
871 738 if (typeof window.gtag == 'function') {
872 739 window.gtag('event', eventAction, {'event_category': eventCategory, 'event_label': ga_track.title});
873 740 } else if (window.dataLayer && (typeof window.dataLayer.push == 'function')) {
874 741 dataLayer.push({'event': 'ContactForms', 'eventAction': eventAction, 'eventCategory': eventCategory, 'eventLabel': ga_track.title});
@@ -903,378 +770,11 @@
903 770 $('input, textarea, select', thisform).change(function(){
904 771 ga_field_event($(this).attr('name'));
905 772 });
906 773
907 - // Disable submit button during form submission
908 - var disableSubmitButton = function() {
909 - submitBtn.prop('disabled', true).attr('aria-busy', 'true').text(submitBtnSendingText);
910 - };
911 -
912 - // Re-enable submit button (on failure or completion)
913 - var enableSubmitButton = function() {
914 - submitBtn.prop('disabled', false).removeAttr('aria-busy').text(submitBtnOriginalText);
915 - };
916 -
917 - // Get field label from the field's container element
918 - var getFieldLabel = function(field) {
919 - var container = field.closest('.pfbc-element');
920 - if (!container.length) {
921 - return '';
922 - }
923 -
924 - // Try floating label first (inline labels mode)
925 - var label = container.find('.pfbc-floating-label').first();
926 - if (!label.length) {
927 - // Try standard label in .pfbc-label
928 - label = container.find('.pfbc-label label').first();
929 - }
930 - if (!label.length) {
931 - // Try any label element
932 - label = container.find('label').first();
933 - }
934 -
935 - if (label.length) {
936 - // Get text content, removing the required asterisk
937 - var text = label.clone().find('.pfbc-required').remove().end().text().trim();
938 - return text;
939 - }
940 -
941 - return '';
942 - };
943 -
944 - // Build required field error message with field name
945 - // Checks per-field data-custom-required-msg attribute first, then falls back to global template
946 - var getRequiredMessage = function(fieldLabel, field) {
947 - var template = $required_message_template;
948 - if (field) {
949 - var custom = field.attr('data-custom-required-msg');
950 - if (custom) {
951 - template = custom;
952 - }
953 - }
954 - if (fieldLabel) {
955 - return template.replace('%s', fieldLabel);
956 - }
957 - return template.replace('%s', '');
958 - };
959 -
960 - // Build email validation error message with field name
961 - // Checks per-field data-custom-format-msg attribute first, then falls back to global template
962 - var getEmailMessage = function(fieldLabel, field) {
963 - var template = $valid_mail_message_template;
964 - if (field) {
965 - var custom = field.attr('data-custom-format-msg');
966 - if (custom) {
967 - template = custom;
968 - }
969 - }
970 - if (fieldLabel) {
971 - return template.replace('%s', fieldLabel);
972 - }
973 - return template.replace('%s', '');
974 - };
975 -
976 - // Build phone validation error message with field name
977 - // Checks per-field data-custom-format-msg attribute first, then falls back to global template
978 - var getPhoneMessage = function(fieldLabel, field) {
979 - var template = $valid_phone_message_template;
980 - if (field) {
981 - var custom = field.attr('data-custom-format-msg');
982 - if (custom) {
983 - template = custom;
984 - }
985 - }
986 - if (fieldLabel) {
987 - return template.replace('%s', fieldLabel);
988 - }
989 - return template.replace('%s', '');
990 - };
991 -
992 - // Check if a telephone field contains only a country prefix (e.g. "+39")
993 - // These values should be treated as empty — the user hasn't entered a real number.
994 - var isTelephonePrefixOnly = function(field) {
995 - if (!field.hasClass('accuaform-telephone')) return false;
996 - var val = field.val();
997 - if (!val) return false;
998 - val = val.trim();
999 - if (val === '' || val.charAt(0) !== '+') return false;
1000 - return val.replace(/\D/g, '').length <= 4;
1001 - };
1002 -
1003 - // Scroll to first invalid field, then focus after scroll completes
1004 - var focusFirstInvalidField = function() {
1005 - var firstInvalidContainer = $('#{$this->buildID} .pfbc-element-has-error').first();
1006 - if (!firstInvalidContainer.length) {
1007 - return;
1008 - }
1009 -
1010 - // Try to find a focusable element in the invalid container
1011 - var focusTarget = null;
1012 -
1013 - // Check for radio/checkbox groups first
1014 - var radioOrCheckbox = firstInvalidContainer.find('input[type="radio"], input[type="checkbox"]').first();
1015 - if (radioOrCheckbox.length) {
1016 - focusTarget = radioOrCheckbox;
1017 - }
1018 -
1019 - // Check for file input (focus the dropzone button if available)
1020 - if (!focusTarget) {
1021 - var fileDropzone = firstInvalidContainer.find('.accua-file-dropzone');
1022 - if (fileDropzone.length) {
1023 - focusTarget = fileDropzone;
1024 - } else {
1025 - var fileInput = firstInvalidContainer.find('input[type="file"]');
1026 - if (fileInput.length) {
1027 - focusTarget = fileInput;
1028 - }
1029 - }
1030 - }
1031 -
1032 - // Check for custom select buttons (post select, etc.)
1033 - if (!focusTarget) {
1034 - var customSelectBtn = firstInvalidContainer.find('.pfbc-post-select-trigger, button[aria-haspopup="listbox"]').first();
1035 - if (customSelectBtn.length) {
1036 - focusTarget = customSelectBtn;
1037 - }
1038 - }
1039 -
1040 - // Standard inputs (text, email, select, textarea)
1041 - if (!focusTarget) {
1042 - focusTarget = firstInvalidContainer.find('input:not([type="hidden"]), textarea, select').first();
1043 - }
1044 -
1045 - if (focusTarget && focusTarget.length) {
1046 - // Scroll into view first with smooth animation
1047 - if (focusTarget[0].scrollIntoView) {
1048 - focusTarget[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
1049 - }
1050 - // Focus after scroll animation completes (typical scroll animation is ~300-500ms)
1051 - setTimeout(function() {
1052 - focusTarget.focus();
1053 - }, 500);
1054 - }
1055 - };
1056 -
1057 - // Scroll to a specific field by ID and focus it
1058 - var scrollToFieldAndFocus = function(fieldId) {
1059 - var element = $('#' + fieldId);
1060 - if (!element.length) {
1061 - return;
1062 - }
1063 -
1064 - var focusTarget = element;
1065 -
1066 - // If it's already an input/select/textarea, use it directly
1067 - if (element.is('input, select, textarea')) {
1068 - focusTarget = element;
1069 - }
1070 - // For containers (pfbc-element), find the first focusable element
1071 - else if (element.hasClass('pfbc-element')) {
1072 - var radioOrCheckbox = element.find('input[type="radio"], input[type="checkbox"]').first();
1073 - if (radioOrCheckbox.length) {
1074 - focusTarget = radioOrCheckbox;
1075 - } else {
1076 - var fileDropzone = element.find('.accua-file-dropzone');
1077 - if (fileDropzone.length) {
1078 - focusTarget = fileDropzone;
1079 - } else {
1080 - focusTarget = element.find('input:not([type="hidden"]), textarea, select').first();
1081 - }
1082 - }
1083 - }
1084 - // For file dropzone wrapper, focus the dropzone
1085 - else if (element.find('.accua-file-dropzone').length) {
1086 - focusTarget = element.find('.accua-file-dropzone').first();
1087 - }
1088 -
1089 - if (focusTarget && focusTarget.length) {
1090 - if (focusTarget[0].scrollIntoView) {
1091 - focusTarget[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
1092 - }
1093 - setTimeout(function() {
1094 - focusTarget.focus();
1095 - }, 500);
1096 - }
1097 - };
1098 -
1099 - // Track if form submission has been attempted
1100 - var submitAttempted = false;
1101 -
1102 - // Array to collect field errors for summary
1103 - var fieldErrorsList = [];
1104 -
1105 - // Get the error ID for a radio/checkbox group (DRY helper)
1106 - var getGroupErrorId = function(field) {
1107 - var groupWrapper = field.closest('.pfbc-radio-buttons, .pfbc-checkboxes').parent();
1108 - var groupId = groupWrapper.attr('id') || field.attr('id');
1109 - return groupId + '-error';
1110 - };
1111 -
1112 - // Remove an error element with smooth animation (generic helper for any field type)
1113 - var removeErrorAnimated = function(errorId) {
1114 - var errorEl = $('#' + errorId);
1115 - if (errorEl.length) {
1116 - errorEl.addClass('pfbc-error-removing');
1117 - setTimeout(function() {
1118 - errorEl.remove();
1119 - }, 150); // Match CSS animation duration
1120 - }
1121 - };
1122 -
1123 - // Remove all existing errors for a group with smooth fade-out (handles both potential IDs)
1124 - var removeGroupErrors = function(field, fieldName, animate) {
1125 - var groupContainer = field.closest('.pfbc-element, .pfbc-fieldwrap');
1126 - var errors = groupContainer.find('.pfbc-inline-error');
1127 -
1128 - // Also collect errors by potential IDs
1129 - var groupWrapper = field.closest('.pfbc-radio-buttons, .pfbc-checkboxes').parent();
1130 - var wrapperId = groupWrapper.attr('id');
1131 - if (wrapperId) {
1132 - errors = errors.add($('#' + wrapperId + '-error'));
1133 - }
1134 - var firstField = $("[name='"+fieldName+"']", thisform).first();
1135 - if (firstField.attr('id')) {
1136 - errors = errors.add($('#' + firstField.attr('id') + '-error'));
1137 - }
1138 -
1139 - if (animate && errors.length) {
1140 - // Smooth fade-out animation
1141 - errors.addClass('pfbc-error-removing');
1142 - setTimeout(function() {
1143 - errors.remove();
1144 - }, 150); // Match CSS transition duration
1145 - } else {
1146 - errors.remove();
1147 - }
1148 - };
1149 -
1150 - // Show or update the error/success/loading summary area
1151 - // state: true (success), false (error), 'loading' (submitting)
1152 - var updateSummaryArea = function(state) {
1153 - var summaryArea = $('#{$this->buildID}-validation-summary');
1154 -
1155 - if (!submitAttempted && state !== 'loading') {
1156 - summaryArea.remove();
1157 - return;
1158 - }
1159 -
1160 - // Ensure summary area exists
1161 - if (!summaryArea.length) {
1162 - summaryArea = $('<div id="{$this->buildID}-validation-summary" class="pfbc-validation-summary" role="status" aria-live="polite"></div>');
1163 - thisform.find('.pfbc-error').remove();
1164 - thisform.append(summaryArea);
1165 - }
1166 -
1167 - if (state === 'loading') {
1168 - // Show loading state - neutral blue with spinner and text
1169 - summaryArea
1170 - .removeClass('pfbc-validation-error pfbc-validation-success')
1171 - .addClass('pfbc-validation-loading')
1172 - .attr('role', 'status')
1173 - .attr('aria-live', 'polite')
1174 - .attr('aria-busy', 'true')
1175 - .html('<span class="pfbc-summary-spinner" aria-hidden="true"></span>' + $loading_summary_message);
1176 - } else if (state === true) {
1177 - // Show success state
1178 - summaryArea
1179 - .removeClass('pfbc-validation-error pfbc-validation-loading')
1180 - .addClass('pfbc-validation-success')
1181 - .attr('role', 'status')
1182 - .attr('aria-live', 'polite')
1183 - .removeAttr('aria-busy')
1184 - .html('<span class="pfbc-summary-icon">✓</span> ' + $success_message);
1185 - } else {
1186 - // Show error state with field list
1187 - summaryArea
1188 - .removeClass('pfbc-validation-success pfbc-validation-loading')
1189 - .addClass('pfbc-validation-error')
1190 - .attr('role', 'alert')
1191 - .attr('aria-live', 'assertive')
1192 - .removeAttr('aria-busy');
1193 -
1194 - var html = '<p class="pfbc-summary-header">' + $error_summary_header + '</p><ul class="pfbc-summary-list">';
1195 - for (var i = 0; i < fieldErrorsList.length; i++) {
1196 - var err = fieldErrorsList[i];
1197 - html += '<li><a href="#' + err.fieldId + '" class="pfbc-summary-link" data-field-id="' + err.fieldId + '">' + err.label + '</a> – ' + err.errorType + '</li>';
1198 - }
1199 - html += '</ul>';
1200 - summaryArea.html(html);
1201 -
1202 - // Attach click handlers to links
1203 - summaryArea.find('.pfbc-summary-link').on('click', function(e) {
1204 - e.preventDefault();
1205 - var fieldId = $(this).data('field-id');
1206 - scrollToFieldAndFocus(fieldId);
1207 - });
1208 - }
1209 - };
1210 -
1211 774 var show_error_messages = function(message) {
1212 - // Show error message in the validation summary area
1213 - // Used when AJAX submission fails (network error, server error, etc.)
1214 - var summaryArea = thisform.find('.pfbc-validation-summary');
1215 - if (summaryArea.length) {
1216 - summaryArea.html('<div class="pfbc-validation-error" role="alert"><strong>' + accua_forms_i18n.check_fields + '</strong><ul>' + message + '</ul></div>').show();
1217 - }
775 + thisform.append('\\x3Cdiv class="pfbc-error ui-state-error ui-corner-all"\\x3E\\x3Cul\\x3E' + message + '\\x3C/ul\\x3E\\x3C/div\\x3E');
1218 776 }
1219 -
1220 - // Update summary area with server-side errors (called from AJAX error response)
1221 - var updateSummaryWithServerErrors = function(errors, elementErrors) {
1222 - // Collect server errors for the summary
1223 - fieldErrorsList = [];
1224 -
1225 - if (elementErrors) {
1226 - jQuery.each(elementErrors, function(fieldName, fieldErrors) {
1227 - var field = jQuery('[name="' + fieldName + '"]', thisform);
1228 - var fieldId = field.attr('id') || fieldName;
1229 - var fieldLabel = getFieldLabel(field);
1230 -
1231 - // If no label found, try to get from the field container or use field name
1232 - if (!fieldLabel || fieldLabel === fieldName) {
1233 - var container = field.closest('.pfbc-element, .pfbc-fieldwrap');
1234 - fieldLabel = container.find('label').first().text().replace(/\s*\*\s*$/, '').trim();
1235 - if (!fieldLabel) {
1236 - // Fallback: humanize the field name
1237 - fieldLabel = fieldName.replace(/[-_]/g, ' ').replace(/\b\w/g, function(l){ return l.toUpperCase(); });
1238 - }
1239 - }
1240 -
1241 - for (var i = 0; i < fieldErrors.length; i++) {
1242 - fieldErrorsList.push({
1243 - fieldId: fieldId,
1244 - label: fieldLabel,
1245 - errorType: fieldErrors[i]
1246 - });
1247 - }
1248 - });
1249 - }
1250 -
1251 - // Also add any general errors from the errors array
1252 - if (errors && errors.length > 0) {
1253 - for (var i = 0; i < errors.length; i++) {
1254 - // Check if this error is already in fieldErrorsList
1255 - var alreadyAdded = false;
1256 - for (var j = 0; j < fieldErrorsList.length; j++) {
1257 - if (fieldErrorsList[j].errorType === errors[i]) {
1258 - alreadyAdded = true;
1259 - break;
1260 - }
1261 - }
1262 - if (!alreadyAdded) {
1263 - fieldErrorsList.push({
1264 - fieldId: '',
1265 - label: '',
1266 - errorType: errors[i]
1267 - });
1268 - }
1269 - }
1270 - }
1271 -
1272 - // Update the summary area to show errors
1273 - if (fieldErrorsList.length > 0) {
1274 - updateSummaryArea(false);
1275 - }
1276 - }
1277 777
1278 778 _handle_ajax_submit_{$js_buildid} = function() {
1279 779 if (_ajax_submitting_{$js_buildid}) {
1280 780 return false;
@@ -1283,129 +783,36 @@
1283 783 JS;
1284 784 $this->error->clear();
1285 785 echo <<<JS
1286 786
1287 - // Mark that submit was attempted
1288 - submitAttempted = true;
1289 -
1290 787 var valid_empty = true;
1291 788 var valid_mail = true;
1292 - var valid_phone = true;
1293 - var fieldErrors = {};
1294 -
1295 - // Reset field errors list for summary
1296 - fieldErrorsList = [];
1297 789
1298 790 $("#{$this->buildID} .pfbc-element").removeClass('pfbc-invalid');
1299 - $("#{$this->buildID} .pfbc-element").removeClass('pfbc-element-has-error');
1300 - $("#{$this->buildID} input, #{$this->buildID} textarea, #{$this->buildID} select").attr('aria-invalid', 'false');
1301 - $("#{$this->buildID} .pfbc-inline-error").remove();
1302 791
1303 - var processedGroups = {}; // Track radio/checkbox groups to avoid duplicate errors
1304 -
1305 792 $('.accuaforms-field-required', thisform).each(function(){
1306 793 var field = $(this);
1307 794 var type = field.attr('type');
1308 - var fieldName = field.attr('name');
1309 795
1310 796 if (type === 'checkbox' || type === 'radio') {
1311 - // Skip if we've already processed this group
1312 - if (processedGroups[fieldName]) {
797 + if ($("[name='"+field.attr("name")+"']:checked", "#{$this->buildID}").length > 0) {
1313 798 return true;
1314 799 }
1315 - processedGroups[fieldName] = true;
1316 -
1317 - if ($("[name='"+fieldName+"']:checked", "#{$this->buildID}").length > 0) {
1318 - return true;
1319 - }
1320 -
1321 - valid_empty = false;
1322 -
1323 - // Find the container for the radio/checkbox group
1324 - var groupContainer = field.closest('.pfbc-element, .pfbc-fieldwrap');
1325 - groupContainer.addClass('pfbc-invalid pfbc-element-has-error');
1326 -
1327 - // Remove any existing errors before adding new one (prevents duplicates)
1328 - removeGroupErrors(field, fieldName);
1329 -
1330 - // Get consistent error ID using helper
1331 - var errorId = getGroupErrorId(field);
1332 -
1333 - // Find the last radio/checkbox in the group
1334 - var lastInGroup = $("[name='"+fieldName+"']", "#{$this->buildID}").last();
1335 -
1336 - // Apply ARIA attributes to all inputs in the group
1337 - $("[name='"+fieldName+"']", "#{$this->buildID}").attr('aria-invalid', 'true');
1338 - $("[name='"+fieldName+"']", "#{$this->buildID}").attr('aria-describedby', errorId);
1339 -
1340 - // Get field label and build error message
1341 - var fieldLabel = getFieldLabel(field);
1342 - var firstInput = $("[name='"+fieldName+"']", "#{$this->buildID}").first();
1343 - var errorMessage = getRequiredMessage(fieldLabel, firstInput);
1344 -
1345 - // Add to field errors list for summary (use first input ID for focusing)
1346 - fieldErrorsList.push({
1347 - fieldId: firstInput.attr('id'),
1348 - label: fieldLabel,
1349 - errorType: $error_type_required
1350 - });
1351 -
1352 - // Add error message after the last item in the group
1353 - var inlineError = $('<div class="pfbc-inline-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1354 -
1355 - // Insert after the last radio/checkbox wrapper
1356 - var lastWrapper = lastInGroup.closest('.pfbc-radio, .pfbc-checkbox');
1357 - if (lastWrapper.length) {
1358 - lastWrapper.after(inlineError);
1359 - } else {
1360 - lastInGroup.after(inlineError);
1361 - }
1362 800 } else {
1363 801 var val = field.val();
1364 802 if (typeof(val) == "string") {
1365 - // Treat "-" and "Select..." as invalid only for dropdowns
1366 - // For telephone fields, prefix-only values (e.g. "+39") are also empty
1367 - var isSelect = field.is('select');
1368 - if (! val.match(/^\s*$/) && (!isSelect || (val !== "Select..." && val !== "-")) && !isTelephonePrefixOnly(field)) {
803 + if (! val.match(/^\s*$/)) {
1369 804 return true;
1370 805 }
1371 - } else if (Array.isArray(val)) {
1372 - // Multiselect: empty array means no selection
1373 - if (val.length > 0) {
1374 - return true;
1375 - }
806 + } else if ((typeof(val) == "object") && val && (val.length > 0)) {
807 + return true;
1376 808 } else if (val) {
1377 809 return true;
1378 810 }
811 + }
1379 812
1380 - valid_empty = false;
1381 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1382 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1383 -
1384 - // Apply ARIA attributes
1385 - field.attr('aria-invalid', 'true');
1386 - var errorId = field.attr('id') + '-error';
1387 - field.attr('aria-describedby', errorId);
1388 -
1389 - // Get field label and build error message
1390 - var fieldLabel = getFieldLabel(field);
1391 - var errorMessage = getRequiredMessage(fieldLabel, field);
1392 - fieldErrorsList.push({
1393 - fieldId: field.attr('id'),
1394 - label: fieldLabel,
1395 - errorType: $error_type_required
1396 - });
1397 -
1398 - var inlineError = $('<div class="pfbc-inline-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1399 -
1400 - // For file inputs with help text, insert error after help text
1401 - var helpText = field.siblings('.pfbc-help').last();
1402 - if (field.is('[type="file"]') && helpText.length) {
1403 - helpText.after(inlineError);
1404 - } else {
1405 - field.after(inlineError);
1406 - }
1407 - }
813 + valid_empty = false;
814 + field.parents("#{$this->buildID} .pfbc-element").addClass('pfbc-invalid');
1408 815 });
1409 816
1410 817 $('.pfbc-textbox[type="email"]', thisform).each(function(){
1411 818 var field = $(this);
@@ -1418,126 +825,15 @@
1418 825 return true;
1419 826 }
1420 827
1421 828 valid_mail = false;
1422 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1423 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1424 -
1425 - // Apply ARIA attributes
1426 - field.attr('aria-invalid', 'true');
1427 - var errorId = field.attr('id') + '-error';
1428 - field.attr('aria-describedby', errorId);
1429 -
1430 - // Get field label and build error message
1431 - var fieldLabel = getFieldLabel(field);
1432 - var errorMessage = getEmailMessage(fieldLabel, field);
1433 -
1434 - // Add to field errors list for summary (use field ID for focusing)
1435 - fieldErrorsList.push({
1436 - fieldId: field.attr('id'),
1437 - label: fieldLabel,
1438 - errorType: $error_type_invalid_email
1439 - });
1440 -
1441 - var inlineError = $('<div class=\"pfbc-inline-error\" id=\"' + errorId + '\" role=\"alert\" aria-live=\"polite\"><div class=\"pfbc-error-message\">' + errorMessage + '</div></div>');
1442 -
1443 - // For file inputs with help text, insert error after help text
1444 - var helpText = field.siblings('.pfbc-help').last();
1445 - if (field.is('[type=\"file\"]') && helpText.length) {
1446 - helpText.after(inlineError);
1447 - } else {
1448 - field.after(inlineError);
1449 - }
829 + field.parents("#{$this->buildID} .pfbc-element").addClass('pfbc-invalid');
1450 830
1451 831 });
1452 832
1453 - // Phone validation - runs for all non-empty phone fields
1454 - $('.accuaform-telephone', thisform).each(function(){
1455 - var field = $(this);
1456 - var value = field.val();
1457 -
1458 - // Skip empty fields (Required validation handles mandatory)
1459 - if (!value || value.trim() === '') {
1460 - return true;
1461 - }
1462 -
1463 - // Skip prefix-only values (e.g. "+39") — treated as empty
1464 - var phoneTrimmed = value.trim();
1465 - if (phoneTrimmed.charAt(0) === '+' && phoneTrimmed.replace(/\D/g, '').length <= 4) {
1466 - return true;
1467 - }
1468 -
1469 - // Get country code from data attribute
1470 - var countryCode = field.attr('data-country') || 'IT';
1471 -
1472 - // Use AccuaPhoneValidation if available, otherwise skip validation
1473 - if (typeof window.AccuaPhoneValidation !== 'undefined' && window.AccuaPhoneValidation.isValid) {
1474 - if (window.AccuaPhoneValidation.isValid(value, countryCode)) {
1475 - return true;
1476 - }
1477 - } else {
1478 - // Fallback: basic validation matching server-side Phone.php
1479 - var trimmed = value.trim();
1480 - // Check for invalid characters
1481 - if (!/^[\d\s\-\.\/\(\)\+]+$/.test(trimmed)) {
1482 - // Invalid characters - fail validation
1483 - } else {
1484 - var plusIndex = trimmed.indexOf('+');
1485 - if (plusIndex > 0 || (trimmed.match(/\+/g) || []).length > 1) {
1486 - // Plus in wrong position or multiple plus signs - fail validation
1487 - } else {
1488 - var digitCount = trimmed.replace(/\D/g, '').length;
1489 - // Prefix-only (1-4 digits with +) treated as empty
1490 - if (trimmed.charAt(0) === '+' && digitCount <= 4) {
1491 - return true;
1492 - }
1493 - // Valid if 5-15 digits (matches server-side Phone.php)
1494 - if (digitCount >= 5 && digitCount <= 15) {
1495 - return true;
1496 - }
1497 - }
1498 - }
1499 - }
1500 -
1501 - valid_phone = false;
1502 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1503 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1504 -
1505 - // Remove any existing blur validation error to avoid duplicates
1506 - $('#' + field.attr('id') + '-phone-error').remove();
1507 -
1508 - // Apply ARIA attributes
1509 - field.attr('aria-invalid', 'true');
1510 - var errorId = field.attr('id') + '-error';
1511 - field.attr('aria-describedby', errorId);
1512 -
1513 - // Get field label and build error message
1514 - var fieldLabel = getFieldLabel(field);
1515 - var errorMessage = getPhoneMessage(fieldLabel, field);
1516 -
1517 - // Add to field errors list for summary
1518 - fieldErrorsList.push({
1519 - fieldId: field.attr('id'),
1520 - label: fieldLabel,
1521 - errorType: $error_type_invalid_phone
1522 - });
1523 -
1524 - var inlineError = $('<div class="pfbc-inline-error pfbc-phone-format-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1525 -
1526 - var helpText = field.siblings('.pfbc-help').last();
1527 - if (helpText.length) {
1528 - helpText.after(inlineError);
1529 - } else {
1530 - field.after(inlineError);
1531 - }
1532 - });
1533 -
1534 - if (valid_empty && valid_mail && valid_phone) {
1535 - // Show loading state in summary during AJAX submission
1536 - updateSummaryArea('loading');
1537 -
833 + if (valid_empty && valid_mail) {
834 + thisform.append(throbbler);
1538 835 _ajax_submitting_{$js_buildid} = true;
1539 - disableSubmitButton();
1540 836 $('input[name="_AccuaForm_tentatives"]', thisform).val(fail_count);
1541 837 disabled_fields = $("input, textarea, button, select", thisform).not('[type="submit"]').not(':disabled');
1542 838 disabled_fields.attr('readonly','readonly');
1543 839 timeout_count = 0;
@@ -1547,20 +843,17 @@
1547 843 }
1548 844 return true;
1549 845 } else {
1550 846 ga_submit_event('formSubmitInvalid');
1551 -
1552 - // Update URL hash to reflect invalid state (for GA tracking and bookmarkability)
1553 - if (history.replaceState) {
1554 - history.replaceState(null, '', '#formSubmitInvalid-'+anchor_id);
847 + location.href = "#formSubmitInvalid-"+anchor_id;
848 + var message = '';
849 + if (!valid_empty) {
850 + message += '\\x3Cli\\x3E' + $required_message + '\\x3C/li\\x3E';
1555 851 }
1556 -
1557 - // Update summary area with error list
1558 - updateSummaryArea(false);
1559 -
1560 - // Focus on first invalid field for accessibility
1561 - focusFirstInvalidField();
1562 -
852 + if (!valid_mail) {
853 + message += '\\x3Cli\\x3E' + $valid_mail_message + '\\x3C/li\\x3E';
854 + }
855 + show_error_messages(message);
1563 856 return false;
1564 857 }
1565 858 }
1566 859
@@ -1602,10 +895,9 @@
1602 895 if (_ajax_submitting_{$js_buildid}) {
1603 896 var response = false;
1604 897 try {
1605 898 response = $.parseJSON(message.data);
1606 - // Accept response if jsuuid matches AND buildID matches or is null (server rejection)
1607 - if (response.jsuuid != jsuuid || (response.buildID != null && response.buildID != "{$this->buildID}")) {
899 + if (response.jsuuid != jsuuid || response.buildID != "{$this->buildID}") {
1608 900 response = false;
1609 901 }
1610 902 } catch (err) {
1611 903 response = false;
@@ -1618,25 +910,13 @@
1618 910
1619 911 _handle_ajax_submit_response_{$js_buildid} = function(response) {
1620 912 if (_ajax_submitting_{$js_buildid}) {
1621 913 if(response && typeof(response) == "object" && typeof(response.submitted) == "boolean") {
1622 - // Only show message container if there's actual content
1623 - if (response.messages && response.messages.trim() !== '') {
1624 - response_messages.html(response.messages).show();
1625 - } else {
1626 - response_messages.empty().hide();
1627 - }
914 + response_messages.html(response.messages);
1628 915 if (response.submitted) {
1629 916 if (response.valid) {
1630 - var gads_track_code = "{$this->gads_conversion_tracking_code}";
1631 - if(gads_track_code != ''){
1632 - gtag('event', 'conversion', {'send_to': gads_track_code});
1633 - }
1634 -
1635 917 ga_submit_event('formSubmitSuccess');
1636 - smoothScrollToElement('formSubmitSuccess-'+anchor_id);
1637 -
1638 -
918 + location.href = "#formSubmitSuccess-"+anchor_id;
1639 919 JS;
1640 920 /*A callback function can be specified to handle any post submission events.*/
1641 921 if(!empty($this->ajaxCallback)) {
1642 922 echo $this->ajaxCallback, "(response);";
@@ -1645,9 +925,9 @@
1645 925 }
1646 926 echo <<<JS
1647 927 } else {
1648 928 ga_submit_event('formSubmitInvalid');
1649 - smoothScrollToElement('formSubmitInvalid-'+anchor_id);
929 + location.href = "#formSubmitInvalid-"+anchor_id;
1650 930 JS;
1651 931 if (method_exists($this->error,'applyAjaxErrorResponseUsingShowErrorMessages')) {
1652 932 $this->error->applyAjaxErrorResponseUsingShowErrorMessages();
1653 933 } else {
@@ -1663,28 +943,13 @@
1663 943 $("input[name='_AccuaForm_iv']", thisform).val(response._AccuaForm_iv);
1664 944 $("input[name='_AccuaForm_data']",thisform).val(response._AccuaForm_data);
1665 945
1666 946 disabled_fields.removeAttr('readonly');
1667 - enableSubmitButton();
1668 947 }
1669 - } else {
1670 - // Server did not recognize the form submission (e.g. expired nonce, stale cached page)
1671 - ga_submit_event('formSubmitError');
1672 - smoothScrollToElement('formSubmitError-'+anchor_id);
1673 - fail_count++;
1674 - if (fail_count > 2) {
1675 - ajax_enabled = false;
1676 - thisform.attr("action", {$post_url} );
1677 - thisform.removeAttr("target");
1678 - $('input[name="_AccuaForm_submit_method"]', thisform).val('fallback');
1679 - }
1680 - show_error_messages( $submit_fail_message );
1681 - disabled_fields.removeAttr('readonly');
1682 - enableSubmitButton();
1683 948 }
1684 949 } else {
1685 950 ga_submit_event('formSubmitError');
1686 - smoothScrollToElement('formSubmitError-'+anchor_id);
951 + location.href = "#formSubmitError-"+anchor_id;
1687 952 fail_count++;
1688 953 if (fail_count > 2) {
1689 954 ajax_enabled = false;
1690 955 thisform.attr("action", {$post_url} );
@@ -1691,9 +956,8 @@
1691 956 thisform.removeAttr("target");
1692 957 $('input[name="_AccuaForm_submit_method"]', thisform).val('fallback');
1693 958 }
1694 959 show_error_messages( $submit_fail_message );
1695 - enableSubmitButton();
1696 960 }
1697 961 $('.accua_forms_show_recaptcha_button', thisform).click();
1698 962 if (((typeof accuaform_recaptcha2_initialized) != 'undefined') && accuaform_recaptcha2_initialized) {
1699 963 $('.accua_forms_recaptcha2_container', thisform).each(function(){
@@ -1699,8 +963,9 @@
1699 963 $('.accua_forms_recaptcha2_container', thisform).each(function(){
1700 964 accua_forms_reload_recaptcha2($(this).attr('id'));
1701 965 });
1702 966 }
967 + throbbler.remove();
1703 968 _ajax_submitting_{$js_buildid} = false;
1704 969 if (timeout_handler) {
1705 970 clearTimeout(timeout_handler);
1706 971 timeout_handler = false;
@@ -1718,172 +983,12 @@
1718 983 } else {
1719 984 thisform.attr("action", {$post_url} );
1720 985 }
1721 986 thisform.attr("onsubmit","return _handle_ajax_submit_{$js_buildid}()");
1722 -
1723 - // Real-time validation for better UX - use change only for checkbox/radio to avoid double-firing
1724 - $('.accuaforms-field-required', thisform).on('change', function() {
1725 - var field = $(this);
1726 - var type = field.attr('type');
1727 - if (type !== 'checkbox' && type !== 'radio') return; // Only handle checkbox/radio on change
1728 -
1729 - var fieldName = field.attr('name');
1730 - var isChecked = $("[name='"+fieldName+"']:checked", thisform).length > 0;
1731 - var groupContainer = field.closest('.pfbc-element, .pfbc-fieldwrap');
1732 - var hasError = groupContainer.hasClass('pfbc-element-has-error');
1733 -
1734 - // Only act if state actually changed to avoid flashing
1735 - if (isChecked && hasError) {
1736 - // Valid now - remove error with animation
1737 - groupContainer.removeClass('pfbc-invalid pfbc-element-has-error');
1738 - $("[name='"+fieldName+"']", thisform).attr('aria-invalid', 'false');
1739 - $("[name='"+fieldName+"']", thisform).removeAttr('aria-describedby');
1740 - removeGroupErrors(field, fieldName, true); // animate=true
1741 - } else if (!isChecked && !hasError && submitAttempted) {
1742 - // Invalid now and we've attempted submit - show error
1743 - var errorId = getGroupErrorId(field);
1744 - groupContainer.addClass('pfbc-invalid pfbc-element-has-error');
1745 - $("[name='"+fieldName+"']", thisform).attr('aria-invalid', 'true');
1746 - $("[name='"+fieldName+"']", thisform).attr('aria-describedby', errorId);
1747 -
1748 - var fieldLabel = getFieldLabel(groupContainer);
1749 - var firstField = $("[name='"+fieldName+"']", thisform).first();
1750 - var errorMessage = getRequiredMessage(fieldLabel, firstField);
1751 - var inlineError = $('<div class="pfbc-inline-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1752 -
1753 - var lastInGroup = $("[name='"+fieldName+"']", thisform).last();
1754 - var lastWrapper = lastInGroup.closest('.pfbc-radio, .pfbc-checkbox');
1755 - if (lastWrapper.length) {
1756 - lastWrapper.after(inlineError);
1757 - } else {
1758 - lastInGroup.after(inlineError);
1759 - }
1760 - }
1761 - });
1762 -
1763 - // Blur handler for text-like fields only
1764 - $('.accuaforms-field-required', thisform).on('blur', function() {
1765 - var field = $(this);
1766 - var type = field.attr('type');
1767 - var fieldName = field.attr('name');
1768 - var isEmpty = false;
1769 -
1770 - // Skip checkbox/radio - handled by change event above
1771 - if (type === 'checkbox' || type === 'radio') return;
1772 -
1773 - var val = field.val();
1774 - if (typeof(val) == "string") {
1775 - // Treat "-" and "Select..." as invalid only for dropdowns
1776 - // For telephone fields, prefix-only values (e.g. "+39") are also empty
1777 - var isSelect = field.is('select');
1778 - isEmpty = val.match(/^\s*$/) || (isSelect && (val === "Select..." || val === "-")) || isTelephonePrefixOnly(field);
1779 - } else if (typeof(val) == "object") {
1780 - isEmpty = !val || val.length === 0;
1781 - } else {
1782 - isEmpty = !val;
1783 - }
1784 -
1785 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1786 - var errorId = field.attr('id') + '-error';
1787 -
1788 - if (isEmpty) {
1789 - // For telephone: phone-validation.js blur handler runs AFTER this one and may
1790 - // still have pfbc-element-has-error set from a previous format error. Check for
1791 - // the specific required error div instead of the parent class to avoid skipping.
1792 - var alreadyHasError = field.hasClass('accuaform-telephone')
1793 - ? $('#' + errorId).length > 0
1794 - : parent.hasClass('pfbc-element-has-error');
1795 - if (!alreadyHasError) {
1796 - // For telephone: remove leftover phone format error since the field is now
1797 - // empty (required error takes priority). phone-validation.js will also clean
1798 - // up on its blur, but this handler runs first.
1799 - if (field.hasClass('accuaform-telephone')) {
1800 - $('#' + field.attr('id') + '-phone-error').remove();
1801 - }
1802 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1803 - field.attr('aria-invalid', 'true');
1804 - field.attr('aria-describedby', errorId);
1805 -
1806 - // Get field label for error message
1807 - var fieldLabel = getFieldLabel(parent);
1808 - var errorMessage = getRequiredMessage(fieldLabel, field);
1809 - var inlineError = $('<div class="pfbc-inline-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1810 -
1811 - // For file inputs with help text, insert error after help text
1812 - var helpText = field.siblings('.pfbc-help').last();
1813 - if (field.is('[type="file"]') && helpText.length) {
1814 - helpText.after(inlineError);
1815 - } else {
1816 - field.after(inlineError);
1817 - }
1818 - }
1819 - } else {
1820 - // Field is not empty — clear required-related errors.
1821 - if (field.hasClass('accuaform-telephone')) {
1822 - // For telephone fields: clear only the required error ({id}-error).
1823 - // phone-validation.js has already run on this same blur event and set the
1824 - // correct error state (phone-error or clean). We must not undo its work.
1825 - // Only remove the required-error div; preserve phone-validation.js state.
1826 - $('#' + errorId).remove();
1827 - // If phone-validation.js left no errors, clear the container state too
1828 - if (!parent.find('.pfbc-inline-error').length) {
1829 - parent.removeClass('pfbc-invalid pfbc-element-has-error');
1830 - field.attr('aria-invalid', 'false');
1831 - field.removeAttr('aria-describedby');
1832 - }
1833 - } else {
1834 - // For non-telephone fields: animated removal for smoother UX
1835 - removeErrorAnimated(errorId);
1836 - parent.removeClass('pfbc-invalid pfbc-element-has-error');
1837 - field.attr('aria-invalid', 'false');
1838 - field.removeAttr('aria-describedby');
1839 - }
1840 - }
1841 - });
1842 -
1843 - $('.pfbc-textbox[type="email"]', thisform).on('blur change', function() {
1844 - var field = $(this);
1845 - var val = field.val();
1846 -
1847 - if (val.match(/^\s*$/)) {
1848 - return; // Empty is handled by required validation
1849 - }
1850 -
1851 - var isValid = val.match(/^([a-zA-Z0-9_.+%-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9])+$/);
1852 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1853 - var errorId = field.attr('id') + '-error';
1854 -
1855 - if (!isValid) {
1856 - if (!$('#' + errorId + ':not(.pfbc-error-removing)').length) {
1857 - $('#' + errorId).remove();
1858 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1859 - field.attr('aria-invalid', 'true');
1860 - field.attr('aria-describedby', errorId);
1861 -
1862 - // Get field label for error message
1863 - var fieldLabel = getFieldLabel(parent);
1864 - var errorMessage = getEmailMessage(fieldLabel, field);
1865 - var inlineError = $('<div class=\"pfbc-inline-error\" id=\"' + errorId + '\" role=\"alert\" aria-live=\"polite\"><div class=\"pfbc-error-message\">' + errorMessage + '</div></div>');
1866 -
1867 - // For file inputs with help text, insert error after help text
1868 - var helpText = field.siblings('.pfbc-help').last();
1869 - if (field.is('[type=\"file\"]') && helpText.length) {
1870 - helpText.after(inlineError);
1871 - } else {
1872 - field.after(inlineError);
1873 - }
1874 - }
1875 - } else {
1876 - parent.removeClass('pfbc-invalid pfbc-element-has-error');
1877 - field.attr('aria-invalid', 'false');
1878 - field.removeAttr('aria-describedby');
1879 - removeErrorAnimated(errorId);
1880 - }
1881 - });
1882 987 });
1883 988 // -->
1884 989 </script>
1885 -<iframe id="submit_target_{$js_buildid}" title="Notification Message" name="submit_target_{$js_buildid}" onload="_handle_ajax_submit_complete_{$js_buildid}()" onerror="_handle_ajax_submit_complete_{$js_buildid}()" style="width:0;height:0;border:0px solid #fff"></iframe>
990 +<iframe id="submit_target_{$js_buildid}" title="" name="submit_target_{$js_buildid}" onload="_handle_ajax_submit_complete_{$js_buildid}()" onerror="_handle_ajax_submit_complete_{$js_buildid}()" style="width:0;height:0;border:0px solid #fff"></iframe>
1886 991 JS;
1887 992 }
1888 993
1889 994 echo <<<JSREFERRER
@@ -1925,28 +1030,8 @@
1925 1030 echo 'jQuery(document).ready(function() {';
1926 1031 /*jQuery is used to set the focus of the form's initial element.*/
1927 1032 if(!in_array("focus", $this->prevent))
1928 1033 echo 'jQuery("#', $id, ' :input:visible:enabled:first").focus();';
1929 -
1930 - // Accessibility: Focus management for validation errors on page load
1931 - echo <<<JS
1932 -
1933 - // If there are errors on page load, focus the error summary or first invalid field
1934 - if (jQuery('.pfbc-error', '#{$id}').length > 0) {
1935 - setTimeout(function() {
1936 - var errorContainer = jQuery('.pfbc-error', '#{$id}').first();
1937 - errorContainer.attr('tabindex', '-1').focus();
1938 - }, 100);
1939 - } else if (jQuery('.pfbc-element-has-error', '#{$id}').length > 0) {
1940 - setTimeout(function() {
1941 - var firstInvalidField = jQuery('.pfbc-element-has-error :input:visible:enabled:first', '#{$id}').first();
1942 - if (firstInvalidField.length) {
1943 - firstInvalidField.focus();
1944 - }
1945 - }, 100);
1946 - }
1947 -
1948 -JS;
1949 1034
1950 1035 $this->view->jQueryDocumentReady();
1951 1036 foreach($this->elements as $element) {
1952 1037 $element->jQueryDocumentReady();
@@ -1988,202 +1073,9 @@
1988 1073 });
1989 1074 // -->
1990 1075 </script>
1991 1076 JS;
1992 -
1993 - // Add JavaScript for inline label floating behavior
1994 - if (strpos($this->attributes['class'], 'accua-form-view-inlinelabel') !== false) {
1995 - echo <<<INLINELABELJS
1996 -<script type="text/javascript">
1997 -<!--
1998 -jQuery(function($) {
1999 - var form = $('#{$this->attributes["id"]}');
2000 -
2001 - /**
2002 - * Inline Label Floating Behavior
2003 - *
2004 - * Handles the Material Design floating label animation:
2005 - * - Floats label up when field is focused
2006 - * - Keeps label up when field has value
2007 - * - Returns label to inline position when field is empty and unfocused
2008 - *
2009 - * Accessibility features:
2010 - * - Maintains proper ARIA relationships
2011 - * - Works with keyboard navigation
2012 - * - Compatible with screen readers
2013 - * - Supports autofill detection
2014 - */
2015 -
2016 - // Function to check if field has value
2017 - function hasValue(field) {
2018 - var val = field.val();
2019 - // For select elements, check if selected value is not empty
2020 - if (field.is('select')) {
2021 - return val !== null && val !== '' && val !== undefined;
2022 - }
2023 - // For date inputs, check if value is set (format: YYYY-MM-DD)
2024 - if (field.attr('type') === 'date') {
2025 - return val !== null && val !== '' && val !== undefined;
2026 - }
2027 - // For text inputs and textareas
2028 - return val !== null && val !== '' && val.trim() !== '';
2029 1077 }
2030 -
2031 - // Function to update wrapper state
2032 - function updateWrapperState(wrapper) {
2033 - var field = wrapper.find('.pfbc-textbox, .pfbc-textarea, .pfbc-select').first();
2034 - var postSelectWrapper = wrapper.find('.pfbc-post-select-wrapper');
2035 -
2036 - // Check if this is a post-select field
2037 - if (postSelectWrapper.length && postSelectWrapper.attr('data-enhanced') === 'true') {
2038 - // For enhanced post-select, check the hidden native select for value
2039 - var nativeSelect = postSelectWrapper.find('select');
2040 - var trigger = postSelectWrapper.find('.pfbc-post-select-trigger');
2041 - var container = postSelectWrapper.find('.pfbc-post-select-container');
2042 -
2043 - var isFocused = trigger.is(':focus') || container.hasClass('open');
2044 - var fieldHasValue = nativeSelect.length && hasValue(nativeSelect);
2045 -
2046 - wrapper.toggleClass('is-focused', isFocused);
2047 - wrapper.toggleClass('has-value', fieldHasValue);
2048 - return;
2049 - }
2050 -
2051 - if (!field.length) return;
2052 -
2053 - var isFocused = field.is(':focus');
2054 - var fieldHasValue = hasValue(field);
2055 -
2056 - // Update wrapper classes
2057 - wrapper.toggleClass('is-focused', isFocused);
2058 - wrapper.toggleClass('has-value', fieldHasValue);
2059 -
2060 - // For date inputs, add class directly to field for Firefox CSS support
2061 - if (field.attr('type') === 'date') {
2062 - field.toggleClass('has-value', fieldHasValue);
2063 - }
2064 -
2065 - // Accessibility: Update ARIA state
2066 - var label = wrapper.find('.pfbc-floating-label');
2067 - if (label.length) {
2068 - // Ensure label is always associated with field
2069 - var fieldId = field.attr('id');
2070 - if (fieldId && !field.attr('aria-labelledby')) {
2071 - // Label is already associated via for/id
2072 - // Additional ARIA not needed, but we ensure proper semantics
2073 - }
2074 - }
2075 - }
2076 -
2077 - // Initialize all inline label wrappers
2078 - form.find('.pfbc-inline-label-wrapper').each(function() {
2079 - var wrapper = $(this);
2080 - updateWrapperState(wrapper);
2081 - });
2082 -
2083 - // Handle focus events
2084 - form.on('focus', '.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select', function() {
2085 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2086 - updateWrapperState(wrapper);
2087 - });
2088 -
2089 - // Handle blur events
2090 - form.on('blur', '.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select', function() {
2091 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2092 - // Small delay to allow value to be set
2093 - setTimeout(function() {
2094 - updateWrapperState(wrapper);
2095 - }, 10);
2096 - });
2097 -
2098 - // Handle input/change events to detect value changes
2099 - form.on('input change', '.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select', function() {
2100 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2101 - updateWrapperState(wrapper);
2102 - });
2103 -
2104 - // Handle post-select trigger focus/blur events
2105 - form.on('focus', '.pfbc-inline-label-wrapper .pfbc-post-select-trigger', function() {
2106 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2107 - updateWrapperState(wrapper);
2108 - });
2109 -
2110 - form.on('blur', '.pfbc-inline-label-wrapper .pfbc-post-select-trigger', function() {
2111 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2112 - setTimeout(function() {
2113 - updateWrapperState(wrapper);
2114 - }, 50);
2115 - });
2116 -
2117 - // Handle post-select value changes (native select change event)
2118 - form.on('change', '.pfbc-inline-label-wrapper .pfbc-post-select-wrapper select', function() {
2119 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2120 - updateWrapperState(wrapper);
2121 - });
2122 -
2123 - // Observe post-select container for open/close state changes
2124 - if (window.MutationObserver) {
2125 - form.find('.pfbc-inline-label-wrapper .pfbc-post-select-container').each(function() {
2126 - var container = this;
2127 - var wrapper = $(container).closest('.pfbc-inline-label-wrapper');
2128 - var containerObserver = new MutationObserver(function(mutations) {
2129 - mutations.forEach(function(mutation) {
2130 - if (mutation.attributeName === 'class') {
2131 - updateWrapperState(wrapper);
2132 - }
2133 - });
2134 - });
2135 - containerObserver.observe(container, {
2136 - attributes: true,
2137 - attributeFilter: ['class']
2138 - });
2139 - });
2140 - }
2141 -
2142 - // Handle browser autofill (multiple browser support)
2143 - // Chrome/Safari autofill detection
2144 - if (window.MutationObserver) {
2145 - var observer = new MutationObserver(function(mutations) {
2146 - mutations.forEach(function(mutation) {
2147 - if (mutation.attributeName === 'value' || mutation.attributeName === 'class') {
2148 - var target = $(mutation.target);
2149 - if (target.hasClass('pfbc-textbox') || target.hasClass('pfbc-textarea') || target.hasClass('pfbc-select')) {
2150 - var wrapper = target.closest('.pfbc-inline-label-wrapper');
2151 - if (wrapper.length) {
2152 - updateWrapperState(wrapper);
2153 - }
2154 - }
2155 - }
2156 - });
2157 - });
2158 -
2159 - form.find('.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select').each(function() {
2160 - observer.observe(this, {
2161 - attributes: true,
2162 - attributeFilter: ['value', 'class']
2163 - });
2164 - });
2165 - }
2166 -
2167 - // Fallback autofill detection with animation frame checking
2168 - setTimeout(function() {
2169 - form.find('.pfbc-inline-label-wrapper').each(function() {
2170 - updateWrapperState($(this));
2171 - });
2172 - }, 100);
2173 -
2174 - // Additional check for autofill after a short delay
2175 - setTimeout(function() {
2176 - form.find('.pfbc-inline-label-wrapper').each(function() {
2177 - updateWrapperState($(this));
2178 - });
2179 - }, 500);
2180 -});
2181 -// -->
2182 -</script>
2183 -INLINELABELJS;
2184 - }
2185 - }
2186 1078
2187 1079 protected function renderCSS() {
2188 1080
2189 1081 }
@@ -2237,8 +1129,9 @@
2237 1129
2238 1130 public static function ajaxSubmit() {
2239 1131 $ret = array(
2240 1132 'valid' => false,
1133 + 'messages' => self::getSubmittedMessages(),
2241 1134 'jsuuid' => self::$rawData['_AccuaForm_jsuuid'],
2242 1135 'buildID' => self::$submittedBuildID,
2243 1136 'files' => array(),
2244 1137 );
@@ -2245,16 +1138,9 @@
2245 1138 if ($ret['submitted'] = self::isSubmit()){
2246 1139 if ($ret['valid'] = self::isValid()) {
2247 1140
2248 1141 } else {
2249 - $errorResponse = self::getAjaxErrorResponse();
2250 - // Support modern error format with both flat list and structured data for ARIA
2251 - if (is_array($errorResponse) && isset($errorResponse['errors'])) {
2252 - $ret = array_merge($ret, $errorResponse);
2253 - } else {
2254 - // Backwards compatibility: old format returned just the flat array
2255 - $ret['errors'] = $errorResponse;
2256 - }
1142 + $ret['errors'] = self::getAjaxErrorResponse();
2257 1143 }
2258 1144 $form = self::$submittedForm;
2259 1145 foreach ($form->files as $fieldname => $file) {
2260 1146 if (!empty($file['name'])) {
@@ -2261,14 +1147,8 @@
2261 1147 $ret['files'][$fieldname] = $form->getElementByName($fieldname)->getAlreadySubmittedText();
2262 1148 }
2263 1149 }
2264 1150 $ret += $form->wp_save();
2265 - // Get messages AFTER isValid() and wp_save() have completed
2266 - // This ensures email sending results are captured in the messages
2267 - $ret['messages'] = self::getSubmittedMessages(self::$submittedID);
2268 - } else {
2269 - // Form not submitted yet - no messages to show
2270 - $ret['messages'] = '';
2271 1151 }
2272 1152 return $ret;
2273 1153 }
2274 1154 }