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