Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
61.24% covered (warning)
61.24%
1114 / 1819
30.19% covered (danger)
30.19%
16 / 53
CRAP
0.00% covered (danger)
0.00%
0 / 1
Contact_Form_Field
61.25% covered (warning)
61.25%
1113 / 1817
30.19% covered (danger)
30.19%
16 / 53
12876.73
0.00% covered (danger)
0.00%
0 / 1
 __construct
95.28% covered (success)
95.28%
101 / 106
0.00% covered (danger)
0.00%
0 / 1
20
 add_error
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_error
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has_value
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 validate
47.69% covered (danger)
47.69%
62 / 130
0.00% covered (danger)
0.00%
0 / 1
504.82
 sanitize_text_field
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_option_value
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 render
60.00% covered (warning)
60.00%
57 / 95
0.00% covered (danger)
0.00%
0 / 1
167.90
 get_computed_field_value
52.63% covered (warning)
52.63%
10 / 19
0.00% covered (danger)
0.00%
0 / 1
34.83
 render_label
73.33% covered (warning)
73.33%
22 / 30
0.00% covered (danger)
0.00%
0 / 1
22.48
 is_label_hidden_by_block_visibility
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 get_block_visibility_aria_label
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get_hidden_label_aria_label_attr
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 get_file_dropzone_aria_label
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 render_legend_as_label
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
10.14
 render_input_field
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
8.19
 get_error_div
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 set_invalid_message
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 render_email_field
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 render_telephone_field
94.57% covered (success)
94.57%
87 / 92
0.00% covered (danger)
0.00%
0 / 1
9.01
 render_url_field
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 render_textarea_field
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
3.00
 render_radio_field
77.53% covered (warning)
77.53%
69 / 89
0.00% covered (danger)
0.00%
0 / 1
38.54
 render_other_input_field
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 render_checkbox_field
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
9
 render_consent_field
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 render_file_field
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 1
12
 render_hidden_field
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_file_field_assets
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 get_unauth_endpoint_url
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 render_checkbox_multiple_field
81.82% covered (warning)
81.82%
63 / 77
0.00% covered (danger)
0.00%
0 / 1
28.76
 render_select_field
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
8.05
 render_date_field
