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