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