93.02% covered (success)
93.02%
80 / 86
0.00% covered (danger)
0.00%
0 / 1
7.02
 render_time_field
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 render_image_select_field
0.00% covered (danger)
0.00%
0 / 128
0.00% covered (danger)
0.00%
0 / 1
1722
 render_number_field
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 render_default_field
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get_form_variation_style_properties
93.10% covered (success)
93.10%
54 / 58
0.00% covered (danger)
0.00%
0 / 1
19.12
 render_outline_label
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 render_animated_label
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 render_below_label
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 render_field
74.29% covered (warning)
74.29%
78 / 105
0.00% covered (danger)
0.00%
0 / 1
51.52
 get_field_extra
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 maybe_override_type
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 is_field_renderable
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
5.25
 get_form_style
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 has_inset_label
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 render_rating_field
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 1
132
 render_slider_field
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
72
 enqueue_slider_field_assets
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 get_translatable_countries
100.00% covered (success)
100.00%
241 / 241
100.00% covered (success)
100.00%
1 / 1
1
 enqueue_phone_field_assets
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
3
 trim_image_select_options
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * Contact_Form_Field class.
4 *
5 * @package automattic/jetpack-forms
6 */
7
8namespace Automattic\Jetpack\Forms\ContactForm;
9
10use Automattic\Jetpack\Assets;
11use Automattic\Jetpack\Constants;
12use Automattic\Jetpack\Forms\Jetpack_Forms;
13
14if ( ! defined( 'ABSPATH' ) ) {
15    exit( 0 );
16}
17
18/**
19 * Class for the contact-field shortcode.
20 * Parses shortcode to output the contact form field as HTML.
21 * Validates input.
22 */
23class Contact_Form_Field extends Contact_Form_Shortcode {
24
25    /**
26     * The shortcode name.
27     *
28     * @var string
29     */
30    public $shortcode_name = 'contact-field';
31
32    /**
33     * The parent form.
34     *
35     * @var Contact_Form
36     */
37    public $form;
38
39    /**
40     * Default or POSTed value.
41     *
42     * @var string|string[]
43     */
44    public $value;
45
46    /**
47     * Is the input valid?
48     *
49     * @var bool
50     */
51    public $error = false;
52
53    /**
54     * Styles to be applied to the field
55     *
56     * @var string
57     */
58    public $block_styles = '';
59
60    /**
61     * Classes to be applied to the field
62     *
63     * @var string
64     */
65    public $field_classes = '';
66
67    /**
68     * Styles to be applied to the field
69     *
70     * @var string
71     */
72    public $field_styles = '';
73
74    /**
75     * Classes to be applied to the field option
76     *
77     * @var string
78     */
79    public $option_classes = '';
80
81    /**
82     * Styles to be applied to the field option
83     *
84     * @var string
85     */
86    public $option_styles = '';
87
88    /**
89     * Classes to be applied to the field
90     *
91     * @var string
92     */
93    public $label_classes = '';
94
95    /**
96     * Styles to be applied to the field
97     *
98     * @var string
99     */
100    public $label_styles = '';
101
102    /**
103     * Constructor function.
104     *
105     * @param array        $attributes An associative array of shortcode attributes.  @see shortcode_atts().
106     * @param null|string  $content Null for selfclosing shortcodes.  The inner content otherwise.
107     * @param Contact_Form $form The parent form.
108     */
109    public function __construct( $attributes, $content = null, $form = null ) {
110        $attributes = shortcode_atts(
111            array(
112                'label'                        => null,
113                'togglelabel'                  => null,
114                'type'                         => 'text',
115                'required'                     => false,
116                'requiredtext'                 => null,
117                'requiredindicator'            => true,
118                'options'                      => array(),
119                'optionsdata'                  => array(),
120                'allowother'                   => null,
121                'isother'                      => null,
122                'otherplaceholder'             => null,
123                'id'                           => null,
124                'style'                        => null,
125                'fieldbackgroundcolor'         => null,
126                'buttonbackgroundcolor'        => null,
127                'buttonborderradius'           => null,
128                'buttonborderwidth'            => null,
129                'textcolor'                    => null,
130                'default'                      => null,
131                'values'                       => null,
132                'placeholder'                  => null,
133                'class'                        => null,
134                'width'                        => null,
135                'consenttype'                  => null,
136                'dateformat'                   => null,
137                'implicitconsentmessage'       => null,
138                'explicitconsentmessage'       => null,
139                'borderradius'                 => null,
140                'borderwidth'                  => null,
141                'lineheight'                   => null,
142                'labellineheight'              => null,
143                'bordercolor'                  => null,
144                'inputcolor'                   => null,
145                'labelcolor'                   => null,
146                'labelfontsize'                => null,
147                'fieldfontsize'                => null,
148                'labelclasses'                 => null,
149                'labelstyles'                  => null,
150                'inputclasses'                 => null,
151                'inputstyles'                  => null,
152                'optionclasses'                => null,
153                'optionstyles'                 => null,
154                'min'                          => null,
155                'max'                          => null,
156                'minlabel'                     => null,
157                'maxlabel'                     => null,
158                'step'                         => null,
159                'maxfiles'                     => null,
160                'fieldwrapperclasses'          => null,
161                'stylevariationattributes'     => array(),
162                'stylevariationclasses'        => null,
163                'stylevariationstyles'         => null,
164                'optionsclasses'               => null,
165                'optionsstyles'                => null,
166                'align'                        => null,
167                'variation'                    => null,
168                'iconstyle'                    => null, // For rating field icon style (lowercase for shortcode compatibility)
169                // full phone field attributes, might become a standalone country list input block
170                'showcountryselector'          => false,
171                'searchplaceholder'            => false,
172                // Image select field attributes
173                'ismultiple'                   => null,
174                'showlabels'                   => null,
175                'issupersized'                 => null,
176                'randomizeoptions'             => null,
177                'showotheroption'              => null,
178                // derived from block metadata for blockVisibility support
179                'labelhiddenbyblockvisibility' => null,
180            ),
181            $attributes,
182            'contact-field'
183        );
184
185        // special default for subject field
186        if ( 'subject' === $attributes['type'] && $attributes['default'] === null && $form !== null ) {
187            $attributes['default'] = $form->get_attribute( 'subject' );
188        }
189
190        // allow required=1 or required=true
191        if ( '1' === $attributes['required'] || 'true' === strtolower( $attributes['required'] ) ) {
192            $attributes['required'] = true;
193        } else {
194            $attributes['required'] = false;
195        }
196
197        if ( $attributes['requiredtext'] === null ) {
198            $attributes['requiredtext'] = __( '(required)', 'jetpack-forms' );
199        }
200
201        // parse out comma-separated options list (for selects, radios, and checkbox-multiples)
202        if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) {
203            $attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) );
204
205            if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) {
206                $attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) );
207            }
208        }
209
210        if ( ! empty( $attributes['optionsdata'] ) ) {
211            $attributes['optionsdata'] = json_decode( html_entity_decode( $attributes['optionsdata'], ENT_COMPAT ), true );
212        }
213
214        // allow boolean values for showcountryselector, only if it's set so we don't pollute other fields attrs
215        if ( isset( $attributes['showcountryselector'] ) ) {
216            if ( true === $attributes['showcountryselector'] || '1' === $attributes['showcountryselector'] || 'true' === strtolower( $attributes['showcountryselector'] ) ) {
217                $attributes['showcountryselector'] = true;
218            } else {
219                $attributes['showcountryselector'] = false;
220            }
221        }
222
223        if ( $form ) {
224            // make a unique field ID based on the label, with an incrementing number if needed to avoid clashes
225            $form_id = $form->get_attribute( 'id' );
226            $id      = $attributes['id'] ?? false;
227
228            $unescaped_label = $this->unesc_attr( $attributes['label'] );
229            $unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs?
230            $unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label );
231
232            if ( empty( $id ) ) {
233                $id        = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label );
234                $i         = 0;
235                $max_tries = 99;
236                while ( isset( $form->fields[ $id ] ) ) {
237                    ++$i;
238                    $id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i );
239
240                    if ( $i > $max_tries ) {
241                        break;
242                    }
243                }
244            }
245
246            $attributes['id'] = $id;
247        }
248
249        parent::__construct( $attributes, $content );
250
251        // Store parent form
252        $this->form = $form;
253    }
254
255    /**
256     * This field's input is invalid.  Flag as invalid and add an error to the parent form
257     *
258     * @param string $message The error message to display on the form.
259     */
260    public function add_error( $message ) {
261        $this->error = true;
262        $this->form->add_error( $this->get_attribute( 'id' ), $message );
263    }
264
265    /**
266     * Is the field input invalid?
267     *
268     * @see $error
269     *
270     * @return bool
271     */
272    public function is_error() {
273        return $this->error;
274    }
275
276    /**
277     * Check if the field has a value.
278     *
279     * This is used to determine if the field has been filled out by the user.
280     *
281     * @return bool True if the field has a value, false otherwise.
282     */
283    public function has_value() {
284        $field_id    = $this->get_attribute( 'id' );
285        $field_value = isset( $_POST[ $field_id ] ) ? wp_unslash( $_POST[ $field_id ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- no site changes.
286
287        if ( is_array( $field_value ) ) {
288            if ( empty( $field_value ) ) {
289                return false;
290            }
291            return ! empty( array_filter( $field_value ) );
292        }
293        return ! empty( trim( $field_value ) );
294    }
295
296    /**
297     * Validates the form input
298     */
299    public function validate() {
300        // If the field is already invalid, don't validate it again.
301        if ( $this->is_error() ) {
302            return;
303        }
304
305        $field_type = $this->maybe_override_type();
306        // If it's not required, there's nothing to validate
307        if ( ! $this->get_attribute( 'required' ) && ! $this->has_value() ) {
308            return;
309        }
310
311        if ( ! $this->is_field_renderable( $field_type ) ) {
312            return;
313        }
314
315        $field_id    = $this->get_attribute( 'id' );
316        $field_label = $this->get_attribute( 'label' );
317
318        if ( isset( $_POST[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes.
319            if ( is_array( $_POST[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes.
320                $field_value = array_map( 'sanitize_text_field', wp_unslash( $_POST[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verification should happen in caller.
321            } else {
322                $field_value = sanitize_text_field( wp_unslash( $_POST[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verification should happen in caller.
323            }
324        } else {
325            $field_value = '';
326        }
327
328        switch ( $field_type ) {
329            case 'url':
330                if ( ! is_string( $field_value ) || empty( $field_value ) || ! preg_match(
331                    // Changes to this regex should be synced with the regex in the render_url_field method of this class as both validate the same input. Note that this regex is in PCRE format.
332                    '%^(?:(?:https?|ftp)://)?(?:\S+(?::\S*)?@|\d{1,3}(?:\.\d{1,3}){3}|(?:(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)(?:\.(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)*(?:\.[a-z\x{00a1}-\x{ffff}]{2,6}))(?::\d+)?(?:[^\s]*)?$%iu',
333                    $field_value
334                ) ) {
335                    /* translators: %s is the name of a form field */
336                    $this->add_error( sprintf( __( '%s: Please enter a valid URL - https://www.example.com.', 'jetpack-forms' ), $field_label ) );
337                }
338                break;
339            case 'email':
340                // Make sure the email address is valid
341                if ( ! is_string( $field_value ) || ! is_email( $field_value ) ) {
342                    /* translators: %s is the name of a form field */
343                    $this->add_error( sprintf( __( '%s requires a valid email address.', 'jetpack-forms' ), $field_label ) );
344                }
345                break;
346            case 'checkbox-multiple':
347                // Check that there is at least one option selected
348                if ( empty( $field_value ) ) {
349                    /* translators: %s is the name of a form field */
350                    $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) );
351                } else {
352
353                    $options_data    = (array) $this->get_attribute( 'optionsdata' );
354                    $possible_values = array();
355                    if ( ! empty( $options_data ) ) {
356                        foreach ( $options_data as $option_index => $option ) {
357                            $option_label = isset( $option['label'] ) ? Contact_Form_Plugin::strip_tags( $option['label'] ) : '';
358                            if ( is_string( $option_label ) && '' !== $option_label ) {
359                                $possible_values[] = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option_label );
360                            }
361                        }
362                    } else {
363                        foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) {
364                            $option = Contact_Form_Plugin::strip_tags( $option );
365                            if ( is_string( $option ) && '' !== $option ) {
366                                $possible_values[] = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option );
367                            }
368                        }
369                    }
370
371                    $non_empty_options = array_map( array( $this, 'sanitize_text_field' ), $possible_values );
372
373                    foreach ( $field_value  as $field_value_item ) {
374                        if ( ! in_array( $field_value_item, $non_empty_options, true ) ) {
375                            /* translators: %s is the name of a form field */
376                            $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) );
377                            break;
378                        }
379                    }
380                }
381                break;
382            case 'radio':
383                // Check that there is at least one option selected
384                if ( empty( $field_value ) ) {
385                    /* translators: %s is the name of a form field */
386                    $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) );
387                } else {
388                    // Check that the selected options are valid
389                    $options      = (array) $this->get_attribute( 'options' );
390                    $options_data = (array) $this->get_attribute( 'optionsdata' );
391                    $allow_other  = $this->get_attribute( 'allowother' );
392
393                    if ( ! empty( $options_data ) ) {
394                        $options = array_map(
395                            function ( $option ) {
396                                return $this->sanitize_text_field( trim( $option['label'] ) );
397                            },
398                            $options_data
399                        );
400                    } else {
401                        $options = array_map( array( $this, 'sanitize_text_field' ), $options );
402                    }
403                    $non_empty_options = array_filter(
404                        $options,
405                        function ( $option ) {
406                            return $option !== '';
407                        }
408                    );
409
410                    // Validate Other option values, which may include custom user text.
411                    $is_valid_other = false;
412                    if ( $allow_other ) {
413                        $other_label = null;
414                        foreach ( $options_data as $option ) {
415                            if ( ! empty( $option['isOther'] ) ) {
416                                $other_label = Contact_Form_Plugin::strip_tags( $option['label'] );
417                                break;
418                            }
419                        }
420
421                        if ( ! empty( $other_label ) ) {
422                            $other_prefix = $other_label . ': ';
423
424                            if ( $field_value === $other_label ) {
425                                $is_valid_other = true;
426                            } elseif ( strpos( $field_value, $other_prefix ) === 0 ) {
427                                $custom_text = trim( substr( $field_value, strlen( $other_prefix ) ) );
428
429                                if ( $this->get_attribute( 'required' ) && empty( $custom_text ) ) {
430                                    /* translators: %1$s is the name of a form field, %2$s is the option label */
431                                    $this->add_error( sprintf( __( '%1$s requires a response when "%2$s" is selected.', 'jetpack-forms' ), $field_label, $other_label ) );
432                                    break;
433                                }
434
435                                $is_valid_other = true;
436                            }
437                        }
438                    }
439
440                    if ( ! in_array( $field_value, $non_empty_options, true ) && ! $is_valid_other ) {
441                        /* translators: %s is the name of a form field */
442                        $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) );
443                        break;
444                    }
445                }
446                break;
447            case 'image-select':
448                // Check that there is at least one option selected
449                if ( empty( $field_value ) ) {
450                    /* translators: %s is the name of a form field */
451                    $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) );
452                } else {
453                    // Check that the selected options are valid
454                    $options      = (array) $this->get_attribute( 'options' );
455                    $options_data = (array) $this->get_attribute( 'optionsdata' );
456
457                    if ( ! empty( $options_data ) ) {
458                        // Extract letters from options_data for validation
459                        $options = array_map(
460                            function ( $option ) {
461                                return sanitize_text_field( trim( $option['letter'] ?? '' ) );
462                            },
463                            $options_data
464                        );
465                    }
466
467                    $non_empty_options = array_filter(
468                        $options,
469                        function ( $option ) {
470                            return $option !== '';
471                        }
472                    );
473
474                    // For single selection (radio), check if the selected value is in the options
475                    if ( ! $this->get_attribute( 'ismultiple' ) ) {
476                        // Decode the JSON response to get the selected value
477                        $decoded_value  = json_decode( $field_value, true );
478                        $selected_value = $decoded_value['selected'] ?? '';
479
480                        if ( ! in_array( $selected_value, $non_empty_options, true ) ) {
481                            /* translators: %s is the name of a form field */
482                            $this->add_error( sprintf( __( '%s requires a valid selection.', 'jetpack-forms' ), $field_label ) );
483                        }
484                    } else {
485                        // For multiple selection (checkbox), check each selected value
486                        foreach ( $field_value as $field_value_item ) {
487                            // Decode the JSON response to get the selected value
488                            $decoded_item   = json_decode( $field_value_item, true );
489                            $selected_value = $decoded_item['selected'] ?? '';
490
491                            if ( ! in_array( $selected_value, $non_empty_options, true ) ) {
492                                /* translators: %s is the name of a form field */
493                                $this->add_error( sprintf( __( '%s requires valid selections.', 'jetpack-forms' ), $field_label ) );
494                                break;
495                            }
496                        }
497                    }
498                }
499                break;
500            case 'number':
501                // Make sure the number address is valid
502                if ( ! is_numeric( $field_value ) ) {
503                    /* translators: %s is the name of a form field */
504                    $this->add_error( sprintf( __( '%s requires a number.', 'jetpack-forms' ), $field_label ) );
505                }
506                break;
507            case 'time':
508                // Make sure the number address is valid
509                if ( ! preg_match( '/^(?:2[0-3]|[01][0-9]):[0-5][0-9]$/', $field_value ) ) {
510                    /* translators: %s is the name of a form field */
511                    $this->add_error( sprintf( __( '%s requires a time', 'jetpack-forms' ), $field_label ) );
512                }
513                break;
514            case 'file':
515                // Make sure the file field is not empty
516                if ( ! is_array( $field_value ) || empty( $field_value[0] ) ) {
517                    /* translators: %s is the name of a form field */
518                    $this->add_error( sprintf( __( '%s requires a file to be uploaded.', 'jetpack-forms' ), $field_label ) );
519                }
520                break;
521            default:
522                // Just check for presence of any text
523                if ( ! is_string( $field_value ) || ! strlen( trim( $field_value ) ) ) {
524                    /* translators: %s is the name of a form field */
525                    $this->add_error( sprintf( __( '%s field is required.', 'jetpack-forms' ), $field_label ) );
526                }
527        }
528    }
529    /**
530     * Sanitize a text field value and html_entity_decode the field.
531     *
532     * @param string $field_value The field value to sanitize.
533     * @return string The sanitized field value.
534     */
535    public function sanitize_text_field( $field_value ) {
536        return sanitize_text_field( html_entity_decode( $field_value, ENT_COMPAT ) );
537    }
538
539    /**
540     * Check the default value for options field
541     *
542     * @param string $value - the value we're checking.
543     * @param int    $index - the index.
544     * @param string $options - default field option.
545     *
546     * @return string
547     */
548    public function get_option_value( $value, $index, $options ) {
549        if ( empty( $value[ $index ] ) ) {
550            return $options;
551        }
552        return $value[ $index ];
553    }
554
555    /**
556     * Outputs the HTML for this form field
557     *
558     * @return string HTML
559     */
560    public function render() {
561
562        $field_id                 = $this->get_attribute( 'id' );
563        $field_type               = $this->maybe_override_type();
564        $field_label              = $this->get_attribute( 'label' );
565        $field_required           = $this->get_attribute( 'required' );
566        $field_required_text      = $this->get_attribute( 'requiredtext' );
567        $field_required_indicator = (bool) $this->get_attribute( 'requiredindicator' );
568        $field_placeholder        = $this->get_attribute( 'placeholder' );
569        $field_width              = $this->get_attribute( 'width' );
570        $class                    = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' );
571
572        $label_classes  = $this->get_attribute( 'labelclasses' );
573        $label_styles   = $this->get_attribute( 'labelstyles' );
574        $input_classes  = $this->get_attribute( 'inputclasses' );
575        $input_styles   = $this->get_attribute( 'inputstyles' );
576        $option_classes = $this->get_attribute( 'optionclasses' );
577        $option_styles  = $this->get_attribute( 'optionstyles' );
578
579        $has_block_support_styles = ! empty( $label_classes ) || ! empty( $label_styles ) || ! empty( $input_classes ) || ! empty( $input_styles ) || ! empty( $option_classes ) || ! empty( $option_styles );
580
581        if ( $has_block_support_styles ) {
582            // Do any of the block support classes need to be applied at the field wrapper level? Do we need to make the classes etc filterable as per the field classes?
583
584            // Classes.
585            if ( ! empty( $label_classes ) ) {
586                $this->label_classes .= esc_attr( $label_classes );
587            }
588            if ( ! empty( $input_classes ) ) {
589                $class              .= $class ? ' ' . esc_attr( $input_classes ) : esc_attr( $input_classes );
590                $this->field_classes = $input_classes;
591            }
592            if ( ! empty( $option_classes ) ) {
593                $class               .= $class ? ' ' . esc_attr( $option_classes ) : esc_attr( $option_classes );
594                $this->option_classes = $option_classes;
595            }
596
597            // Styles.
598            if ( ! empty( $label_styles ) ) {
599                $this->label_styles .= esc_attr( $label_styles );
600            }
601            if ( ! empty( $input_styles ) ) {
602                $this->field_styles .= esc_attr( $input_styles );
603            }
604            if ( ! empty( $option_styles ) ) {
605                $this->option_styles .= esc_attr( $option_styles );
606            }
607
608            // For Outline style support.
609            $form_style = $this->get_form_style();
610            if ( 'outlined' === $form_style || 'animated' === $form_style ) {
611                $output_data         = $this->get_form_variation_style_properties( $form_style );
612                $this->block_styles .= esc_attr( $output_data['css_vars'] );
613            }
614        } else {
615            if ( is_numeric( $this->get_attribute( 'borderradius' ) ) ) {
616                $this->block_styles .= '--jetpack--contact-form--border-radius: ' . esc_attr( $this->get_attribute( 'borderradius' ) ) . 'px;';
617                $this->field_styles .= 'border-radius: ' . (int) $this->get_attribute( 'borderradius' ) . 'px;';
618            }
619
620            if ( is_numeric( $this->get_attribute( 'borderwidth' ) ) ) {
621                $this->block_styles .= '--jetpack--contact-form--border-size: ' . esc_attr( $this->get_attribute( 'borderwidth' ) ) . 'px;';
622                $this->field_styles .= 'border-width: ' . (int) $this->get_attribute( 'borderwidth' ) . 'px;';
623            }
624
625            if ( is_numeric( $this->get_attribute( 'lineheight' ) ) ) {
626                $this->block_styles  .= '--jetpack--contact-form--line-height: ' . esc_attr( $this->get_attribute( 'lineheight' ) ) . ';';
627                $this->field_styles  .= 'line-height: ' . (int) $this->get_attribute( 'lineheight' ) . ';';
628                $this->option_styles .= 'line-height: ' . (int) $this->get_attribute( 'lineheight' ) . ';';
629            }
630
631            if ( ! empty( $this->get_attribute( 'bordercolor' ) ) ) {
632                $this->block_styles .= '--jetpack--contact-form--border-color: ' . esc_attr( $this->get_attribute( 'bordercolor' ) ) . ';';
633                $this->field_styles .= 'border-color: ' . esc_attr( $this->get_attribute( 'bordercolor' ) ) . ';';
634            }
635
636            if ( ! empty( $this->get_attribute( 'inputcolor' ) ) ) {
637                $this->block_styles  .= '--jetpack--contact-form--text-color: ' . esc_attr( $this->get_attribute( 'inputcolor' ) ) . ';';
638                $this->block_styles  .= '--jetpack--contact-form--button-outline--text-color: ' . esc_attr( $this->get_attribute( 'inputcolor' ) ) . ';';
639                $this->field_styles  .= 'color: ' . esc_attr( $this->get_attribute( 'inputcolor' ) ) . ';';
640                $this->option_styles .= 'color: ' . esc_attr( $this->get_attribute( 'inputcolor' ) ) . ';';
641            }
642
643            if ( ! empty( $this->get_attribute( 'fieldbackgroundcolor' ) ) ) {
644                $this->block_styles .= '--jetpack--contact-form--input-background: ' . esc_attr( $this->get_attribute( 'fieldbackgroundcolor' ) ) . ';';
645                $this->field_styles .= 'background-color: ' . esc_attr( $this->get_attribute( 'fieldbackgroundcolor' ) ) . ';';
646            }
647
648            if ( ! empty( $this->get_attribute( 'fieldfontsize' ) ) ) {
649                $this->block_styles  .= '--jetpack--contact-form--font-size: ' . esc_attr( $this->get_attribute( 'fieldfontsize' ) ) . ';';
650                $this->field_styles  .= 'font-size: ' . esc_attr( $this->get_attribute( 'fieldfontsize' ) ) . ';';
651                $this->option_styles .= 'font-size: ' . esc_attr( $this->get_attribute( 'fieldfontsize' ) ) . ';';
652            }
653
654            if ( ! empty( $this->get_attribute( 'labelcolor' ) ) ) {
655                $this->label_styles .= 'color: ' . esc_attr( $this->get_attribute( 'labelcolor' ) ) . ';';
656            }
657
658            if ( ! empty( $this->get_attribute( 'labelfontsize' ) ) ) {
659                $this->label_styles .= 'font-size: ' . esc_attr( $this->get_attribute( 'labelfontsize' ) ) . ';';
660            }
661
662            if ( is_numeric( $this->get_attribute( 'labellineheight' ) ) ) {
663                $this->label_styles .= 'line-height: ' . (int) $this->get_attribute( 'labellineheight' ) . ';';
664            }
665        }
666
667        if ( ! empty( $this->get_attribute( 'buttonbackgroundcolor' ) ) ) {
668            $this->block_styles .= '--jetpack--contact-form--button-outline--background-color: ' . esc_attr( $this->get_attribute( 'buttonbackgroundcolor' ) ) . ';';
669        }
670        if ( is_numeric( $this->get_attribute( 'buttonborderradius' ) ) ) {
671            $this->block_styles .= '--jetpack--contact-form--button-outline--border-radius: ' . esc_attr( $this->get_attribute( 'buttonborderradius' ) ) . 'px;';
672        }
673        if ( is_numeric( $this->get_attribute( 'buttonborderwidth' ) ) ) {
674            $this->block_styles .= '--jetpack--contact-form--button-outline--border-size: ' . esc_attr( $this->get_attribute( 'buttonborderwidth' ) ) . 'px;';
675
676        }
677
678        if ( ! empty( $field_width ) && ! $this->has_inset_label() ) {
679            $class .= ' grunion-field-width-' . $field_width;
680        }
681
682        /**
683         * Filters the "class" attribute of the contact form input
684         *
685         * @module contact-form
686         *
687         * @since 6.6.0
688         *
689         * @param string $class Additional CSS classes for input class attribute.
690         */
691        $field_class = apply_filters( 'jetpack_contact_form_input_class', $class );
692
693        $this->value = $this->get_computed_field_value( $field_type, $field_id );
694
695        $field_value = Contact_Form_Plugin::strip_tags( $this->value );
696        $field_label = Contact_Form_Plugin::strip_tags( $field_label );
697
698        $extra_attrs = array();
699
700        if ( $field_type === 'number' || $field_type === 'slider' ) {
701            if ( is_numeric( $this->get_attribute( 'min' ) ) ) {
702                $extra_attrs['min'] = $this->get_attribute( 'min' );
703            }
704            if ( is_numeric( $this->get_attribute( 'max' ) ) ) {
705                $extra_attrs['max'] = $this->get_attribute( 'max' );
706            }
707            if ( is_numeric( $this->get_attribute( 'step' ) ) ) {
708                $extra_attrs['step'] = $this->get_attribute( 'step' );
709            }
710        }
711
712        if ( $field_type === 'slider' ) {
713            $minlabel = $this->get_attribute( 'minlabel' );
714            $maxlabel = $this->get_attribute( 'maxlabel' );
715            if ( null !== $minlabel && '' !== $minlabel ) {
716                $extra_attrs['minLabel'] = $minlabel;
717            }
718            if ( null !== $maxlabel && '' !== $maxlabel ) {
719                $extra_attrs['maxLabel'] = $maxlabel;
720            }
721        }
722
723        $rendered_field = $this->render_field( $field_type, $field_id, $field_label, $field_value, $field_class, $field_placeholder, $field_required, $field_required_text, $extra_attrs, $field_required_indicator );
724
725        /**
726         * Filter the HTML of the Contact Form.
727         *
728         * @module contact-form
729         *
730         * @since 2.6.0
731         *
732         * @param string $rendered_field Contact Form HTML output.
733         * @param string $field_label Field label.
734         * @param int|null $id Post ID.
735         */
736        return apply_filters( 'grunion_contact_form_field_html', $rendered_field, $field_label, ( in_the_loop() ? get_the_ID() : null ) );
737    }
738    /**
739     * Returns the computed field value for a field. It uses the POST, GET, Logged in data.
740     *
741     * @module contact-form
742     *
743     * @param string $field_type The field type.
744     * @param string $field_id The field id.
745     *
746     * @return string
747     */
748    public function get_computed_field_value( $field_type, $field_id ) {
749        global $current_user, $user_identity;
750        // Use the POST Field if it is available.
751        if ( isset( $_POST[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes.
752            if ( is_array( $_POST[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes.
753                return array_map( 'sanitize_textarea_field', wp_unslash( $_POST[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes.
754            }
755
756            return sanitize_textarea_field( wp_unslash( $_POST[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes.
757        }
758
759        // Use the GET Field if it is available.
760        if ( isset( $_GET[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
761            if ( is_array( $_GET[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
762                return array_map( 'sanitize_textarea_field', wp_unslash( $_GET[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
763            }
764
765            return sanitize_textarea_field( wp_unslash( $_GET[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
766        }
767
768        if ( ! is_user_logged_in() ) {
769            return $this->get_attribute( 'default' );
770        }
771
772        /**
773         * Allow third-party tools to prefill the contact form with the user's details when they're logged in.
774         *
775         * @module contact-form
776         *
777        * @since 3.2.0
778        *
779        * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false.
780        */
781        $filter_value = apply_filters( 'jetpack_auto_fill_logged_in_user', false );
782        if ( ( ! current_user_can( 'manage_options' ) && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) || $filter_value ) {
783            switch ( $field_type ) {
784                case 'email':
785                    return $current_user->data->user_email;
786
787                case 'name':
788                    return ! empty( $user_identity ) ? $user_identity : $current_user->data->display_name;
789
790                case 'url':
791                    return $current_user->data->user_url;
792            }
793        }
794
795        return $this->get_attribute( 'default' );
796    }
797
798    /**
799     * Return the HTML for the label.
800     *
801     * @param string $type - the field type.
802     * @param int    $id - the ID.
803     * @param string $label - the label.
804     * @param bool   $required - if the field is marked as required.
805     * @param string $required_field_text - the text in the required text field.
806     * @param array  $extra_attrs Array of key/value pairs to append as attributes to the element.
807     * @param bool   $always_render - if the label should always be shown.
808     * @param bool   $required_indicator Whether to display the required indicator.
809     *
810     * @return string HTML
811     */
812    public function render_label( $type, $id, $label, $required, $required_field_text, $extra_attrs = array(), $always_render = false, $required_indicator = true ) {
813        if ( $this->attributes['labelhiddenbyblockvisibility'] ) {
814            return '';
815        }
816        $form_style = $this->get_form_style();
817
818        if ( ! empty( $form_style ) && $form_style !== 'default' ) {
819            if ( ! in_array( $type, array( 'checkbox', 'checkbox-multiple', 'radio', 'consent', 'file' ), true ) ) {
820                switch ( $form_style ) {
821                    case 'outlined':
822                        return $this->render_outline_label( $id, $label, $required, $required_field_text, $required_indicator );
823                    case 'animated':
824                        return $this->render_animated_label( $id, $label, $required, $required_field_text, $required_indicator );
825                    case 'below':
826                        return $this->render_below_label( $id, $label, $required, $required_field_text, $required_indicator );
827                }
828            }
829
830            if ( ! $always_render ) {
831                return '';
832            }
833        }
834
835        if ( ! empty( $this->label_styles ) ) {
836            $extra_attrs['style'] = $this->label_styles;
837        }
838
839        $type_class = $type ? ' ' . $type : '';
840
841        $extra_attrs['class'] = "grunion-field-label{$type_class}" . ( $this->is_error() ? ' form-error' : '' );
842
843        if ( ! empty( $this->label_classes ) ) {
844            $extra_attrs['class'] .= ' ' . $this->label_classes;
845        }
846
847        $extra_attrs_string = '';
848
849        foreach ( $extra_attrs as $attr => $val ) {
850            $extra_attrs_string .= sprintf( '%s="%s" ', esc_attr( $attr ), esc_attr( $val ) );
851        }
852
853        $type_class = $type ? ' ' . $type : '';
854        return "<label
855                for='" . esc_attr( $id ) . "' "
856                . $extra_attrs_string
857                . '>'
858                . wp_kses_post( $label )
859                . ( $required && $required_indicator ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
860            "</label>\n";
861    }
862
863    /**
864     * Whether the field's label is hidden by block visibility in any mode.
865     *
866     * Two modes hide a label: "hide everywhere" (labelhiddenbyblockvisibility,
867     * the label is not rendered at all) and per-viewport (the label is rendered
868     * but carries a wp-block-hidden-{viewport} class that display:none's it on
869     * that viewport). Either way the label is absent from the accessibility tree
870     * where hidden, so the field needs an aria-label fallback to keep a name.
871     *
872     * The visibility control now only survives on the label block (see
873     * FORMS-694), so this is the single place that decides "is the label hidden?"
874     *
875     * @return bool
876     */
877    private function is_label_hidden_by_block_visibility() {
878        if ( $this->get_attribute( 'labelhiddenbyblockvisibility' ) ) {
879            return true;
880        }
881
882        return strpos( (string) $this->get_attribute( 'labelclasses' ), 'wp-block-hidden-' ) !== false;
883    }
884
885    /**
886     * The accessible name to fall back to when the label is hidden by block
887     * visibility. Prefers the label text (it matches what sighted users see on
888     * viewports where the label is visible, for the per-viewport case) and only
889     * falls back to a secondary name when there is no label. See FORMS-694.
890     *
891     * @param string|null $fallback Secondary name to use when there is no label.
892     *                              Must be a raw string, not a built attribute
893     *                              fragment. Defaults to the raw placeholder.
894     * @return string
895     */
896    private function get_block_visibility_aria_label( $fallback = null ) {
897        $label = Contact_Form_Plugin::strip_tags( (string) $this->get_attribute( 'label' ) );
898        if ( '' !== $label ) {
899            return $label;
900        }
901
902        if ( null === $fallback ) {
903            $fallback = $this->get_attribute( 'placeholder' );
904        }
905
906        return Contact_Form_Plugin::strip_tags( (string) $fallback );
907    }
908
909    /**
910     * Return an ` aria-label='…'` attribute string when the field's label is
911     * hidden by block visibility (full or per-viewport), or '' otherwise.
912     *
913     * A hidden label (removed on full-hide, display:none per-viewport) is absent
914     * from the a11y tree, so every field that renders its own label â€” single
915     * inputs and the grouped <fieldset> legend alike â€” needs this fallback to
916     * keep an accessible name. Shared by all paths so none get missed. The
917     * attribute is skipped when there is no name to give. See FORMS-694.
918     *
919     * @param string|null $fallback Secondary name when there is no label (raw
920     *                              string, not a built attribute fragment).
921     * @return string
922     */
923    private function get_hidden_label_aria_label_attr( $fallback = null ) {
924        if ( ! $this->is_label_hidden_by_block_visibility() ) {
925            return '';
926        }
927
928        $name = $this->get_block_visibility_aria_label( $fallback );
929        if ( '' === $name ) {
930            return '';
931        }
932
933        return " aria-label='" . esc_attr( $name ) . "'";
934    }
935
936    /**
937     * Accessible name for the file field's dropzone <div role="button">.
938     *
939     * The dropzone is the field's interactive control, but its name is otherwise
940     * the generic "Select a file to upload.", so two hidden-label upload fields
941     * would be announced identically. When the label is hidden (full or
942     * per-viewport), prefix the field name â€” the same fallback the other single
943     * inputs use. Falls back to the plain instruction when the label is visible
944     * or there is no name to give. See FORMS-694.
945     *
946     * @return string
947     */
948    private function get_file_dropzone_aria_label() {
949        $select_file_text = __( 'Select a file to upload.', 'jetpack-forms' );
950
951        if ( ! $this->is_label_hidden_by_block_visibility() ) {
952            return $select_file_text;
953        }
954
955        $hidden_name = $this->get_block_visibility_aria_label();
956        if ( '' === $hidden_name ) {
957            return $select_file_text;
958        }
959
960        return sprintf(
961            /* translators: 1: the form field's label, 2: the "Select a file to upload." instruction. */
962            _x( '%1$s: %2$s', 'file upload field accessible name', 'jetpack-forms' ),
963            $hidden_name,
964            $select_file_text
965        );
966    }
967
968    /**
969     * Return the HTML for a legend that shares the same style as a label.
970     *
971     * @param string $type - the field type.
972     * @param int    $id - the ID.
973     * @param string $legend - the legend.
974     * @param bool   $required - if the field is marked as required.
975     * @param string $required_field_text - the text in the required text field.
976     * @param array  $extra_attrs Array of key/value pairs to append as attributes to the element.
977     * @param bool   $required_indicator Whether to display the required indicator.
978     *
979     * @return string HTML
980     */
981    public function render_legend_as_label( $type, $id, $legend, $required, $required_field_text, $extra_attrs = array(), $required_indicator = true ) {
982        // Full-hide via blockVisibility, mirroring render_label(). Grouped fields
983        // (radio, checkbox-multiple, image-select, rating) render their label as a
984        // legend, so they need the same guard. Per-viewport hiding still works via
985        // the label_classes applied below.
986        if ( $this->attributes['labelhiddenbyblockvisibility'] ) {
987            return '';
988        }
989        if ( ! empty( $this->label_styles ) ) {
990            $extra_attrs['style'] = $this->label_styles;
991        }
992
993        $type_class           = $type ? ' ' . $type : '';
994        $extra_attrs['class'] = "grunion-field-label{$type_class}" . ( $this->is_error() ? ' form-error' : '' );
995
996        if ( ! empty( $this->label_classes ) ) {
997            $extra_attrs['class'] .= ' ' . $this->label_classes;
998        }
999
1000        $extra_attrs_string = '';
1001        if ( is_array( $extra_attrs ) ) {
1002            foreach ( $extra_attrs as $attr => $val ) {
1003                $extra_attrs_string .= sprintf( '%s="%s" ', esc_attr( $attr ), esc_attr( $val ) );
1004            }
1005        }
1006
1007        return '<legend '
1008                . $extra_attrs_string
1009                . '>'
1010                . '<span class="grunion-label-text">' . wp_kses_post( $legend ) . '</span>'
1011                . ( $required && $required_indicator ? '<span class="grunion-label-required">' . $required_field_text . '</span>' : '' )
1012                . "</legend>\n";
1013    }
1014
1015    /**
1016     * Return the HTML for the input field.
1017     *
1018     * @param string $type - the field type.
1019     * @param int    $id - the ID.
1020     * @param string $value - the value of the field.
1021     * @param string $class - the field class.
1022     * @param string $placeholder - the field placeholder content.
1023     * @param bool   $required - if the field is marked as required.
1024     * @param array  $extra_attrs Array of key/value pairs to append as attributes to the element.
1025     *
1026     * @return string HTML
1027     */
1028    public function render_input_field( $type, $id, $value, $class, $placeholder, $required, $extra_attrs = array() ) {
1029        if ( ! is_string( $value ) ) {
1030            $value = '';
1031        }
1032
1033        $extra_attrs_string = '';
1034
1035        if ( ! empty( $this->field_styles ) ) {
1036            $extra_attrs['style'] = $this->field_styles;
1037        }
1038
1039        if ( is_array( $extra_attrs ) && ! empty( $extra_attrs ) ) {
1040            foreach ( $extra_attrs as $attr => $val ) {
1041                $extra_attrs_string .= sprintf( '%s="%s" ', esc_attr( $attr ), esc_attr( $val ) );
1042            }
1043        }
1044
1045        // this is a hack for Firefox to prevent users from falsely entering a something other than a number into a number field.
1046        if ( $type === 'number' ) {
1047            $extra_attrs_string .= " data-wp-on--keypress='actions.handleNumberKeyPress' ";
1048        }
1049
1050        $extra_attrs_string .= $this->get_hidden_label_aria_label_attr();
1051
1052        return "<input
1053                    type='" . esc_attr( $type ) . "'
1054                    name='" . esc_attr( $id ) . "'
1055                    id='" . esc_attr( $id ) . "'
1056                    value='" . esc_attr( $value ) . "'
1057
1058                    data-wp-bind--aria-invalid='state.fieldAriaInvalid'
1059                    data-wp-bind--value='state.getFieldValue'
1060                    aria-describedby='" . esc_attr( $id ) . '-' . esc_attr( $type ) . "-error-message'
1061                    data-wp-on--input='actions.onFieldChange'
1062                    data-wp-on--blur='actions.onFieldBlur'
1063                    data-wp-class--has-value='state.hasFieldValue'
1064
1065                    " . $class . $placeholder . '
1066                    ' . ( $required ? "required='true' aria-required='true' " : '' ) .
1067                    $extra_attrs_string .
1068                    " />\n " . $this->get_error_div( $id, $type ) . " \n";
1069    }
1070
1071    /**
1072     * Return the HTML for the error div.
1073     *
1074     * @param string $id - the field ID.
1075     * @param string $type - the field type.
1076     * @param bool   $override_render - if the error div should be rendered even if the label is inset.
1077     *
1078     * @return string HTML
1079     */
1080    private function get_error_div( $id, $type, $override_render = false ) {
1081
1082        if ( $this->has_inset_label() && ! $override_render ) {
1083            return '';
1084        }
1085        return '
1086            <div id="' . esc_attr( $id ) . '-' . esc_attr( $type ) . '-error" class="contact-form__input-error" data-wp-class--has-errors="state.fieldHasErrors">
1087                <span class="contact-form__warning-icon" aria-hidden="true">
1088                    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1089                        <path d="M8.50015 11.6402H7.50015V10.6402H8.50015V11.6402Z" />
1090                        <path d="M7.50015 9.64018H8.50015V6.30684H7.50015V9.64018Z" />
1091                        <path fill-rule="evenodd" clip-rule="evenodd" d="M6.98331 3.0947C7.42933 2.30177 8.57096 2.30177 9.01698 3.09469L13.8771 11.7349C14.3145 12.5126 13.7525 13.4735 12.8602 13.4735H3.14004C2.24774 13.4735 1.68575 12.5126 2.12321 11.7349L6.98331 3.0947ZM8.14541 3.58496C8.08169 3.47168 7.9186 3.47168 7.85488 3.58496L2.99478 12.2251C2.93229 12.3362 3.01257 12.4735 3.14004 12.4735H12.8602C12.9877 12.4735 13.068 12.3362 13.0055 12.2251L8.14541 3.58496Z" />
1092                    </svg>
1093                </span>
1094                <span data-wp-text="state.errorMessage" id="' . esc_attr( $id ) . '-' . esc_attr( $type ) . '-error-message"></span>
1095            </div>';
1096    }
1097
1098    /**
1099     * Set the invalid message for specific field types.
1100     *
1101     * @param string $type - the field type.
1102     * @param string $message - the message to display.
1103     *
1104     * @return void
1105     */
1106    private function set_invalid_message( $type, $message ) {
1107        wp_interactivity_config(
1108            'jetpack/form',
1109            array(
1110                'error_types' => array(
1111                    'invalid_' . $type => $message,
1112                ),
1113            )
1114        );
1115    }
1116
1117    /**
1118     * Return the HTML for the email field.
1119     *
1120     * @param int    $id - the ID.
1121     * @param string $label - the label.
1122     * @param string $value - the value of the field.
1123     * @param string $class - the field class.
1124     * @param bool   $required - if the field is marked as required.
1125     * @param string $required_field_text - the text in the required text field.
1126     * @param string $placeholder - the field placeholder content.
1127     * @param bool   $required_indicator Whether to display the required indicator.
1128     *
1129     * @return string HTML
1130     */
1131    public function render_email_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) {
1132        $this->set_invalid_message( 'email', __( 'Please enter a valid email address', 'jetpack-forms' ) );
1133        $field  = $this->render_label( 'email', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
1134        $field .= $this->render_input_field( 'email', $id, $value, $class, $placeholder, $required );
1135        return $field;
1136    }
1137
1138    /**
1139     * Return the HTML for the telephone field.
1140     *
1141     * @param int    $id - the ID.
1142     * @param string $label - the label.
1143     * @param string $value - the value of the field.
1144     * @param string $class - the field class.
1145     * @param bool   $required - if the field is marked as required.
1146     * @param string $required_field_text - the text in the required text field.
1147     * @param string $placeholder - the field placeholder content.
1148     * @param bool   $required_indicator Whether to display the required indicator.
1149     *
1150     * @return string HTML
1151     */
1152    public function render_telephone_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) {
1153        $show_country_selector = $this->get_attribute( 'showcountryselector' );
1154        $default_country       = $this->get_attribute( 'default' );
1155        $search_placeholder    = $this->get_attribute( 'searchplaceholder' );
1156
1157        if ( ! $show_country_selector ) {
1158            // old telephone field treatment
1159            $this->set_invalid_message( 'telephone', __( 'Please enter a valid phone number', 'jetpack-forms' ) );
1160            $label = $this->render_label( 'telephone', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
1161            $field = $this->render_input_field( 'tel', $id, $value, $class, $placeholder, $required );
1162            return $label . $field;
1163        }
1164
1165        if ( empty( $search_placeholder ) ) {
1166            $search_placeholder = __( 'Search countries…', 'jetpack-forms' );
1167        }
1168
1169        $this->enqueue_phone_field_assets();
1170
1171        // $class is ill-formed, so we need to fix it
1172        // Strip 'class=' and quotes to get just the class names
1173        $class_names = preg_replace( "/^class=['\"]([^'\"]*)['\"].*$/", '$1', $class );
1174        // somehow we are getting the class jetpack-field__input-element on the wrong wrapper
1175        // .jetpack-field__input-element is meant to be applied just to the input element, not its wrapper.
1176        // Remove the jetpack-field__input-element class token regardless of its position and normalize whitespace.
1177        $class_names = preg_replace( '/\s*\bjetpack-field__input-element\b\s*/', ' ', $class_names );
1178        $class_names = trim( preg_replace( '/\s+/', ' ', $class_names ) );
1179
1180        $link_label_id = $id . '-number';
1181
1182        $this->set_invalid_message( 'phone', __( 'Please enter a valid phone number', 'jetpack-forms' ) );
1183        $label = $this->render_label( 'phone', $link_label_id, $label, $required, $required_field_text, array(), false, $required_indicator );
1184        if ( ! is_string( $value ) ) {
1185            $value = '';
1186        }
1187
1188        $translated_countries = $this->get_translatable_countries();
1189        $global_config        = array(
1190            'i18n' => array(
1191                'countryNames' => $translated_countries,
1192            ),
1193        );
1194        wp_interactivity_config( 'jetpack/field-phone', $global_config );
1195        ob_start();
1196        ?>
1197        <div
1198            class="jetpack-field__input-phone-wrapper <?php echo esc_attr( $this->get_attribute( 'stylevariationclasses' ) ); ?> <?php echo esc_attr( $class_names ); ?>"
1199            style="<?php echo ( ! empty( $this->field_styles ) && is_string( $this->field_styles ) ? esc_attr( $this->field_styles ) : '' ); ?>"
1200            data-wp-on--jetpack-form-reset='actions.phoneResetHandler'
1201            data-wp-class--is-combobox-open="context.comboboxOpen"
1202            <?php
1203            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- function is supposed to work this way
1204            echo wp_interactivity_data_wp_context(
1205                array(
1206                    'fieldId'             => $id,
1207                    'defaultCountry'      => $default_country,
1208                    'showCountrySelector' => $this->get_attribute( 'showcountryselector' ),
1209                    // dynamic
1210                    'phoneNumber'         => '',
1211                    'phoneCountryCode'    => $default_country,
1212                    'fullPhoneNumber'     => '',
1213                    'countryPrefix'       => '',
1214                    // combobox state
1215                    'useCombobox'         => true,
1216                    'comboboxOpen'        => false,
1217                    'searchTerm'          => '',
1218                    'allCountries'        => array(),
1219                    'filteredCountries'   => array(),
1220                    'selectedCountry'     => array(),
1221                )
1222            );
1223            ?>
1224            >
1225                <div class="jetpack-field__input-prefix"
1226                    data-wp-bind--hidden="!context.showCountrySelector"
1227                    data-wp-on-document--click="actions.phoneComboboxDocumentClickHandler">
1228                    <div class="jetpack-custom-combobox">
1229
1230                        <button
1231                            class="jetpack-combobox-trigger"
1232                            type="button"
1233                            data-wp-on--click="actions.phoneComboboxToggle"
1234                            data-wp-bind--aria-expanded="context.comboboxOpen">
1235                            <span
1236                                class="jetpack-combobox-selected wp-exclude-emoji"
1237                                data-wp-watch="callbacks.updateSelectedFlag">&#8203;</span>
1238                            <span
1239                                class="jetpack-combobox-trigger-arrow"
1240                                data-wp-class--is-open="context.comboboxOpen">
1241                                <svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
1242                                    <path d="M1 1L5 5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1243                                </svg>
1244                            </span>
1245                            <span
1246                                class="jetpack-combobox-selected"
1247                                data-wp-text="context.selectedCountry.value"></span>
1248                        </button>
1249                        <div
1250                            class="jetpack-combobox-dropdown <?php echo esc_attr( $this->get_attribute( 'stylevariationclasses' ) ); ?>"
1251                            style="<?php echo ( ! empty( $this->field_styles ) && is_string( $this->field_styles ) ? esc_attr( $this->field_styles ) : '' ); ?>"
1252                            data-wp-bind--hidden="!context.comboboxOpen">
1253                            <input
1254                                class="jetpack-combobox-search"
1255                                type="text"
1256                                placeholder="<?php echo esc_attr( $search_placeholder ); ?>"
1257                                data-wp-on--input="actions.phoneComboboxInputHandler"
1258                                data-wp-on--keydown="actions.phoneComboboxKeydownHandler"
1259                                data-wp-init="callbacks.registerPhoneComboboxSearchInput">
1260                            <div class="jetpack-combobox-options" data-wp-init="callbacks.registerPhoneComboboxOptionsList">
1261                                <template
1262                                    data-wp-each--filtered="context.filteredCountries"
1263                                    data-wp-each-key="context.filtered.code">
1264                                    <div
1265                                        class="jetpack-combobox-option"
1266                                        data-wp-key="context.filtered.code"
1267                                        data-wp-class--jetpack-combobox-option-selected="context.filtered.selected"
1268                                        data-wp-on--click="actions.phoneCountryChangeHandler">
1269                                        <span class="jetpack-combobox-option-icon wp-exclude-emoji" data-wp-watch="callbacks.updateOptionFlag">&#8203;</span>
1270                                        <span class="jetpack-combobox-option-value" data-wp-text="context.filtered.value"></span>
1271                                        <span class="jetpack-combobox-option-description" data-wp-text="context.filtered.country"></span>
1272                                    </div>
1273                                </template>
1274                            </div>
1275                        </div>
1276                    </div>
1277                </div>
1278                <input
1279                    class="jetpack-field__input-element"
1280                    <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- both are escaped in calling function ?>
1281                    <?php echo $placeholder; ?>
1282                    type="tel"
1283                    <?php if ( $required ) { ?>
1284                        required="true"
1285                        aria-required="true"
1286                    <?php } ?>
1287                    id="<?php echo esc_attr( $link_label_id ); ?>"
1288                    name="<?php echo esc_attr( $link_label_id ); ?>"
1289                    data-wp-bind--disabled='state.isSubmitting'
1290                    data-wp-bind--aria-invalid='state.fieldAriaInvalid'
1291                    data-wp-bind--value='context.phoneNumber'
1292                    aria-describedby="<?php echo esc_attr( $id ); ?>-telephone-error-message"
1293                    data-wp-on--input='actions.phoneNumberInputHandler'
1294                    data-wp-on--blur='actions.onFieldBlur'
1295                    data-wp-on--focus='actions.phoneNumberFocusHandler'
1296                    data-wp-class--has-value='context.phoneNumber'
1297                    data-wp-init="callbacks.registerPhoneInput"
1298                    data-wp-init--phone-field-custom-combobox="callbacks.initializePhoneFieldCustomComboBox"
1299                    <?php echo $this->get_hidden_label_aria_label_attr(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute value is escaped in the helper. ?>
1300                    />
1301                <input type="hidden"
1302                    id="<?php echo esc_attr( $id ); ?>"
1303                    name="<?php echo esc_attr( $id ); ?>"
1304                    data-wp-bind--value='context.fullPhoneNumber' />
1305        </div>
1306        <?php
1307        $input = ob_get_clean();
1308
1309        $field = $label . $input . $this->get_error_div( $id, 'telephone' );
1310        return $field;
1311    }
1312
1313    /**
1314     * Return the HTML for the URL field.
1315     *
1316     * @param int    $id - the ID.
1317     * @param string $label - the label.
1318     * @param string $value - the value of the field.
1319     * @param string $class - the field class.
1320     * @param bool   $required - if the field is marked as required.
1321     * @param string $required_field_text - the text in the required text field.
1322     * @param string $placeholder - the field placeholder content.
1323     * @param bool   $required_indicator Whether to display the required indicator.
1324     *
1325     * @return string HTML
1326     */
1327    public function render_url_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) {
1328        $this->set_invalid_message( 'url', __( 'Please enter a valid URL - https://www.example.com', 'jetpack-forms' ) );
1329
1330        $field  = $this->render_label( 'url', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
1331        $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
1332        return $field;
1333    }
1334
1335    /**
1336     * Return the HTML for the text area field.
1337     *
1338     * @param int    $id - the ID.
1339     * @param string $label - the label.
1340     * @param string $value - the value of the field.
1341     * @param string $class - the field class.
1342     * @param bool   $required - if the field is marked as required.
1343     * @param string $required_field_text - the text in the required text field.
1344     * @param string $placeholder - the field placeholder content.
1345     * @param bool   $required_indicator Whether to display the required indicator.
1346     *
1347     * @return string HTML
1348     */
1349    public function render_textarea_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) {
1350        if ( ! is_string( $value ) ) {
1351            $value = '';
1352        }
1353
1354        $field  = $this->render_label( 'textarea', 'contact-form-comment-' . $id, $label, $required, $required_field_text, array(), false, $required_indicator );
1355        $field .= "<textarea
1356                        style='" . $this->field_styles . "'
1357                        name='" . esc_attr( $id ) . "'
1358                        id='contact-form-comment-" . esc_attr( $id ) . "'
1359                        rows='20'
1360                        data-wp-text='state.getFieldValue'
1361                        data-wp-on--input='actions.onFieldChange'
1362                        data-wp-on--blur='actions.onFieldBlur'
1363                        data-wp-class--has-value='state.hasFieldValue'
1364                        aria-describedby='" . esc_attr( $id ) . "-textarea-error-message'
1365                        data-wp-bind--aria-invalid='state.fieldAriaInvalid'
1366                        "
1367                        . $this->get_hidden_label_aria_label_attr()
1368                        . $class
1369                        . $placeholder
1370                        . ' ' . ( $required ? "required aria-required='true'" : '' ) .
1371                        '>' . esc_textarea( $value )
1372                . "</textarea>\n " . $this->get_error_div( $id, 'textarea' ) . "\n";
1373        return $field;
1374    }
1375
1376    /**
1377     * Return the HTML for the radio field.
1378     *
1379     * @param string $id - the ID (starts with 'g' - see constructor).
1380     * @param string $label - the label.
1381     * @param string $value - the value of the field.
1382     * @param string $class - the field class.
1383     * @param bool   $required - if the field is marked as required.
1384     * @param string $required_field_text - the text in the required text field.
1385     * @param bool   $required_indicator Whether to display the required indicator.
1386     *
1387     * @return string HTML
1388     */
1389    public function render_radio_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
1390        $this->set_invalid_message( 'radio', __( 'Please select one of the options.', 'jetpack-forms' ) );
1391        $options_classes   = $this->get_attribute( 'optionsclasses' );
1392        $options_styles    = $this->get_attribute( 'optionsstyles' );
1393        $form_style        = $this->get_form_style();
1394        $is_outlined_style = 'outlined' === $form_style;
1395        $fieldset_id       = "id='" . esc_attr( "$id-label" ) . "'" . $this->get_hidden_label_aria_label_attr();
1396
1397        if ( $is_outlined_style ) {
1398            $style_variation_attributes = $this->get_attribute( 'stylevariationattributes' );
1399
1400            if ( ! empty( $style_variation_attributes ) ) {
1401                $style_variation_attributes = json_decode( html_entity_decode( $style_variation_attributes, ENT_COMPAT ), true );
1402            }
1403
1404            // When there's an outlined style, and border radius is set, the existing inline border radius is overridden to apply
1405            // a limit of `100px` to the radius on the x axis. This achieves the same look and feel as other fields
1406            // that use the notch html (`notched-label__leading` has a max-width of `100px` to prevent it from getting too wide).
1407            // It prevents large border radius values from disrupting the look and feel of the fields.
1408            if ( isset( $style_variation_attributes['border']['radius'] ) ) {
1409                $options_styles          = $options_styles ?? '';
1410                $radius                  = $style_variation_attributes['border']['radius'];
1411                $has_split_radius_values = is_array( $radius );
1412                $top_left_radius         = $has_split_radius_values ? $radius['topLeft'] : $radius;
1413                $top_right_radius        = $has_split_radius_values ? $radius['topRight'] : $radius;
1414                $bottom_left_radius      = $has_split_radius_values ? $radius['bottomLeft'] : $radius;
1415                $bottom_right_radius     = $has_split_radius_values ? $radius['bottomRight'] : $radius;
1416                $options_styles         .= "border-top-left-radius: min(100px, {$top_left_radius}{$top_left_radius};";
1417                $options_styles         .= "border-top-right-radius: min(100px, {$top_right_radius}{$top_right_radius};";
1418                $options_styles         .= "border-bottom-left-radius: min(100px, {$bottom_left_radius}{$bottom_left_radius};";
1419                $options_styles         .= "border-bottom-right-radius: min(100px, {$bottom_right_radius}{$bottom_right_radius};";
1420            }
1421
1422            /*
1423             * For the "outlined" style, the styles and classes are applied to the fieldset element.
1424             */
1425            $field = "<fieldset {$fieldset_id} class='grunion-radio-options " . esc_attr( $options_classes ) . "' style='" . esc_attr( $options_styles ) . "' data-wp-bind--aria-invalid='state.fieldAriaInvalid' >";
1426        } else {
1427            $field = "<fieldset {$fieldset_id} class='jetpack-field-multiple__fieldset' data-wp-bind--aria-invalid='state.fieldAriaInvalid' >";
1428        }
1429
1430        $field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text, array(), $required_indicator );
1431
1432        if ( ! $is_outlined_style ) {
1433            $field .= "<div class='grunion-radio-options " . esc_attr( $options_classes ) . "' style='" . esc_attr( $options_styles ) . "'>";
1434        }
1435
1436        $options_data  = $this->get_attribute( 'optionsdata' );
1437        $used_html_ids = array();
1438
1439        if ( ! empty( $options_data ) ) {
1440            foreach ( $options_data as $option_index => $option ) {
1441                $option_label = Contact_Form_Plugin::strip_tags( $option['label'] );
1442                if ( is_string( $option_label ) && '' !== $option_label ) {
1443                    $radio_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option_label );
1444                    $radio_id    = $id . '-' . sanitize_html_class( $radio_value );
1445
1446                    // If exact id was already used in this radio group, append option index.
1447                    // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc.
1448                    if ( isset( $used_html_ids[ $radio_id ] ) ) {
1449                        $radio_id .= '-' . $option_index;
1450                    }
1451                    $used_html_ids[ $radio_id ] = true;
1452
1453                    $default_classes = 'contact-form-field';
1454                    $option_styles   = empty( $option['style'] ) ? '' : "style='" . esc_attr( $option['style'] ) . "'";
1455                    $option_classes  = empty( $option['class'] ) ? $default_classes : $default_classes . ' ' . esc_attr( $option['class'] );
1456
1457                    $is_other_option  = ! empty( $option['isOther'] );
1458                    $change_action    = $is_other_option ? 'actions.onOtherRadioChange' : 'actions.onFieldChange';
1459                    $other_label_attr = $is_other_option ? " data-other-label='" . esc_attr( $option_label ) . "'" : '';
1460
1461                    $field .= "<label {$option_styles} class='{$option_classes}'>";
1462                    $field .= "<input
1463                                    id='" . esc_attr( $radio_id ) . "'
1464                                    type='radio'
1465                                    name='" . esc_attr( $id ) . "'
1466                                    value='" . esc_attr( $radio_value ) . "'
1467                                    data-wp-on--change='" . $change_action . "'"
1468                                    . $other_label_attr . ' '
1469                                    . $class
1470                                    . checked( $option_label, $value, false ) . ' '
1471                                    . ( $required ? "required aria-required='true'" : '' )
1472                                    . '/> ';
1473                    $field .= "<span class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
1474                    $field .= "<span class='grunion-field-text'>" . esc_html( $option_label ) . '</span>';
1475                    $field .= '</span>';
1476                    $field .= '</label>';
1477
1478                    if ( $is_other_option ) {
1479                        $placeholder = ! empty( $option['otherPlaceholder'] ) ? $option['otherPlaceholder'] : '';
1480                        $field      .= $this->render_other_input_field( $radio_id, $required, $id, $this->field_styles, $placeholder );
1481                    }
1482                }
1483            }
1484        } else {
1485            $field_style = 'style="' . $this->option_styles . '"';
1486
1487            foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) {
1488                $option = Contact_Form_Plugin::strip_tags( $option );
1489                if ( is_string( $option ) && '' !== $option ) {
1490                    $radio_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option );
1491                    $radio_id    = $id . '-' . sanitize_html_class( $radio_value );
1492
1493                    // If exact id was already used in this radio group, append option index.
1494                    // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc.
1495                    if ( isset( $used_html_ids[ $radio_id ] ) ) {
1496                        $radio_id .= '-' . $option_index;
1497                    }
1498                    $used_html_ids[ $radio_id ] = true;
1499
1500                    $field .= "<p class='contact-form-field'>";
1501                    $field .= "<input
1502                                    id='" . esc_attr( $radio_id ) . "'
1503                                    type='radio'
1504                                    name='" . esc_attr( $id ) . "'
1505                                    value='" . esc_attr( $radio_value ) . "'
1506                                    data-wp-on--change='actions.onFieldChange' "
1507                                    . $class
1508                                    . checked( $option, $value, false ) . ' '
1509                                    . ( $required ? "required aria-required='true'" : '' )
1510                                    . '/> ';
1511                    $field .= "<label for='" . esc_attr( $radio_id ) . "{$field_style} class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
1512                    $field .= "<span class='grunion-field-text'>" . esc_html( $option ) . '</span>';
1513                    $field .= '</label>';
1514                    $field .= '</p>';
1515                }
1516            }
1517        }
1518
1519        if ( ! $is_outlined_style ) {
1520            $field .= '</div>';
1521        }
1522        $field .= $this->get_error_div( $id, 'radio' ) . '</fieldset>';
1523        return $field;
1524    }
1525
1526    /**
1527     * Render the "Other" text input field for radio and checkbox fields.
1528     *
1529     * @param string $option_id The ID of the "Other" option.
1530     * @param bool   $required Whether the main field is required.
1531     * @param string $id The base ID of the main field.
1532     * @param string $field_styles The styles to apply to the text input field.
1533     * @param string $placeholder Placeholder text for the input field.
1534     *
1535     * @return string The HTML for the "Other" text input field.
1536     */
1537    private function render_other_input_field( $option_id, $required, $id, $field_styles, $placeholder ) {
1538        $other_text_id      = esc_attr( $option_id ) . '-other-text';
1539        $other_label_id     = esc_attr( $option_id ) . '-other-label';
1540        $other_label_text   = ! empty( $placeholder ) ? $placeholder : __( 'Please specify…', 'jetpack-forms' );
1541        $aria_required_attr = $required ? "aria-required='true'" : '';
1542        $other_input_styles = ! empty( $field_styles ) ? " style='" . esc_attr( $field_styles ) . "' " : '';
1543
1544        $field  = "<div class='jetpack-other-text-input-wrapper' data-wp-class--is-visible='state.isOtherSelected' role='region'>";
1545        $field .= "<label id='" . $other_label_id . "' for='" . $other_text_id . "' class='screen-reader-text'>" . esc_html( $other_label_text ) . '</label>';
1546        $field .= "<input
1547                    id='" . $other_text_id . "'
1548                    name='" . esc_attr( $id ) . "-other-text'
1549                    type='text'
1550                    class='grunion-field'
1551                    " . $other_input_styles . "
1552                    placeholder='" . esc_attr( $placeholder ) . "'
1553                    value=''
1554                    aria-labelledby='" . $other_label_id . "'
1555                    " . $aria_required_attr . "
1556                    data-wp-on--input='actions.onOtherTextInput'
1557                    data-wp-bind--disabled='!state.isOtherSelected'
1558                    data-wp-class--has-value='state.hasFieldValue' />";
1559        $field .= '</div>';
1560
1561        return $field;
1562    }
1563
1564    /**
1565     * Return the HTML for the checkbox field.
1566     *
1567     * @param int    $id - the ID.
1568     * @param string $label - the label.
1569     * @param string $value - the value of the field.
1570     * @param string $class - the field class.
1571     * @param bool   $required - if the field is marked as required.
1572     * @param string $required_field_text - the text in the required text field.
1573     * @param bool   $required_indicator Whether to display the required indicator.
1574     *
1575     * @return string HTML
1576     */
1577    public function render_checkbox_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
1578        $label_class                   = 'grunion-field-label checkbox';
1579        $label_class                  .= $this->is_error() ? ' form-error' : '';
1580        $label_class                  .= $this->label_classes ? ' ' . $this->label_classes : '';
1581        $label_class                  .= $this->option_classes ? ' ' . $this->option_classes : '';
1582        $has_inner_block_option_styles = ! empty( $this->get_attribute( 'optionstyles' ) );
1583
1584        $field  = "<div class='contact-form__checkbox-wrap' style='" . ( $has_inner_block_option_styles ? esc_attr( $this->option_styles ) : '' ) . "' >";
1585        $field .= "<input id='" . esc_attr( $id ) . "' type='checkbox' data-wp-on--change='actions.onFieldChange' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack-forms' ) . "' " . $class . checked( (bool) $value, true, false ) . ' ' . ( $required ? "required aria-required='true'" : '' ) . "/> \n";
1586        $field .= "<label for='" . esc_attr( $id ) . "' class='" . esc_attr( $label_class ) . "' style='" . esc_attr( $this->label_styles ) . ( $has_inner_block_option_styles ? esc_attr( $this->option_styles ) : '' ) . "'>";
1587        $field .= wp_kses_post( $label ) . ( $required && $required_indicator ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' );
1588        $field .= "</label>\n";
1589        $field .= "<div class='clear-form'></div>\n";
1590        $field .= '</div>';
1591        return $field . $this->get_error_div( $id, 'checkbox' );
1592    }
1593
1594    /**
1595     * Return the HTML for the consent field.
1596     *
1597     * @param string $id field id.
1598     * @param string $class html classes (can be set by the admin).
1599     */
1600    private function render_consent_field( $id, $class ) {
1601        $consent_type                  = 'explicit' === $this->get_attribute( 'consenttype' ) ? 'explicit' : 'implicit';
1602        $consent_message               = 'explicit' === $consent_type ? $this->get_attribute( 'explicitconsentmessage' ) : $this->get_attribute( 'implicitconsentmessage' );
1603        $label_class                   = 'grunion-field-label consent consent-' . esc_attr( $consent_type );
1604        $label_class                  .= $this->option_classes ? ' ' . $this->option_classes : '';
1605        $label_class                  .= $this->is_error() ? ' form-error' : '';
1606        $has_inner_block_option_styles = ! empty( $this->get_attribute( 'optionstyles' ) );
1607
1608        $field = "<label class='" . esc_attr( $label_class ) . "' style='" . esc_attr( $this->label_styles ) . ( $has_inner_block_option_styles ? esc_attr( $this->option_styles ) : '' ) . "'>";
1609
1610        if ( 'implicit' === $consent_type ) {
1611            $field .= "\t\t<input type='hidden' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack-forms' ) . "' /> \n";
1612        } else {
1613            $field .= "\t\t<input type='checkbox' data-wp-on--change='actions.onFieldChange' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack-forms' ) . "' " . $class . "/> \n";
1614        }
1615        $field .= "\t\t" . wp_kses_post( $consent_message );
1616        $field .= "</label>\n";
1617        $field .= "<div class='clear-form'></div>\n";
1618        return $field . $this->get_error_div( $id, 'checkbox' );
1619    }
1620
1621    /**
1622     * Return the HTML for the file field.
1623     *
1624     * Renders a file upload field with drag-and-drop functionality.
1625     *
1626     * @since 0.45.0
1627     *
1628     * @param string $id - the field ID.
1629     * @param string $label - the field label.
1630     * @param string $class - the field CSS class.
1631     * @param bool   $required - if the field is marked as required.
1632     * @param string $required_field_text - the text in the required text field.
1633     * @param bool   $required_indicator Whether to display the required indicator.
1634     *
1635     * @return string HTML for the file upload field.
1636     */
1637    private function render_file_field( $id, $label, $class, $required, $required_field_text, $required_indicator = true ) {
1638        // Check if Jetpack is active
1639        if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) {
1640            return '<div class="jetpack-form-field-error">' .
1641                esc_html__( 'File upload field requires Jetpack to be active.', 'jetpack-forms' ) .
1642                '</div>';
1643        }
1644
1645        $this->set_invalid_message( 'file_uploading', __( 'Please wait a moment, file is currently uploading.', 'jetpack-forms' ) );
1646        $this->set_invalid_message( 'file_has_errors', __( 'Please remove any file upload errors.', 'jetpack-forms' ) );
1647
1648        // Enqueue necessary scripts and styles.
1649        $this->enqueue_file_field_assets();
1650
1651        // Get allowed MIME types for display in the field.
1652        $accepted_file_types = array_values(
1653            array(
1654                'jpg|jpeg|jpe'    => 'image/jpeg',
1655                'png'             => 'image/png',
1656                'gif'             => 'image/gif',
1657                'pdf'             => 'application/pdf',
1658                'doc'             => 'application/msword',
1659                'docx'            => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1660                'docm'            => 'application/vnd.ms-word.document.macroEnabled.12',
1661                'pot|pps|ppt'     => 'application/vnd.ms-powerpoint',
1662                'pptx'            => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1663                'pptm'            => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
1664                'odt'             => 'application/vnd.oasis.opendocument.text',
1665                'ppsx'            => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
1666                'ppsm'            => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
1667                'csv'             => 'text/csv',
1668                'xla|xls|xlt|xlw' => 'application/vnd.ms-excel',
1669                'xlsx'            => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1670                'xlsm'            => 'application/vnd.ms-excel.sheet.macroEnabled.12',
1671                'xlsb'            => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
1672                'key'             => 'application/vnd.apple.keynote',
1673                'webp'            => 'image/webp',
1674                'heic'            => 'image/heic',
1675                'heics'           => 'image/heic-sequence',
1676                'heif'            => 'image/heif',
1677                'heifs'           => 'image/heif-sequence',
1678                'asc'             => 'application/pgp-keys',
1679            )
1680        );
1681
1682        $accept_attribute_value = implode( ', ', $accepted_file_types );
1683
1684        // Add accessibility attributes and required status if needed.
1685        $input_attrs = array(
1686            'type'       => 'file',
1687            'class'      => 'jetpack-form-file-field ' . esc_attr( $class ),
1688            'name'       => esc_attr( $id ),
1689            'id'         => esc_attr( $id ),
1690            'accept'     => esc_attr( $accept_attribute_value ),
1691            'aria-label' => esc_attr( $label ),
1692        );
1693
1694        if ( $required ) {
1695            $input_attrs['required']      = 'required';
1696            $input_attrs['aria-required'] = 'true';
1697        }
1698
1699        $max_files       = 1; // TODO: Dynamically retrieve the max number of files using $this->get_attribute( 'maxfiles' ) if needed in the future.
1700        $max_file_size   = 20 * 1024 * 1024; // 20MB
1701        $file_size_units = array(
1702            _x( 'B', 'unit symbol', 'jetpack-forms' ),
1703            _x( 'KB', 'unit symbol', 'jetpack-forms' ),
1704            _x( 'MB', 'unit symbol', 'jetpack-forms' ),
1705            _x( 'GB', 'unit symbol', 'jetpack-forms' ),
1706        );
1707
1708        $global_config = array(
1709            'i18n'          => array(
1710                'language'           => get_bloginfo( 'language' ),
1711                'fileSizeUnits'      => $file_size_units,
1712                'zeroBytes'          => __( '0 Bytes', 'jetpack-forms' ),
1713                'uploadError'        => __( 'Error uploading file', 'jetpack-forms' ),
1714                'folderNotSupported' => __( 'Folder uploads are not supported', 'jetpack-forms' ),
1715                // translators: %s is the formatted maximum file size.
1716                'fileTooLarge'       => sprintf( __( 'File is too large. Maximum allowed size is %s.', 'jetpack-forms' ), size_format( $max_file_size ) ),
1717                'invalidType'        => __( 'This file type is not allowed.', 'jetpack-forms' ),
1718                'maxFiles'           => __( 'You have exceeded the number of files that you can upload.', 'jetpack-forms' ),
1719                'uploadFailed'       => __( 'File upload failed, try again.', 'jetpack-forms' ),
1720            ),
1721            'endpoint'      => $this->get_unauth_endpoint_url(),
1722            'iconsPath'     => Jetpack_Forms::plugin_url() . 'contact-form/images/file-icons/',
1723            'maxUploadSize' => $max_file_size,
1724        );
1725
1726        wp_interactivity_config( 'jetpack/field-file', $global_config );
1727
1728        $context = array(
1729            'isDropping'       => false,
1730            'fieldId'          => $id,
1731            'files'            => array(),
1732            'allowedMimeTypes' => $accepted_file_types,
1733            'maxFiles'         => $max_files, // max number of files.
1734            'hasMaxFiles'      => false,
1735        );
1736
1737        $field = $this->render_label( 'file', $id, $label, $required, $required_field_text, array(), true, $required_indicator );
1738
1739        $dropzone_aria_label = $this->get_file_dropzone_aria_label();
1740
1741        ob_start();
1742        ?>
1743        <div
1744            class="jetpack-form-file-field__container"
1745            id="<?php echo esc_attr( $id ); ?>"
1746            name="dropzone-<?php echo esc_attr( $id ); ?>"
1747            data-wp-interactive="jetpack/field-file"
1748            <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output is pre-escaped by method ?>
1749            <?php echo wp_interactivity_data_wp_context( $context ); ?>
1750            data-wp-on--dragover="actions.dragOver"
1751            data-wp-on--dragleave="actions.dragLeave"
1752            data-wp-on--mouseleave="actions.dragLeave"
1753            data-wp-on--drop="actions.fileDropped"
1754            data-wp-on--jetpack-form-reset="actions.resetFiles"
1755            data-is-required="<?php echo esc_attr( $required ); ?>"
1756        >
1757            <div class="jetpack-form-file-field__dropzone" data-wp-class--is-dropping="context.isDropping" data-wp-class--is-hidden="state.hasMaxFiles">
1758                <div class="jetpack-form-file-field__dropzone-inner" data-wp-on--click="actions.openFilePicker" data-wp-on--keydown="actions.handleKeyDown" tabindex="0" role="button" aria-label="<?php echo esc_attr( $dropzone_aria_label ); ?>"></div>
1759                <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is intentionally unescaped as it contains block content that was previously escaped ?>
1760                <?php echo html_entity_decode( $this->content, ENT_COMPAT, 'UTF-8' ); ?>
1761                <input
1762                    type="file" class="jetpack-form-file-field"
1763                    accept="<?php echo esc_attr( $accept_attribute_value ); ?>"
1764                    data-wp-on--change="actions.fileAdded"  />
1765            </div>
1766            <div class="jetpack-form-file-field__preview-wrap" name="file-field-<?php echo esc_attr( $id ); ?>" data-wp-class--is-active="state.hasFiles">
1767                <template data-wp-each--file="context.files" data-wp-key="context.file.id">
1768                    <div class="jetpack-form-file-field__preview" tabindex="0" data-wp-bind--aria-label="context.file.name" data-wp-init--focus="callbacks.focusElement" data-wp-class--is-error="context.file.hasError" data-wp-class--is-complete="context.file.isUploaded">
1769                        <input type="hidden" name="<?php echo esc_attr( $id ); ?>[]" class="jetpack-form-file-field__hidden include-hidden" data-wp-bind--value='context.file.fileJson' value="">
1770                        <div class="jetpack-form-file-field__image-wrap" data-wp-style----progress="context.file.progress" data-wp-class--has-icon="context.file.hasIcon">
1771                            <div class="jetpack-form-file-field__image" data-wp-style--background-image="context.file.url" data-wp-style--mask-image="context.file.mask"></div>
1772                            <div class="jetpack-form-file-field__progress-bar" ></div>
1773                        </div>
1774
1775                        <div class="jetpack-form-file-field__file-wrap">
1776                            <strong class="jetpack-form-file-field__file-name" data-wp-text="context.file.name"></strong>
1777                            <div class="jetpack-form-file-field__file-info" data-wp-class--is-error="context.file.error" data-wp-class--is-complete="context.file.file_id">
1778                                <span class="jetpack-form-file-field__file-size" data-wp-text="context.file.formattedSize"></span>
1779                                <span class="jetpack-form-file-field__seperator"> &middot; </span>
1780                                <span aria-live="polite">
1781                                    <span class="jetpack-form-file-field__uploading"><?php esc_html_e( 'Uploading…', 'jetpack-forms' ); ?></span>
1782                                    <span class="jetpack-form-file-field__success"><?php esc_html_e( 'Uploaded', 'jetpack-forms' ); ?></span>
1783                                    <span class="jetpack-form-file-field__error" data-wp-text="context.file.error"></span>
1784                                </span>
1785                            </div>
1786                        </div>
1787                        <a href="#" class="jetpack-form-file-field__remove" data-wp-bind--data-id='context.file.id' aria-label="<?php esc_attr_e( 'Remove file', 'jetpack-forms' ); ?>" data-wp-on--click="actions.removeFile" data-wp-on--keydown="actions.removeFileKeydown" title="<?php esc_attr_e( 'Remove', 'jetpack-forms' ); ?>"> </a>
1788                    </div>
1789                </template>
1790            </div>
1791        </div>
1792        <?php
1793        return $field . ob_get_clean() . $this->get_error_div( $id, 'file' );
1794    }
1795
1796    /**
1797     * Render a hidden field.
1798     *
1799     * @param string $id - the field ID.
1800     * @param string $label - the field label.
1801     * @param string $value - the value of the field.
1802     *
1803     * @return string HTML for the hidden field.
1804     */
1805    private function render_hidden_field( $id, $label, $value ) {
1806        /**
1807         *
1808         * Filter the value of the hidden field.
1809         *
1810         * @since 6.3.0
1811         *
1812         * @param string $value The value of the hidden field.
1813         * @param string $label The label of the hidden field.
1814         * @param string $id The ID of the hidden field.
1815         *
1816         * @return string The modified value of the hidden field.
1817         */
1818        $value = apply_filters( 'jetpack_forms_hidden_field_value', $value, $label, $id );
1819        return "<input type='hidden' name='" . esc_attr( $id ) . "' id='" . esc_attr( $id ) . "' value='" . esc_attr( $value ) . "' />\n";
1820    }
1821
1822    /**
1823     * Enqueues scripts and styles needed for the file field.
1824     *
1825     * @since 0.45.0
1826     *
1827     * @return void
1828     */
1829    private function enqueue_file_field_assets() {
1830        $version = Constants::get_constant( 'JETPACK__VERSION' );
1831
1832        \wp_enqueue_script_module(
1833            'jetpack-form-file-field',
1834            plugins_url( '../../dist/modules/file-field/view.js', __FILE__ ),
1835            array( '@wordpress/interactivity' ),
1836            $version
1837        );
1838
1839        \wp_enqueue_style(
1840            'jetpack-form-file-field',
1841            plugins_url( '../../dist/contact-form/css/file-field.css', __FILE__ ),
1842            array(),
1843            $version
1844        );
1845    }
1846
1847    /**
1848     * Returns the URL for the unauthenticated file upload endpoint.
1849     *
1850     * @return string
1851     */
1852    private function get_unauth_endpoint_url() {
1853        // Return a placeholder URL if Jetpack is not active
1854        if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) {
1855            return '#jetpack-not-active';
1856        }
1857
1858        return sprintf( 'https://public-api.wordpress.com/wpcom/v2/sites/%d/unauth-file-upload', \Jetpack_Options::get_option( 'id' ) );
1859    }
1860
1861    /**
1862     * Return the HTML for the multiple checkbox field.
1863     *
1864     * @param string $id - the ID (starts with 'g' - see constructor).
1865     * @param string $label - the label.
1866     * @param string $value - the value of the field.
1867     * @param string $class - the field class.
1868     * @param bool   $required - if the field is marked as required.
1869     * @param string $required_field_text - the text in the required text field.
1870     * @param bool   $required_indicator Whether to display the required indicator.
1871     *
1872     * @return string HTML
1873     */
1874    public function render_checkbox_multiple_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
1875        $options_classes   = $this->get_attribute( 'optionsclasses' );
1876        $options_styles    = $this->get_attribute( 'optionsstyles' );
1877        $form_style        = $this->get_form_style();
1878        $is_outlined_style = 'outlined' === $form_style;
1879
1880        /*
1881         * The `data-required` attribute is used in `accessible-form.js` to ensure at least one
1882         * checkbox is checked. Unlike radio buttons, for which the required attribute is satisfied if
1883         * any of the radio buttons in the group is selected, adding a required attribute directly to
1884         * a checkbox means that this specific checkbox must be checked.
1885         */
1886        $fieldset_id = "id='" . esc_attr( "$id-label" ) . "'" . $this->get_hidden_label_aria_label_attr();
1887
1888        if ( $is_outlined_style ) {
1889            $style_variation_attributes = $this->get_attribute( 'stylevariationattributes' );
1890
1891            if ( ! empty( $style_variation_attributes ) ) {
1892                $style_variation_attributes = json_decode( html_entity_decode( $style_variation_attributes, ENT_COMPAT ), true );
1893            }
1894
1895            /*
1896             * When there's an outlined style, and border radius is set, the existing inline border radius is overridden to apply
1897             * a limit of `100px` to the radius on the x axis. This achieves the same look and feel as other fields
1898             * that use the notch html (`notched-label__leading` has a max-width of `100px` to prevent it from getting too wide).
1899             * It prevents large border radius values from disrupting the look and feel of the fields.
1900             */
1901            if ( isset( $style_variation_attributes['border']['radius'] ) ) {
1902                $options_styles          = $options_styles ?? '';
1903                $radius                  = $style_variation_attributes['border']['radius'];
1904                $has_split_radius_values = is_array( $radius );
1905                $top_left_radius         = $has_split_radius_values ? $radius['topLeft'] : $radius;
1906                $top_right_radius        = $has_split_radius_values ? $radius['topRight'] : $radius;
1907                $bottom_left_radius      = $has_split_radius_values ? $radius['bottomLeft'] : $radius;
1908                $bottom_right_radius     = $has_split_radius_values ? $radius['bottomRight'] : $radius;
1909                $options_styles         .= "border-top-left-radius: min(100px, {$top_left_radius}{$top_left_radius};";
1910                $options_styles         .= "border-top-right-radius: min(100px, {$top_right_radius}{$top_right_radius};";
1911                $options_styles         .= "border-bottom-left-radius: min(100px, {$bottom_left_radius}{$bottom_left_radius};";
1912                $options_styles         .= "border-bottom-right-radius: min(100px, {$bottom_right_radius}{$bottom_right_radius};";
1913            }
1914
1915            /*
1916             * For the "outlined" style, the styles and classes are applied to the fieldset element.
1917             */
1918            $field = "<fieldset {$fieldset_id} class='grunion-checkbox-multiple-options " . esc_attr( $options_classes ) . "' style='" . esc_attr( $options_styles ) . "' " . ( $required ? 'data-required' : '' ) . ' data-wp-bind--aria-invalid="state.fieldAriaInvalid">';
1919        } else {
1920            $field = "<fieldset {$fieldset_id} class='jetpack-field-multiple__fieldset'" . ( $required ? 'data-required' : '' ) . ' data-wp-bind--aria-invalid="state.fieldAriaInvalid">';
1921        }
1922
1923        $field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text, array(), $required_indicator );
1924
1925        if ( ! $is_outlined_style ) {
1926            $field .= "<div class='grunion-checkbox-multiple-options " . esc_attr( $options_classes ) . "' style='" . esc_attr( $options_styles ) . "' " . '>';
1927        }
1928
1929        $options_data  = $this->get_attribute( 'optionsdata' );
1930        $used_html_ids = array();
1931
1932        if ( ! empty( $options_data ) ) {
1933            foreach ( $options_data as $option_index => $option ) {
1934                $option_label = Contact_Form_Plugin::strip_tags( $option['label'] );
1935                if ( is_string( $option_label ) && '' !== $option_label ) {
1936                    $checkbox_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option_label );
1937                    $checkbox_id    = $id . '-' . sanitize_html_class( $checkbox_value );
1938
1939                    // If exact id was already used in this checkbox group, append option index.
1940                    // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc.
1941                    if ( isset( $used_html_ids[ $checkbox_id ] ) ) {
1942                        $checkbox_id .= '-' . $option_index;
1943                    }
1944                    $used_html_ids[ $checkbox_id ] = true;
1945
1946                    $default_classes = 'contact-form-field';
1947                    $option_styles   = empty( $option['style'] ) ? '' : "style='" . esc_attr( $option['style'] ) . "'";
1948                    $option_classes  = empty( $option['class'] ) ? $default_classes : $default_classes . ' ' . esc_attr( $option['class'] );
1949
1950                    $field .= "<label {$option_styles} class='{$option_classes}'>";
1951                    $field .= "<input
1952                                id='" . esc_attr( $checkbox_id ) . "'
1953                                type='checkbox'
1954                                data-wp-on--change='actions.onMultipleFieldChange'
1955                                name='" . esc_attr( $id ) . "[]'
1956                                value='" . esc_attr( $checkbox_value ) . "' "
1957                                . $class
1958                                . checked( in_array( $option_label, (array) $value, true ), true, false )
1959                                . ' /> ';
1960                    $field .= "<span class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
1961                    $field .= "<span class='grunion-field-text'>" . esc_html( $option_label ) . '</span>';
1962                    $field .= '</span>';
1963                    $field .= '</label>';
1964                }
1965            }
1966        } else {
1967            $field_style = 'style="' . $this->option_styles . '"';
1968
1969            foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) {
1970                $option = Contact_Form_Plugin::strip_tags( $option );
1971                if ( is_string( $option ) && '' !== $option ) {
1972                    $checkbox_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option );
1973                    $checkbox_id    = $id . '-' . sanitize_html_class( $checkbox_value );
1974
1975                    // If exact id was already used in this checkbox group, append option index.
1976                    // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc.
1977                    if ( isset( $used_html_ids[ $checkbox_id ] ) ) {
1978                        $checkbox_id .= '-' . $option_index;
1979                    }
1980                    $used_html_ids[ $checkbox_id ] = true;
1981
1982                    $field .= "<label class='contact-form-field'>";
1983                    $field .= "<input
1984                                id='" . esc_attr( $checkbox_id ) . "'
1985                                data-wp-on--change='actions.onMultipleFieldChange'
1986                                type='checkbox'
1987                                name='" . esc_attr( $id ) . "[]'
1988                                value='" . esc_attr( $checkbox_value ) . "' "
1989                                . $class
1990                                . checked( in_array( $option, (array) $value, true ), true, false )
1991                                . ' /> ';
1992                    $field .= "<span {$field_style} class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
1993                    $field .= "<span class='grunion-field-text'>" . esc_html( $option ) . '</span>';
1994                    $field .= '</span>';
1995                    $field .= '</label>';
1996                }
1997            }
1998        }
1999        if ( ! $is_outlined_style ) {
2000            $field .= '</div>';
2001        }
2002        $field .= $this->get_error_div( $id, 'select' ) . '</fieldset>';
2003        return $field;
2004    }
2005
2006    /**
2007     * Return the HTML for the select field.
2008     *
2009     * @param int    $id - the ID.
2010     * @param string $label - the label.
2011     * @param string $value - the value of the field.
2012     * @param string $class - the field class.
2013     * @param bool   $required - if the field is marked as required.
2014     * @param string $required_field_text - the text in the required text field.
2015     * @param bool   $required_indicator Whether to display the required indicator.
2016     *
2017     * @return string HTML
2018     */
2019    public function render_select_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
2020        $field      = $this->render_label( 'select', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
2021        $class      = preg_replace( "/class=['\"]([^'\"]*)['\"]/", 'class="contact-form__select-wrapper $1"', $class );
2022        $field     .= "<div {$class} style='" . esc_attr( $this->field_styles ) . "'>";
2023        $aria_label = ! empty( $this->get_attribute( 'togglelabel' ) )
2024            ? Contact_Form_Plugin::strip_tags( $this->get_attribute( 'togglelabel' ) )
2025            : __( 'Select an option', 'jetpack-forms' ); // selects don't have a default label
2026        $field     .= "\t<span class='contact-form__select-element-wrapper'><select name='" . esc_attr( $id ) . "' id='" . esc_attr( $id ) . "' " . ( $required ? "required aria-required='true'" : '' ) . " data-wp-on--change='actions.onFieldChange' data-wp-bind--aria-invalid='state.fieldAriaInvalid' " . $this->get_hidden_label_aria_label_attr( $aria_label ) . ">\n";
2027
2028        if ( $this->get_attribute( 'togglelabel' ) ) {
2029            $field .= "\t\t<option value=''>" . $this->get_attribute( 'togglelabel' ) . "</option>\n";
2030        } elseif ( ! $this->get_attribute( 'default' ) ) {
2031            // For select fields without an explicit togglelabel or default value (e.g., shortcode-based forms),
2032            // add a placeholder option to ensure users must make an explicit selection.
2033            $field .= "\t\t<option value=''>" . esc_html__( 'Select an option', 'jetpack-forms' ) . "</option>\n";
2034        }
2035
2036        foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) {
2037            $option = Contact_Form_Plugin::strip_tags( $option );
2038            if ( is_string( $option ) && $option !== '' ) {
2039                $field .= "\t\t<option"
2040                                . selected( $option, $value, false )
2041                                . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option ) )
2042                                . "'>" . esc_html( $option )
2043                                . "</option>\n";
2044            }
2045        }
2046        $field .= "\t</select><span class='jetpack-field-dropdown__icon'></span></span>\n";
2047        $field .= "</div>\n";
2048
2049        return $field . $this->get_error_div( $id, 'select' );
2050    }
2051
2052    /**
2053     * Return the HTML for the date field.
2054     *
2055     * @param int    $id - the ID.
2056     * @param string $label - the label.
2057     * @param string $value - the value of the field.
2058     * @param string $class - the field class.
2059     * @param bool   $required - if the field is marked as required.
2060     * @param string $required_field_text - the text in the required text field.
2061     * @param string $placeholder - the field placeholder content.
2062     * @param bool   $required_indicator Whether to display the required indicator.
2063     *
2064     * @return string HTML
2065     */
2066    public function render_date_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) {
2067        static $is_loaded = false;
2068        $this->set_invalid_message( 'date', __( 'Please enter a valid date.', 'jetpack-forms' ) );
2069        // WARNING: sync data with DATE_FORMATS in jetpack-field-datepicker.js
2070        $formats = array(
2071            'mm/dd/yy' => array(
2072                /* translators: date format. DD is the day of the month, MM the month, and YYYY the year (e.g., 12/31/2023). */
2073                'label' => __( 'MM/DD/YYYY', 'jetpack-forms' ),
2074            ),
2075            'dd/mm/yy' => array(
2076                /* translators: date format. DD is the day of the month, MM the month, and YYYY the year (e.g., 31/12/2023). */
2077                'label' => __( 'DD/MM/YYYY', 'jetpack-forms' ),
2078            ),
2079            'yy-mm-dd' => array(
2080                /* translators: date format. DD is the day of the month, MM the month, and YYYY the year (e.g., 2023-12-31). */
2081                'label' => __( 'YYYY-MM-DD', 'jetpack-forms' ),
2082            ),
2083        );
2084
2085        $date_format = $this->get_attribute( 'dateformat' );
2086        $date_format = isset( $date_format ) && ! empty( $date_format ) ? $date_format : 'yy-mm-dd';
2087        $label       = isset( $formats[ $date_format ] ) ? $label . ' (' . $formats[ $date_format ]['label'] . ')' : $label;
2088        $extra_attrs = array( 'data-format' => $date_format );
2089
2090        $field  = $this->render_label( 'date', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
2091        $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required, $extra_attrs );
2092
2093        /* For AMP requests, use amp-date-picker element: https://amp.dev/documentation/components/amp-date-picker */
2094        if ( class_exists( 'Jetpack_AMP_Support' ) && \Jetpack_AMP_Support::is_amp_request() ) {
2095            return sprintf(
2096                '<%1$s mode="overlay" layout="container" type="single" input-selector="[name=%2$s]">%3$s</%1$s>',
2097                'amp-date-picker',
2098                esc_attr( $id ),
2099                $field
2100            );
2101        }
2102
2103        Assets::register_script(
2104            'jp-forms-date-picker',
2105            '../../dist/contact-form/js/date-picker.js',
2106            __FILE__,
2107            array(
2108                'enqueue'      => true,
2109                'dependencies' => array(),
2110                'version'      => Constants::get_constant( 'JETPACK__VERSION' ),
2111            )
2112        );
2113
2114        /**
2115         * Filter the localized date picker script.
2116         */
2117        if ( ! $is_loaded ) {
2118            \wp_localize_script(
2119                'jp-forms-date-picker',
2120                'jpDatePicker',
2121                array(
2122                    'offset' => intval( get_option( 'start_of_week', 1 ) ),
2123                    'lang'   => array(
2124                        // translators: These are the two letter abbreviated name of the week.
2125                        'days'      => array(
2126                            __( 'Su', 'jetpack-forms' ),
2127                            __( 'Mo', 'jetpack-forms' ),
2128                            __( 'Tu', 'jetpack-forms' ),
2129                            __( 'We', 'jetpack-forms' ),
2130                            __( 'Th', 'jetpack-forms' ),
2131                            __( 'Fr', 'jetpack-forms' ),
2132                            __( 'Sa', 'jetpack-forms' ),
2133                        ),
2134                        'months'    => array(
2135                            __( 'January', 'jetpack-forms' ),
2136                            __( 'February', 'jetpack-forms' ),
2137                            __( 'March', 'jetpack-forms' ),
2138                            __( 'April', 'jetpack-forms' ),
2139                            __( 'May', 'jetpack-forms' ),
2140                            __( 'June', 'jetpack-forms' ),
2141                            __( 'July', 'jetpack-forms' ),
2142                            __( 'August', 'jetpack-forms' ),
2143                            __( 'September', 'jetpack-forms' ),
2144                            __( 'October', 'jetpack-forms' ),
2145                            __( 'November', 'jetpack-forms' ),
2146                            __( 'December', 'jetpack-forms' ),
2147                        ),
2148                        'today'     => __( 'Today', 'jetpack-forms' ),
2149                        'clear'     => __( 'Clear', 'jetpack-forms' ),
2150                        'close'     => __( 'Close', 'jetpack-forms' ),
2151                        'ariaLabel' => array(
2152                            'enterPicker'       => __( 'You are on a date picker input. Use the down key to focus into the date picker. Or type the date in the format MM/DD/YYYY', 'jetpack-forms' ),
2153                            'dayPicker'         => __( 'You are currently inside the date picker, use the arrow keys to navigate between the dates. Use tab key to jump to more controls.', 'jetpack-forms' ),
2154                            'monthPicker'       => __( 'You are currently inside the month picker, use the arrow keys to navigate between the months. Use the space key to select it.', 'jetpack-forms' ),
2155                            'yearPicker'        => __( 'You are currently inside the year picker, use the up and down arrow keys to navigate between the years. Use the space key to select it.', 'jetpack-forms' ),
2156                            'monthPickerButton' => __( 'Month picker. Use the space key to enter the month picker.', 'jetpack-forms' ),
2157                            'yearPickerButton'  => __( 'Year picker. Use the space key to enter the month picker.', 'jetpack-forms' ),
2158                            'dayButton'         => __( 'Use the space key to select the date.', 'jetpack-forms' ),
2159                            'todayButton'       => __( 'Today button. Use the space key to select the current date.', 'jetpack-forms' ),
2160                            'clearButton'       => __( 'Clear button. Use the space key to clear the date picker.', 'jetpack-forms' ),
2161                            'closeButton'       => __( 'Close button. Use the space key to close the date picker.', 'jetpack-forms' ),
2162                        ),
2163                    ),
2164                )
2165            );
2166            $is_loaded = true;
2167        }
2168
2169        return $field;
2170    }
2171
2172    /**
2173     * Return the HTML for the time field.
2174     *
2175     * @param int    $id - the ID.
2176     * @param string $label - the label.
2177     * @param string $value - the value of the field.
2178     * @param string $class - the field class.
2179     * @param bool   $required - if the field is marked as required.
2180     * @param string $required_field_text - the text in the required text field.
2181     * @param string $placeholder - the field placeholder content.
2182     * @param bool   $required_indicator Whether to display the required indicator.
2183     *
2184     * @return string HTML
2185     */
2186    public function render_time_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) {
2187        $this->set_invalid_message( 'time', __( 'Please enter a valid time.', 'jetpack-forms' ) );
2188
2189        $field  = $this->render_label( 'time', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
2190        $field .= $this->render_input_field( 'time', $id, $value, $class, $placeholder, $required );
2191
2192        return $field;
2193    }
2194
2195    /**
2196     * Return the HTML for the image select field.
2197     *
2198     * @param int    $id - the ID.
2199     * @param string $label - the label.
2200     * @param string $value - the value of the field.
2201     * @param string $class - the field class.
2202     * @param bool   $required - if the field is marked as required.
2203     * @param string $required_field_text - the text in the required text field.
2204     * @param bool   $required_indicator Whether to display the required indicator.
2205     *
2206     * @return string HTML
2207     */
2208    public function render_image_select_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
2209        wp_enqueue_style( 'jetpack-form-field-image-select-style', plugins_url( '../../dist/blocks/field-image-select/style.css', __FILE__ ), array(), Constants::get_constant( 'JETPACK__VERSION' ) );
2210
2211        $is_multiple       = $this->get_attribute( 'ismultiple' );
2212        $show_labels       = $this->get_attribute( 'showlabels' );
2213        $randomize_options = $this->get_attribute( 'randomizeoptions' );
2214        $is_supersized     = $this->get_attribute( 'issupersized' );
2215
2216        $input_type = $is_multiple ? 'checkbox' : 'radio';
2217        $input_name = $is_multiple ? $id . '[]' : $id;
2218
2219        $field = "<div class='jetpack-field jetpack-field-image-select'>";
2220
2221        $fieldset_id = "id='" . esc_attr( "$id-label" ) . "'" . $this->get_hidden_label_aria_label_attr();
2222
2223        $field .= "<fieldset {$fieldset_id} data-wp-bind--aria-invalid='state.fieldAriaInvalid' >";
2224
2225        $field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text, array(), $required_indicator );
2226
2227        $options_classes = $this->get_attribute( 'optionsclasses' );
2228        $options_styles  = $this->get_attribute( 'optionsstyles' );
2229
2230        $field .= "<div class='" . esc_attr( $options_classes ) . " jetpack-field jetpack-fieldset-image-options' style='" . esc_attr( $options_styles ) . "'>";
2231        $field .= "<div class='jetpack-fieldset-image-options__wrapper'>";
2232
2233        $options_data = $this->get_attribute( 'optionsdata' );
2234
2235        // Filter out empty options from the end
2236        $options_data = $this->trim_image_select_options( $options_data );
2237
2238        $used_html_ids = array();
2239
2240        // Create a separate array of original letters in sequence (A, B, C...)
2241        $perceived_letters = array();
2242
2243        foreach ( $options_data as $option ) {
2244            $perceived_letters[] = Contact_Form_Plugin::strip_tags( $option['letter'] );
2245        }
2246
2247        // Create a working copy of options for potential randomization
2248        $working_options = $options_data;
2249
2250        // Randomize options if requested, but preserve original letter values
2251        if ( $randomize_options ) {
2252            shuffle( $working_options );
2253
2254            // Trims options after randomization to ensure the last option has a label or image.
2255            $working_options = $this->trim_image_select_options( $working_options );
2256        }
2257
2258        // Calculate row options count for CSS variable
2259        $total_options_count = count( $working_options );
2260        // Those values are halved on mobile via CSS media query
2261        $max_images_per_row = $is_supersized ? 2 : 4;
2262        $row_options_count  = min( $total_options_count, $max_images_per_row );
2263
2264        foreach ( $working_options as $option_index => $option ) {
2265            $option_label  = Contact_Form_Plugin::strip_tags( $option['label'] );
2266            $option_letter = Contact_Form_Plugin::strip_tags( $option['letter'] );
2267            $image_block   = $option['image'] ?? null;
2268            $image_id      = ( is_array( $image_block ) && isset( $image_block['attrs'] ) && is_array( $image_block['attrs'] ) ) ? ( $image_block['attrs']['id'] ?? null ) : null;
2269
2270            $rendered_image_block = is_array( $image_block ) ? render_block( $image_block ) : '';
2271            // Remove any links from the rendered block
2272            $rendered_image_block = preg_replace( '/<a[^>]*>(.*?)<\/a>/s', '$1', $rendered_image_block );
2273
2274            // Extract image src from rendered block
2275            $image_src = '';
2276
2277            if ( ! empty( $rendered_image_block ) ) {
2278                if ( preg_match( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $rendered_image_block, $matches ) ) {
2279                    $extracted_src = $matches[1];
2280
2281                    if ( filter_var( $extracted_src, FILTER_VALIDATE_URL ) || str_starts_with( $extracted_src, 'data:' ) ) {
2282                        $image_src = $extracted_src;
2283                    }
2284                }
2285            } else {
2286                $rendered_image_block = '<figure class="wp-block-image jetpack-input-image-option__empty-image"><img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" alt="" style="aspect-ratio:1;object-fit:cover"/></figure>';
2287            }
2288
2289            $option_value                = wp_json_encode(
2290                array(
2291                    'perceived'  => $perceived_letters[ $option_index ],
2292                    'selected'   => $option_letter,
2293                    'label'      => $option_label,
2294                    'showLabels' => $show_labels,
2295                    'image'      => array(
2296                        'id'  => $image_id,
2297                        'src' => $image_src ?? null,
2298                    ),
2299                ),
2300                JSON_HEX_AMP | JSON_UNESCAPED_SLASHES
2301            );
2302            $option_id                   = $id . '-' . $option_letter;
2303            $used_html_ids[ $option_id ] = true;
2304
2305            $figcaption_id = esc_attr( $option_id . '-figcaption' );
2306
2307            // Add id attribute to figcaption for accessibility
2308            if ( ! empty( $rendered_image_block ) && strpos( $rendered_image_block, '<figcaption' ) !== false ) {
2309                $rendered_image_block = preg_replace( '/(<figcaption[^>]*)(>)/', '$1 id="' . $figcaption_id . '"$2', $rendered_image_block );
2310            }
2311
2312            // To be able to apply the backdrop-filter for the hover effect, we need to separate the background into an outer div.
2313            // This outer div needs the color styles separately, and also the border radius to match the inner div without sticking out.
2314            $option_outer_classes = 'jetpack-input-image-option__outer ' . ( $option['classcolor'] ?? '' );
2315
2316            if ( $is_supersized ) {
2317                $option_outer_classes .= ' is-supersized';
2318            }
2319
2320            $border_styles = '';
2321            if ( ! empty( $option['style'] ) ) {
2322                preg_match( '/border-radius:([^;]+)/', $option['style'], $radius_match );
2323                preg_match( '/border-width:([^;]+)/', $option['style'], $width_match );
2324
2325                if ( ! empty( $radius_match[1] ) ) {
2326                    $radius_value = trim( $radius_match[1] );
2327
2328                    if ( ! empty( $width_match[1] ) ) {
2329                            $width_value   = trim( $width_match[1] );
2330                            $border_styles = "border-radius:calc({$radius_value} + {$width_value});";
2331                    } else {
2332                            $border_styles = "border-radius:{$radius_value};";
2333                    }
2334                } else {
2335                    // Handle individual border radius properties when border-radius is not present
2336                    preg_match( '/border-top-left-radius:([^;]+)/', $option['style'], $top_left_match );
2337                    preg_match( '/border-top-right-radius:([^;]+)/', $option['style'], $top_right_match );
2338                    preg_match( '/border-bottom-right-radius:([^;]+)/', $option['style'], $bottom_right_match );
2339                    preg_match( '/border-bottom-left-radius:([^;]+)/', $option['style'], $bottom_left_match );
2340
2341                    if ( ! empty( $top_left_match[1] ) || ! empty( $top_right_match[1] ) || ! empty( $bottom_right_match[1] ) || ! empty( $bottom_left_match[1] ) ) {
2342                        $width_value = ! empty( $width_match[1] ) ? trim( $width_match[1] ) : '1px';
2343
2344                        $top_left     = ! empty( $top_left_match[1] ) ? trim( $top_left_match[1] ) : '4px';
2345                        $top_right    = ! empty( $top_right_match[1] ) ? trim( $top_right_match[1] ) : '4px';
2346                        $bottom_right = ! empty( $bottom_right_match[1] ) ? trim( $bottom_right_match[1] ) : '4px';
2347                        $bottom_left  = ! empty( $bottom_left_match[1] ) ? trim( $bottom_left_match[1] ) : '4px';
2348
2349                        $border_styles = "border-radius:calc({$top_left} + {$width_value}) calc({$top_right} + {$width_value}) calc({$bottom_right} + {$width_value}) calc({$bottom_left} + {$width_value});";
2350                    }
2351                }
2352            }
2353
2354            $option_outer_styles  = ( empty( $option['stylecolor'] ) ? '' : $option['stylecolor'] ) . $border_styles;
2355            $option_outer_styles .= "--row-options-count: {$row_options_count};";
2356            $option_outer_styles  = empty( $option_outer_styles ) ? '' : "style='" . esc_attr( $option_outer_styles ) . "'";
2357
2358            $field .= "<div class='{$option_outer_classes}{$option_outer_styles}>";
2359
2360            $default_classes = 'jetpack-field jetpack-input-image-option';
2361            $option_styles   = empty( $option['style'] ) ? '' : "style='" . esc_attr( $option['style'] ) . "'";
2362            $option_classes  = "class='" . ( empty( $option['class'] ) ? $default_classes : $default_classes . ' ' . esc_attr( $option['class'] ) ) . "'";
2363
2364            $field .= "<div {$option_classes} {$option_styles} data-wp-on--click='actions.onImageOptionClick' data-wp-init='callbacks.setImageOptionOutlineColor'>";
2365
2366            $input_id = esc_attr( $option_id );
2367            $label_id = esc_attr( $option_id . '-label' );
2368
2369            /* translators: %s is the letter associated with the option, e.g. "Option A" */
2370            $aria_label_parts = array( sprintf( __( 'Option %s', 'jetpack-forms' ), $perceived_letters[ $option_index ] ) );
2371
2372            if ( ! empty( $option_label ) ) {
2373                $aria_label_parts[] = $option_label;
2374            }
2375
2376            $aria_label = implode( ': ', $aria_label_parts );
2377
2378            // Build aria-describedby to reference label and figcaption
2379            $aria_describedby_parts = array( $label_id );
2380
2381            if ( ! empty( $rendered_image_block ) && strpos( $rendered_image_block, '<figcaption' ) !== false ) {
2382                $aria_describedby_parts[] = $figcaption_id;
2383            }
2384
2385            $aria_describedby = implode( ' ', $aria_describedby_parts );
2386
2387            $field .= "<div class='jetpack-input-image-option__wrapper'>";
2388            $field .= "<input
2389            id='" . $input_id . "'
2390            class='jetpack-input-image-option__input'
2391            type='" . esc_attr( $input_type ) . "'
2392            name='" . esc_attr( $input_name ) . "'
2393            value='" . esc_attr( $option_value ) . "'
2394            aria-label='" . esc_attr( $aria_label ) . "'
2395            aria-describedby='" . esc_attr( $aria_describedby ) . "'
2396            data-wp-init='callbacks.setImageOptionCheckColor'
2397            data-wp-on--keydown='actions.onKeyDownImageOption'
2398            data-wp-on--change='" . ( $is_multiple ? 'actions.onMultipleFieldChange' : 'actions.onFieldChange' ) . "' "
2399            . $class
2400            . ( $is_multiple ? checked( in_array( $option_value, (array) $value, true ), true, false ) : checked( $option_value, $value, false ) ) . ' '
2401            . ( $required ? "required aria-required='true'" : '' )
2402            . '/> ';
2403
2404            $field .= $rendered_image_block;
2405            $field .= '</div>';
2406
2407            $field .= "<div class='jetpack-input-image-option__label-wrapper'>";
2408            $field .= "<div class='jetpack-input-image-option__label-code'>" . esc_html( $perceived_letters[ $option_index ] ) . '</div>';
2409
2410            $label_classes  = 'jetpack-input-image-option__label';
2411            $label_classes .= $show_labels ? '' : ' visually-hidden';
2412            $field         .= "<span id='{$label_id}' class='{$label_classes}'>" . esc_html( $option_label ) . '</span>';
2413            $field         .= '</div></div></div>';
2414        }
2415
2416        $field .= '</div></div>';
2417
2418        $field .= $this->get_error_div( $id, 'image-select' );
2419
2420        $field .= '</fieldset>';
2421
2422        $field .= '</div>';
2423
2424        return $field;
2425    }
2426
2427    /**
2428     * Return the HTML for the number field.
2429     *
2430     * @param int    $id - the ID.
2431     * @param string $label - the label.
2432     * @param string $value - the value of the field.
2433     * @param string $class - the field class.
2434     * @param bool   $required - if the field is marked as required.
2435     * @param string $required_field_text - the text in the required text field.
2436     * @param string $placeholder - the field placeholder content.
2437     * @param array  $extra_attrs - Extra attributes used in number field, namely `min` and `max`.
2438     * @param bool   $required_indicator Whether to display the required indicator.
2439     *
2440     * @return string HTML
2441     */
2442    public function render_number_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $extra_attrs = array(), $required_indicator = true ) {
2443        $this->set_invalid_message( 'number', __( 'Please enter a valid number', 'jetpack-forms' ) );
2444        if ( isset( $extra_attrs['min'] ) ) {
2445            // translators: %d is the minimum value.
2446            $this->set_invalid_message( 'min_number', __( 'Please select a value that is no less than %d.', 'jetpack-forms' ) );
2447        }
2448        if ( isset( $extra_attrs['max'] ) ) {
2449            // translators: %d is the maximum value.
2450            $this->set_invalid_message( 'max_number', __( 'Please select a value that is no more than %d.', 'jetpack-forms' ) );
2451        }
2452        $field  = $this->render_label( 'number', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
2453        $field .= $this->render_input_field( 'number', $id, $value, $class, $placeholder, $required, $extra_attrs );
2454        return $field;
2455    }
2456
2457    /**
2458     * Return the HTML for the default field.
2459     *
2460     * @param int    $id - the ID.
2461     * @param string $label - the label.
2462     * @param string $value - the value of the field.
2463     * @param string $class - the field class.
2464     * @param bool   $required - if the field is marked as required.
2465     * @param string $required_field_text - the text in the required text field.
2466     * @param string $placeholder - the field placeholder content.
2467     * @param string $type - the type.
2468     * @param bool   $required_indicator Whether to display the required indicator.
2469     *
2470     * @return string HTML
2471     */
2472    public function render_default_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $type, $required_indicator = true ) {
2473        $field  = $this->render_label( $type, $id, $label, $required, $required_field_text, array(), false, $required_indicator );
2474        $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
2475        return $field;
2476    }
2477
2478    /**
2479     * Returns the styles, classes and CSS vars necessary to render fields in the "Outlined" style.
2480     * The "Animated" style variation shares the CSS vars, which require similar calculations for the left offset and label left position.
2481     * At the block level, the styles are extracted and added to the shortcode attributes in
2482     * Contact_Form_Plugin::get_outlined_style_attributes().
2483     * This function extracts those styles and applies them to the field,
2484     * and ensures any global or theme styles are applied.
2485     *
2486     * @param string $form_style (optional) The form style.
2487     *
2488     * @return array {
2489     *     @type string $style_attrs The style attributes.
2490     *     @type string $css_vars The CSS variables.
2491     *     @type string $class_name The class name.
2492     * }
2493     */
2494    private function get_form_variation_style_properties( $form_style = 'outlined' ) {
2495        $css_vars             = '';
2496        $variation_attributes = $this->get_attribute( 'stylevariationattributes' );
2497        $variation_attributes = ! empty( $variation_attributes ) ? json_decode( html_entity_decode( $variation_attributes, ENT_COMPAT ), true ) : array();
2498        $variation_classes    = $this->get_attribute( 'stylevariationclasses' );
2499        $variation_style      = $this->get_attribute( 'stylevariationstyles' );
2500        $block_name           = 'jetpack/input';
2501
2502        if ( $this->maybe_override_type() === 'radio' || $this->maybe_override_type() === 'checkbox-multiple' ) {
2503            $block_name = 'jetpack/options';
2504        }
2505
2506        $global_styles = wp_get_global_styles(
2507            array( 'border' ),
2508            array(
2509                'block_name' => $block_name,
2510                'transforms' => array( 'resolve-variables' ),
2511            )
2512        );
2513
2514        /*
2515         * The `borderwidth` attribute contains the border value that forms used before the migration to global styles.
2516         * Any old forms saved in a post will still use this attribute, so it needs to be factored into the css vars for border
2517         * to properly support backwards compatibility. So we check if the attribute is set and if it's not empty or '0', which is a valid width value.
2518         * For newer forms that use global styles or the block supports styles, this value will be empty and is ignored.
2519         */
2520        $border_width_attribute = $this->get_attribute( 'borderwidth' );
2521        $legacy_border_size     = ! empty( $border_width_attribute ) || $border_width_attribute === '0' ? $border_width_attribute . 'px' : null;
2522
2523        $border_radius_attribute = $this->get_attribute( 'borderradius' );
2524        $legacy_border_radius    = ! empty( $border_radius_attribute ) || $border_radius_attribute === '0' ? $border_radius_attribute . 'px' : $variation_attributes['border']['radius'] ?? null;
2525
2526        $border_top_size = $legacy_border_size ??
2527            $variation_attributes['border']['width'] ??
2528            $variation_attributes['border']['top']['width'] ??
2529            $global_styles['width'] ??
2530            $global_styles['top']['width'] ?? null;
2531
2532        $border_right_size = $legacy_border_size ??
2533
2534            $variation_attributes['border']['right']['width'] ??
2535            $global_styles['width'] ??
2536            $global_styles['right']['width'] ?? null;
2537
2538        $border_bottom_size = $legacy_border_size ??
2539            $variation_attributes['border']['width'] ??
2540            $variation_attributes['border']['bottom']['width'] ??
2541            $global_styles['width'] ??
2542            $global_styles['bottom']['width'] ?? null;
2543
2544        $border_left_size = $legacy_border_size ??
2545            $variation_attributes['border']['width'] ??
2546            $variation_attributes['border']['left']['width'] ??
2547            $global_styles['width'] ??
2548            $global_styles['left']['width'] ?? null;
2549
2550        $border_radius = $legacy_border_radius ??
2551            $global_styles['radius'] ?? null;
2552
2553        // Border size to accommodate legacy border width attribute.
2554        $css_vars = $legacy_border_size ? '--jetpack--contact-form--border-size: ' . $legacy_border_size . ';' : '';
2555
2556        // Border side sizes to accommodate global styles split values.
2557        $css_vars .= $border_top_size ? '--jetpack--contact-form--border-top-size: ' . $border_top_size . ';' : '';
2558        $css_vars .= $border_right_size ? '--jetpack--contact-form--border-right-size: ' . $border_right_size . ';' : '';
2559        $css_vars .= $border_bottom_size ? '--jetpack--contact-form--border-bottom-size: ' . $border_bottom_size . ';' : '';
2560        $css_vars .= $border_left_size ? '--jetpack--contact-form--border-left-size: ' . $border_left_size . ';' : '';
2561
2562        // Check if border radius is split or a single value.
2563        if ( is_array( $border_radius ) ) {
2564            // If corner radii are set on the top-left or bottom-left of the block, take the maximum of the two.
2565            // We check the left side due to writing direction—this variable is used to offset text.
2566            // TODO: this should factor in RTL languages.
2567            $css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: max(' . ( $border_radius['topLeft'] ?? '0' ) . ',' . ( $border_radius['bottomLeft'] ?? '0' ) . ');' : '';
2568        } elseif ( isset( $border_radius ) ) {
2569            $css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: ' . $border_radius . ';' : '';
2570        }
2571
2572        if ( 'outlined' === $form_style ) {
2573            $css_vars .= '--jetpack--contact-form--notch-width: max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius));';
2574        } elseif ( 'animated' === $form_style ) {
2575            $css_vars .= '--jetpack--contact-form--animated-left-offset: 16px;';
2576        }
2577
2578        return array(
2579            'style'      => $variation_style,
2580            'css_vars'   => $css_vars,
2581            'class_name' => $variation_classes,
2582        );
2583    }
2584
2585    /**
2586     * Return the HTML for the outlined label.
2587     *
2588     * @param int    $id - the ID.
2589     * @param string $label - the label.
2590     * @param bool   $required - if the field is marked as required.
2591     * @param string $required_field_text - the text in the required text field.
2592     * @param bool   $required_indicator Whether to display the required indicator.
2593     *
2594     * @return string HTML
2595     */
2596    public function render_outline_label( $id, $label, $required, $required_field_text, $required_indicator = true ) {
2597        $classes  = 'notched-label__label';
2598        $classes .= $this->is_error() ? ' form-error' : '';
2599        $classes .= $this->label_classes ? ' ' . $this->label_classes : '';
2600
2601        $output_data = $this->get_form_variation_style_properties();
2602
2603        return '
2604            <div class="notched-label">
2605                <div class="notched-label__leading' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
2606                <div class="notched-label__notch' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '">
2607                    <label
2608                        for="' . esc_attr( $id ) . '"
2609                        class=" ' . $classes . '"
2610                        style="' . $this->label_styles . esc_attr( $output_data['css_vars'] ) . '"
2611                    >
2612                    <span class="grunion-label-text">' . esc_html( $label ) . '</span>'
2613                    . ( $required && $required_indicator ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
2614            '</label>
2615                </div>
2616                <div class="notched-label__filler' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
2617                <div class="notched-label__trailing' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
2618            </div>';
2619    }
2620
2621    /**
2622     * Return the HTML for the animated label.
2623     *
2624     * @param int    $id - the ID.
2625     * @param string $label - the label.
2626     * @param bool   $required - if the field is marked as required.
2627     * @param string $required_field_text - the text in the required text field.
2628     * @param bool   $required_indicator Whether to display the required indicator.
2629     *
2630     * @return string HTML
2631     */
2632    public function render_animated_label( $id, $label, $required, $required_field_text, $required_indicator = true ) {
2633        $classes  = 'animated-label__label';
2634        $classes .= $this->is_error() ? ' form-error' : '';
2635        $classes .= $this->label_classes ? ' ' . $this->label_classes : '';
2636
2637        return '
2638            <label
2639                for="' . esc_attr( $id ) . '"
2640                class="' . $classes . '"
2641                style="' . $this->label_styles . '"
2642            >
2643                <span class="grunion-label-text">' . wp_kses_post( $label ) . '</span>'
2644                . ( $required && $required_indicator !== 'hidden' ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
2645            '</label>';
2646    }
2647
2648    /**
2649     * Return the HTML for the below label.
2650     *
2651     * @param int    $id - the ID.
2652     * @param string $label - the label.
2653     * @param bool   $required - if the field is marked as required.
2654     * @param string $required_field_text - the text in the required text field.
2655     * @param bool   $required_indicator Whether to display the required indicator.
2656     *
2657     * @return string HTML
2658     */
2659    public function render_below_label( $id, $label, $required, $required_field_text, $required_indicator = true ) {
2660        return '
2661            <label
2662                for="' . esc_attr( $id ) . '"
2663                class="below-label__label ' . ( $this->is_error() ? ' form-error' : '' ) . ( $this->label_classes ? ' ' . esc_attr( $this->label_classes ) : '' ) . '"
2664            >'
2665            . esc_html( $label )
2666            . ( $required && $required_indicator ? '<span>' . $required_field_text . '</span>' : '' ) .
2667            '</label>';
2668    }
2669
2670    /**
2671     * Return the HTML for the email field.
2672     *
2673     * @param string $type - the type.
2674     * @param int    $id - the ID.
2675     * @param string $label - the label.
2676     * @param string $value - the value of the field.
2677     * @param string $class - the field class.
2678     * @param string $placeholder - the field placeholder content.
2679     * @param bool   $required - if the field is marked as required.
2680     * @param string $required_field_text - the text for a field marked as required.
2681     * @param array  $extra_attrs - extra attributes to be passed to render functions.
2682     * @param bool   $required_indicator Whether to display the required indicator.
2683     *
2684     * @return string HTML
2685     */
2686    public function render_field( $type, $id, $label, $value, $class, $placeholder, $required, $required_field_text, $extra_attrs = array(), $required_indicator = true ) {
2687        if ( ! $this->is_field_renderable( $type ) ) {
2688            return '';
2689        }
2690
2691        if ( $type === 'hidden' ) {
2692            // For hidden fields, we don't need to render the label or any other HTML.
2693            return $this->render_hidden_field( $id, $label, $value );
2694        }
2695
2696        $trimmed_type = trim( esc_attr( $type ) );
2697        $class       .= ' grunion-field';
2698
2699        $form_style = $this->get_form_style();
2700        if ( ! empty( $form_style ) && $form_style !== 'default' ) {
2701            if ( isset( $placeholder ) && '' !== $placeholder ) {
2702                $class .= ' has-placeholder';
2703            }
2704        }
2705
2706        // Field classes.
2707        $field_class = "class='" . $trimmed_type . ' ' . esc_attr( $class ) . "' ";
2708
2709        // Shell wrapper classes. Add -wrap to each class.
2710        $wrap_classes          = empty( $class ) ? '' : implode( '-wrap ', array_filter( explode( ' ', $class ) ) ) . '-wrap';
2711        $field_wrapper_classes = $this->get_attribute( 'fieldwrapperclasses' ) ? $this->get_attribute( 'fieldwrapperclasses' ) . ' ' : '';
2712
2713        if ( empty( $label ) && ! $required ) {
2714            $wrap_classes .= ' no-label';
2715        }
2716
2717        $shell_field_class = "class='" . $field_wrapper_classes . 'grunion-field-' . $trimmed_type . '-wrap ' . esc_attr( $wrap_classes ) . "' ";
2718
2719        /**
2720         * Filter the Contact Form required field text
2721         *
2722         * @module contact-form
2723         *
2724         * @since 3.8.0
2725         *
2726         * @param string $var Required field text. Default is "(required)".
2727         */
2728        $required_field_text = wp_kses_post( apply_filters( 'jetpack_required_field_text', $required_field_text ) );
2729
2730        $block_style       = 'style="' . $this->block_styles . '"';
2731        $has_inset_label   = $this->has_inset_label();
2732        $field             = '';
2733        $field_placeholder = ! empty( $placeholder ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
2734
2735        $context = array(
2736            'fieldId'           => $id,
2737            'fieldType'         => $type,
2738            'fieldLabel'        => $label,
2739            'fieldValue'        => $value,
2740            'fieldPlaceholder'  => $placeholder,
2741            'fieldIsRequired'   => $required,
2742            'fieldErrorMessage' => '',
2743            'fieldExtra'        => $this->get_field_extra( $type, $extra_attrs ),
2744            'formHash'          => $this->form->hash,
2745        );
2746
2747        $interactivity_attrs = ' data-wp-interactive="jetpack/form" ' . wp_interactivity_data_wp_context( $context ) . ' ';
2748
2749        // Fields with an inset label need an extra wrapper to show the error message below the input.
2750        if ( $has_inset_label ) {
2751            $field_width       = $this->get_attribute( 'width' );
2752            $inset_label_class = array( 'contact-form__inset-label-wrap' );
2753
2754            if ( ! empty( $field_width ) ) {
2755                array_push( $inset_label_class, 'grunion-field-width-' . $field_width . '-wrap' );
2756            }
2757
2758            $field              .= "\n<div class='" . implode( ' ', $inset_label_class ) . "{$interactivity_attrs} >\n";
2759            $interactivity_attrs = ''; // Reset interactivity attributes for the field wrapper.
2760        }
2761
2762        $field .= "\n<div {$block_style} {$interactivity_attrs} {$shell_field_class} data-wp-init='callbacks.initializeField' data-wp-on--jetpack-form-reset='callbacks.initializeField' >\n"; // new in Jetpack 6.8.0
2763
2764        switch ( $type ) {
2765            case 'email':
2766                $field .= $this->render_email_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2767                break;
2768            case 'phone':
2769            case 'telephone':
2770                $field .= $this->render_telephone_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2771                break;
2772            case 'url':
2773                $field .= $this->render_url_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2774                break;
2775            case 'textarea':
2776                $field .= $this->render_textarea_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2777                break;
2778            case 'radio':
2779                $field .= $this->render_radio_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2780                break;
2781            case 'checkbox':
2782                $field .= $this->render_checkbox_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2783                break;
2784            case 'checkbox-multiple':
2785                $field .= $this->render_checkbox_multiple_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2786                break;
2787            case 'select':
2788                $field .= $this->render_select_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2789                break;
2790            case 'date':
2791                $field .= $this->render_date_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2792                break;
2793            case 'consent':
2794                $field .= $this->render_consent_field( $id, $field_class );
2795                break;
2796            case 'number':
2797                $field .= $this->render_number_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $extra_attrs, $required_indicator );
2798                break;
2799            case 'slider':
2800                $field .= $this->render_slider_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $extra_attrs, $required_indicator );
2801                break;
2802            case 'file':
2803                $field .= $this->render_file_field( $id, $label, $field_class, $required, $required_field_text, $required_indicator );
2804                break;
2805            case 'rating':
2806                $field .= $this->render_rating_field(
2807                    $id,
2808                    $label,
2809                    $value,
2810                    $field_class,
2811                    $required,
2812                    $required_field_text,
2813                    $required_indicator
2814                );
2815                break;
2816            case 'time':
2817                $field .= $this->render_time_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2818                break;
2819            case 'image-select':
2820                $field .= $this->render_image_select_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2821                break;
2822            default: // text field
2823                $field .= $this->render_default_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $type, $required_indicator );
2824                break;
2825        }
2826
2827        $field .= "\t</div>\n";
2828
2829        if ( $has_inset_label ) {
2830            $field .= $this->get_error_div( $id, $type, true );
2831            // Close the extra wrapper for inset labels.
2832            $field .= "\t</div>\n";
2833        }
2834
2835        return $field;
2836    }
2837
2838    /**
2839     * Returns the extra attributes for the field.
2840     * That are used in field validation.
2841     *
2842     * @param string $type - the field type.
2843     * @param array  $extra_attrs - the extra attributes.
2844     *
2845     * @return string|array The extra attributes.
2846     */
2847    private function get_field_extra( $type, $extra_attrs ) {
2848        if ( 'date' === $type ) {
2849            $date_format = $this->get_attribute( 'dateformat' );
2850            return isset( $date_format ) && ! empty( $date_format ) ? $date_format : 'yy-mm-dd';
2851        }
2852
2853        return $extra_attrs;
2854    }
2855
2856    /**
2857     * Overrides input type (maybe).
2858     *
2859     * @module contact-form
2860     *
2861     * Custom input types, like URL, will rely on browser's implementation to validate
2862     * the value. If the input carries a data-type-override, we allow to override
2863     * the type at render/submit so it can be validated with custom patterns.
2864     * This method will try to match the input's type to a custom data-type-override
2865     * attribute and return it. Defaults to input's type.
2866     *
2867     * @return string The input's type attribute or the overriden type.
2868     */
2869    private function maybe_override_type() {
2870        // Define overridables-to-custom-type, extend as needed.
2871        $overridable_types = array( 'text' => array( 'url' ) );
2872        $type              = $this->get_attribute( 'type' );
2873
2874        if ( ! array_key_exists( $type, $overridable_types ) ) {
2875            return $type;
2876        }
2877
2878        $override_type = $this->get_attribute( 'data-type-override' );
2879
2880        if ( in_array( $override_type, $overridable_types[ $type ], true ) ) {
2881            return $override_type;
2882        }
2883
2884        return $type;
2885    }
2886
2887    /**
2888     * Determines if a form field is valid.
2889     *
2890     * Add checks here to confirm if any given form field
2891     * is configured correctly and thus should be rendered
2892     * on the frontend.
2893     *
2894     * @param string $type - the field type.
2895     *
2896     * @return bool
2897     */
2898    public function is_field_renderable( $type ) {
2899        // Check that radio, select, multiple choice, and image select
2900        // fields have at least one valid option.
2901        if ( $type === 'radio' || $type === 'checkbox-multiple' || $type === 'select' ) {
2902            $options           = (array) $this->get_attribute( 'options' );
2903            $non_empty_options = array_filter(
2904                $options,
2905                function ( $option ) {
2906                    return $option !== '';
2907                }
2908            );
2909            return count( $non_empty_options ) > 0;
2910        }
2911
2912        if ( $type === 'image-select' ) {
2913            $options_data         = (array) $this->get_attribute( 'optionsdata' );
2914            $trimmed_options_data = $this->trim_image_select_options( $options_data );
2915
2916            return ! empty( $trimmed_options_data );
2917        }
2918
2919        return true;
2920    }
2921
2922    /**
2923     * Gets the form style based on its CSS class.
2924     *
2925     * @return string The form style type.
2926     */
2927    private function get_form_style() {
2928        $class_name = $this->form->get_attribute( 'className' );
2929        preg_match( '/is-style-([^\s]+)/i', $class_name, $matches );
2930        return count( $matches ) >= 2 ? $matches[1] : null;
2931    }
2932
2933    /**
2934     * Checks if the field has an inset label, i.e., a label displayed inside the field instead of above.
2935     *
2936     * @return boolean
2937     */
2938    private function has_inset_label() {
2939        $form_style = $this->get_form_style();
2940
2941        return in_array( $form_style, array( 'outlined', 'animated' ), true );
2942    }
2943
2944    /**
2945     * Return the HTML for the rating (stars/hearts/etc.) field.
2946     *
2947     * This field is purely decorative (spans acting as buttons) and stores the
2948     * selected rating in a hidden input so it is handled by existing form
2949     * validation/submission logic.
2950     *
2951     * @since 0.46.0
2952     *
2953     * @param string $id                 Field ID.
2954     * @param string $label              Field label.
2955     * @param string $value              Current value.
2956     * @param string $class              Additional CSS classes.
2957     * @param bool   $required           Whether field is required.
2958     * @param string $required_field_text Required label text.
2959     * @param bool   $required_indicator Whether to display the required indicator.
2960     * @return string HTML markup.
2961     */
2962    private function render_rating_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
2963        // Enqueue stylesheet for rating field.
2964        wp_enqueue_style( 'jetpack-form-field-rating-style', plugins_url( '../../dist/blocks/field-rating/style.css', __FILE__ ), array(), Constants::get_constant( 'JETPACK__VERSION' ) );
2965
2966        // Read block attributes needed for rendering.
2967        $max_attr   = $this->get_attribute( 'max' );
2968        $max_rating = is_numeric( $max_attr ) && (int) $max_attr > 0 ? (int) $max_attr : 5;
2969
2970        $initial_rating = (int) $value ? (int) $value : 0;
2971
2972        $label_html = $this->render_legend_as_label( 'rating', $id, $label, $required, $required_field_text, array(), $required_indicator );
2973
2974        /*
2975         * Determine which icon SVG to use based on the 'iconstyle' attribute.
2976         * Note: attribute name is lowercase due to WordPress shortcode processing
2977         */
2978        $icon_style       = $this->get_attribute( 'iconstyle' );
2979        $has_hearts_style = ( 'hearts' === $icon_style );
2980
2981        // SVG icon definitions - keep in sync with JavaScript icons.js
2982        $star_svg  = '<svg class="jetpack-field-rating__icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.46 4.73L5.82 21z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg>';
2983        $heart_svg = '<svg class="jetpack-field-rating__icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg>';
2984
2985        $icon_svg = $has_hearts_style ? $heart_svg : $star_svg;
2986
2987        $options = '';
2988        for ( $i = 1; $i <= $max_rating; $i++ ) {
2989            $radio_id = $id . '-' . $i;
2990            $options .= sprintf(
2991                '<div class="jetpack-field-rating__option">
2992                    <input
2993                        id="%1$s"
2994                        type="radio"
2995                        name="%2$s"
2996                        value="%3$s/%4$s"
2997                        data-wp-on--change="actions.onFieldChange"
2998                        class="jetpack-field-rating__input visually-hidden"
2999                        %5$s
3000                        %6$s />
3001                    <label for="%1$s" class="jetpack-field-rating__label">
3002                        %7$s
3003                    </label>
3004                </div>',
3005                esc_attr( $radio_id ),         // %1$s: id and label for
3006                esc_attr( $id ),               // %2$s: name
3007                esc_attr( $i ),                // %3$s: value (current rating)
3008                esc_attr( $max_rating ),       // %4$s: value (max rating)
3009                checked( $i, $initial_rating, false ), // %5$s: checked attribute
3010                $required ? 'required' : '',   // %6$s: required attribute
3011                $icon_svg                      // %7$s: icon SVG
3012            );
3013        }
3014
3015        $style_attr = '';
3016
3017        $css_styles = array_filter( array_map( 'trim', explode( ';', $this->field_styles ) ) );
3018
3019        $css_key_value_pairs = array_reduce(
3020            $css_styles,
3021            function ( $pairs, $style ) {
3022                list( $key, $value )   = explode( ':', $style );
3023                $pairs[ trim( $key ) ] = trim( $value );
3024                return $pairs;
3025            },
3026            array()
3027        );
3028
3029        // The rating input overwrites the text color, so we are using a custom logic to set the star color as a CSS variable.
3030        $has_star_color = isset( $css_key_value_pairs['color'] );
3031
3032        if ( $has_star_color ) {
3033            $color_value = $css_key_value_pairs['color'];
3034            $style_attr  = 'style="--jetpack--contact-form--rating-star-color: ' . esc_attr( $color_value ) . ';';
3035            unset( $css_key_value_pairs['color'] );
3036        } else {
3037            // Theme colors are set in the field_classes attribute
3038            $preset_colors = array(
3039                'has-base-color'     => '--wp--preset--color--base',
3040                'has-contrast-color' => '--wp--preset--color--contrast',
3041            );
3042
3043            if ( preg_match( '/has-accent-(\d+)-color/', $this->field_classes, $matches ) ) {
3044                $accent_number = $matches[1];
3045                $preset_colors[ 'has-accent-' . $accent_number . '-color' ] = '--wp--preset--color--accent-' . $accent_number;
3046            }
3047
3048            foreach ( $preset_colors as $class => $css_var ) {
3049                if ( strpos( $this->field_classes, $class ) !== false ) {
3050                    $style_attr = 'style="--jetpack--contact-form--rating-star-color: var(' . esc_attr( $css_var ) . ');';
3051
3052                    break;
3053                }
3054            }
3055        }
3056
3057        $remaining_styles = array_map(
3058            function ( $key, $value ) {
3059                return $key . ': ' . $value;
3060            },
3061            array_keys( $css_key_value_pairs ),
3062            array_values( $css_key_value_pairs )
3063        );
3064
3065        $style_attr .= ' ' . implode( ';', $remaining_styles ) . '"';
3066
3067        return sprintf(
3068            '<fieldset id="%4$s-label" class="jetpack-field-multiple__fieldset jetpack-field-rating" %1$s%6$s>
3069                %5$s
3070                <div class="jetpack-field-rating__options %3$s">%2$s</div>
3071            </fieldset>',
3072            $style_attr,
3073            $options,
3074            esc_attr( $this->field_classes ),
3075            esc_attr( $id ),
3076            $label_html,
3077            $this->get_hidden_label_aria_label_attr()
3078        ) . $this->get_error_div( $id, 'rating' );
3079    }
3080
3081    /**
3082     * Return the HTML for the slider field.
3083     *
3084     * @since 5.1.0
3085     *
3086     * @param int    $id The field ID.
3087     * @param string $label The field label.
3088     * @param string $value The field value.
3089     * @param string $class The field class.
3090     * @param bool   $required Whether the field is required.
3091     * @param string $required_field_text The required field text.
3092     * @param string $placeholder The field placeholder.
3093     * @param array  $extra_attrs Extra attributes (e.g., min, max).
3094     * @param bool   $required_indicator Whether to display the required indicator.
3095     *
3096     * @return string HTML for the slider field.
3097     */
3098    public function render_slider_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $extra_attrs = array(), $required_indicator = true ) {
3099        $this->enqueue_slider_field_assets();
3100        $this->set_invalid_message( 'slider', __( 'Please select a valid value', 'jetpack-forms' ) );
3101        if ( isset( $extra_attrs['min'] ) ) {
3102            // translators: %d is the minimum value.
3103            $this->set_invalid_message( 'min_slider', __( 'Please select a value that is no less than %d.', 'jetpack-forms' ) );
3104        }
3105        if ( isset( $extra_attrs['max'] ) ) {
3106            // translators: %d is the maximum value.
3107            $this->set_invalid_message( 'max_slider', __( 'Please select a value that is no more than %d.', 'jetpack-forms' ) );
3108        }
3109        $min            = $extra_attrs['min'] ?? 0;
3110        $max            = $extra_attrs['max'] ?? 100;
3111        $starting_value = $extra_attrs['default'] ?? 0;
3112        $step           = $extra_attrs['step'] ?? 1;
3113        $current_value  = ( $value !== '' && $value !== null ) ? $value : $starting_value;
3114        $min_text_label = $extra_attrs['minLabel'] ?? '';
3115        $max_text_label = $extra_attrs['maxLabel'] ?? '';
3116
3117        $field = $this->render_label( 'slider', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
3118
3119        ob_start();
3120        ?>
3121        <div class="jetpack-field-slider__input-row <?php echo esc_attr( $this->field_classes ); ?>"
3122            data-wp-context='
3123            <?php
3124            echo esc_attr(
3125                wp_json_encode(
3126                    array(
3127                        'min'     => $min,
3128                        'max'     => $max,
3129                        'default' => $starting_value,
3130                        'step'    => $step,
3131                    ),
3132                    JSON_HEX_AMP | JSON_UNESCAPED_SLASHES
3133                )
3134            );
3135            ?>
3136            '>
3137            <span class="jetpack-field-slider__min-label"><?php echo esc_html( $min ); ?></span>
3138            <div class="jetpack-field-slider__input-container">
3139                <input
3140                    type="range"
3141                    name="<?php echo esc_attr( $id ); ?>"
3142                    id="<?php echo esc_attr( $id ); ?>"
3143                    value="<?php echo esc_attr( $current_value ); ?>"
3144                    min="<?php echo esc_attr( $min ); ?>"
3145                    max="<?php echo esc_attr( $max ); ?>"
3146                    step="<?php echo esc_attr( $step ); ?>"
3147                    class="<?php echo esc_attr( trim( $class . ' jetpack-field-slider__range' ) ); ?>"
3148                    placeholder="<?php echo esc_attr( $placeholder ); ?>"
3149                    <?php
3150                    if ( $required ) :
3151                        ?>
3152                    required<?php endif; ?>
3153                    data-wp-bind--value="state.getSliderValue"
3154                    data-wp-on--input="actions.onSliderChange"
3155                    data-wp-bind--aria-invalid="state.fieldAriaInvalid"
3156                    <?php echo $this->get_hidden_label_aria_label_attr(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute value is escaped in the helper. ?>
3157                />
3158                <div
3159                    class="jetpack-field-slider__value-indicator"
3160                    data-wp-text="state.getSliderValue"
3161                    data-wp-style--left="state.getSliderPosition"
3162                ><?php echo esc_html( $current_value ); ?></div>
3163            </div>
3164            <span class="jetpack-field-slider__max-label"><?php echo esc_html( $max ); ?></span>
3165        </div>
3166        <?php if ( '' !== $min_text_label || '' !== $max_text_label ) : ?>
3167            <div class="jetpack-field-slider__text-labels <?php echo esc_attr( $this->field_classes ); ?>" aria-hidden="true">
3168                <span class="jetpack-field-slider__min-text-label"><?php echo esc_html( $min_text_label ); ?></span>
3169                <span class="jetpack-field-slider__max-text-label"><?php echo esc_html( $max_text_label ); ?></span>
3170            </div>
3171        <?php endif; ?>
3172        <?php
3173        $field .= ob_get_clean();
3174        return $field . $this->get_error_div( $id, 'slider' );
3175    }
3176
3177    /**
3178     * Enqueues scripts and styles needed for the slider field.
3179     *
3180     * @since 5.1.0
3181     *
3182     * @return void
3183     */
3184    private function enqueue_slider_field_assets() {
3185        $version = defined( 'JETPACK__VERSION' ) ? \JETPACK__VERSION : '0.1';
3186
3187        \wp_enqueue_style(
3188            'jetpack-form-slider-field',
3189            plugins_url( '../../dist/blocks/input-range/style.css', __FILE__ ),
3190            array(),
3191            $version
3192        );
3193
3194        \wp_enqueue_script_module(
3195            'jetpack-form-slider-field',
3196            plugins_url( '../../dist/modules/slider-field/view.js', __FILE__ ),
3197            array( '@wordpress/interactivity' ),
3198            $version
3199        );
3200    }
3201
3202    /**
3203     * Gets an array of translatable country names indexed by their two-letter country codes.
3204     *
3205     * @since 6.2.1
3206     *
3207     * @return array Array of country names with two-letter country codes as keys.
3208     */
3209    public function get_translatable_countries() {
3210        return array(
3211            'AF' => __( 'Afghanistan', 'jetpack-forms' ),
3212            'AL' => __( 'Albania', 'jetpack-forms' ),
3213            'DZ' => __( 'Algeria', 'jetpack-forms' ),
3214            'AS' => __( 'American Samoa', 'jetpack-forms' ),
3215            'AD' => __( 'Andorra', 'jetpack-forms' ),
3216            'AO' => __( 'Angola', 'jetpack-forms' ),
3217            'AI' => __( 'Anguilla', 'jetpack-forms' ),
3218            'AG' => __( 'Antigua and Barbuda', 'jetpack-forms' ),
3219            'AR' => __( 'Argentina', 'jetpack-forms' ),
3220            'AM' => __( 'Armenia', 'jetpack-forms' ),
3221            'AW' => __( 'Aruba', 'jetpack-forms' ),
3222            'AU' => __( 'Australia', 'jetpack-forms' ),
3223            'AT' => __( 'Austria', 'jetpack-forms' ),
3224            'AZ' => __( 'Azerbaijan', 'jetpack-forms' ),
3225            'BS' => __( 'Bahamas', 'jetpack-forms' ),
3226            'BH' => __( 'Bahrain', 'jetpack-forms' ),
3227            'BD' => __( 'Bangladesh', 'jetpack-forms' ),
3228            'BB' => __( 'Barbados', 'jetpack-forms' ),
3229            'BY' => __( 'Belarus', 'jetpack-forms' ),
3230            'BE' => __( 'Belgium', 'jetpack-forms' ),
3231            'BZ' => __( 'Belize', 'jetpack-forms' ),
3232            'BJ' => __( 'Benin', 'jetpack-forms' ),
3233            'BM' => __( 'Bermuda', 'jetpack-forms' ),
3234            'BT' => __( 'Bhutan', 'jetpack-forms' ),
3235            'BO' => __( 'Bolivia', 'jetpack-forms' ),
3236            'BA' => __( 'Bosnia and Herzegovina', 'jetpack-forms' ),
3237            'BW' => __( 'Botswana', 'jetpack-forms' ),
3238            'BR' => __( 'Brazil', 'jetpack-forms' ),
3239            'IO' => __( 'British Indian Ocean Territory', 'jetpack-forms' ),
3240            'VG' => __( 'British Virgin Islands', 'jetpack-forms' ),
3241            'BN' => __( 'Brunei', 'jetpack-forms' ),
3242            'BG' => __( 'Bulgaria', 'jetpack-forms' ),
3243            'BF' => __( 'Burkina Faso', 'jetpack-forms' ),
3244            'BI' => __( 'Burundi', 'jetpack-forms' ),
3245            'KH' => __( 'Cambodia', 'jetpack-forms' ),
3246            'CM' => __( 'Cameroon', 'jetpack-forms' ),
3247            'CA' => __( 'Canada', 'jetpack-forms' ),
3248            'CV' => __( 'Cape Verde', 'jetpack-forms' ),
3249            'KY' => __( 'Cayman Islands', 'jetpack-forms' ),
3250            'CF' => __( 'Central African Republic', 'jetpack-forms' ),
3251            'TD' => __( 'Chad', 'jetpack-forms' ),
3252            'CL' => __( 'Chile', 'jetpack-forms' ),
3253            'CN' => __( 'China', 'jetpack-forms' ),
3254            'CX' => __( 'Christmas Island', 'jetpack-forms' ),
3255            'CC' => __( 'Cocos (Keeling) Islands', 'jetpack-forms' ),
3256            'CO' => __( 'Colombia', 'jetpack-forms' ),
3257            'KM' => __( 'Comoros', 'jetpack-forms' ),
3258            'CG' => __( 'Congo - Brazzaville', 'jetpack-forms' ),
3259            'CD' => __( 'Congo - Kinshasa', 'jetpack-forms' ),
3260            'CK' => __( 'Cook Islands', 'jetpack-forms' ),
3261            'CR' => __( 'Costa Rica', 'jetpack-forms' ),
3262            'HR' => __( 'Croatia', 'jetpack-forms' ),
3263            'CU' => __( 'Cuba', 'jetpack-forms' ),
3264            'CY' => __( 'Cyprus', 'jetpack-forms' ),
3265            'CZ' => __( 'Czech Republic', 'jetpack-forms' ),
3266            'CI' => __( "Côte d'Ivoire", 'jetpack-forms' ),
3267            'DK' => __( 'Denmark', 'jetpack-forms' ),
3268            'DJ' => __( 'Djibouti', 'jetpack-forms' ),
3269            'DM' => __( 'Dominica', 'jetpack-forms' ),
3270            'DO' => __( 'Dominican Republic', 'jetpack-forms' ),
3271            'EC' => __( 'Ecuador', 'jetpack-forms' ),
3272            'EG' => __( 'Egypt', 'jetpack-forms' ),
3273            'SV' => __( 'El Salvador', 'jetpack-forms' ),
3274            'GQ' => __( 'Equatorial Guinea', 'jetpack-forms' ),
3275            'ER' => __( 'Eritrea', 'jetpack-forms' ),
3276            'EE' => __( 'Estonia', 'jetpack-forms' ),
3277            'SZ' => __( 'Eswatini', 'jetpack-forms' ),
3278            'ET' => __( 'Ethiopia', 'jetpack-forms' ),
3279            'FK' => __( 'Falkland Islands', 'jetpack-forms' ),
3280            'FO' => __( 'Faroe Islands', 'jetpack-forms' ),
3281            'FJ' => __( 'Fiji', 'jetpack-forms' ),
3282            'FI' => __( 'Finland', 'jetpack-forms' ),
3283            'FR' => __( 'France', 'jetpack-forms' ),
3284            'GF' => __( 'French Guiana', 'jetpack-forms' ),
3285            'PF' => __( 'French Polynesia', 'jetpack-forms' ),
3286            'GA' => __( 'Gabon', 'jetpack-forms' ),
3287            'GM' => __( 'Gambia', 'jetpack-forms' ),
3288            'GE' => __( 'Georgia', 'jetpack-forms' ),
3289            'DE' => __( 'Germany', 'jetpack-forms' ),
3290            'GH' => __( 'Ghana', 'jetpack-forms' ),
3291            'GI' => __( 'Gibraltar', 'jetpack-forms' ),
3292            'GR' => __( 'Greece', 'jetpack-forms' ),
3293            'GL' => __( 'Greenland', 'jetpack-forms' ),
3294            'GD' => __( 'Grenada', 'jetpack-forms' ),
3295            'GP' => __( 'Guadeloupe', 'jetpack-forms' ),
3296            'GU' => __( 'Guam', 'jetpack-forms' ),
3297            'GT' => __( 'Guatemala', 'jetpack-forms' ),
3298            'GG' => __( 'Guernsey', 'jetpack-forms' ),
3299            'GN' => __( 'Guinea', 'jetpack-forms' ),
3300            'GW' => __( 'Guinea-Bissau', 'jetpack-forms' ),
3301            'GY' => __( 'Guyana', 'jetpack-forms' ),
3302            'HT' => __( 'Haiti', 'jetpack-forms' ),
3303            'HN' => __( 'Honduras', 'jetpack-forms' ),
3304            'HK' => __( 'Hong Kong', 'jetpack-forms' ),
3305            'HU' => __( 'Hungary', 'jetpack-forms' ),
3306            'IS' => __( 'Iceland', 'jetpack-forms' ),
3307            'IN' => __( 'India', 'jetpack-forms' ),
3308            'ID' => __( 'Indonesia', 'jetpack-forms' ),
3309            'IR' => __( 'Iran', 'jetpack-forms' ),
3310            'IQ' => __( 'Iraq', 'jetpack-forms' ),
3311            'IE' => __( 'Ireland', 'jetpack-forms' ),
3312            'IM' => __( 'Isle of Man', 'jetpack-forms' ),
3313            'IL' => __( 'Israel', 'jetpack-forms' ),
3314            'IT' => __( 'Italy', 'jetpack-forms' ),
3315            'JM' => __( 'Jamaica', 'jetpack-forms' ),
3316            'JP' => __( 'Japan', 'jetpack-forms' ),
3317            'JE' => __( 'Jersey', 'jetpack-forms' ),
3318            'JO' => __( 'Jordan', 'jetpack-forms' ),
3319            'KZ' => __( 'Kazakhstan', 'jetpack-forms' ),
3320            'KE' => __( 'Kenya', 'jetpack-forms' ),
3321            'KI' => __( 'Kiribati', 'jetpack-forms' ),
3322            'XK' => __( 'Kosovo', 'jetpack-forms' ),
3323            'KW' => __( 'Kuwait', 'jetpack-forms' ),
3324            'KG' => __( 'Kyrgyzstan', 'jetpack-forms' ),
3325            'LA' => __( 'Laos', 'jetpack-forms' ),
3326            'LV' => __( 'Latvia', 'jetpack-forms' ),
3327            'LB' => __( 'Lebanon', 'jetpack-forms' ),
3328            'LS' => __( 'Lesotho', 'jetpack-forms' ),
3329            'LR' => __( 'Liberia', 'jetpack-forms' ),
3330            'LY' => __( 'Libya', 'jetpack-forms' ),
3331            'LI' => __( 'Liechtenstein', 'jetpack-forms' ),
3332            'LT' => __( 'Lithuania', 'jetpack-forms' ),
3333            'LU' => __( 'Luxembourg', 'jetpack-forms' ),
3334            'MO' => __( 'Macao', 'jetpack-forms' ),
3335            'MG' => __( 'Madagascar', 'jetpack-forms' ),
3336            'MW' => __( 'Malawi', 'jetpack-forms' ),
3337            'MY' => __( 'Malaysia', 'jetpack-forms' ),
3338            'MV' => __( 'Maldives', 'jetpack-forms' ),
3339            'ML' => __( 'Mali', 'jetpack-forms' ),
3340            'MT' => __( 'Malta', 'jetpack-forms' ),
3341            'MH' => __( 'Marshall Islands', 'jetpack-forms' ),
3342            'MQ' => __( 'Martinique', 'jetpack-forms' ),
3343            'MR' => __( 'Mauritania', 'jetpack-forms' ),
3344            'MU' => __( 'Mauritius', 'jetpack-forms' ),
3345            'YT' => __( 'Mayotte', 'jetpack-forms' ),
3346            'MX' => __( 'Mexico', 'jetpack-forms' ),
3347            'FM' => __( 'Micronesia', 'jetpack-forms' ),
3348            'MD' => __( 'Moldova', 'jetpack-forms' ),
3349            'MC' => __( 'Monaco', 'jetpack-forms' ),
3350            'MN' => __( 'Mongolia', 'jetpack-forms' ),
3351            'ME' => __( 'Montenegro', 'jetpack-forms' ),
3352            'MS' => __( 'Montserrat', 'jetpack-forms' ),
3353            'MA' => __( 'Morocco', 'jetpack-forms' ),
3354            'MZ' => __( 'Mozambique', 'jetpack-forms' ),
3355            'MM' => __( 'Myanmar', 'jetpack-forms' ),
3356            'NA' => __( 'Namibia', 'jetpack-forms' ),
3357            'NR' => __( 'Nauru', 'jetpack-forms' ),
3358            'NP' => __( 'Nepal', 'jetpack-forms' ),
3359            'NL' => __( 'Netherlands', 'jetpack-forms' ),
3360            'NC' => __( 'New Caledonia', 'jetpack-forms' ),
3361            'NZ' => __( 'New Zealand', 'jetpack-forms' ),
3362            'NI' => __( 'Nicaragua', 'jetpack-forms' ),
3363            'NE' => __( 'Niger', 'jetpack-forms' ),
3364            'NG' => __( 'Nigeria', 'jetpack-forms' ),
3365            'NU' => __( 'Niue', 'jetpack-forms' ),
3366            'NF' => __( 'Norfolk Island', 'jetpack-forms' ),
3367            'KP' => __( 'North Korea', 'jetpack-forms' ),
3368            'MK' => __( 'North Macedonia', 'jetpack-forms' ),
3369            'MP' => __( 'Northern Mariana Islands', 'jetpack-forms' ),
3370            'NO' => __( 'Norway', 'jetpack-forms' ),
3371            'OM' => __( 'Oman', 'jetpack-forms' ),
3372            'PK' => __( 'Pakistan', 'jetpack-forms' ),
3373            'PW' => __( 'Palau', 'jetpack-forms' ),
3374            'PS' => __( 'Palestine', 'jetpack-forms' ),
3375            'PA' => __( 'Panama', 'jetpack-forms' ),
3376            'PG' => __( 'Papua New Guinea', 'jetpack-forms' ),
3377            'PY' => __( 'Paraguay', 'jetpack-forms' ),
3378            'PE' => __( 'Peru', 'jetpack-forms' ),
3379            'PH' => __( 'Philippines', 'jetpack-forms' ),
3380            'PN' => __( 'Pitcairn Islands', 'jetpack-forms' ),
3381            'PL' => __( 'Poland', 'jetpack-forms' ),
3382            'PT' => __( 'Portugal', 'jetpack-forms' ),
3383            'PR' => __( 'Puerto Rico', 'jetpack-forms' ),
3384            'QA' => __( 'Qatar', 'jetpack-forms' ),
3385            'RO' => __( 'Romania', 'jetpack-forms' ),
3386            'RU' => __( 'Russia', 'jetpack-forms' ),
3387            'RW' => __( 'Rwanda', 'jetpack-forms' ),
3388            'RE' => __( 'Réunion', 'jetpack-forms' ),
3389            'BL' => __( 'Saint Barthélemy', 'jetpack-forms' ),
3390            'SH' => __( 'Saint Helena', 'jetpack-forms' ),
3391            'KN' => __( 'Saint Kitts and Nevis', 'jetpack-forms' ),
3392            'LC' => __( 'Saint Lucia', 'jetpack-forms' ),
3393            'MF' => __( 'Saint Martin', 'jetpack-forms' ),
3394            'PM' => __( 'Saint Pierre and Miquelon', 'jetpack-forms' ),
3395            'VC' => __( 'Saint Vincent and the Grenadines', 'jetpack-forms' ),
3396            'WS' => __( 'Samoa', 'jetpack-forms' ),
3397            'SM' => __( 'San Marino', 'jetpack-forms' ),
3398            'SA' => __( 'Saudi Arabia', 'jetpack-forms' ),
3399            'SN' => __( 'Senegal', 'jetpack-forms' ),
3400            'RS' => __( 'Serbia', 'jetpack-forms' ),
3401            'SC' => __( 'Seychelles', 'jetpack-forms' ),
3402            'SL' => __( 'Sierra Leone', 'jetpack-forms' ),
3403            'SG' => __( 'Singapore', 'jetpack-forms' ),
3404            'SK' => __( 'Slovakia', 'jetpack-forms' ),
3405            'SI' => __( 'Slovenia', 'jetpack-forms' ),
3406            'SB' => __( 'Solomon Islands', 'jetpack-forms' ),
3407            'SO' => __( 'Somalia', 'jetpack-forms' ),
3408            'ZA' => __( 'South Africa', 'jetpack-forms' ),
3409            'GS' => __( 'South Georgia and the South Sandwich Islands', 'jetpack-forms' ),
3410            'KR' => __( 'South Korea', 'jetpack-forms' ),
3411            'ES' => __( 'Spain', 'jetpack-forms' ),
3412            'LK' => __( 'Sri Lanka', 'jetpack-forms' ),
3413            'SD' => __( 'Sudan', 'jetpack-forms' ),
3414            'SR' => __( 'Suriname', 'jetpack-forms' ),
3415            'SJ' => __( 'Svalbard and Jan Mayen', 'jetpack-forms' ),
3416            'SE' => __( 'Sweden', 'jetpack-forms' ),
3417            'CH' => __( 'Switzerland', 'jetpack-forms' ),
3418            'SY' => __( 'Syria', 'jetpack-forms' ),
3419            'ST' => __( 'São Tomé and Príncipe', 'jetpack-forms' ),
3420            'TW' => __( 'Taiwan', 'jetpack-forms' ),
3421            'TJ' => __( 'Tajikistan', 'jetpack-forms' ),
3422            'TZ' => __( 'Tanzania', 'jetpack-forms' ),
3423            'TH' => __( 'Thailand', 'jetpack-forms' ),
3424            'TL' => __( 'Timor-Leste', 'jetpack-forms' ),
3425            'TG' => __( 'Togo', 'jetpack-forms' ),
3426            'TK' => __( 'Tokelau', 'jetpack-forms' ),
3427            'TO' => __( 'Tonga', 'jetpack-forms' ),
3428            'TT' => __( 'Trinidad and Tobago', 'jetpack-forms' ),
3429            'TN' => __( 'Tunisia', 'jetpack-forms' ),
3430            'TR' => __( 'Turkey', 'jetpack-forms' ),
3431            'TM' => __( 'Turkmenistan', 'jetpack-forms' ),
3432            'TC' => __( 'Turks and Caicos Islands', 'jetpack-forms' ),
3433            'TV' => __( 'Tuvalu', 'jetpack-forms' ),
3434            'VI' => __( 'U.S. Virgin Islands', 'jetpack-forms' ),
3435            'UG' => __( 'Uganda', 'jetpack-forms' ),
3436            'UA' => __( 'Ukraine', 'jetpack-forms' ),
3437            'AE' => __( 'United Arab Emirates', 'jetpack-forms' ),
3438            'GB' => __( 'United Kingdom', 'jetpack-forms' ),
3439            'US' => __( 'United States', 'jetpack-forms' ),
3440            'UY' => __( 'Uruguay', 'jetpack-forms' ),
3441            'UZ' => __( 'Uzbekistan', 'jetpack-forms' ),
3442            'VU' => __( 'Vanuatu', 'jetpack-forms' ),
3443            'VA' => __( 'Vatican City', 'jetpack-forms' ),
3444            'VE' => __( 'Venezuela', 'jetpack-forms' ),
3445            'VN' => __( 'Vietnam', 'jetpack-forms' ),
3446            'WF' => __( 'Wallis and Futuna', 'jetpack-forms' ),
3447            'YE' => __( 'Yemen', 'jetpack-forms' ),
3448            'ZM' => __( 'Zambia', 'jetpack-forms' ),
3449            'ZW' => __( 'Zimbabwe', 'jetpack-forms' ),
3450        );
3451    }
3452
3453    /**
3454     * Enqueues scripts and styles needed for the slider field.
3455     *
3456     * @since 6.2.1
3457     *
3458     * @return void
3459     */
3460    private function enqueue_phone_field_assets() {
3461        $version = defined( 'JETPACK__VERSION' ) ? \JETPACK__VERSION : '0.1';
3462
3463        // extra cache busting strategy for view.js, seems they are left out of cache clearing on deploys
3464        $asset_file = plugin_dir_path( __FILE__ ) . '../../dist/modules/field-phone/view.asset.php';
3465        $asset      = file_exists( $asset_file ) ? require $asset_file : null;
3466        $version   .= $asset['version'] ?? '';
3467
3468        // combobox styles
3469        \wp_enqueue_style(
3470            'jetpack-form-combobox',
3471            plugins_url( '../../dist/contact-form/css/combobox.css', __FILE__ ),
3472            array(),
3473            $version
3474        );
3475
3476        \wp_enqueue_style(
3477            'jetpack-form-phone-field',
3478            plugins_url( '../../dist/contact-form/css/phone-field.css', __FILE__ ),
3479            array(),
3480            $version
3481        );
3482
3483        \wp_enqueue_script_module(
3484            'jetpack-form-phone-field',
3485            plugins_url( '../../dist/modules/field-phone/view.js', __FILE__ ),
3486            array( '@wordpress/interactivity', 'jp-forms-view' ),
3487            $version
3488        );
3489    }
3490
3491    /**
3492     * Trims the image select options from the end of the array if they are empty.
3493     *
3494     * @param array $options The options to trim.
3495     *
3496     * @return array The trimmed options array.
3497     */
3498    private function trim_image_select_options( $options ) {
3499        if ( empty( $options ) ) {
3500            return $options;
3501        }
3502
3503        // Work backwards through the array to find the last valid option
3504        $last_valid_index = -1;
3505
3506        for ( $i = count( $options ) - 1; $i >= 0; $i-- ) {
3507            $option = $options[ $i ];
3508
3509            // Check if option has a label
3510            $has_label = ! empty( $option['label'] );
3511
3512            // Check if option has an image with src
3513            $has_image = false;
3514
3515            if ( isset( $option['image']['innerHTML'] ) ) {
3516                // Extract src from innerHTML using regex
3517                preg_match( '/src="([^"]*)"/', $option['image']['innerHTML'], $matches );
3518                $has_image = ! empty( $matches[1] );
3519            }
3520
3521            // If this option has either a label or an image, it's valid
3522            if ( $has_label || $has_image ) {
3523                $last_valid_index = $i;
3524                break;
3525            }
3526        }
3527
3528        return array_slice( $options, 0, $last_valid_index + 1 );
3529    }
3530}