PluginProbe
Contact Forms by Cimatti / 2.2.0
Contact Forms by Cimatti v2.2.0
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
contact-forms / assets / js / frontend / phone-validation.js

phone-validation.js in Contact Forms by Cimatti 2.2.0, at assets/js/frontend/phone-validation.js

305 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Phone Validation for Contact Forms using libphonenumber-js
3 *
4 * Provides real-time client-side phone number validation using Google's
5 * libphonenumber library (min bundle). Supports international phone numbers
6 * with and without country prefix.
7 *
8 * The validation is lenient (isPossible) rather than strict (isValid) to avoid
9 * false rejections when metadata becomes outdated. This is the library author's
10 * recommendation for contact forms.
11 *
12 * @package Contact Forms
13 * @since 2.0.0-beta.34
14 */
15
16 (function($) {
17 'use strict';
18
19 /**
20 * Validate phone number using libphonenumber-js
21 *
22 * Uses isPossiblePhoneNumber() for lenient validation (length-based).
23 * Supports both prefixed (+39...) and non-prefixed numbers.
24 * Falls back to basic regex validation if library is unavailable.
25 *
26 * @param {string} value Phone number to validate
27 * @param {string} countryCode ISO 3166-1 alpha-2 country code (default: 'IT')
28 * @returns {boolean} True if valid or empty
29 */
30 function isValidPhone(value, countryCode) {
31 if (!value || value.trim() === '') {
32 return true; // Empty is valid (Required validation handles mandatory)
33 }
34
35 value = value.trim();
36 countryCode = countryCode || 'IT';
37
38 // Treat prefix-only values (1-4 digits starting with +) as empty
39 // Matches server-side Phone.php behavior
40 var prefixDigits = value.replace(/\D/g, '').length;
41 if (value.charAt(0) === '+' && prefixDigits <= 4) {
42 return true;
43 }
44
45 // Check if libphonenumber is available
46 if (typeof libphonenumber !== 'undefined' && libphonenumber.isPossiblePhoneNumber) {
47 try {
48 // If starts with '+', let libphonenumber auto-detect country
49 if (value.charAt(0) === '+') {
50 return libphonenumber.isPossiblePhoneNumber(value);
51 }
52 // Without prefix, use the specified country code
53 return libphonenumber.isPossiblePhoneNumber(value, countryCode);
54 } catch (e) {
55 // Parse error - fall back to basic validation
56 return isValidPhoneBasic(value);
57 }
58 }
59
60 // Fallback if library not loaded
61 return isValidPhoneBasic(value);
62 }
63
64 /**
65 * Basic phone validation fallback (regex-based)
66 *
67 * Used when libphonenumber-js is not available.
68 * Follows E.164 standard (max 15 digits) with lenient formatting.
69 * Requires minimum 5 digits for actual phone numbers (matches server-side Phone.php).
70 *
71 * @param {string} value Phone number to validate
72 * @returns {boolean} True if valid format
73 */
74 function isValidPhoneBasic(value) {
75 if (!value || value.trim() === '') {
76 return true;
77 }
78
79 value = value.trim();
80
81 // Check for invalid characters (only digits, spaces, dashes, dots, slashes, parentheses, plus)
82 if (!/^[\d\s\-\.\/\(\)\+]+$/.test(value)) {
83 return false;
84 }
85
86 // Plus sign only at start, max one
87 var plusIndex = value.indexOf('+');
88 if (plusIndex > 0 || (value.match(/\+/g) || []).length > 1) {
89 return false;
90 }
91
92 var digitCount = value.replace(/\D/g, '').length;
93
94 // Treat prefix-only values (1-4 digits starting with +) as empty for optional fields
95 if (value.charAt(0) === '+' && digitCount <= 4) {
96 return true;
97 }
98
99 // Require minimum 5 digits for actual phone numbers (matches server-side Phone.php)
100 if (digitCount < 5) {
101 return false;
102 }
103
104 // E.164 standard: maximum 15 digits
105 if (digitCount > 15) {
106 return false;
107 }
108
109 return true;
110 }
111
112 /**
113 * Build phone error message with field label
114 *
115 * Checks data-custom-format-msg attribute first (per-field/per-form custom message),
116 * then falls back to the localized default message.
117 * Matches the priority chain used by AccuaForm.php getPhoneMessage().
118 *
119 * @param {jQuery} $input The phone input element
120 * @returns {string} Formatted error message with field label
121 */
122 function getPhoneErrorMessage($input) {
123 // Check for custom format message (set by server from field definition or form override)
124 var customMsg = $input.attr('data-custom-format-msg');
125 if (customMsg) {
126 // Custom message — resolve %s with field label and return as-is
127 var fieldLabel = getFieldLabel($input);
128 return customMsg.replace('%s', fieldLabel);
129 }
130
131 var baseMsg = (typeof accuaPhoneL10n !== 'undefined' && accuaPhoneL10n.phoneError)
132 ? accuaPhoneL10n.phoneError
133 : 'Please enter a valid phone number';
134
135 var fieldLabel = getFieldLabel($input);
136 if (fieldLabel) {
137 return fieldLabel + ': ' + baseMsg;
138 }
139
140 return baseMsg;
141 }
142
143 /**
144 * Get field label text from the DOM
145 *
146 * Matches AccuaForm.php getFieldLabel logic — searches for floating label,
147 * standard label, or any label element within the field container.
148 *
149 * @param {jQuery} $input The input element
150 * @returns {string} The field label text, or empty string
151 */
152 function getFieldLabel($input) {
153 var $container = $input.closest('.pfbc-element');
154 if (!$container.length) {
155 return '';
156 }
157
158 var $label = $container.find('.pfbc-floating-label').first();
159 if (!$label.length) {
160 $label = $container.find('.pfbc-label label').first();
161 }
162 if (!$label.length) {
163 $label = $container.find('label').first();
164 }
165
166 if ($label.length) {
167 return $label.clone().find('.pfbc-required').remove().end().text().trim();
168 }
169
170 return '';
171 }
172
173 /**
174 * Initialize phone validation on forms
175 */
176 function initPhoneValidation() {
177 var phoneInputs = $('input[type="tel"], .accuaform-telephone, .accuaform-fieldtype-telephone input');
178
179 phoneInputs.each(function() {
180 var $input = $(this);
181
182 if ($input.data('phone-init')) return;
183 $input.data('phone-init', true);
184
185 // Get country code from data attribute (set by Telephone.php)
186 var countryCode = $input.attr('data-country') || 'IT';
187
188 // Validate on blur — coordinates with AccuaForm.php submit handler.
189 // Uses {fieldId}-phone-error for blur errors; submit handler uses {fieldId}-error.
190 // Always cleans up both IDs before showing a new error to prevent duplicates.
191 $input.on('blur', function() {
192 var value = $input.val();
193 var trimmed = value ? value.trim() : '';
194 var fieldId = $input.attr('id');
195 var phoneErrorId = fieldId + '-phone-error';
196 var submitErrorId = fieldId + '-error';
197 var $parent = $input.closest('.pfbc-element, .pfbc-fieldwrap');
198
199 // Treat prefix-only values (≤4 digits starting with +) as empty,
200 // matching server-side Phone.php behavior
201 var blurDigits = trimmed.replace(/\D/g, '').length;
202 var isPrefixOnly = trimmed.charAt(0) === '+' && blurDigits <= 4;
203
204 if (trimmed === '' || isPrefixOnly) {
205 // Empty/prefix-only: clean up any phone format errors from a previous blur,
206 // then let Required blur handler (AccuaForm.php) manage the empty state.
207 $('#' + phoneErrorId).remove();
208 var $submitErr = $('#' + submitErrorId);
209 if ($submitErr.length && $submitErr.hasClass('pfbc-phone-format-error')) {
210 $submitErr.remove();
211 }
212 // Clear parent error state if no error divs remain, so the Required blur
213 // handler's guard (!parent.hasClass('pfbc-element-has-error')) allows it to fire.
214 if (!$parent.find('.pfbc-inline-error').length) {
215 $parent.removeClass('pfbc-invalid pfbc-element-has-error');
216 $input.attr('aria-invalid', 'false');
217 $input.removeAttr('aria-describedby');
218 }
219 return;
220 }
221
222 var isValid = isValidPhone(trimmed, countryCode);
223
224 if (!isValid) {
225 // Remove ALL existing inline errors for this field (both blur and submit IDs)
226 // to guarantee no duplicates. Use immediate .remove() — no animation delay.
227 $('#' + phoneErrorId).remove();
228 $('#' + submitErrorId).remove();
229
230 // Mark field and container as invalid (same pattern as AccuaForm.php)
231 $parent.addClass('pfbc-invalid pfbc-element-has-error');
232 $input.attr('aria-invalid', 'true');
233 $input.attr('aria-describedby', phoneErrorId);
234
235 // Build error message with field label (matching submit handler format)
236 var errorMsg = getPhoneErrorMessage($input);
237
238 var $error = $('<div/>', {
239 'id': phoneErrorId,
240 'class': 'pfbc-inline-error pfbc-phone-format-error',
241 'role': 'alert',
242 'aria-live': 'polite'
243 }).append($('<div/>', {'class': 'pfbc-error-message'}).text(errorMsg));
244
245 var $help = $input.siblings('.pfbc-help');
246 $help.length ? $help.after($error) : $input.after($error);
247 } else {
248 // Valid phone — clear phone-specific error and submit error if it was a phone error.
249 // But do NOT clear required errors set by AccuaForm.php required blur handler.
250 $('#' + phoneErrorId).remove();
251 // Only clear submit-generated error if it exists and was a phone format error
252 // (not a required error). Check for the marker class first, then fall back to
253 // keyword matching for errors generated by the submit handler.
254 // Only clear submit-generated error if it was a phone format error
255 // (not a required error). Check for the marker class.
256 var $submitErr = $('#' + submitErrorId);
257 if ($submitErr.length && $submitErr.hasClass('pfbc-phone-format-error')) {
258 $submitErr.remove();
259 }
260
261 // Only clear invalid state if no other errors remain
262 if (!$parent.find('.pfbc-inline-error').length) {
263 $parent.removeClass('pfbc-invalid pfbc-element-has-error');
264 $input.attr('aria-invalid', 'false');
265 $input.removeAttr('aria-describedby');
266 }
267 }
268 });
269
270 // Clear phone error on valid input (real-time feedback)
271 $input.on('input', function() {
272 var val = $input.val();
273 var trimmed = val ? val.trim() : '';
274
275 if (trimmed !== '' && isValidPhone(trimmed, countryCode)) {
276 var fieldId = $input.attr('id');
277 var $parent = $input.closest('.pfbc-element, .pfbc-fieldwrap');
278
279 $('#' + fieldId + '-phone-error').remove();
280
281 // Only clear invalid state if no other errors remain
282 if (!$parent.find('.pfbc-inline-error').length) {
283 $parent.removeClass('pfbc-invalid pfbc-element-has-error');
284 $input.attr('aria-invalid', 'false');
285 $input.removeAttr('aria-describedby');
286 }
287 }
288 });
289 });
290 }
291
292 $(function() {
293 initPhoneValidation();
294 $(document).on('accuaform:loaded', initPhoneValidation);
295 });
296
297 // Expose for external use and testing
298 window.AccuaPhoneValidation = {
299 isValid: isValidPhone,
300 isValidBasic: isValidPhoneBasic,
301 init: initPhoneValidation
302 };
303
304 })(jQuery);
305