# contact-forms/trunk/assets/js/frontend/phone-validation.js

Contact Forms by Cimatti, version trunk. 305 lines.

- Page: https://pluginprobe.com/plugins/contact-forms/trunk/code/assets/js/frontend/phone-validation.js
- Raw: https://pluginprobe.com/plugins/contact-forms/trunk/raw/assets/js/frontend/phone-validation.js
- Modified: 2026-08-21T08:39:40+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/contact-forms/trunk/code/assets/js/frontend/phone-validation.js#L10-L20`.

```javascript
/**
 * Phone Validation for Contact Forms using libphonenumber-js
 * 
 * Provides real-time client-side phone number validation using Google's
 * libphonenumber library (min bundle). Supports international phone numbers
 * with and without country prefix.
 * 
 * The validation is lenient (isPossible) rather than strict (isValid) to avoid
 * false rejections when metadata becomes outdated. This is the library author's
 * recommendation for contact forms.
 * 
 * @package Contact Forms
 * @since 2.0.0-beta.34
 */

(function($) {
  'use strict';

  /**
   * Validate phone number using libphonenumber-js
   * 
   * Uses isPossiblePhoneNumber() for lenient validation (length-based).
   * Supports both prefixed (+39...) and non-prefixed numbers.
   * Falls back to basic regex validation if library is unavailable.
   * 
   * @param {string} value Phone number to validate
   * @param {string} countryCode ISO 3166-1 alpha-2 country code (default: 'IT')
   * @returns {boolean} True if valid or empty
   */
  function isValidPhone(value, countryCode) {
    if (!value || value.trim() === '') {
      return true; // Empty is valid (Required validation handles mandatory)
    }
    
    value = value.trim();
    countryCode = countryCode || 'IT';
    
    // Treat prefix-only values (1-4 digits starting with +) as empty
    // Matches server-side Phone.php behavior
    var prefixDigits = value.replace(/\D/g, '').length;
    if (value.charAt(0) === '+' && prefixDigits <= 4) {
      return true;
    }
    
    // Check if libphonenumber is available
    if (typeof libphonenumber !== 'undefined' && libphonenumber.isPossiblePhoneNumber) {
      try {
        // If starts with '+', let libphonenumber auto-detect country
        if (value.charAt(0) === '+') {
          return libphonenumber.isPossiblePhoneNumber(value);
        }
        // Without prefix, use the specified country code
        return libphonenumber.isPossiblePhoneNumber(value, countryCode);
      } catch (e) {
        // Parse error - fall back to basic validation
        return isValidPhoneBasic(value);
      }
    }
    
    // Fallback if library not loaded
    return isValidPhoneBasic(value);
  }

  /**
   * Basic phone validation fallback (regex-based)
   * 
   * Used when libphonenumber-js is not available.
   * Follows E.164 standard (max 15 digits) with lenient formatting.
   * Requires minimum 5 digits for actual phone numbers (matches server-side Phone.php).
   * 
   * @param {string} value Phone number to validate
   * @returns {boolean} True if valid format
   */
  function isValidPhoneBasic(value) {
    if (!value || value.trim() === '') {
      return true;
    }
    
    value = value.trim();
    
    // Check for invalid characters (only digits, spaces, dashes, dots, slashes, parentheses, plus)
    if (!/^[\d\s\-\.\/\(\)\+]+$/.test(value)) {
      return false;
    }
    
    // Plus sign only at start, max one
    var plusIndex = value.indexOf('+');
    if (plusIndex > 0 || (value.match(/\+/g) || []).length > 1) {
      return false;
    }
    
    var digitCount = value.replace(/\D/g, '').length;
    
    // Treat prefix-only values (1-4 digits starting with +) as empty for optional fields
    if (value.charAt(0) === '+' && digitCount <= 4) {
      return true;
    }
    
    // Require minimum 5 digits for actual phone numbers (matches server-side Phone.php)
    if (digitCount < 5) {
      return false;
    }
    
    // E.164 standard: maximum 15 digits
    if (digitCount > 15) {
      return false;
    }
    
    return true;
  }

  /**
   * Build phone error message with field label
   * 
   * Checks data-custom-format-msg attribute first (per-field/per-form custom message),
   * then falls back to the localized default message.
   * Matches the priority chain used by AccuaForm.php getPhoneMessage().
   * 
   * @param {jQuery} $input The phone input element
   * @returns {string} Formatted error message with field label
   */
  function getPhoneErrorMessage($input) {
    // Check for custom format message (set by server from field definition or form override)
    var customMsg = $input.attr('data-custom-format-msg');
    if (customMsg) {
      // Custom message - resolve %s with field label and return as-is
      var fieldLabel = getFieldLabel($input);
      return customMsg.replace('%s', fieldLabel);
    }

    var baseMsg = (typeof accuaPhoneL10n !== 'undefined' && accuaPhoneL10n.phoneError) 
      ? accuaPhoneL10n.phoneError 
      : 'Please enter a valid phone number';
    
    var fieldLabel = getFieldLabel($input);
    if (fieldLabel) {
      return fieldLabel + ': ' + baseMsg;
    }
    
    return baseMsg;
  }

  /**
   * Get field label text from the DOM
   * 
   * Matches AccuaForm.php getFieldLabel logic - searches for floating label,
   * standard label, or any label element within the field container.
   * 
   * @param {jQuery} $input The input element
   * @returns {string} The field label text, or empty string
   */
  function getFieldLabel($input) {
    var $container = $input.closest('.pfbc-element');
    if (!$container.length) {
      return '';
    }
    
    var $label = $container.find('.pfbc-floating-label').first();
    if (!$label.length) {
      $label = $container.find('.pfbc-label label').first();
    }
    if (!$label.length) {
      $label = $container.find('label').first();
    }
    
    if ($label.length) {
      return $label.clone().find('.pfbc-required').remove().end().text().trim();
    }
    
    return '';
  }

  /**
   * Initialize phone validation on forms
   */
  function initPhoneValidation() {
    var phoneInputs = $('input[type="tel"], .accuaform-telephone, .accuaform-fieldtype-telephone input');
    
    phoneInputs.each(function() {
      var $input = $(this);
      
      if ($input.data('phone-init')) return;
      $input.data('phone-init', true);
      
      // Get country code from data attribute (set by Telephone.php)
      var countryCode = $input.attr('data-country') || 'IT';
      
      // Validate on blur - coordinates with AccuaForm.php submit handler.
      // Uses {fieldId}-phone-error for blur errors; submit handler uses {fieldId}-error.
      // Always cleans up both IDs before showing a new error to prevent duplicates.
      $input.on('blur', function() {
        var value = $input.val();
        var trimmed = value ? value.trim() : '';
        var fieldId = $input.attr('id');
        var phoneErrorId = fieldId + '-phone-error';
        var submitErrorId = fieldId + '-error';
        var $parent = $input.closest('.pfbc-element, .pfbc-fieldwrap');
        
        // Treat prefix-only values (≤4 digits starting with +) as empty,
        // matching server-side Phone.php behavior
        var blurDigits = trimmed.replace(/\D/g, '').length;
        var isPrefixOnly = trimmed.charAt(0) === '+' && blurDigits <= 4;
        
        if (trimmed === '' || isPrefixOnly) {
          // Empty/prefix-only: clean up any phone format errors from a previous blur,
          // then let Required blur handler (AccuaForm.php) manage the empty state.
          $('#' + phoneErrorId).remove();
          var $submitErr = $('#' + submitErrorId);
          if ($submitErr.length && $submitErr.hasClass('pfbc-phone-format-error')) {
            $submitErr.remove();
          }
          // Clear parent error state if no error divs remain, so the Required blur
          // handler's guard (!parent.hasClass('pfbc-element-has-error')) allows it to fire.
          if (!$parent.find('.pfbc-inline-error').length) {
            $parent.removeClass('pfbc-invalid pfbc-element-has-error');
            $input.attr('aria-invalid', 'false');
            $input.removeAttr('aria-describedby');
          }
          return;
        }
        
        var isValid = isValidPhone(trimmed, countryCode);
        
        if (!isValid) {
          // Remove ALL existing inline errors for this field (both blur and submit IDs)
          // to guarantee no duplicates. Use immediate .remove() - no animation delay.
          $('#' + phoneErrorId).remove();
          $('#' + submitErrorId).remove();
          
          // Mark field and container as invalid (same pattern as AccuaForm.php)
          $parent.addClass('pfbc-invalid pfbc-element-has-error');
          $input.attr('aria-invalid', 'true');
          $input.attr('aria-describedby', phoneErrorId);
          
          // Build error message with field label (matching submit handler format)
          var errorMsg = getPhoneErrorMessage($input);
          
          var $error = $('<div/>', {
            'id': phoneErrorId,
            'class': 'pfbc-inline-error pfbc-phone-format-error',
            'role': 'alert',
            'aria-live': 'polite'
          }).append($('<div/>', {'class': 'pfbc-error-message'}).text(errorMsg));
          
          var $help = $input.siblings('.pfbc-help');
          $help.length ? $help.after($error) : $input.after($error);
        } else {
          // Valid phone - clear phone-specific error and submit error if it was a phone error.
          // But do NOT clear required errors set by AccuaForm.php required blur handler.
          $('#' + phoneErrorId).remove();
          // Only clear submit-generated error if it exists and was a phone format error
          // (not a required error). Check for the marker class first, then fall back to
          // keyword matching for errors generated by the submit handler.
          // Only clear submit-generated error if it was a phone format error
          // (not a required error). Check for the marker class.
          var $submitErr = $('#' + submitErrorId);
          if ($submitErr.length && $submitErr.hasClass('pfbc-phone-format-error')) {
            $submitErr.remove();
          }
          
          // Only clear invalid state if no other errors remain
          if (!$parent.find('.pfbc-inline-error').length) {
            $parent.removeClass('pfbc-invalid pfbc-element-has-error');
            $input.attr('aria-invalid', 'false');
            $input.removeAttr('aria-describedby');
          }
        }
      });
      
      // Clear phone error on valid input (real-time feedback)
      $input.on('input', function() {
        var val = $input.val();
        var trimmed = val ? val.trim() : '';
        
        if (trimmed !== '' && isValidPhone(trimmed, countryCode)) {
          var fieldId = $input.attr('id');
          var $parent = $input.closest('.pfbc-element, .pfbc-fieldwrap');
          
          $('#' + fieldId + '-phone-error').remove();
          
          // Only clear invalid state if no other errors remain
          if (!$parent.find('.pfbc-inline-error').length) {
            $parent.removeClass('pfbc-invalid pfbc-element-has-error');
            $input.attr('aria-invalid', 'false');
            $input.removeAttr('aria-describedby');
          }
        }
      });
    });
  }

  $(function() {
    initPhoneValidation();
    $(document).on('accuaform:loaded', initPhoneValidation);
  });

  // Expose for external use and testing
  window.AccuaPhoneValidation = {
    isValid: isValidPhone,
    isValidBasic: isValidPhoneBasic,
    init: initPhoneValidation
  };

})(jQuery);

```
