Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.65% covered (warning)
62.65%
1092 / 1743
33.33% covered (danger)
33.33%
16 / 48
CRAP
0.00% covered (danger)
0.00%
0 / 1
Contact_Form_Field
62.72% covered (warning)
62.72%
1092 / 1741
33.33% covered (danger)
33.33%
16 / 48
10468.67
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 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 render_file_field
0.00% covered (danger)
0.00%
0 / 104
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"></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"
1090                                data-wp-text="context.selectedCountry.flag"></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" data-wp-text="context.filtered.flag"></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        $has_inner_block_option_styles = ! empty( $this->get_attribute( 'optionstyles' ) );
1416
1417        $field = "<label class='" . esc_attr( $label_class ) . "' style='" . esc_attr( $this->label_styles ) . ( $has_inner_block_option_styles ? esc_attr( $this->option_styles ) : '' ) . "'>";
1418
1419        if ( 'implicit' === $consent_type ) {
1420            $field .= "\t\t<input type='hidden' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack-forms' ) . "' /> \n";
1421        } else {
1422            $field .= "\t\t<input type='checkbox' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack-forms' ) . "' " . $class . "/> \n";
1423        }
1424        $field .= "\t\t" . wp_kses_post( $consent_message );
1425        $field .= "</label>\n";
1426        $field .= "<div class='clear-form'></div>\n";
1427        return $field;
1428    }
1429
1430    /**
1431     * Return the HTML for the file field.
1432     *
1433     * Renders a file upload field with drag-and-drop functionality.
1434     *
1435     * @since 0.45.0
1436     *
1437     * @param string $id - the field ID.
1438     * @param string $label - the field label.
1439     * @param string $class - the field CSS class.
1440     * @param bool   $required - if the field is marked as required.
1441     * @param string $required_field_text - the text in the required text field.
1442     * @param bool   $required_indicator Whether to display the required indicator.
1443     *
1444     * @return string HTML for the file upload field.
1445     */
1446    private function render_file_field( $id, $label, $class, $required, $required_field_text, $required_indicator = true ) {
1447        // Check if Jetpack is active
1448        if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) {
1449            return '<div class="jetpack-form-field-error">' .
1450                esc_html__( 'File upload field requires Jetpack to be active.', 'jetpack-forms' ) .
1451                '</div>';
1452        }
1453
1454        $this->set_invalid_message( 'file_uploading', __( 'Please wait a moment, file is currently uploading.', 'jetpack-forms' ) );
1455        $this->set_invalid_message( 'file_has_errors', __( 'Please remove any file upload errors.', 'jetpack-forms' ) );
1456
1457        // Enqueue necessary scripts and styles.
1458        $this->enqueue_file_field_assets();
1459
1460        // Get allowed MIME types for display in the field.
1461        $accepted_file_types = array_values(
1462            array(
1463                'jpg|jpeg|jpe'    => 'image/jpeg',
1464                'png'             => 'image/png',
1465                'gif'             => 'image/gif',
1466                'pdf'             => 'application/pdf',
1467                'doc'             => 'application/msword',
1468                'docx'            => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1469                'docm'            => 'application/vnd.ms-word.document.macroEnabled.12',
1470                'pot|pps|ppt'     => 'application/vnd.ms-powerpoint',
1471                'pptx'            => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1472                'pptm'            => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
1473                'odt'             => 'application/vnd.oasis.opendocument.text',
1474                'ppsx'            => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
1475                'ppsm'            => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
1476                'xla|xls|xlt|xlw' => 'application/vnd.ms-excel',
1477                'xlsx'            => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1478                'xlsm'            => 'application/vnd.ms-excel.sheet.macroEnabled.12',
1479                'xlsb'            => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
1480                'key'             => 'application/vnd.apple.keynote',
1481                'webp'            => 'image/webp',
1482                'heic'            => 'image/heic',
1483                'heics'           => 'image/heic-sequence',
1484                'heif'            => 'image/heif',
1485                'heifs'           => 'image/heif-sequence',
1486                'asc'             => 'application/pgp-keys',
1487            )
1488        );
1489
1490        $accept_attribute_value = implode( ', ', $accepted_file_types );
1491
1492        // Add accessibility attributes and required status if needed.
1493        $input_attrs = array(
1494            'type'       => 'file',
1495            'class'      => 'jetpack-form-file-field ' . esc_attr( $class ),
1496            'name'       => esc_attr( $id ),
1497            'id'         => esc_attr( $id ),
1498            'accept'     => esc_attr( $accept_attribute_value ),
1499            'aria-label' => esc_attr( $label ),
1500        );
1501
1502        if ( $required ) {
1503            $input_attrs['required']      = 'required';
1504            $input_attrs['aria-required'] = 'true';
1505        }
1506
1507        $max_files       = 1; // TODO: Dynamically retrieve the max number of files using $this->get_attribute( 'maxfiles' ) if needed in the future.
1508        $max_file_size   = 20 * 1024 * 1024; // 20MB
1509        $file_size_units = array(
1510            _x( 'B', 'unit symbol', 'jetpack-forms' ),
1511            _x( 'KB', 'unit symbol', 'jetpack-forms' ),
1512            _x( 'MB', 'unit symbol', 'jetpack-forms' ),
1513            _x( 'GB', 'unit symbol', 'jetpack-forms' ),
1514        );
1515
1516        $global_config = array(
1517            'i18n'          => array(
1518                'language'           => get_bloginfo( 'language' ),
1519                'fileSizeUnits'      => $file_size_units,
1520                'zeroBytes'          => __( '0 Bytes', 'jetpack-forms' ),
1521                'uploadError'        => __( 'Error uploading file', 'jetpack-forms' ),
1522                'folderNotSupported' => __( 'Folder uploads are not supported', 'jetpack-forms' ),
1523                // translators: %s is the formatted maximum file size.
1524                'fileTooLarge'       => sprintf( __( 'File is too large. Maximum allowed size is %s.', 'jetpack-forms' ), size_format( $max_file_size ) ),
1525                'invalidType'        => __( 'This file type is not allowed.', 'jetpack-forms' ),
1526                'maxFiles'           => __( 'You have exceeded the number of files that you can upload.', 'jetpack-forms' ),
1527                'uploadFailed'       => __( 'File upload failed, try again.', 'jetpack-forms' ),
1528            ),
1529            'endpoint'      => $this->get_unauth_endpoint_url(),
1530            'iconsPath'     => Jetpack_Forms::plugin_url() . 'contact-form/images/file-icons/',
1531            'maxUploadSize' => $max_file_size,
1532        );
1533
1534        wp_interactivity_config( 'jetpack/field-file', $global_config );
1535
1536        $context = array(
1537            'isDropping'       => false,
1538            'fieldId'          => $id,
1539            'files'            => array(),
1540            'allowedMimeTypes' => $accepted_file_types,
1541            'maxFiles'         => $max_files, // max number of files.
1542            'hasMaxFiles'      => false,
1543        );
1544
1545        $field = $this->render_label( 'file', $id, $label, $required, $required_field_text, array(), true, $required_indicator );
1546
1547        ob_start();
1548        ?>
1549        <div
1550            class="jetpack-form-file-field__container"
1551            id="<?php echo esc_attr( $id ); ?>"
1552            name="dropzone-<?php echo esc_attr( $id ); ?>"
1553            data-wp-interactive="jetpack/field-file"
1554            <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output is pre-escaped by method ?>
1555            <?php echo wp_interactivity_data_wp_context( $context ); ?>
1556            data-wp-on--dragover="actions.dragOver"
1557            data-wp-on--dragleave="actions.dragLeave"
1558            data-wp-on--mouseleave="actions.dragLeave"
1559            data-wp-on--drop="actions.fileDropped"
1560            data-wp-on--jetpack-form-reset="actions.resetFiles"
1561            data-is-required="<?php echo esc_attr( $required ); ?>"
1562        >
1563            <div class="jetpack-form-file-field__dropzone" data-wp-class--is-dropping="context.isDropping" data-wp-class--is-hidden="state.hasMaxFiles">
1564                <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>
1565                <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is intentionally unescaped as it contains block content that was previously escaped ?>
1566                <?php echo html_entity_decode( $this->content, ENT_COMPAT, 'UTF-8' ); ?>
1567                <input
1568                    type="file" class="jetpack-form-file-field"
1569                    accept="<?php echo esc_attr( $accept_attribute_value ); ?>"
1570                    data-wp-on--change="actions.fileAdded"  />
1571            </div>
1572            <div class="jetpack-form-file-field__preview-wrap" name="file-field-<?php echo esc_attr( $id ); ?>" data-wp-class--is-active="state.hasFiles">
1573                <template data-wp-each--file="context.files" data-wp-key="context.file.id">
1574                    <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">
1575                        <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="">
1576                        <div class="jetpack-form-file-field__image-wrap" data-wp-style----progress="context.file.progress" data-wp-class--has-icon="context.file.hasIcon">
1577                            <div class="jetpack-form-file-field__image" data-wp-style--background-image="context.file.url" data-wp-style--mask-image="context.file.mask"></div>
1578                            <div class="jetpack-form-file-field__progress-bar" ></div>
1579                        </div>
1580
1581                        <div class="jetpack-form-file-field__file-wrap">
1582                            <strong class="jetpack-form-file-field__file-name" data-wp-text="context.file.name"></strong>
1583                            <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">
1584                                <span class="jetpack-form-file-field__file-size" data-wp-text="context.file.formattedSize"></span>
1585                                <span class="jetpack-form-file-field__seperator"> &middot; </span>
1586                                <span aria-live="polite">
1587                                    <span class="jetpack-form-file-field__uploading"><?php esc_html_e( 'Uploading…', 'jetpack-forms' ); ?></span>
1588                                    <span class="jetpack-form-file-field__success"><?php esc_html_e( 'Uploaded', 'jetpack-forms' ); ?></span>
1589                                    <span class="jetpack-form-file-field__error" data-wp-text="context.file.error"></span>
1590                                </span>
1591                            </div>
1592                        </div>
1593                        <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>
1594                    </div>
1595                </template>
1596            </div>
1597        </div>
1598        <?php
1599        return $field . ob_get_clean() . $this->get_error_div( $id, 'file' );
1600    }
1601
1602    /**
1603     * Render a hidden field.
1604     *
1605     * @param string $id - the field ID.
1606     * @param string $label - the field label.
1607     * @param string $value - the value of the field.
1608     *
1609     * @return string HTML for the hidden field.
1610     */
1611    private function render_hidden_field( $id, $label, $value ) {
1612        /**
1613         *
1614         * Filter the value of the hidden field.
1615         *
1616         * @since 6.3.0
1617         *
1618         * @param string $value The value of the hidden field.
1619         * @param string $label The label of the hidden field.
1620         * @param string $id The ID of the hidden field.
1621         *
1622         * @return string The modified value of the hidden field.
1623         */
1624        $value = apply_filters( 'jetpack_forms_hidden_field_value', $value, $label, $id );
1625        return "<input type='hidden' name='" . esc_attr( $id ) . "' id='" . esc_attr( $id ) . "' value='" . esc_attr( $value ) . "' />\n";
1626    }
1627
1628    /**
1629     * Enqueues scripts and styles needed for the file field.
1630     *
1631     * @since 0.45.0
1632     *
1633     * @return void
1634     */
1635    private function enqueue_file_field_assets() {
1636        $version = Constants::get_constant( 'JETPACK__VERSION' );
1637
1638        \wp_enqueue_script_module(
1639            'jetpack-form-file-field',
1640            plugins_url( '../../dist/modules/file-field/view.js', __FILE__ ),
1641            array( '@wordpress/interactivity' ),
1642            $version
1643        );
1644
1645        \wp_enqueue_style(
1646            'jetpack-form-file-field',
1647            plugins_url( '../../dist/contact-form/css/file-field.css', __FILE__ ),
1648            array(),
1649            $version
1650        );
1651    }
1652
1653    /**
1654     * Returns the URL for the unauthenticated file upload endpoint.
1655     *
1656     * @return string
1657     */
1658    private function get_unauth_endpoint_url() {
1659        // Return a placeholder URL if Jetpack is not active
1660        if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) {
1661            return '#jetpack-not-active';
1662        }
1663
1664        return sprintf( 'https://public-api.wordpress.com/wpcom/v2/sites/%d/unauth-file-upload', \Jetpack_Options::get_option( 'id' ) );
1665    }
1666
1667    /**
1668     * Return the HTML for the multiple checkbox field.
1669     *
1670     * @param string $id - the ID (starts with 'g' - see constructor).
1671     * @param string $label - the label.
1672     * @param string $value - the value of the field.
1673     * @param string $class - the field class.
1674     * @param bool   $required - if the field is marked as required.
1675     * @param string $required_field_text - the text in the required text field.
1676     * @param bool   $required_indicator Whether to display the required indicator.
1677     *
1678     * @return string HTML
1679     */
1680    public function render_checkbox_multiple_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
1681        $options_classes   = $this->get_attribute( 'optionsclasses' );
1682        $options_styles    = $this->get_attribute( 'optionsstyles' );
1683        $form_style        = $this->get_form_style();
1684        $is_outlined_style = 'outlined' === $form_style;
1685
1686        /*
1687         * The `data-required` attribute is used in `accessible-form.js` to ensure at least one
1688         * checkbox is checked. Unlike radio buttons, for which the required attribute is satisfied if
1689         * any of the radio buttons in the group is selected, adding a required attribute directly to
1690         * a checkbox means that this specific checkbox must be checked.
1691         */
1692        $fieldset_id = "id='" . esc_attr( "$id-label" ) . "'";
1693
1694        if ( $is_outlined_style ) {
1695            $style_variation_attributes = $this->get_attribute( 'stylevariationattributes' );
1696
1697            if ( ! empty( $style_variation_attributes ) ) {
1698                $style_variation_attributes = json_decode( html_entity_decode( $style_variation_attributes, ENT_COMPAT ), true );
1699            }
1700
1701            /*
1702             * When there's an outlined style, and border radius is set, the existing inline border radius is overridden to apply
1703             * a limit of `100px` to the radius on the x axis. This achieves the same look and feel as other fields
1704             * that use the notch html (`notched-label__leading` has a max-width of `100px` to prevent it from getting too wide).
1705             * It prevents large border radius values from disrupting the look and feel of the fields.
1706             */
1707            if ( isset( $style_variation_attributes['border']['radius'] ) ) {
1708                $options_styles          = $options_styles ?? '';
1709                $radius                  = $style_variation_attributes['border']['radius'];
1710                $has_split_radius_values = is_array( $radius );
1711                $top_left_radius         = $has_split_radius_values ? $radius['topLeft'] : $radius;
1712                $top_right_radius        = $has_split_radius_values ? $radius['topRight'] : $radius;
1713                $bottom_left_radius      = $has_split_radius_values ? $radius['bottomLeft'] : $radius;
1714                $bottom_right_radius     = $has_split_radius_values ? $radius['bottomRight'] : $radius;
1715                $options_styles         .= "border-top-left-radius: min(100px, {$top_left_radius}{$top_left_radius};";
1716                $options_styles         .= "border-top-right-radius: min(100px, {$top_right_radius}{$top_right_radius};";
1717                $options_styles         .= "border-bottom-left-radius: min(100px, {$bottom_left_radius}{$bottom_left_radius};";
1718                $options_styles         .= "border-bottom-right-radius: min(100px, {$bottom_right_radius}{$bottom_right_radius};";
1719            }
1720
1721            /*
1722             * For the "outlined" style, the styles and classes are applied to the fieldset element.
1723             */
1724            $field = "<fieldset {$fieldset_id} class='grunion-checkbox-multiple-options " . $options_classes . "' style='" . $options_styles . "' " . ( $required ? 'data-required' : '' ) . ' data-wp-bind--aria-invalid="state.fieldHasErrors">';
1725        } else {
1726            $field = "<fieldset {$fieldset_id} class='jetpack-field-multiple__fieldset'" . ( $required ? 'data-required' : '' ) . ' data-wp-bind--aria-invalid="state.fieldHasErrors">';
1727        }
1728
1729        $field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text, array(), $required_indicator );
1730
1731        if ( ! $is_outlined_style ) {
1732            $field .= "<div class='grunion-checkbox-multiple-options " . $options_classes . "' style='" . $options_styles . "' " . '>';
1733        }
1734
1735        $options_data  = $this->get_attribute( 'optionsdata' );
1736        $used_html_ids = array();
1737
1738        if ( ! empty( $options_data ) ) {
1739            foreach ( $options_data as $option_index => $option ) {
1740                $option_label = Contact_Form_Plugin::strip_tags( $option['label'] );
1741                if ( is_string( $option_label ) && '' !== $option_label ) {
1742                    $checkbox_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option_label );
1743                    $checkbox_id    = $id . '-' . sanitize_html_class( $checkbox_value );
1744
1745                    // If exact id was already used in this checkbox group, append option index.
1746                    // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc.
1747                    if ( isset( $used_html_ids[ $checkbox_id ] ) ) {
1748                        $checkbox_id .= '-' . $option_index;
1749                    }
1750                    $used_html_ids[ $checkbox_id ] = true;
1751
1752                    $default_classes = 'contact-form-field';
1753                    $option_styles   = empty( $option['style'] ) ? '' : "style='" . esc_attr( $option['style'] ) . "'";
1754                    $option_classes  = empty( $option['class'] ) ? $default_classes : $default_classes . ' ' . esc_attr( $option['class'] );
1755
1756                    $field .= "<label {$option_styles} class='{$option_classes}'>";
1757                    $field .= "<input
1758                                id='" . esc_attr( $checkbox_id ) . "'
1759                                type='checkbox'
1760                                data-wp-on--change='actions.onMultipleFieldChange'
1761                                name='" . esc_attr( $id ) . "[]'
1762                                value='" . esc_attr( $checkbox_value ) . "' "
1763                                . $class
1764                                . checked( in_array( $option_label, (array) $value, true ), true, false )
1765                                . ' /> ';
1766                    $field .= "<span class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
1767                    $field .= "<span class='grunion-field-text'>" . esc_html( $option_label ) . '</span>';
1768                    $field .= '</span>';
1769                    $field .= '</label>';
1770                }
1771            }
1772        } else {
1773            $field_style = 'style="' . $this->option_styles . '"';
1774
1775            foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) {
1776                $option = Contact_Form_Plugin::strip_tags( $option );
1777                if ( is_string( $option ) && '' !== $option ) {
1778                    $checkbox_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option );
1779                    $checkbox_id    = $id . '-' . sanitize_html_class( $checkbox_value );
1780
1781                    // If exact id was already used in this checkbox group, append option index.
1782                    // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc.
1783                    if ( isset( $used_html_ids[ $checkbox_id ] ) ) {
1784                        $checkbox_id .= '-' . $option_index;
1785                    }
1786                    $used_html_ids[ $checkbox_id ] = true;
1787
1788                    $field .= "<label class='contact-form-field'>";
1789                    $field .= "<input
1790                                id='" . esc_attr( $checkbox_id ) . "'
1791                                data-wp-on--change='actions.onMultipleFieldChange'
1792                                type='checkbox'
1793                                name='" . esc_attr( $id ) . "[]'
1794                                value='" . esc_attr( $checkbox_value ) . "' "
1795                                . $class
1796                                . checked( in_array( $option, (array) $value, true ), true, false )
1797                                . ' /> ';
1798                    $field .= "<span {$field_style} class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>";
1799                    $field .= "<span class='grunion-field-text'>" . esc_html( $option ) . '</span>';
1800                    $field .= '</span>';
1801                    $field .= '</label>';
1802                }
1803            }
1804        }
1805        if ( ! $is_outlined_style ) {
1806            $field .= '</div>';
1807        }
1808        $field .= $this->get_error_div( $id, 'select' ) . '</fieldset>';
1809        return $field;
1810    }
1811
1812    /**
1813     * Return the HTML for the select field.
1814     *
1815     * @param int    $id - the ID.
1816     * @param string $label - the label.
1817     * @param string $value - the value of the field.
1818     * @param string $class - the field class.
1819     * @param bool   $required - if the field is marked as required.
1820     * @param string $required_field_text - the text in the required text field.
1821     * @param bool   $required_indicator Whether to display the required indicator.
1822     *
1823     * @return string HTML
1824     */
1825    public function render_select_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
1826        $field      = $this->render_label( 'select', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
1827        $class      = preg_replace( "/class=['\"]([^'\"]*)['\"]/", 'class="contact-form__select-wrapper $1"', $class );
1828        $field     .= "<div {$class} style='" . esc_attr( $this->field_styles ) . "'>";
1829        $aria_label = ! empty( $this->get_attribute( 'togglelabel' ) )
1830            ? Contact_Form_Plugin::strip_tags( $this->get_attribute( 'togglelabel' ) )
1831            : __( 'Select an option', 'jetpack-forms' ); // selects don't have a default label
1832        $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";
1833
1834        if ( $this->get_attribute( 'togglelabel' ) ) {
1835            $field .= "\t\t<option value=''>" . $this->get_attribute( 'togglelabel' ) . "</option>\n";
1836        }
1837
1838        foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) {
1839            $option = Contact_Form_Plugin::strip_tags( $option );
1840            if ( is_string( $option ) && $option !== '' ) {
1841                $field .= "\t\t<option"
1842                                . selected( $option, $value, false )
1843                                . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option ) )
1844                                . "'>" . esc_html( $option )
1845                                . "</option>\n";
1846            }
1847        }
1848        $field .= "\t</select><span class='jetpack-field-dropdown__icon'></span></span>\n";
1849        $field .= "</div>\n";
1850
1851        return $field . $this->get_error_div( $id, 'select' );
1852    }
1853
1854    /**
1855     * Return the HTML for the date field.
1856     *
1857     * @param int    $id - the ID.
1858     * @param string $label - the label.
1859     * @param string $value - the value of the field.
1860     * @param string $class - the field class.
1861     * @param bool   $required - if the field is marked as required.
1862     * @param string $required_field_text - the text in the required text field.
1863     * @param string $placeholder - the field placeholder content.
1864     * @param bool   $required_indicator Whether to display the required indicator.
1865     *
1866     * @return string HTML
1867     */
1868    public function render_date_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) {
1869        static $is_loaded = false;
1870        $this->set_invalid_message( 'date', __( 'Please enter a valid date.', 'jetpack-forms' ) );
1871        // WARNING: sync data with DATE_FORMATS in jetpack-field-datepicker.js
1872        $formats = array(
1873            'mm/dd/yy' => array(
1874                /* translators: date format. DD is the day of the month, MM the month, and YYYY the year (e.g., 12/31/2023). */
1875                'label' => __( 'MM/DD/YYYY', 'jetpack-forms' ),
1876            ),
1877            'dd/mm/yy' => array(
1878                /* translators: date format. DD is the day of the month, MM the month, and YYYY the year (e.g., 31/12/2023). */
1879                'label' => __( 'DD/MM/YYYY', 'jetpack-forms' ),
1880            ),
1881            'yy-mm-dd' => array(
1882                /* translators: date format. DD is the day of the month, MM the month, and YYYY the year (e.g., 2023-12-31). */
1883                'label' => __( 'YYYY-MM-DD', 'jetpack-forms' ),
1884            ),
1885        );
1886
1887        $date_format = $this->get_attribute( 'dateformat' );
1888        $date_format = isset( $date_format ) && ! empty( $date_format ) ? $date_format : 'yy-mm-dd';
1889        $label       = isset( $formats[ $date_format ] ) ? $label . ' (' . $formats[ $date_format ]['label'] . ')' : $label;
1890        $extra_attrs = array( 'data-format' => $date_format );
1891
1892        $field  = $this->render_label( 'date', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
1893        $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required, $extra_attrs );
1894
1895        /* For AMP requests, use amp-date-picker element: https://amp.dev/documentation/components/amp-date-picker */
1896        if ( class_exists( 'Jetpack_AMP_Support' ) && \Jetpack_AMP_Support::is_amp_request() ) {
1897            return sprintf(
1898                '<%1$s mode="overlay" layout="container" type="single" input-selector="[name=%2$s]">%3$s</%1$s>',
1899                'amp-date-picker',
1900                esc_attr( $id ),
1901                $field
1902            );
1903        }
1904
1905        Assets::register_script(
1906            'jp-forms-date-picker',
1907            '../../dist/contact-form/js/date-picker.js',
1908            __FILE__,
1909            array(
1910                'enqueue'      => true,
1911                'dependencies' => array(),
1912                'version'      => Constants::get_constant( 'JETPACK__VERSION' ),
1913            )
1914        );
1915
1916        /**
1917         * Filter the localized date picker script.
1918         */
1919        if ( ! $is_loaded ) {
1920            \wp_localize_script(
1921                'jp-forms-date-picker',
1922                'jpDatePicker',
1923                array(
1924                    'offset' => intval( get_option( 'start_of_week', 1 ) ),
1925                    'lang'   => array(
1926                        // translators: These are the two letter abbreviated name of the week.
1927                        'days'      => array(
1928                            __( 'Su', 'jetpack-forms' ),
1929                            __( 'Mo', 'jetpack-forms' ),
1930                            __( 'Tu', 'jetpack-forms' ),
1931                            __( 'We', 'jetpack-forms' ),
1932                            __( 'Th', 'jetpack-forms' ),
1933                            __( 'Fr', 'jetpack-forms' ),
1934                            __( 'Sa', 'jetpack-forms' ),
1935                        ),
1936                        'months'    => array(
1937                            __( 'January', 'jetpack-forms' ),
1938                            __( 'February', 'jetpack-forms' ),
1939                            __( 'March', 'jetpack-forms' ),
1940                            __( 'April', 'jetpack-forms' ),
1941                            __( 'May', 'jetpack-forms' ),
1942                            __( 'June', 'jetpack-forms' ),
1943                            __( 'July', 'jetpack-forms' ),
1944                            __( 'August', 'jetpack-forms' ),
1945                            __( 'September', 'jetpack-forms' ),
1946                            __( 'October', 'jetpack-forms' ),
1947                            __( 'November', 'jetpack-forms' ),
1948                            __( 'December', 'jetpack-forms' ),
1949                        ),
1950                        'today'     => __( 'Today', 'jetpack-forms' ),
1951                        'clear'     => __( 'Clear', 'jetpack-forms' ),
1952                        'close'     => __( 'Close', 'jetpack-forms' ),
1953                        'ariaLabel' => array(
1954                            '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' ),
1955                            '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' ),
1956                            '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' ),
1957                            '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' ),
1958                            'monthPickerButton' => __( 'Month picker. Use the space key to enter the month picker.', 'jetpack-forms' ),
1959                            'yearPickerButton'  => __( 'Year picker. Use the space key to enter the month picker.', 'jetpack-forms' ),
1960                            'dayButton'         => __( 'Use the space key to select the date.', 'jetpack-forms' ),
1961                            'todayButton'       => __( 'Today button. Use the space key to select the current date.', 'jetpack-forms' ),
1962                            'clearButton'       => __( 'Clear button. Use the space key to clear the date picker.', 'jetpack-forms' ),
1963                            'closeButton'       => __( 'Close button. Use the space key to close the date picker.', 'jetpack-forms' ),
1964                        ),
1965                    ),
1966                )
1967            );
1968            $is_loaded = true;
1969        }
1970
1971        return $field;
1972    }
1973
1974    /**
1975     * Return the HTML for the time field.
1976     *
1977     * @param int    $id - the ID.
1978     * @param string $label - the label.
1979     * @param string $value - the value of the field.
1980     * @param string $class - the field class.
1981     * @param bool   $required - if the field is marked as required.
1982     * @param string $required_field_text - the text in the required text field.
1983     * @param string $placeholder - the field placeholder content.
1984     * @param bool   $required_indicator Whether to display the required indicator.
1985     *
1986     * @return string HTML
1987     */
1988    public function render_time_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) {
1989        $this->set_invalid_message( 'time', __( 'Please enter a valid time.', 'jetpack-forms' ) );
1990
1991        $field  = $this->render_label( 'time', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
1992        $field .= $this->render_input_field( 'time', $id, $value, $class, $placeholder, $required );
1993
1994        return $field;
1995    }
1996
1997    /**
1998     * Return the HTML for the image select field.
1999     *
2000     * @param int    $id - the ID.
2001     * @param string $label - the label.
2002     * @param string $value - the value of the field.
2003     * @param string $class - the field class.
2004     * @param bool   $required - if the field is marked as required.
2005     * @param string $required_field_text - the text in the required text field.
2006     * @param bool   $required_indicator Whether to display the required indicator.
2007     *
2008     * @return string HTML
2009     */
2010    public function render_image_select_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
2011        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' ) );
2012
2013        $is_multiple       = $this->get_attribute( 'ismultiple' );
2014        $show_labels       = $this->get_attribute( 'showlabels' );
2015        $randomize_options = $this->get_attribute( 'randomizeoptions' );
2016        $is_supersized     = $this->get_attribute( 'issupersized' );
2017
2018        $input_type = $is_multiple ? 'checkbox' : 'radio';
2019        $input_name = $is_multiple ? $id . '[]' : $id;
2020
2021        $field = "<div class='jetpack-field jetpack-field-image-select'>";
2022
2023        $fieldset_id = "id='" . esc_attr( "$id-label" ) . "'";
2024
2025        $field .= "<fieldset {$fieldset_id} data-wp-bind--aria-invalid='state.fieldHasErrors' >";
2026
2027        $field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text, array(), $required_indicator );
2028
2029        $options_classes = $this->get_attribute( 'optionsclasses' );
2030        $options_styles  = $this->get_attribute( 'optionsstyles' );
2031
2032        $field .= "<div class='" . esc_attr( $options_classes ) . " jetpack-field jetpack-fieldset-image-options' style='" . esc_attr( $options_styles ) . "'>";
2033        $field .= "<div class='jetpack-fieldset-image-options__wrapper'>";
2034
2035        $options_data = $this->get_attribute( 'optionsdata' );
2036
2037        // Filter out empty options from the end
2038        $options_data = $this->trim_image_select_options( $options_data );
2039
2040        $used_html_ids = array();
2041
2042        // Create a separate array of original letters in sequence (A, B, C...)
2043        $perceived_letters = array();
2044
2045        foreach ( $options_data as $option ) {
2046            $perceived_letters[] = Contact_Form_Plugin::strip_tags( $option['letter'] );
2047        }
2048
2049        // Create a working copy of options for potential randomization
2050        $working_options = $options_data;
2051
2052        // Randomize options if requested, but preserve original letter values
2053        if ( $randomize_options ) {
2054            shuffle( $working_options );
2055
2056            // Trims options after randomization to ensure the last option has a label or image.
2057            $working_options = $this->trim_image_select_options( $working_options );
2058        }
2059
2060        // Calculate row options count for CSS variable
2061        $total_options_count = count( $working_options );
2062        // Those values are halved on mobile via CSS media query
2063        $max_images_per_row = $is_supersized ? 2 : 4;
2064        $row_options_count  = min( $total_options_count, $max_images_per_row );
2065
2066        foreach ( $working_options as $option_index => $option ) {
2067            $option_label  = Contact_Form_Plugin::strip_tags( $option['label'] );
2068            $option_letter = Contact_Form_Plugin::strip_tags( $option['letter'] );
2069            $image_block   = $option['image'];
2070
2071            $rendered_image_block = render_block( $image_block );
2072            // Remove any links from the rendered block
2073            $rendered_image_block = preg_replace( '/<a[^>]*>(.*?)<\/a>/s', '$1', $rendered_image_block );
2074
2075            // Extract image src from rendered block
2076            $image_src = '';
2077
2078            if ( ! empty( $rendered_image_block ) ) {
2079                if ( preg_match( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $rendered_image_block, $matches ) ) {
2080                    $extracted_src = $matches[1];
2081
2082                    if ( filter_var( $extracted_src, FILTER_VALIDATE_URL ) || str_starts_with( $extracted_src, 'data:' ) ) {
2083                        $image_src = $extracted_src;
2084                    }
2085                }
2086            } else {
2087                $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>';
2088            }
2089
2090            $option_value                = wp_json_encode(
2091                array(
2092                    'perceived'  => $perceived_letters[ $option_index ],
2093                    'selected'   => $option_letter,
2094                    'label'      => $option_label,
2095                    'showLabels' => $show_labels,
2096                    'image'      => array(
2097                        'id'  => $image_block['attrs']['id'] ?? null,
2098                        'src' => $image_src ?? null,
2099                    ),
2100                ),
2101                JSON_HEX_AMP | JSON_UNESCAPED_SLASHES
2102            );
2103            $option_id                   = $id . '-' . $option_letter;
2104            $used_html_ids[ $option_id ] = true;
2105
2106            $figcaption_id = esc_attr( $option_id . '-figcaption' );
2107
2108            // Add id attribute to figcaption for accessibility
2109            if ( ! empty( $rendered_image_block ) && strpos( $rendered_image_block, '<figcaption' ) !== false ) {
2110                $rendered_image_block = preg_replace( '/(<figcaption[^>]*)(>)/', '$1 id="' . $figcaption_id . '"$2', $rendered_image_block );
2111            }
2112
2113            // To be able to apply the backdrop-filter for the hover effect, we need to separate the background into an outer div.
2114            // This outer div needs the color styles separately, and also the border radius to match the inner div without sticking out.
2115            $option_outer_classes = 'jetpack-input-image-option__outer ' . ( isset( $option['classcolor'] ) ? $option['classcolor'] : '' );
2116
2117            if ( $is_supersized ) {
2118                $option_outer_classes .= ' is-supersized';
2119            }
2120
2121            $border_styles = '';
2122            if ( ! empty( $option['style'] ) ) {
2123                preg_match( '/border-radius:([^;]+)/', $option['style'], $radius_match );
2124                preg_match( '/border-width:([^;]+)/', $option['style'], $width_match );
2125
2126                if ( ! empty( $radius_match[1] ) ) {
2127                    $radius_value = trim( $radius_match[1] );
2128
2129                    if ( ! empty( $width_match[1] ) ) {
2130                            $width_value   = trim( $width_match[1] );
2131                            $border_styles = "border-radius:calc({$radius_value} + {$width_value});";
2132                    } else {
2133                            $border_styles = "border-radius:{$radius_value};";
2134                    }
2135                } else {
2136                    // Handle individual border radius properties when border-radius is not present
2137                    preg_match( '/border-top-left-radius:([^;]+)/', $option['style'], $top_left_match );
2138                    preg_match( '/border-top-right-radius:([^;]+)/', $option['style'], $top_right_match );
2139                    preg_match( '/border-bottom-right-radius:([^;]+)/', $option['style'], $bottom_right_match );
2140                    preg_match( '/border-bottom-left-radius:([^;]+)/', $option['style'], $bottom_left_match );
2141
2142                    if ( ! empty( $top_left_match[1] ) || ! empty( $top_right_match[1] ) || ! empty( $bottom_right_match[1] ) || ! empty( $bottom_left_match[1] ) ) {
2143                        $width_value = ! empty( $width_match[1] ) ? trim( $width_match[1] ) : '1px';
2144
2145                        $top_left     = ! empty( $top_left_match[1] ) ? trim( $top_left_match[1] ) : '4px';
2146                        $top_right    = ! empty( $top_right_match[1] ) ? trim( $top_right_match[1] ) : '4px';
2147                        $bottom_right = ! empty( $bottom_right_match[1] ) ? trim( $bottom_right_match[1] ) : '4px';
2148                        $bottom_left  = ! empty( $bottom_left_match[1] ) ? trim( $bottom_left_match[1] ) : '4px';
2149
2150                        $border_styles = "border-radius:calc({$top_left} + {$width_value}) calc({$top_right} + {$width_value}) calc({$bottom_right} + {$width_value}) calc({$bottom_left} + {$width_value});";
2151                    }
2152                }
2153            }
2154
2155            $option_outer_styles  = ( empty( $option['stylecolor'] ) ? '' : $option['stylecolor'] ) . $border_styles;
2156            $option_outer_styles .= "--row-options-count: {$row_options_count};";
2157            $option_outer_styles  = empty( $option_outer_styles ) ? '' : "style='" . esc_attr( $option_outer_styles ) . "'";
2158
2159            $field .= "<div class='{$option_outer_classes}{$option_outer_styles}>";
2160
2161            $default_classes = 'jetpack-field jetpack-input-image-option';
2162            $option_styles   = empty( $option['style'] ) ? '' : "style='" . esc_attr( $option['style'] ) . "'";
2163            $option_classes  = "class='" . ( empty( $option['class'] ) ? $default_classes : $default_classes . ' ' . $option['class'] ) . "'";
2164
2165            $field .= "<div {$option_classes} {$option_styles} data-wp-on--click='actions.onImageOptionClick' data-wp-init='callbacks.setImageOptionOutlineColor'>";
2166
2167            $input_id = esc_attr( $option_id );
2168            $label_id = esc_attr( $option_id . '-label' );
2169
2170            /* translators: %s is the letter associated with the option, e.g. "Option A" */
2171            $aria_label_parts = array( sprintf( __( 'Option %s', 'jetpack-forms' ), $perceived_letters[ $option_index ] ) );
2172
2173            if ( ! empty( $option_label ) ) {
2174                $aria_label_parts[] = $option_label;
2175            }
2176
2177            $aria_label = implode( ': ', $aria_label_parts );
2178
2179            // Build aria-describedby to reference label and figcaption
2180            $aria_describedby_parts = array( $label_id );
2181
2182            if ( ! empty( $rendered_image_block ) && strpos( $rendered_image_block, '<figcaption' ) !== false ) {
2183                $aria_describedby_parts[] = $figcaption_id;
2184            }
2185
2186            $aria_describedby = implode( ' ', $aria_describedby_parts );
2187
2188            $field .= "<div class='jetpack-input-image-option__wrapper'>";
2189            $field .= "<input
2190            id='" . $input_id . "'
2191            class='jetpack-input-image-option__input'
2192            type='" . esc_attr( $input_type ) . "'
2193            name='" . esc_attr( $input_name ) . "'
2194            value='" . esc_attr( $option_value ) . "'
2195            aria-label='" . esc_attr( $aria_label ) . "'
2196            aria-describedby='" . esc_attr( $aria_describedby ) . "'
2197            data-wp-init='callbacks.setImageOptionCheckColor'
2198            data-wp-on--keydown='actions.onKeyDownImageOption'
2199            data-wp-on--change='" . ( $is_multiple ? 'actions.onMultipleFieldChange' : 'actions.onFieldChange' ) . "' "
2200            . $class
2201            . ( $is_multiple ? checked( in_array( $option_value, (array) $value, true ), true, false ) : checked( $option_value, $value, false ) ) . ' '
2202            . ( $required ? "required aria-required='true'" : '' )
2203            . '/> ';
2204
2205            $field .= $rendered_image_block;
2206            $field .= '</div>';
2207
2208            $field .= "<div class='jetpack-input-image-option__label-wrapper'>";
2209            $field .= "<div class='jetpack-input-image-option__label-code'>" . esc_html( $perceived_letters[ $option_index ] ) . '</div>';
2210
2211            $label_classes  = 'jetpack-input-image-option__label';
2212            $label_classes .= $show_labels ? '' : ' visually-hidden';
2213            $field         .= "<span id='{$label_id}' class='{$label_classes}'>" . esc_html( $option_label ) . '</span>';
2214            $field         .= '</div></div></div>';
2215        }
2216
2217        $field .= '</div></div>';
2218
2219        $field .= $this->get_error_div( $id, 'image-select' );
2220
2221        $field .= '</fieldset>';
2222
2223        $field .= '</div>';
2224
2225        return $field;
2226    }
2227
2228    /**
2229     * Return the HTML for the number field.
2230     *
2231     * @param int    $id - the ID.
2232     * @param string $label - the label.
2233     * @param string $value - the value of the field.
2234     * @param string $class - the field class.
2235     * @param bool   $required - if the field is marked as required.
2236     * @param string $required_field_text - the text in the required text field.
2237     * @param string $placeholder - the field placeholder content.
2238     * @param array  $extra_attrs - Extra attributes used in number field, namely `min` and `max`.
2239     * @param bool   $required_indicator Whether to display the required indicator.
2240     *
2241     * @return string HTML
2242     */
2243    public function render_number_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $extra_attrs = array(), $required_indicator = true ) {
2244        $this->set_invalid_message( 'number', __( 'Please enter a valid number', 'jetpack-forms' ) );
2245        if ( isset( $extra_attrs['min'] ) ) {
2246            // translators: %d is the minimum value.
2247            $this->set_invalid_message( 'min_number', __( 'Please select a value that is no less than %d.', 'jetpack-forms' ) );
2248        }
2249        if ( isset( $extra_attrs['max'] ) ) {
2250            // translators: %d is the maximum value.
2251            $this->set_invalid_message( 'max_number', __( 'Please select a value that is no more than %d.', 'jetpack-forms' ) );
2252        }
2253        $field  = $this->render_label( 'number', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
2254        $field .= $this->render_input_field( 'number', $id, $value, $class, $placeholder, $required, $extra_attrs );
2255        return $field;
2256    }
2257
2258    /**
2259     * Return the HTML for the default field.
2260     *
2261     * @param int    $id - the ID.
2262     * @param string $label - the label.
2263     * @param string $value - the value of the field.
2264     * @param string $class - the field class.
2265     * @param bool   $required - if the field is marked as required.
2266     * @param string $required_field_text - the text in the required text field.
2267     * @param string $placeholder - the field placeholder content.
2268     * @param string $type - the type.
2269     * @param bool   $required_indicator Whether to display the required indicator.
2270     *
2271     * @return string HTML
2272     */
2273    public function render_default_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $type, $required_indicator = true ) {
2274        $field  = $this->render_label( $type, $id, $label, $required, $required_field_text, array(), false, $required_indicator );
2275        $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required );
2276        return $field;
2277    }
2278
2279    /**
2280     * Returns the styles, classes and CSS vars necessary to render fields in the "Outlined" style.
2281     * The "Animated" style variation shares the CSS vars, which require similar calculations for the left offset and label left position.
2282     * At the block level, the styles are extracted and added to the shortcode attributes in
2283     * Contact_Form_Plugin::get_outlined_style_attributes().
2284     * This function extracts those styles and applies them to the field,
2285     * and ensures any global or theme styles are applied.
2286     *
2287     * @param string $form_style (optional) The form style.
2288     *
2289     * @return array {
2290     *     @type string $style_attrs The style attributes.
2291     *     @type string $css_vars The CSS variables.
2292     *     @type string $class_name The class name.
2293     * }
2294     */
2295    private function get_form_variation_style_properties( $form_style = 'outlined' ) {
2296        $css_vars             = '';
2297        $variation_attributes = $this->get_attribute( 'stylevariationattributes' );
2298        $variation_attributes = ! empty( $variation_attributes ) ? json_decode( html_entity_decode( $variation_attributes, ENT_COMPAT ), true ) : array();
2299        $variation_classes    = $this->get_attribute( 'stylevariationclasses' );
2300        $variation_style      = $this->get_attribute( 'stylevariationstyles' );
2301        $block_name           = 'jetpack/input';
2302
2303        if ( $this->maybe_override_type() === 'radio' || $this->maybe_override_type() === 'checkbox-multiple' ) {
2304            $block_name = 'jetpack/options';
2305        }
2306
2307        $global_styles = wp_get_global_styles(
2308            array( 'border' ),
2309            array(
2310                'block_name' => $block_name,
2311                'transforms' => array( 'resolve-variables' ),
2312            )
2313        );
2314
2315        /*
2316         * The `borderwidth` attribute contains the border value that forms used before the migration to global styles.
2317         * Any old forms saved in a post will still use this attribute, so it needs to be factored into the css vars for border
2318         * 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.
2319         * For newer forms that use global styles or the block supports styles, this value will be empty and is ignored.
2320         */
2321        $border_width_attribute = $this->get_attribute( 'borderwidth' );
2322        $legacy_border_size     = ! empty( $border_width_attribute ) || $border_width_attribute === '0' ? $border_width_attribute . 'px' : null;
2323
2324        $border_radius_attribute = $this->get_attribute( 'borderradius' );
2325        $legacy_border_radius    = ! empty( $border_radius_attribute ) || $border_radius_attribute === '0' ? $border_radius_attribute . 'px' : $variation_attributes['border']['radius'] ?? null;
2326
2327        $border_top_size = $legacy_border_size ??
2328            $variation_attributes['border']['width'] ??
2329            $variation_attributes['border']['top']['width'] ??
2330            $global_styles['width'] ??
2331            $global_styles['top']['width'] ?? null;
2332
2333        $border_right_size = $legacy_border_size ??
2334
2335            $variation_attributes['border']['right']['width'] ??
2336            $global_styles['width'] ??
2337            $global_styles['right']['width'] ?? null;
2338
2339        $border_bottom_size = $legacy_border_size ??
2340            $variation_attributes['border']['width'] ??
2341            $variation_attributes['border']['bottom']['width'] ??
2342            $global_styles['width'] ??
2343            $global_styles['bottom']['width'] ?? null;
2344
2345        $border_left_size = $legacy_border_size ??
2346            $variation_attributes['border']['width'] ??
2347            $variation_attributes['border']['left']['width'] ??
2348            $global_styles['width'] ??
2349            $global_styles['left']['width'] ?? null;
2350
2351        $border_radius = $legacy_border_radius ??
2352            $global_styles['radius'] ?? null;
2353
2354        // Border size to accommodate legacy border width attribute.
2355        $css_vars = $legacy_border_size ? '--jetpack--contact-form--border-size: ' . $legacy_border_size . ';' : '';
2356
2357        // Border side sizes to accommodate global styles split values.
2358        $css_vars .= $border_top_size ? '--jetpack--contact-form--border-top-size: ' . $border_top_size . ';' : '';
2359        $css_vars .= $border_right_size ? '--jetpack--contact-form--border-right-size: ' . $border_right_size . ';' : '';
2360        $css_vars .= $border_bottom_size ? '--jetpack--contact-form--border-bottom-size: ' . $border_bottom_size . ';' : '';
2361        $css_vars .= $border_left_size ? '--jetpack--contact-form--border-left-size: ' . $border_left_size . ';' : '';
2362
2363        // Check if border radius is split or a single value.
2364        if ( is_array( $border_radius ) ) {
2365            // If corner radii are set on the top-left or bottom-left of the block, take the maximum of the two.
2366            // We check the left side due to writing direction—this variable is used to offset text.
2367            // TODO: this should factor in RTL languages.
2368            $css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: max(' . ( $border_radius['topLeft'] ?? '0' ) . ',' . ( $border_radius['bottomLeft'] ?? '0' ) . ');' : '';
2369        } elseif ( isset( $border_radius ) ) {
2370            $css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: ' . $border_radius . ';' : '';
2371        }
2372
2373        if ( 'outlined' === $form_style ) {
2374            $css_vars .= '--jetpack--contact-form--notch-width: max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius));';
2375        } elseif ( 'animated' === $form_style ) {
2376            $css_vars .= '--jetpack--contact-form--animated-left-offset: 16px;';
2377        }
2378
2379        return array(
2380            'style'      => $variation_style,
2381            'css_vars'   => $css_vars,
2382            'class_name' => $variation_classes,
2383        );
2384    }
2385
2386    /**
2387     * Return the HTML for the outlined label.
2388     *
2389     * @param int    $id - the ID.
2390     * @param string $label - the label.
2391     * @param bool   $required - if the field is marked as required.
2392     * @param string $required_field_text - the text in the required text field.
2393     * @param bool   $required_indicator Whether to display the required indicator.
2394     *
2395     * @return string HTML
2396     */
2397    public function render_outline_label( $id, $label, $required, $required_field_text, $required_indicator = true ) {
2398        $classes  = 'notched-label__label';
2399        $classes .= $this->is_error() ? ' form-error' : '';
2400        $classes .= $this->label_classes ? ' ' . $this->label_classes : '';
2401
2402        $output_data = $this->get_form_variation_style_properties();
2403
2404        return '
2405            <div class="notched-label">
2406                <div class="notched-label__leading' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
2407                <div class="notched-label__notch' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '">
2408                    <label
2409                        for="' . esc_attr( $id ) . '"
2410                        class=" ' . $classes . '"
2411                        style="' . $this->label_styles . esc_attr( $output_data['css_vars'] ) . '"
2412                    >
2413                    <span class="grunion-label-text">' . esc_html( $label ) . '</span>'
2414                    . ( $required && $required_indicator ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
2415            '</label>
2416                </div>
2417                <div class="notched-label__filler' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
2418                <div class="notched-label__trailing' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div>
2419            </div>';
2420    }
2421
2422    /**
2423     * Return the HTML for the animated label.
2424     *
2425     * @param int    $id - the ID.
2426     * @param string $label - the label.
2427     * @param bool   $required - if the field is marked as required.
2428     * @param string $required_field_text - the text in the required text field.
2429     * @param bool   $required_indicator Whether to display the required indicator.
2430     *
2431     * @return string HTML
2432     */
2433    public function render_animated_label( $id, $label, $required, $required_field_text, $required_indicator = true ) {
2434        $classes  = 'animated-label__label';
2435        $classes .= $this->is_error() ? ' form-error' : '';
2436        $classes .= $this->label_classes ? ' ' . $this->label_classes : '';
2437
2438        return '
2439            <label
2440                for="' . esc_attr( $id ) . '"
2441                class="' . $classes . '"
2442                style="' . $this->label_styles . '"
2443            >
2444                <span class="grunion-label-text">' . wp_kses_post( $label ) . '</span>'
2445                . ( $required && $required_indicator !== 'hidden' ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) .
2446            '</label>';
2447    }
2448
2449    /**
2450     * Return the HTML for the below label.
2451     *
2452     * @param int    $id - the ID.
2453     * @param string $label - the label.
2454     * @param bool   $required - if the field is marked as required.
2455     * @param string $required_field_text - the text in the required text field.
2456     * @param bool   $required_indicator Whether to display the required indicator.
2457     *
2458     * @return string HTML
2459     */
2460    public function render_below_label( $id, $label, $required, $required_field_text, $required_indicator = true ) {
2461        return '
2462            <label
2463                for="' . esc_attr( $id ) . '"
2464                class="below-label__label ' . ( $this->is_error() ? ' form-error' : '' ) . '"
2465            >'
2466            . esc_html( $label )
2467            . ( $required && $required_indicator ? '<span>' . $required_field_text . '</span>' : '' ) .
2468            '</label>';
2469    }
2470
2471    /**
2472     * Return the HTML for the email field.
2473     *
2474     * @param string $type - the type.
2475     * @param int    $id - the ID.
2476     * @param string $label - the label.
2477     * @param string $value - the value of the field.
2478     * @param string $class - the field class.
2479     * @param string $placeholder - the field placeholder content.
2480     * @param bool   $required - if the field is marked as required.
2481     * @param string $required_field_text - the text for a field marked as required.
2482     * @param array  $extra_attrs - extra attributes to be passed to render functions.
2483     * @param bool   $required_indicator Whether to display the required indicator.
2484     *
2485     * @return string HTML
2486     */
2487    public function render_field( $type, $id, $label, $value, $class, $placeholder, $required, $required_field_text, $extra_attrs = array(), $required_indicator = true ) {
2488        if ( ! $this->is_field_renderable( $type ) ) {
2489            return '';
2490        }
2491
2492        if ( $type === 'hidden' ) {
2493            // For hidden fields, we don't need to render the label or any other HTML.
2494            return $this->render_hidden_field( $id, $label, $value );
2495        }
2496
2497        $trimmed_type = trim( esc_attr( $type ) );
2498        $class       .= ' grunion-field';
2499
2500        $form_style = $this->get_form_style();
2501        if ( ! empty( $form_style ) && $form_style !== 'default' ) {
2502            if ( isset( $placeholder ) && '' !== $placeholder ) {
2503                $class .= ' has-placeholder';
2504            }
2505        }
2506
2507        // Field classes.
2508        $field_class = "class='" . $trimmed_type . ' ' . esc_attr( $class ) . "' ";
2509
2510        // Shell wrapper classes. Add -wrap to each class.
2511        $wrap_classes          = empty( $class ) ? '' : implode( '-wrap ', array_filter( explode( ' ', $class ) ) ) . '-wrap';
2512        $field_wrapper_classes = $this->get_attribute( 'fieldwrapperclasses' ) ? $this->get_attribute( 'fieldwrapperclasses' ) . ' ' : '';
2513
2514        if ( empty( $label ) && ! $required ) {
2515            $wrap_classes .= ' no-label';
2516        }
2517
2518        $shell_field_class = "class='" . $field_wrapper_classes . 'grunion-field-' . $trimmed_type . '-wrap ' . esc_attr( $wrap_classes ) . "' ";
2519
2520        /**
2521         * Filter the Contact Form required field text
2522         *
2523         * @module contact-form
2524         *
2525         * @since 3.8.0
2526         *
2527         * @param string $var Required field text. Default is "(required)".
2528         */
2529        $required_field_text = wp_kses_post( apply_filters( 'jetpack_required_field_text', $required_field_text ) );
2530
2531        $block_style       = 'style="' . $this->block_styles . '"';
2532        $has_inset_label   = $this->has_inset_label();
2533        $field             = '';
2534        $field_placeholder = ! empty( $placeholder ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : '';
2535
2536        $context = array(
2537            'fieldId'           => $id,
2538            'fieldType'         => $type,
2539            'fieldLabel'        => $label,
2540            'fieldValue'        => $value,
2541            'fieldPlaceholder'  => $placeholder,
2542            'fieldIsRequired'   => $required,
2543            'fieldErrorMessage' => '',
2544            'fieldExtra'        => $this->get_field_extra( $type, $extra_attrs ),
2545            'formHash'          => $this->form->hash,
2546        );
2547
2548        $interactivity_attrs = ' data-wp-interactive="jetpack/form" ' . wp_interactivity_data_wp_context( $context ) . ' ';
2549
2550        // Fields with an inset label need an extra wrapper to show the error message below the input.
2551        if ( $has_inset_label ) {
2552            $field_width       = $this->get_attribute( 'width' );
2553            $inset_label_class = array( 'contact-form__inset-label-wrap' );
2554
2555            if ( ! empty( $field_width ) ) {
2556                array_push( $inset_label_class, 'grunion-field-width-' . $field_width . '-wrap' );
2557            }
2558
2559            $field              .= "\n<div class='" . implode( ' ', $inset_label_class ) . "{$interactivity_attrs} >\n";
2560            $interactivity_attrs = ''; // Reset interactivity attributes for the field wrapper.
2561        }
2562
2563        $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
2564
2565        switch ( $type ) {
2566            case 'email':
2567                $field .= $this->render_email_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2568                break;
2569            case 'phone':
2570            case 'telephone':
2571                $field .= $this->render_telephone_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2572                break;
2573            case 'url':
2574                $field .= $this->render_url_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2575                break;
2576            case 'textarea':
2577                $field .= $this->render_textarea_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2578                break;
2579            case 'radio':
2580                $field .= $this->render_radio_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2581                break;
2582            case 'checkbox':
2583                $field .= $this->render_checkbox_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2584                break;
2585            case 'checkbox-multiple':
2586                $field .= $this->render_checkbox_multiple_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2587                break;
2588            case 'select':
2589                $field .= $this->render_select_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2590                break;
2591            case 'date':
2592                $field .= $this->render_date_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2593                break;
2594            case 'consent':
2595                $field .= $this->render_consent_field( $id, $field_class );
2596                break;
2597            case 'number':
2598                $field .= $this->render_number_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $extra_attrs, $required_indicator );
2599                break;
2600            case 'slider':
2601                $field .= $this->render_slider_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $extra_attrs, $required_indicator );
2602                break;
2603            case 'file':
2604                $field .= $this->render_file_field( $id, $label, $field_class, $required, $required_field_text, $required_indicator );
2605                break;
2606            case 'rating':
2607                $field .= $this->render_rating_field(
2608                    $id,
2609                    $label,
2610                    $value,
2611                    $field_class,
2612                    $required,
2613                    $required_field_text,
2614                    $required_indicator
2615                );
2616                break;
2617            case 'time':
2618                $field .= $this->render_time_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator );
2619                break;
2620            case 'image-select':
2621                $field .= $this->render_image_select_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator );
2622                break;
2623            default: // text field
2624                $field .= $this->render_default_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $type, $required_indicator );
2625                break;
2626        }
2627
2628        $field .= "\t</div>\n";
2629
2630        if ( $has_inset_label ) {
2631            $field .= $this->get_error_div( $id, $type, true );
2632            // Close the extra wrapper for inset labels.
2633            $field .= "\t</div>\n";
2634        }
2635
2636        return $field;
2637    }
2638
2639    /**
2640     * Returns the extra attributes for the field.
2641     * That are used in field validation.
2642     *
2643     * @param string $type - the field type.
2644     * @param array  $extra_attrs - the extra attributes.
2645     *
2646     * @return string|array The extra attributes.
2647     */
2648    private function get_field_extra( $type, $extra_attrs ) {
2649        if ( 'date' === $type ) {
2650            $date_format = $this->get_attribute( 'dateformat' );
2651            return isset( $date_format ) && ! empty( $date_format ) ? $date_format : 'yy-mm-dd';
2652        }
2653
2654        return $extra_attrs;
2655    }
2656
2657    /**
2658     * Overrides input type (maybe).
2659     *
2660     * @module contact-form
2661     *
2662     * Custom input types, like URL, will rely on browser's implementation to validate
2663     * the value. If the input carries a data-type-override, we allow to override
2664     * the type at render/submit so it can be validated with custom patterns.
2665     * This method will try to match the input's type to a custom data-type-override
2666     * attribute and return it. Defaults to input's type.
2667     *
2668     * @return string The input's type attribute or the overriden type.
2669     */
2670    private function maybe_override_type() {
2671        // Define overridables-to-custom-type, extend as needed.
2672        $overridable_types = array( 'text' => array( 'url' ) );
2673        $type              = $this->get_attribute( 'type' );
2674
2675        if ( ! array_key_exists( $type, $overridable_types ) ) {
2676            return $type;
2677        }
2678
2679        $override_type = $this->get_attribute( 'data-type-override' );
2680
2681        if ( in_array( $override_type, $overridable_types[ $type ], true ) ) {
2682            return $override_type;
2683        }
2684
2685        return $type;
2686    }
2687
2688    /**
2689     * Determines if a form field is valid.
2690     *
2691     * Add checks here to confirm if any given form field
2692     * is configured correctly and thus should be rendered
2693     * on the frontend.
2694     *
2695     * @param string $type - the field type.
2696     *
2697     * @return bool
2698     */
2699    public function is_field_renderable( $type ) {
2700        // Check that radio, select, multiple choice, and image select
2701        // fields have at least one valid option.
2702        if ( $type === 'radio' || $type === 'checkbox-multiple' || $type === 'select' ) {
2703            $options           = (array) $this->get_attribute( 'options' );
2704            $non_empty_options = array_filter(
2705                $options,
2706                function ( $option ) {
2707                    return $option !== '';
2708                }
2709            );
2710            return count( $non_empty_options ) > 0;
2711        }
2712
2713        if ( $type === 'image-select' ) {
2714            $options_data         = (array) $this->get_attribute( 'optionsdata' );
2715            $trimmed_options_data = $this->trim_image_select_options( $options_data );
2716
2717            return ! empty( $trimmed_options_data );
2718        }
2719
2720        return true;
2721    }
2722
2723    /**
2724     * Gets the form style based on its CSS class.
2725     *
2726     * @return string The form style type.
2727     */
2728    private function get_form_style() {
2729        $class_name = $this->form->get_attribute( 'className' );
2730        preg_match( '/is-style-([^\s]+)/i', $class_name, $matches );
2731        return count( $matches ) >= 2 ? $matches[1] : null;
2732    }
2733
2734    /**
2735     * Checks if the field has an inset label, i.e., a label displayed inside the field instead of above.
2736     *
2737     * @return boolean
2738     */
2739    private function has_inset_label() {
2740        $form_style = $this->get_form_style();
2741
2742        return in_array( $form_style, array( 'outlined', 'animated' ), true );
2743    }
2744
2745    /**
2746     * Return the HTML for the rating (stars/hearts/etc.) field.
2747     *
2748     * This field is purely decorative (spans acting as buttons) and stores the
2749     * selected rating in a hidden input so it is handled by existing form
2750     * validation/submission logic.
2751     *
2752     * @since 0.46.0
2753     *
2754     * @param string $id                 Field ID.
2755     * @param string $label              Field label.
2756     * @param string $value              Current value.
2757     * @param string $class              Additional CSS classes.
2758     * @param bool   $required           Whether field is required.
2759     * @param string $required_field_text Required label text.
2760     * @param bool   $required_indicator Whether to display the required indicator.
2761     * @return string HTML markup.
2762     */
2763    private function render_rating_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) {
2764        // Enqueue stylesheet for rating field.
2765        wp_enqueue_style( 'jetpack-form-field-rating-style', plugins_url( '../../dist/blocks/field-rating/style.css', __FILE__ ), array(), Constants::get_constant( 'JETPACK__VERSION' ) );
2766
2767        // Read block attributes needed for rendering.
2768        $max_attr   = $this->get_attribute( 'max' );
2769        $max_rating = is_numeric( $max_attr ) && (int) $max_attr > 0 ? (int) $max_attr : 5;
2770
2771        $initial_rating = (int) $value ? (int) $value : 0;
2772
2773        $label_html = $this->render_legend_as_label( 'rating', $id, $label, $required, $required_field_text, array(), $required_indicator );
2774
2775        /*
2776         * Determine which icon SVG to use based on the 'iconstyle' attribute.
2777         * Note: attribute name is lowercase due to WordPress shortcode processing
2778         */
2779        $icon_style       = $this->get_attribute( 'iconstyle' );
2780        $has_hearts_style = ( 'hearts' === $icon_style );
2781
2782        // SVG icon definitions - keep in sync with JavaScript icons.js
2783        $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>';
2784        $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>';
2785
2786        $icon_svg = $has_hearts_style ? $heart_svg : $star_svg;
2787
2788        $options = '';
2789        for ( $i = 1; $i <= $max_rating; $i++ ) {
2790            $radio_id = $id . '-' . $i;
2791            $options .= sprintf(
2792                '<div class="jetpack-field-rating__option">
2793                    <input
2794                        id="%1$s"
2795                        type="radio"
2796                        name="%2$s"
2797                        value="%3$s/%4$s"
2798                        data-wp-on--change="actions.onFieldChange"
2799                        class="jetpack-field-rating__input visually-hidden"
2800                        %5$s
2801                        %6$s />
2802                    <label for="%1$s" class="jetpack-field-rating__label">
2803                        %7$s
2804                    </label>
2805                </div>',
2806                esc_attr( $radio_id ),         // %1$s: id and label for
2807                esc_attr( $id ),               // %2$s: name
2808                esc_attr( $i ),                // %3$s: value (current rating)
2809                esc_attr( $max_rating ),       // %4$s: value (max rating)
2810                checked( $i, $initial_rating, false ), // %5$s: checked attribute
2811                $required ? 'required' : '',   // %6$s: required attribute
2812                $icon_svg                      // %7$s: icon SVG
2813            );
2814        }
2815
2816        $style_attr = '';
2817
2818        $css_styles = array_filter( array_map( 'trim', explode( ';', $this->field_styles ) ) );
2819
2820        $css_key_value_pairs = array_reduce(
2821            $css_styles,
2822            function ( $pairs, $style ) {
2823                list( $key, $value )   = explode( ':', $style );
2824                $pairs[ trim( $key ) ] = trim( $value );
2825                return $pairs;
2826            },
2827            array()
2828        );
2829
2830        // The rating input overwrites the text color, so we are using a custom logic to set the star color as a CSS variable.
2831        $has_star_color = isset( $css_key_value_pairs['color'] );
2832
2833        if ( $has_star_color ) {
2834            $color_value = $css_key_value_pairs['color'];
2835            $style_attr  = 'style="--jetpack--contact-form--rating-star-color: ' . esc_attr( $color_value ) . ';';
2836            unset( $css_key_value_pairs['color'] );
2837        } else {
2838            // Theme colors are set in the field_classes attribute
2839            $preset_colors = array(
2840                'has-base-color'     => '--wp--preset--color--base',
2841                'has-contrast-color' => '--wp--preset--color--contrast',
2842            );
2843
2844            if ( preg_match( '/has-accent-(\d+)-color/', $this->field_classes, $matches ) ) {
2845                $accent_number = $matches[1];
2846                $preset_colors[ 'has-accent-' . $accent_number . '-color' ] = '--wp--preset--color--accent-' . $accent_number;
2847            }
2848
2849            foreach ( $preset_colors as $class => $css_var ) {
2850                if ( strpos( $this->field_classes, $class ) !== false ) {
2851                    $style_attr = 'style="--jetpack--contact-form--rating-star-color: var(' . esc_attr( $css_var ) . ');';
2852
2853                    break;
2854                }
2855            }
2856        }
2857
2858        $remaining_styles = array_map(
2859            function ( $key, $value ) {
2860                return $key . ': ' . $value;
2861            },
2862            array_keys( $css_key_value_pairs ),
2863            array_values( $css_key_value_pairs )
2864        );
2865
2866        $style_attr .= ' ' . implode( ';', $remaining_styles ) . '"';
2867
2868        return sprintf(
2869            '<fieldset id="%4$s-label" class="jetpack-field-multiple__fieldset jetpack-field-rating" %1$s>
2870                %5$s
2871                <div class="jetpack-field-rating__options %3$s">%2$s</div>
2872            </fieldset>',
2873            $style_attr,
2874            $options,
2875            $this->field_classes,
2876            esc_attr( $id ),
2877            $label_html
2878        ) . $this->get_error_div( $id, 'rating' );
2879    }
2880
2881    /**
2882     * Return the HTML for the slider field.
2883     *
2884     * @since 5.1.0
2885     *
2886     * @param int    $id The field ID.
2887     * @param string $label The field label.
2888     * @param string $value The field value.
2889     * @param string $class The field class.
2890     * @param bool   $required Whether the field is required.
2891     * @param string $required_field_text The required field text.
2892     * @param string $placeholder The field placeholder.
2893     * @param array  $extra_attrs Extra attributes (e.g., min, max).
2894     * @param bool   $required_indicator Whether to display the required indicator.
2895     *
2896     * @return string HTML for the slider field.
2897     */
2898    public function render_slider_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $extra_attrs = array(), $required_indicator = true ) {
2899        $this->enqueue_slider_field_assets();
2900        $this->set_invalid_message( 'slider', __( 'Please select a valid value', 'jetpack-forms' ) );
2901        if ( isset( $extra_attrs['min'] ) ) {
2902            // translators: %d is the minimum value.
2903            $this->set_invalid_message( 'min_slider', __( 'Please select a value that is no less than %d.', 'jetpack-forms' ) );
2904        }
2905        if ( isset( $extra_attrs['max'] ) ) {
2906            // translators: %d is the maximum value.
2907            $this->set_invalid_message( 'max_slider', __( 'Please select a value that is no more than %d.', 'jetpack-forms' ) );
2908        }
2909        $min            = isset( $extra_attrs['min'] ) ? $extra_attrs['min'] : 0;
2910        $max            = isset( $extra_attrs['max'] ) ? $extra_attrs['max'] : 100;
2911        $starting_value = isset( $extra_attrs['default'] ) ? $extra_attrs['default'] : 0;
2912        $step           = isset( $extra_attrs['step'] ) ? $extra_attrs['step'] : 1;
2913        $current_value  = ( $value !== '' && $value !== null ) ? $value : $starting_value;
2914        $min_text_label = isset( $extra_attrs['minLabel'] ) ? $extra_attrs['minLabel'] : '';
2915        $max_text_label = isset( $extra_attrs['maxLabel'] ) ? $extra_attrs['maxLabel'] : '';
2916
2917        $field = $this->render_label( 'slider', $id, $label, $required, $required_field_text, array(), false, $required_indicator );
2918
2919        ob_start();
2920        ?>
2921        <div class="jetpack-field-slider__input-row <?php echo esc_attr( $this->field_classes ); ?>"
2922            data-wp-context='
2923            <?php
2924            echo esc_attr(
2925                wp_json_encode(
2926                    array(
2927                        'min'     => $min,
2928                        'max'     => $max,
2929                        'default' => $starting_value,
2930                        'step'    => $step,
2931                    ),
2932                    JSON_HEX_AMP | JSON_UNESCAPED_SLASHES
2933                )
2934            );
2935            ?>
2936            '>
2937            <span class="jetpack-field-slider__min-label"><?php echo esc_html( $min ); ?></span>
2938            <div class="jetpack-field-slider__input-container">
2939                <input
2940                    type="range"
2941                    name="<?php echo esc_attr( $id ); ?>"
2942                    id="<?php echo esc_attr( $id ); ?>"
2943                    value="<?php echo esc_attr( $current_value ); ?>"
2944                    min="<?php echo esc_attr( $min ); ?>"
2945                    max="<?php echo esc_attr( $max ); ?>"
2946                    step="<?php echo esc_attr( $step ); ?>"
2947                    class="<?php echo esc_attr( trim( $class . ' jetpack-field-slider__range' ) ); ?>"
2948                    placeholder="<?php echo esc_attr( $placeholder ); ?>"
2949                    <?php
2950                    if ( $required ) :
2951                        ?>
2952                    required<?php endif; ?>
2953                    data-wp-bind--value="state.getSliderValue"
2954                    data-wp-on--input="actions.onSliderChange"
2955                    data-wp-bind--aria-invalid="state.fieldHasErrors"
2956                />
2957                <div
2958                    class="jetpack-field-slider__value-indicator"
2959                    data-wp-text="state.getSliderValue"
2960                    data-wp-style--left="state.getSliderPosition"
2961                ><?php echo esc_html( $current_value ); ?></div>
2962            </div>
2963            <span class="jetpack-field-slider__max-label"><?php echo esc_html( $max ); ?></span>
2964        </div>
2965        <?php if ( '' !== $min_text_label || '' !== $max_text_label ) : ?>
2966            <div class="jetpack-field-slider__text-labels <?php echo esc_attr( $this->field_classes ); ?>" aria-hidden="true">
2967                <span class="jetpack-field-slider__min-text-label"><?php echo esc_html( $min_text_label ); ?></span>
2968                <span class="jetpack-field-slider__max-text-label"><?php echo esc_html( $max_text_label ); ?></span>
2969            </div>
2970        <?php endif; ?>
2971        <?php
2972        $field .= ob_get_clean();
2973        return $field . $this->get_error_div( $id, 'slider' );
2974    }
2975
2976    /**
2977     * Enqueues scripts and styles needed for the slider field.
2978     *
2979     * @since 5.1.0
2980     *
2981     * @return void
2982     */
2983    private function enqueue_slider_field_assets() {
2984        $version = defined( 'JETPACK__VERSION' ) ? \JETPACK__VERSION : '0.1';
2985
2986        \wp_enqueue_style(
2987            'jetpack-form-slider-field',
2988            plugins_url( '../../dist/blocks/input-range/style.css', __FILE__ ),
2989            array(),
2990            $version
2991        );
2992
2993        \wp_enqueue_script_module(
2994            'jetpack-form-slider-field',
2995            plugins_url( '../../dist/modules/slider-field/view.js', __FILE__ ),
2996            array( '@wordpress/interactivity' ),
2997            $version
2998        );
2999    }
3000
3001    /**
3002     * Gets an array of translatable country names indexed by their two-letter country codes.
3003     *
3004     * @since 6.2.1
3005     *
3006     * @return array Array of country names with two-letter country codes as keys.
3007     */
3008    public function get_translatable_countries() {
3009        return array(
3010            'AF' => __( 'Afghanistan', 'jetpack-forms' ),
3011            'AL' => __( 'Albania', 'jetpack-forms' ),
3012            'DZ' => __( 'Algeria', 'jetpack-forms' ),
3013            'AS' => __( 'American Samoa', 'jetpack-forms' ),
3014            'AD' => __( 'Andorra', 'jetpack-forms' ),
3015            'AO' => __( 'Angola', 'jetpack-forms' ),
3016            'AI' => __( 'Anguilla', 'jetpack-forms' ),
3017            'AG' => __( 'Antigua and Barbuda', 'jetpack-forms' ),
3018            'AR' => __( 'Argentina', 'jetpack-forms' ),
3019            'AM' => __( 'Armenia', 'jetpack-forms' ),
3020            'AW' => __( 'Aruba', 'jetpack-forms' ),
3021            'AU' => __( 'Australia', 'jetpack-forms' ),
3022            'AT' => __( 'Austria', 'jetpack-forms' ),
3023            'AZ' => __( 'Azerbaijan', 'jetpack-forms' ),
3024            'BS' => __( 'Bahamas', 'jetpack-forms' ),
3025            'BH' => __( 'Bahrain', 'jetpack-forms' ),
3026            'BD' => __( 'Bangladesh', 'jetpack-forms' ),
3027            'BB' => __( 'Barbados', 'jetpack-forms' ),
3028            'BY' => __( 'Belarus', 'jetpack-forms' ),
3029            'BE' => __( 'Belgium', 'jetpack-forms' ),
3030            'BZ' => __( 'Belize', 'jetpack-forms' ),
3031            'BJ' => __( 'Benin', 'jetpack-forms' ),
3032            'BM' => __( 'Bermuda', 'jetpack-forms' ),
3033            'BT' => __( 'Bhutan', 'jetpack-forms' ),
3034            'BO' => __( 'Bolivia', 'jetpack-forms' ),
3035            'BA' => __( 'Bosnia and Herzegovina', 'jetpack-forms' ),
3036            'BW' => __( 'Botswana', 'jetpack-forms' ),
3037            'BR' => __( 'Brazil', 'jetpack-forms' ),
3038            'IO' => __( 'British Indian Ocean Territory', 'jetpack-forms' ),
3039            'VG' => __( 'British Virgin Islands', 'jetpack-forms' ),
3040            'BN' => __( 'Brunei', 'jetpack-forms' ),
3041            'BG' => __( 'Bulgaria', 'jetpack-forms' ),
3042            'BF' => __( 'Burkina Faso', 'jetpack-forms' ),
3043            'BI' => __( 'Burundi', 'jetpack-forms' ),
3044            'KH' => __( 'Cambodia', 'jetpack-forms' ),
3045            'CM' => __( 'Cameroon', 'jetpack-forms' ),
3046            'CA' => __( 'Canada', 'jetpack-forms' ),
3047            'CV' => __( 'Cape Verde', 'jetpack-forms' ),
3048            'KY' => __( 'Cayman Islands', 'jetpack-forms' ),
3049            'CF' => __( 'Central African Republic', 'jetpack-forms' ),
3050            'TD' => __( 'Chad', 'jetpack-forms' ),
3051            'CL' => __( 'Chile', 'jetpack-forms' ),
3052            'CN' => __( 'China', 'jetpack-forms' ),
3053            'CX' => __( 'Christmas Island', 'jetpack-forms' ),
3054            'CC' => __( 'Cocos (Keeling) Islands', 'jetpack-forms' ),
3055            'CO' => __( 'Colombia', 'jetpack-forms' ),
3056            'KM' => __( 'Comoros', 'jetpack-forms' ),
3057            'CG' => __( 'Congo - Brazzaville', 'jetpack-forms' ),
3058            'CD' => __( 'Congo - Kinshasa', 'jetpack-forms' ),
3059            'CK' => __( 'Cook Islands', 'jetpack-forms' ),
3060            'CR' => __( 'Costa Rica', 'jetpack-forms' ),
3061            'HR' => __( 'Croatia', 'jetpack-forms' ),
3062            'CU' => __( 'Cuba', 'jetpack-forms' ),
3063            'CY' => __( 'Cyprus', 'jetpack-forms' ),
3064            'CZ' => __( 'Czech Republic', 'jetpack-forms' ),
3065            'CI' => __( "Côte d'Ivoire", 'jetpack-forms' ),
3066            'DK' => __( 'Denmark', 'jetpack-forms' ),
3067            'DJ' => __( 'Djibouti', 'jetpack-forms' ),
3068            'DM' => __( 'Dominica', 'jetpack-forms' ),
3069            'DO' => __( 'Dominican Republic', 'jetpack-forms' ),
3070            'EC' => __( 'Ecuador', 'jetpack-forms' ),
3071            'EG' => __( 'Egypt', 'jetpack-forms' ),
3072            'SV' => __( 'El Salvador', 'jetpack-forms' ),
3073            'GQ' => __( 'Equatorial Guinea', 'jetpack-forms' ),
3074            'ER' => __( 'Eritrea', 'jetpack-forms' ),
3075            'EE' => __( 'Estonia', 'jetpack-forms' ),
3076            'SZ' => __( 'Eswatini', 'jetpack-forms' ),
3077            'ET' => __( 'Ethiopia', 'jetpack-forms' ),
3078            'FK' => __( 'Falkland Islands', 'jetpack-forms' ),
3079            'FO' => __( 'Faroe Islands', 'jetpack-forms' ),
3080            'FJ' => __( 'Fiji', 'jetpack-forms' ),
3081            'FI' => __( 'Finland', 'jetpack-forms' ),
3082            'FR' => __( 'France', 'jetpack-forms' ),
3083            'GF' => __( 'French Guiana', 'jetpack-forms' ),
3084            'PF' => __( 'French Polynesia', 'jetpack-forms' ),
3085            'GA' => __( 'Gabon', 'jetpack-forms' ),
3086            'GM' => __( 'Gambia', 'jetpack-forms' ),
3087            'GE' => __( 'Georgia', 'jetpack-forms' ),
3088            'DE' => __( 'Germany', 'jetpack-forms' ),
3089            'GH' => __( 'Ghana', 'jetpack-forms' ),
3090            'GI' => __( 'Gibraltar', 'jetpack-forms' ),
3091            'GR' => __( 'Greece', 'jetpack-forms' ),
3092            'GL' => __( 'Greenland', 'jetpack-forms' ),
3093            'GD' => __( 'Grenada', 'jetpack-forms' ),
3094            'GP' => __( 'Guadeloupe', 'jetpack-forms' ),
3095            'GU' => __( 'Guam', 'jetpack-forms' ),
3096            'GT' => __( 'Guatemala', 'jetpack-forms' ),
3097            'GG' => __( 'Guernsey', 'jetpack-forms' ),
3098            'GN' => __( 'Guinea', 'jetpack-forms' ),
3099            'GW' => __( 'Guinea-Bissau', 'jetpack-forms' ),
3100            'GY' => __( 'Guyana', 'jetpack-forms' ),
3101            'HT' => __( 'Haiti', 'jetpack-forms' ),
3102            'HN' => __( 'Honduras', 'jetpack-forms' ),
3103            'HK' => __( 'Hong Kong', 'jetpack-forms' ),
3104            'HU' => __( 'Hungary', 'jetpack-forms' ),
3105            'IS' => __( 'Iceland', 'jetpack-forms' ),
3106            'IN' => __( 'India', 'jetpack-forms' ),
3107            'ID' => __( 'Indonesia', 'jetpack-forms' ),
3108            'IR' => __( 'Iran', 'jetpack-forms' ),
3109            'IQ' => __( 'Iraq', 'jetpack-forms' ),
3110            'IE' => __( 'Ireland', 'jetpack-forms' ),
3111            'IM' => __( 'Isle of Man', 'jetpack-forms' ),
3112            'IL' => __( 'Israel', 'jetpack-forms' ),
3113            'IT' => __( 'Italy', 'jetpack-forms' ),
3114            'JM' => __( 'Jamaica', 'jetpack-forms' ),
3115            'JP' => __( 'Japan', 'jetpack-forms' ),
3116            'JE' => __( 'Jersey', 'jetpack-forms' ),
3117            'JO' => __( 'Jordan', 'jetpack-forms' ),
3118            'KZ' => __( 'Kazakhstan', 'jetpack-forms' ),
3119            'KE' => __( 'Kenya', 'jetpack-forms' ),
3120            'KI' => __( 'Kiribati', 'jetpack-forms' ),
3121            'XK' => __( 'Kosovo', 'jetpack-forms' ),
3122            'KW' => __( 'Kuwait', 'jetpack-forms' ),
3123            'KG' => __( 'Kyrgyzstan', 'jetpack-forms' ),
3124            'LA' => __( 'Laos', 'jetpack-forms' ),
3125            'LV' => __( 'Latvia', 'jetpack-forms' ),
3126            'LB' => __( 'Lebanon', 'jetpack-forms' ),
3127            'LS' => __( 'Lesotho', 'jetpack-forms' ),
3128            'LR' => __( 'Liberia', 'jetpack-forms' ),
3129            'LY' => __( 'Libya', 'jetpack-forms' ),
3130            'LI' => __( 'Liechtenstein', 'jetpack-forms' ),
3131            'LT' => __( 'Lithuania', 'jetpack-forms' ),
3132            'LU' => __( 'Luxembourg', 'jetpack-forms' ),
3133            'MO' => __( 'Macao', 'jetpack-forms' ),
3134            'MG' => __( 'Madagascar', 'jetpack-forms' ),
3135            'MW' => __( 'Malawi', 'jetpack-forms' ),
3136            'MY' => __( 'Malaysia', 'jetpack-forms' ),
3137            'MV' => __( 'Maldives', 'jetpack-forms' ),
3138            'ML' => __( 'Mali', 'jetpack-forms' ),
3139            'MT' => __( 'Malta', 'jetpack-forms' ),
3140            'MH' => __( 'Marshall Islands', 'jetpack-forms' ),
3141            'MQ' => __( 'Martinique', 'jetpack-forms' ),
3142            'MR' => __( 'Mauritania', 'jetpack-forms' ),
3143            'MU' => __( 'Mauritius', 'jetpack-forms' ),
3144            'YT' => __( 'Mayotte', 'jetpack-forms' ),
3145            'MX' => __( 'Mexico', 'jetpack-forms' ),
3146            'FM' => __( 'Micronesia', 'jetpack-forms' ),
3147            'MD' => __( 'Moldova', 'jetpack-forms' ),
3148            'MC' => __( 'Monaco', 'jetpack-forms' ),
3149            'MN' => __( 'Mongolia', 'jetpack-forms' ),
3150            'ME' => __( 'Montenegro', 'jetpack-forms' ),
3151            'MS' => __( 'Montserrat', 'jetpack-forms' ),
3152            'MA' => __( 'Morocco', 'jetpack-forms' ),
3153            'MZ' => __( 'Mozambique', 'jetpack-forms' ),
3154            'MM' => __( 'Myanmar', 'jetpack-forms' ),
3155            'NA' => __( 'Namibia', 'jetpack-forms' ),
3156            'NR' => __( 'Nauru', 'jetpack-forms' ),
3157            'NP' => __( 'Nepal', 'jetpack-forms' ),
3158            'NL' => __( 'Netherlands', 'jetpack-forms' ),
3159            'NC' => __( 'New Caledonia', 'jetpack-forms' ),
3160            'NZ' => __( 'New Zealand', 'jetpack-forms' ),
3161            'NI' => __( 'Nicaragua', 'jetpack-forms' ),
3162            'NE' => __( 'Niger', 'jetpack-forms' ),
3163            'NG' => __( 'Nigeria', 'jetpack-forms' ),
3164            'NU' => __( 'Niue', 'jetpack-forms' ),
3165            'NF' => __( 'Norfolk Island', 'jetpack-forms' ),
3166            'KP' => __( 'North Korea', 'jetpack-forms' ),
3167            'MK' => __( 'North Macedonia', 'jetpack-forms' ),
3168            'MP' => __( 'Northern Mariana Islands', 'jetpack-forms' ),
3169            'NO' => __( 'Norway', 'jetpack-forms' ),
3170            'OM' => __( 'Oman', 'jetpack-forms' ),
3171            'PK' => __( 'Pakistan', 'jetpack-forms' ),
3172            'PW' => __( 'Palau', 'jetpack-forms' ),
3173            'PS' => __( 'Palestine', 'jetpack-forms' ),
3174            'PA' => __( 'Panama', 'jetpack-forms' ),
3175            'PG' => __( 'Papua New Guinea', 'jetpack-forms' ),
3176            'PY' => __( 'Paraguay', 'jetpack-forms' ),
3177            'PE' => __( 'Peru', 'jetpack-forms' ),
3178            'PH' => __( 'Philippines', 'jetpack-forms' ),
3179            'PN' => __( 'Pitcairn Islands', 'jetpack-forms' ),
3180            'PL' => __( 'Poland', 'jetpack-forms' ),
3181            'PT' => __( 'Portugal', 'jetpack-forms' ),
3182            'PR' => __( 'Puerto Rico', 'jetpack-forms' ),
3183            'QA' => __( 'Qatar', 'jetpack-forms' ),
3184            'RO' => __( 'Romania', 'jetpack-forms' ),
3185            'RU' => __( 'Russia', 'jetpack-forms' ),
3186            'RW' => __( 'Rwanda', 'jetpack-forms' ),
3187            'RE' => __( 'Réunion', 'jetpack-forms' ),
3188            'BL' => __( 'Saint Barthélemy', 'jetpack-forms' ),
3189            'SH' => __( 'Saint Helena', 'jetpack-forms' ),
3190            'KN' => __( 'Saint Kitts and Nevis', 'jetpack-forms' ),
3191            'LC' => __( 'Saint Lucia', 'jetpack-forms' ),
3192            'MF' => __( 'Saint Martin', 'jetpack-forms' ),
3193            'PM' => __( 'Saint Pierre and Miquelon', 'jetpack-forms' ),
3194            'VC' => __( 'Saint Vincent and the Grenadines', 'jetpack-forms' ),
3195            'WS' => __( 'Samoa', 'jetpack-forms' ),
3196            'SM' => __( 'San Marino', 'jetpack-forms' ),
3197            'SA' => __( 'Saudi Arabia', 'jetpack-forms' ),
3198            'SN' => __( 'Senegal', 'jetpack-forms' ),
3199            'RS' => __( 'Serbia', 'jetpack-forms' ),
3200            'SC' => __( 'Seychelles', 'jetpack-forms' ),
3201            'SL' => __( 'Sierra Leone', 'jetpack-forms' ),
3202            'SG' => __( 'Singapore', 'jetpack-forms' ),
3203            'SK' => __( 'Slovakia', 'jetpack-forms' ),
3204            'SI' => __( 'Slovenia', 'jetpack-forms' ),
3205            'SB' => __( 'Solomon Islands', 'jetpack-forms' ),
3206            'SO' => __( 'Somalia', 'jetpack-forms' ),
3207            'ZA' => __( 'South Africa', 'jetpack-forms' ),
3208            'GS' => __( 'South Georgia and the South Sandwich Islands', 'jetpack-forms' ),
3209            'KR' => __( 'South Korea', 'jetpack-forms' ),
3210            'ES' => __( 'Spain', 'jetpack-forms' ),
3211            'LK' => __( 'Sri Lanka', 'jetpack-forms' ),
3212            'SD' => __( 'Sudan', 'jetpack-forms' ),
3213            'SR' => __( 'Suriname', 'jetpack-forms' ),
3214            'SJ' => __( 'Svalbard and Jan Mayen', 'jetpack-forms' ),
3215            'SE' => __( 'Sweden', 'jetpack-forms' ),
3216            'CH' => __( 'Switzerland', 'jetpack-forms' ),
3217            'SY' => __( 'Syria', 'jetpack-forms' ),
3218            'ST' => __( 'São Tomé and Príncipe', 'jetpack-forms' ),
3219            'TW' => __( 'Taiwan', 'jetpack-forms' ),
3220            'TJ' => __( 'Tajikistan', 'jetpack-forms' ),
3221            'TZ' => __( 'Tanzania', 'jetpack-forms' ),
3222            'TH' => __( 'Thailand', 'jetpack-forms' ),
3223            'TL' => __( 'Timor-Leste', 'jetpack-forms' ),
3224            'TG' => __( 'Togo', 'jetpack-forms' ),
3225            'TK' => __( 'Tokelau', 'jetpack-forms' ),
3226            'TO' => __( 'Tonga', 'jetpack-forms' ),
3227            'TT' => __( 'Trinidad and Tobago', 'jetpack-forms' ),
3228            'TN' => __( 'Tunisia', 'jetpack-forms' ),
3229            'TR' => __( 'Turkey', 'jetpack-forms' ),
3230            'TM' => __( 'Turkmenistan', 'jetpack-forms' ),
3231            'TC' => __( 'Turks and Caicos Islands', 'jetpack-forms' ),
3232            'TV' => __( 'Tuvalu', 'jetpack-forms' ),
3233            'VI' => __( 'U.S. Virgin Islands', 'jetpack-forms' ),
3234            'UG' => __( 'Uganda', 'jetpack-forms' ),
3235            'UA' => __( 'Ukraine', 'jetpack-forms' ),
3236            'AE' => __( 'United Arab Emirates', 'jetpack-forms' ),
3237            'GB' => __( 'United Kingdom', 'jetpack-forms' ),
3238            'US' => __( 'United States', 'jetpack-forms' ),
3239            'UY' => __( 'Uruguay', 'jetpack-forms' ),
3240            'UZ' => __( 'Uzbekistan', 'jetpack-forms' ),
3241            'VU' => __( 'Vanuatu', 'jetpack-forms' ),
3242            'VA' => __( 'Vatican City', 'jetpack-forms' ),
3243            'VE' => __( 'Venezuela', 'jetpack-forms' ),
3244            'VN' => __( 'Vietnam', 'jetpack-forms' ),
3245            'WF' => __( 'Wallis and Futuna', 'jetpack-forms' ),
3246            'YE' => __( 'Yemen', 'jetpack-forms' ),
3247            'ZM' => __( 'Zambia', 'jetpack-forms' ),
3248            'ZW' => __( 'Zimbabwe', 'jetpack-forms' ),
3249        );
3250    }
3251
3252    /**
3253     * Enqueues scripts and styles needed for the slider field.
3254     *
3255     * @since 6.2.1
3256     *
3257     * @return void
3258     */
3259    private function enqueue_phone_field_assets() {
3260        $version = defined( 'JETPACK__VERSION' ) ? \JETPACK__VERSION : '0.1';
3261
3262        // extra cache busting strategy for view.js, seems they are left out of cache clearing on deploys
3263        $asset_file = plugin_dir_path( __FILE__ ) . '../../dist/modules/field-phone/view.asset.php';
3264        $asset      = file_exists( $asset_file ) ? require $asset_file : null;
3265        $version   .= $asset['version'] ?? '';
3266
3267        // combobox styles
3268        \wp_enqueue_style(
3269            'jetpack-form-combobox',
3270            plugins_url( '../../dist/contact-form/css/combobox.css', __FILE__ ),
3271            array(),
3272            $version
3273        );
3274
3275        \wp_enqueue_style(
3276            'jetpack-form-phone-field',
3277            plugins_url( '../../dist/contact-form/css/phone-field.css', __FILE__ ),
3278            array(),
3279            $version
3280        );
3281
3282        \wp_enqueue_script_module(
3283            'jetpack-form-phone-field',
3284            plugins_url( '../../dist/modules/field-phone/view.js', __FILE__ ),
3285            array( '@wordpress/interactivity' ),
3286            $version
3287        );
3288    }
3289
3290    /**
3291     * Trims the image select options from the end of the array if they are empty.
3292     *
3293     * @param array $options The options to trim.
3294     *
3295     * @return array The trimmed options array.
3296     */
3297    private function trim_image_select_options( $options ) {
3298        if ( empty( $options ) ) {
3299            return $options;
3300        }
3301
3302        // Work backwards through the array to find the last valid option
3303        $last_valid_index = -1;
3304
3305        for ( $i = count( $options ) - 1; $i >= 0; $i-- ) {
3306            $option = $options[ $i ];
3307
3308            // Check if option has a label
3309            $has_label = ! empty( $option['label'] );
3310
3311            // Check if option has an image with src
3312            $has_image = false;
3313
3314            if ( isset( $option['image']['innerHTML'] ) ) {
3315                // Extract src from innerHTML using regex
3316                preg_match( '/src="([^"]*)"/', $option['image']['innerHTML'], $matches );
3317                $has_image = ! empty( $matches[1] );
3318            }
3319
3320            // If this option has either a label or an image, it's valid
3321            if ( $has_label || $has_image ) {
3322                $last_valid_index = $i;
3323                break;
3324            }
3325        }
3326
3327        return array_slice( $options, 0, $last_valid_index + 1 );
3328    }
3329}