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 -1211 2.2.41.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,9 +41,8 @@
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 47 load_plugin_textdomain( 'contact-forms', false, ACCUA_FORM_API_PLUGIN_TEXTDOMAIN_PATH);
59 48 require_once( ABSPATH . WPINC . '/locale.php' );
@@ -69,9 +58,8 @@
69 58 if ($this->forced_language) {
70 59 unset($GLOBALS['wp_locale']);
71 60 unset($GLOBALS['l10n']);
72 61
73 - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- qTranslate plugin integration
74 62 $GLOBALS['q_config']['language'] = $this->original_language;
75 63 $GLOBALS['l10n'] =& $this->original_l10n;
76 64 $GLOBALS['wp_locale'] =& $this->original_locale;
77 65 if ($GLOBALS['wp_locale']) {
@@ -145,10 +133,8 @@
145 133 error_log("headers already sent at {$file}:{$line}");
146 134 }
147 135 }
148 136 $sessionid = session_id();
149 - // Close the session immediately after getting the ID to prevent REST API interference
150 - session_write_close();
151 137 }
152 138 return $sessionid;
153 139 }
154 140
@@ -154,17 +140,13 @@
154 140
155 141 public static function getBaseURL() {
156 142 static $ret = null;
157 143 if ($ret === null) {
158 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
159 144 $s = empty($_SERVER["HTTPS"]) ? '' : (($_SERVER["HTTPS"] == "on") ? "s" : "");
160 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
161 - $sp = isset($_SERVER["SERVER_PROTOCOL"]) ? strtolower(sanitize_text_field(wp_unslash($_SERVER["SERVER_PROTOCOL"]))) : 'http/1.1';
145 + $sp = strtolower($_SERVER["SERVER_PROTOCOL"]);
162 146 $protocol = substr($sp, 0, strpos($sp, "/")) . $s;
163 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
164 - $port = (!isset($_SERVER["SERVER_PORT"]) || $_SERVER["SERVER_PORT"] == "80") ? "" : (":" . absint($_SERVER["SERVER_PORT"]));
165 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
166 - $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;
167 149 }
168 150 return $ret;
169 151 }
170 152
@@ -203,9 +185,8 @@
203 185 'layout' => 'sidebyside',
204 186 'title' => '',
205 187 'track_submit' => false,
206 188 'track_fields' => false,
207 - 'gads_conversion_tracking_code' => '',
208 189 );
209 190 if (is_array($params)) {
210 191 $params += $predefined_params;
211 192 $width = $params['width'];
@@ -213,11 +194,8 @@
213 194 $width = $params;
214 195 $params = $predefined_params;
215 196 $params['width'] = $width;
216 197 }
217 - // Always initialize the property to avoid undefined property warnings
218 - $this->gads_conversion_tracking_code = $params['gads_conversion_tracking_code'];
219 -
220 198 $class = 'accua-form ' . $id;
221 199 switch ($params['layout']) {
222 200 case 'toplabel':
223 201 $this->view = new AccuaForm_View_Standard();
@@ -222,14 +200,8 @@
222 200 case 'toplabel':
223 201 $this->view = new AccuaForm_View_Standard();
224 202 $class .= ' accua-form-view-standard';
225 203 break;
226 - case 'inlinelabel':
227 - $this->view = new AccuaForm_View_InlineLabel();
228 - $class .= ' accua-form-view-inlinelabel';
229 - // Prevent PFBC from adding inline width styles for inline label layout
230 - $width = '';
231 - break;
232 204 case 'sidebyside':
233 205 default:
234 206 $this->view = new AccuaForm_View_SideBySide(
235 207 '19',
@@ -240,11 +212,9 @@
240 212 $this->error = new AccuaForm_Error_Standard(array(
241 213 'errorfound' => '',
242 214 'errorsfound' => '',
243 215 ));
244 - // For inline label view, we don't set default width
245 - // The CSS handles all widths with width: 100%
246 - if ($width === "" && $params['layout'] !== 'inlinelabel'){
216 + if ($width === ""){
247 217 $width = "100%";
248 218 }
249 219 $this->attributes = array(
250 220 'class' => $class,
@@ -276,10 +246,11 @@
276 246 * - Quando ricevi il form, decripta _AccuaForm_serialized e controlla che sia valido
277 247 *
278 248 * */
279 249
250 + //$this->addElement(new Element_Hidden('PHPSESSID', self::sessionID()));
280 251 $this->prevent = array('jQuery', 'jQueryUI', 'jQueryUIButtons', 'focus', 'style');
281 - $this->configure(array('action' => '#'));
252 + $this->configure(array('action' => ''));
282 253
283 254 if (function_exists('qtrans_getLanguage')) {
284 255 $this->language = qtrans_getLanguage();
285 256 $this->locale = $GLOBALS['q_config']['locale'][$this->language];
@@ -291,23 +262,18 @@
291 262
292 263 global $post;
293 264 $pid = empty($post->ID) ? 0 : $post->ID;
294 265
295 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- URL stored for logging, escaped on output
296 - $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'];
297 267
298 268 $url = self::getBaseURL() . $uri ;
299 269
300 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Referrer stored for logging, escaped on output
301 - $referrer = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
270 + $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
302 271
303 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- IP stored for logging, escaped on output
304 - $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
305 -
306 272 $this->stats = array(
307 273 'pid' => $pid,
308 - 'ip' => $remote_addr, //updated after submit
309 - '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
310 276 'uri' => $uri,
311 277 'url' => $url,
312 278 'referrer' => $referrer, //updated after submit using javascript
313 279 'original_referrer' => $referrer, //unreliable if using a static page caching system
@@ -331,44 +297,41 @@
331 297 }
332 298 self::$submitted = false;
333 299 $sessid = self::sessionID();
334 300
335 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Checking request method for form handling
336 - $request_method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : 'GET';
337 - if ($request_method === 'POST') {
301 + if($_SERVER['REQUEST_METHOD'] == 'POST') {
338 302 self::$rawData = stripslashes_deep($_POST);
339 303 } else {
340 304 self::$rawData = stripslashes_deep($_GET);
341 305 }
342 306
343 - // Ensure basic fields for nonce verification are present.
344 - 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'])) {
345 309 return false;
346 310 }
347 311
348 - // Verify nonce for CSRF protection (moved earlier).
349 - // The $id variable (action for nonce) is self::$rawData['_AccuaForm_buildID'].
350 - $requested_buildID = self::$rawData['_AccuaForm_buildID'];
351 - if (!wp_verify_nonce(self::$rawData['_AccuaForm_wpnonce'], $requested_buildID)) {
312 + /*
313 + if (self::$rawData['PHPSESSID'] !== $sessid) {
352 314 return false;
353 315 }
316 + */
354 317
355 - // Now check for fields required for form recovery and other validations.
356 - if (empty(self::$rawData['_AccuaForm_ID'])
357 - || 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)) {
358 322 return false;
359 323 }
360 324
361 - // $id was self::$rawData['_AccuaForm_buildID'], now using $requested_buildID for clarity.
362 - $form = self::wp_recover(self::$rawData['_AccuaForm_hash'], self::$rawData['_AccuaForm_iv'], self::$rawData['_AccuaForm_data']);
363 -
364 - if (empty($form)) {
325 + if ($form->formID !== self::$rawData['_AccuaForm_ID'] || $form->buildID !== $id ) {
365 326 return false;
366 327 }
367 328
368 - if ($form->formID !== self::$rawData['_AccuaForm_ID'] || $form->buildID !== $requested_buildID ) {
329 + /*
330 + if (!wp_verify_nonce(self::$rawData['_AccuaForm_wpnonce'], $id)) {
369 331 return false;
370 332 }
333 + */
371 334
372 335 /* check if already submitted from uuid and other parameters */
373 336 $already_submitted = false;
374 337 if (!empty(self::$rawData['_AccuaForm_jsuuid'])) {
@@ -373,20 +336,13 @@
373 336 $already_submitted = false;
374 337 if (!empty(self::$rawData['_AccuaForm_jsuuid'])) {
375 338 if (preg_match("/^[a-z0-9]{25}$/", self::$rawData['_AccuaForm_jsuuid'])) {
376 339 $already_submitted_data = get_transient('accuaformsub_'.self::$rawData['_AccuaForm_jsuuid']);
377 - if ($already_submitted_data && $already_submitted_data['buildID'] == $form->buildID) {
378 - // Only use cached result for successful submissions to prevent duplicate processing
379 - if (!empty($already_submitted_data['valid'])) {
380 - $form = $already_submitted_data['form'];
381 - self::$submittedMessages = $already_submitted_data['submittedMessages'];
382 - self::$valid = true;
383 - $already_submitted = true;
384 - } else {
385 - // For failed validations, delete the transient so form can be re-validated
386 - // This fixes the bug where correcting a field and resubmitting caused form to disappear
387 - delete_transient('accuaformsub_'.self::$rawData['_AccuaForm_jsuuid']);
388 - }
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;
389 345 }
390 346 } else {
391 347 return false;
392 348 }
@@ -393,10 +349,9 @@
393 349 }
394 350
395 351 if (!$already_submitted) {
396 352 $form->stats['submitted'] = time();
397 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- IP stored for logging, escaped on output
398 - $form->stats['ip'] = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
353 + $form->stats['ip'] = $_SERVER['REMOTE_ADDR'];
399 354 foreach (array('referrer', 'jsuuid', 'user_agent', 'platform', 'tentatives', 'submit_method') as $i){
400 355 $form->stats[$i] = isset(self::$rawData["_AccuaForm_{$i}"]) ? ((string)self::$rawData["_AccuaForm_{$i}"]) : '';
401 356 }
402 357 }
@@ -443,16 +398,14 @@
443 398 not $_GET or $_POST.*/
444 399 if($element instanceof AccuaForm_Element_File) {
445 400 if (!empty($form->files[$name]['name'])) {
446 401 $value = $form->files[$name]['name'];
447 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- File handling uses $_FILES, nonce verified earlier
448 - } 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)) {
449 403 //$file = $_FILES[$name];
450 404 //include_once( ABSPATH . '/wp-admin/includes/file.php' );
451 405 //$overrides = array( 'test_form' => false );
452 406 //$file = wp_handle_upload( $file, $overrides );
453 407
454 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File array passed to WordPress handler
455 408 $file = $element->handle_upload($_FILES[$name]);
456 409 $value = $file['name'];
457 410
458 411 if (empty($file['errors'])) {
@@ -465,20 +418,17 @@
465 418 } else {
466 419 $value = null;
467 420 }
468 421 } else if ($element instanceof Element_File){
469 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Legacy file handling
470 - $value = isset($_FILES[$name]["name"]) ? sanitize_file_name($_FILES[$name]["name"]) : '';
422 + $value = $_FILES[$name]["name"];
471 423 } else if (isset(self::$rawData[$name])) {
472 424 $value = self::$rawData[$name];
473 425 if(is_array($value)) {
474 426 foreach($value as $key => $value_i) {
475 - // Sanitize null bytes to prevent mail() ValueError in PHP 8+
476 - $value[$key] = str_replace("\0", '', (string) $value_i);
427 + $value[$key] = (string) $value_i;
477 428 }
478 429 } else {
479 - // Sanitize null bytes to prevent mail() ValueError in PHP 8+
480 - $value = str_replace("\0", '', (string) $value);
430 + $value = (string) $value;
481 431 }
482 432 } else {
483 433 $value = null;
484 434 }
@@ -495,11 +445,12 @@
495 445 }
496 446 }
497 447
498 448 if($invalidFile) {
499 - // File cleanup is handled in handle_upload() when validation fails
500 - // The uploaded tmp file is already deleted by PHP after request completes
501 - unset($form->files[$name]);
449 + if (isset($file['tmp_name'])) {
450 + unlink($element->getDestPath . $file['tmp_name']);
451 + unset($form->files[$name]);
452 + }
502 453 } else if($name !== '') {
503 454 self::$submittedData[$name] = $value;
504 455 }
505 456 }
@@ -531,38 +482,28 @@
531 482 }
532 483 }
533 484 }
534 485
535 - /*Apply errors from session to form elements for accessibility (ARIA attributes).
536 - This must be done BEFORE session_write_close() is called, so errors persist in element objects.*/
537 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form error handling
538 - if (!$valid && !empty($_SESSION["pfbc"][$id]["errors"])) {
539 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form error handling
540 - $sessionErrors = $_SESSION["pfbc"][$id]["errors"];
541 - $elements = $form->getElements();
542 - if (!empty($elements)) {
543 - foreach ($elements as $element) {
544 - $name = $element->getName();
545 - if (substr($name, -2) == "[]")
546 - $name = substr($name, 0, -2);
547 -
548 - if (isset($sessionErrors[$name]) && is_array($sessionErrors[$name])) {
549 - $element->setErrors($sessionErrors[$name]);
550 - }
551 - }
552 - }
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);
553 492 }
493 + */
554 494
495 + //TODO: Should i save here?
496 + //$form->save();
497 + //$_SESSION["pfbc"][$id]["form"] = serialize($form);
498 +
555 499 $form->restore_language();
556 500
557 - /*Store form with errors in transient for accessibility on redirect.
558 - Both successful and failed submissions are stored so the form can be properly re-rendered with ARIA attributes.*/
559 - if (self::$rawData['_AccuaForm_jsuuid'] ) {
501 + if ($valid && self::$rawData['_AccuaForm_jsuuid'] ) {
560 502 set_transient('accuaformsub_'.self::$rawData['_AccuaForm_jsuuid'], array(
561 503 'buildID' => self::$submittedBuildID,
562 504 'form' => self::$submittedForm,
563 505 'submittedMessages' => self::$submittedMessages,
564 - 'valid' => $valid,
565 506 ), 86400);
566 507 }
567 508
568 509 return self::$valid = (bool)$valid;
@@ -681,11 +622,9 @@
681 622 '_AccuaForm_hash' => base64_encode($hash),
682 623 '_AccuaForm_iv' => base64_encode($iv),
683 624 '_AccuaForm_data' => base64_encode($data),
684 625 );
685 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form values
686 626 if (isset($_SESSION["pfbc"][$this->buildID]["values"])) {
687 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form values
688 627 $_SESSION["pfbc"][$this->buildID]["values"] = $ret + $_SESSION["pfbc"][$this->buildID]["values"];
689 628 }
690 629 $this->setValues($ret);
691 630 return $ret;
@@ -706,53 +645,18 @@
706 645 public function addSubmitFunction($function_name, $priority = 0) {
707 646 $this->submit_functions[$function_name] = $priority;
708 647 }
709 648
710 - /**
711 - * Get submitted messages for a specific form or all forms.
712 - * @param string|null $formId Optional form ID to get message for specific form
713 - * @return string|array If $formId provided, returns string message (or empty string). Otherwise returns all messages as array.
714 - */
715 - public static function getSubmittedMessages($formId = null){
716 - if ($formId !== null) {
717 - return isset(self::$submittedMessages[$formId]) ? self::$submittedMessages[$formId] : '';
718 - }
719 - // Backwards compatibility: if old code expects a string, join all messages
720 - if (is_array(self::$submittedMessages)) {
721 - return implode('', self::$submittedMessages);
722 - }
649 + public static function getSubmittedMessages(){
723 650 return self::$submittedMessages;
724 651 }
725 652
726 - /**
727 - * Set submitted message for a specific form.
728 - * @param string $msg The message content
729 - * @param string|null $formId Optional form ID. If null, uses currently submitted form ID.
730 - * @return string The message that was set
731 - */
732 - public static function setSubmittedMessages($msg, $formId = null){
733 - if ($formId === null) {
734 - $formId = self::$submittedID ?: '__default__';
735 - }
736 - self::$submittedMessages[$formId] = $msg;
737 - return $msg;
653 + public static function setSubmittedMessages($msg){
654 + return self::$submittedMessages = $msg;
738 655 }
739 656
740 - /**
741 - * Append to submitted message for a specific form.
742 - * @param string $msg The message content to append
743 - * @param string|null $formId Optional form ID. If null, uses currently submitted form ID.
744 - * @return string The full message after appending
745 - */
746 - public static function appendSubmittedmessages($msg, $formId = null){
747 - if ($formId === null) {
748 - $formId = self::$submittedID ?: '__default__';
749 - }
750 - if (!isset(self::$submittedMessages[$formId])) {
751 - self::$submittedMessages[$formId] = '';
752 - }
753 - self::$submittedMessages[$formId] .= $msg;
754 - return self::$submittedMessages[$formId];
657 + public static function appendSubmittedmessages($msg){
658 + return self::$submittedMessages .= $msg;
755 659 }
756 660
757 661 protected static function get_anchor_id($id) {
758 662 static $used_anchor_id = array();
@@ -767,38 +671,23 @@
767 671 $id = $id . '-' . $used_anchor_id[$id];
768 672 }
769 673 return $id;
770 674 }
675 +
771 676 public function render($returnHTML = false) {
772 677 $this->wp_save();
773 678 if($returnHTML) {
774 679 ob_start();
775 680 }
776 -
777 681 parent::render(false);
778 682 if (!empty($this->accua_ajax)) {
779 683 $ajax_url = _accua_forms_json_encode(admin_url('admin-ajax.php?action=accua_form_submit'));
780 - $submit_fail_message = _accua_forms_json_encode('<li>'.__('Form submission failed. Please try again.', 'contact-forms').'</li>');
781 -
782 - /* translators: %s is the field name/label */
783 - $required_message_template = _accua_forms_json_encode(__('%s is a required field', 'contact-forms'));
784 - /* translators: %s is the field name/label */
785 - $valid_mail_message_template = _accua_forms_json_encode(__('%s: please enter a valid email address', 'contact-forms'));
786 - /* translators: %s is the field name/label */
787 - $valid_phone_message_template = _accua_forms_json_encode(__('%s: please enter a valid phone number (e.g. +39 333 1234567)', 'contact-forms'));
788 - $error_summary_header = _accua_forms_json_encode(__('Check the following fields to continue:', 'contact-forms'));
789 - $error_type_required = _accua_forms_json_encode(__('required field', 'contact-forms'));
790 - $error_type_invalid_email = _accua_forms_json_encode(__('invalid email address', 'contact-forms'));
791 - $error_type_invalid_phone = _accua_forms_json_encode(__('invalid phone number', 'contact-forms'));
792 - $success_message = _accua_forms_json_encode(__('All fields are valid. Ready to submit!', 'contact-forms'));
793 - $sending_message = _accua_forms_json_encode(__('Sending...', 'contact-forms'));
794 - $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'));
795 687 $js_buildid = preg_replace('/[^a-zA-Z0-9_]/m','_',$this->buildID);
796 688 $formid = $this->getId();
797 - // $hostname = _accua_forms_json_encode($_SERVER['SERVER_NAME']); - rimosso perché non affidabile con proxy reversi e CDN
798 - // Extract hostname from ajax_url for proper cross-domain detection
799 - $ajax_url_parsed = parse_url(admin_url('admin-ajax.php?action=accua_form_submit'));
800 - $ajax_hostname = _accua_forms_json_encode($ajax_url_parsed['host']);
689 + $hostname = _accua_forms_json_encode($_SERVER['SERVER_NAME']);
801 690
802 691 $post_url = _accua_forms_json_encode($this->stats['url']);
803 692 $js_ga_track = _accua_forms_json_encode($this->ga_track);
804 693
@@ -814,32 +703,18 @@
814 703 var _handle_ajax_submit_response_{$js_buildid} = function() {}
815 704
816 705 jQuery(function($) {
817 706 var thisform = $("#{$this->buildID}");
818 - var ajax_enabled = {$ajax_hostname} == location.hostname ;
707 + var ajax_enabled = {$hostname} == location.hostname ;
819 708 var anchor_id = $anchor_id ;
820 709
821 710 var response_messages = $("#_response_messages_{$this->buildID}");
822 711 if (! response_messages.length) {
823 - // Create all three anchors for success, invalid, and error states
824 - thisform.before('\\x3Ca id="formSubmitSuccess-'+anchor_id+'" name="formSubmitSuccess-'+anchor_id+'" class="accua-form-anchor" /\\x3E');
825 - thisform.before('\\x3Ca id="formSubmitInvalid-'+anchor_id+'" name="formSubmitInvalid-'+anchor_id+'" class="accua-form-anchor" /\\x3E');
826 - 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');
827 713 response_messages = $('\\x3Cdiv id="_response_messages_{$this->buildID}" class="accua-form-messages"\\x3E\\x3C/div\\x3E');
828 714 thisform.before(response_messages);
829 715 }
830 -
831 - // Smooth scroll helper function - also updates URL hash for GA tracking and bookmarkability
832 - var smoothScrollToElement = function(elementId) {
833 - var target = document.getElementById(elementId);
834 - if (target) {
835 - target.scrollIntoView({ behavior: 'smooth', block: 'start' });
836 - // Update URL hash without adding a browser history entry (replaceState prevents instant jump)
837 - if (history.replaceState) {
838 - history.replaceState(null, '', '#' + elementId);
839 - }
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="#" 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,171 +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 (!parent.hasClass('pfbc-element-has-error')) {
1857 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1858 - field.attr('aria-invalid', 'true');
1859 - field.attr('aria-describedby', errorId);
1860 -
1861 - // Get field label for error message
1862 - var fieldLabel = getFieldLabel(parent);
1863 - var errorMessage = getEmailMessage(fieldLabel, field);
1864 - var inlineError = $('<div class=\"pfbc-inline-error\" id=\"' + errorId + '\" role=\"alert\" aria-live=\"polite\"><div class=\"pfbc-error-message\">' + errorMessage + '</div></div>');
1865 -
1866 - // For file inputs with help text, insert error after help text
1867 - var helpText = field.siblings('.pfbc-help').last();
1868 - if (field.is('[type=\"file\"]') && helpText.length) {
1869 - helpText.after(inlineError);
1870 - } else {
1871 - field.after(inlineError);
1872 - }
1873 - }
1874 - } else {
1875 - parent.removeClass('pfbc-invalid pfbc-element-has-error');
1876 - field.attr('aria-invalid', 'false');
1877 - field.removeAttr('aria-describedby');
1878 - removeErrorAnimated(errorId);
1879 - }
1880 - });
1881 987 });
1882 988 // -->
1883 989 </script>
1884 -<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>
1885 991 JS;
1886 992 }
1887 993
1888 994 echo <<<JSREFERRER
@@ -1924,28 +1030,8 @@
1924 1030 echo 'jQuery(document).ready(function() {';
1925 1031 /*jQuery is used to set the focus of the form's initial element.*/
1926 1032 if(!in_array("focus", $this->prevent))
1927 1033 echo 'jQuery("#', $id, ' :input:visible:enabled:first").focus();';
1928 -
1929 - // Accessibility: Focus management for validation errors on page load
1930 - echo <<<JS
1931 -
1932 - // If there are errors on page load, focus the error summary or first invalid field
1933 - if (jQuery('.pfbc-error', '#{$id}').length > 0) {
1934 - setTimeout(function() {
1935 - var errorContainer = jQuery('.pfbc-error', '#{$id}').first();
1936 - errorContainer.attr('tabindex', '-1').focus();
1937 - }, 100);
1938 - } else if (jQuery('.pfbc-element-has-error', '#{$id}').length > 0) {
1939 - setTimeout(function() {
1940 - var firstInvalidField = jQuery('.pfbc-element-has-error :input:visible:enabled:first', '#{$id}').first();
1941 - if (firstInvalidField.length) {
1942 - firstInvalidField.focus();
1943 - }
1944 - }, 100);
1945 - }
1946 -
1947 -JS;
1948 1034
1949 1035 $this->view->jQueryDocumentReady();
1950 1036 foreach($this->elements as $element) {
1951 1037 $element->jQueryDocumentReady();
@@ -1987,202 +1073,9 @@
1987 1073 });
1988 1074 // -->
1989 1075 </script>
1990 1076 JS;
1991 -
1992 - // Add JavaScript for inline label floating behavior
1993 - if (strpos($this->attributes['class'], 'accua-form-view-inlinelabel') !== false) {
1994 - echo <<<INLINELABELJS
1995 -<script type="text/javascript">
1996 -<!--
1997 -jQuery(function($) {
1998 - var form = $('#{$this->attributes["id"]}');
1999 -
2000 - /**
2001 - * Inline Label Floating Behavior
2002 - *
2003 - * Handles the Material Design floating label animation:
2004 - * - Floats label up when field is focused
2005 - * - Keeps label up when field has value
2006 - * - Returns label to inline position when field is empty and unfocused
2007 - *
2008 - * Accessibility features:
2009 - * - Maintains proper ARIA relationships
2010 - * - Works with keyboard navigation
2011 - * - Compatible with screen readers
2012 - * - Supports autofill detection
2013 - */
2014 -
2015 - // Function to check if field has value
2016 - function hasValue(field) {
2017 - var val = field.val();
2018 - // For select elements, check if selected value is not empty
2019 - if (field.is('select')) {
2020 - return val !== null && val !== '' && val !== undefined;
2021 - }
2022 - // For date inputs, check if value is set (format: YYYY-MM-DD)
2023 - if (field.attr('type') === 'date') {
2024 - return val !== null && val !== '' && val !== undefined;
2025 - }
2026 - // For text inputs and textareas
2027 - return val !== null && val !== '' && val.trim() !== '';
2028 1077 }
2029 -
2030 - // Function to update wrapper state
2031 - function updateWrapperState(wrapper) {
2032 - var field = wrapper.find('.pfbc-textbox, .pfbc-textarea, .pfbc-select').first();
2033 - var postSelectWrapper = wrapper.find('.pfbc-post-select-wrapper');
2034 -
2035 - // Check if this is a post-select field
2036 - if (postSelectWrapper.length && postSelectWrapper.attr('data-enhanced') === 'true') {
2037 - // For enhanced post-select, check the hidden native select for value
2038 - var nativeSelect = postSelectWrapper.find('select');
2039 - var trigger = postSelectWrapper.find('.pfbc-post-select-trigger');
2040 - var container = postSelectWrapper.find('.pfbc-post-select-container');
2041 -
2042 - var isFocused = trigger.is(':focus') || container.hasClass('open');
2043 - var fieldHasValue = nativeSelect.length && hasValue(nativeSelect);
2044 -
2045 - wrapper.toggleClass('is-focused', isFocused);
2046 - wrapper.toggleClass('has-value', fieldHasValue);
2047 - return;
2048 - }
2049 -
2050 - if (!field.length) return;
2051 -
2052 - var isFocused = field.is(':focus');
2053 - var fieldHasValue = hasValue(field);
2054 -
2055 - // Update wrapper classes
2056 - wrapper.toggleClass('is-focused', isFocused);
2057 - wrapper.toggleClass('has-value', fieldHasValue);
2058 -
2059 - // For date inputs, add class directly to field for Firefox CSS support
2060 - if (field.attr('type') === 'date') {
2061 - field.toggleClass('has-value', fieldHasValue);
2062 - }
2063 -
2064 - // Accessibility: Update ARIA state
2065 - var label = wrapper.find('.pfbc-floating-label');
2066 - if (label.length) {
2067 - // Ensure label is always associated with field
2068 - var fieldId = field.attr('id');
2069 - if (fieldId && !field.attr('aria-labelledby')) {
2070 - // Label is already associated via for/id
2071 - // Additional ARIA not needed, but we ensure proper semantics
2072 - }
2073 - }
2074 - }
2075 -
2076 - // Initialize all inline label wrappers
2077 - form.find('.pfbc-inline-label-wrapper').each(function() {
2078 - var wrapper = $(this);
2079 - updateWrapperState(wrapper);
2080 - });
2081 -
2082 - // Handle focus events
2083 - form.on('focus', '.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select', function() {
2084 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2085 - updateWrapperState(wrapper);
2086 - });
2087 -
2088 - // Handle blur events
2089 - form.on('blur', '.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select', function() {
2090 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2091 - // Small delay to allow value to be set
2092 - setTimeout(function() {
2093 - updateWrapperState(wrapper);
2094 - }, 10);
2095 - });
2096 -
2097 - // Handle input/change events to detect value changes
2098 - form.on('input change', '.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select', function() {
2099 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2100 - updateWrapperState(wrapper);
2101 - });
2102 -
2103 - // Handle post-select trigger focus/blur events
2104 - form.on('focus', '.pfbc-inline-label-wrapper .pfbc-post-select-trigger', function() {
2105 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2106 - updateWrapperState(wrapper);
2107 - });
2108 -
2109 - form.on('blur', '.pfbc-inline-label-wrapper .pfbc-post-select-trigger', function() {
2110 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2111 - setTimeout(function() {
2112 - updateWrapperState(wrapper);
2113 - }, 50);
2114 - });
2115 -
2116 - // Handle post-select value changes (native select change event)
2117 - form.on('change', '.pfbc-inline-label-wrapper .pfbc-post-select-wrapper select', function() {
2118 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2119 - updateWrapperState(wrapper);
2120 - });
2121 -
2122 - // Observe post-select container for open/close state changes
2123 - if (window.MutationObserver) {
2124 - form.find('.pfbc-inline-label-wrapper .pfbc-post-select-container').each(function() {
2125 - var container = this;
2126 - var wrapper = $(container).closest('.pfbc-inline-label-wrapper');
2127 - var containerObserver = new MutationObserver(function(mutations) {
2128 - mutations.forEach(function(mutation) {
2129 - if (mutation.attributeName === 'class') {
2130 - updateWrapperState(wrapper);
2131 - }
2132 - });
2133 - });
2134 - containerObserver.observe(container, {
2135 - attributes: true,
2136 - attributeFilter: ['class']
2137 - });
2138 - });
2139 - }
2140 -
2141 - // Handle browser autofill (multiple browser support)
2142 - // Chrome/Safari autofill detection
2143 - if (window.MutationObserver) {
2144 - var observer = new MutationObserver(function(mutations) {
2145 - mutations.forEach(function(mutation) {
2146 - if (mutation.attributeName === 'value' || mutation.attributeName === 'class') {
2147 - var target = $(mutation.target);
2148 - if (target.hasClass('pfbc-textbox') || target.hasClass('pfbc-textarea') || target.hasClass('pfbc-select')) {
2149 - var wrapper = target.closest('.pfbc-inline-label-wrapper');
2150 - if (wrapper.length) {
2151 - updateWrapperState(wrapper);
2152 - }
2153 - }
2154 - }
2155 - });
2156 - });
2157 -
2158 - form.find('.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select').each(function() {
2159 - observer.observe(this, {
2160 - attributes: true,
2161 - attributeFilter: ['value', 'class']
2162 - });
2163 - });
2164 - }
2165 -
2166 - // Fallback autofill detection with animation frame checking
2167 - setTimeout(function() {
2168 - form.find('.pfbc-inline-label-wrapper').each(function() {
2169 - updateWrapperState($(this));
2170 - });
2171 - }, 100);
2172 -
2173 - // Additional check for autofill after a short delay
2174 - setTimeout(function() {
2175 - form.find('.pfbc-inline-label-wrapper').each(function() {
2176 - updateWrapperState($(this));
2177 - });
2178 - }, 500);
2179 -});
2180 -// -->
2181 -</script>
2182 -INLINELABELJS;
2183 - }
2184 - }
2185 1078
2186 1079 protected function renderCSS() {
2187 1080
2188 1081 }
@@ -2236,8 +1129,9 @@
2236 1129
2237 1130 public static function ajaxSubmit() {
2238 1131 $ret = array(
2239 1132 'valid' => false,
1133 + 'messages' => self::getSubmittedMessages(),
2240 1134 'jsuuid' => self::$rawData['_AccuaForm_jsuuid'],
2241 1135 'buildID' => self::$submittedBuildID,
2242 1136 'files' => array(),
2243 1137 );
@@ -2244,16 +1138,9 @@
2244 1138 if ($ret['submitted'] = self::isSubmit()){
2245 1139 if ($ret['valid'] = self::isValid()) {
2246 1140
2247 1141 } else {
2248 - $errorResponse = self::getAjaxErrorResponse();
2249 - // Support modern error format with both flat list and structured data for ARIA
2250 - if (is_array($errorResponse) && isset($errorResponse['errors'])) {
2251 - $ret = array_merge($ret, $errorResponse);
2252 - } else {
2253 - // Backwards compatibility: old format returned just the flat array
2254 - $ret['errors'] = $errorResponse;
2255 - }
1142 + $ret['errors'] = self::getAjaxErrorResponse();
2256 1143 }
2257 1144 $form = self::$submittedForm;
2258 1145 foreach ($form->files as $fieldname => $file) {
2259 1146 if (!empty($file['name'])) {
@@ -2260,14 +1147,8 @@
2260 1147 $ret['files'][$fieldname] = $form->getElementByName($fieldname)->getAlreadySubmittedText();
2261 1148 }
2262 1149 }
2263 1150 $ret += $form->wp_save();
2264 - // Get messages AFTER isValid() and wp_save() have completed
2265 - // This ensures email sending results are captured in the messages
2266 - $ret['messages'] = self::getSubmittedMessages(self::$submittedID);
2267 - } else {
2268 - // Form not submitted yet - no messages to show
2269 - $ret['messages'] = '';
2270 1151 }
2271 1152 return $ret;
2272 1153 }
2273 1154 }