Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
77.82% |
1572 / 2020 |
|
60.56% |
43 / 71 |
CRAP | |
0.00% |
0 / 1 |
| Contact_Form_Field | |
77.90% |
1572 / 2018 |
|
60.56% |
43 / 71 |
3488.17 | |
0.00% |
0 / 1 |
| __construct | |
94.17% |
113 / 120 |
|
0.00% |
0 / 1 |
26.13 | |||
| add_error | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| is_error | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| has_value | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
| has_conditional_logic | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| validate | |
55.17% |
80 / 145 |
|
0.00% |
0 / 1 |
349.68 | |||
| sanitize_text_field | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_option_value | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| render | |
69.47% |
66 / 95 |
|
0.00% |
0 / 1 |
99.07 | |||
| get_computed_field_value | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
21 | |||
| get_conditional_logic_value | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| get_filtered_hidden_field_value | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| is_submitted_hidden_field_value | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| render_label | |
94.12% |
32 / 34 |
|
0.00% |
0 / 1 |
19.07 | |||
| is_label_hidden_by_block_visibility | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| get_block_visibility_aria_label | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| get_hidden_label_aria_label_attr | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| get_file_dropzone_aria_label | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
4 | |||
| render_legend_as_label | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
10.02 | |||
| render_input_field | |
85.71% |
18 / 21 |
|
0.00% |
0 / 1 |
8.19 | |||
| get_error_div | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| get_help_text | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
| get_format_hint | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| get_described_by | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| get_description_parts | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
3 | |||
| get_field_descriptions | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
| set_invalid_message | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
| render_email_field | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| render_telephone_field | |
95.65% |
88 / 92 |
|
0.00% |
0 / 1 |
9 | |||
| render_url_field | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| render_textarea_field | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
3.00 | |||
| render_radio_field | |
77.53% |
69 / 89 |
|
0.00% |
0 / 1 |
38.54 | |||
| render_other_input_field | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
| render_checkbox_field | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
9 | |||
| render_consent_field | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
| render_file_field | |
0.00% |
0 / 66 |
|
0.00% |
0 / 1 |
12 | |||
| sanitize_file_field_content | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
| get_file_field_allowed_html | |
100.00% |
49 / 49 |
|
100.00% |
1 / 1 |
7 | |||
| render_hidden_field | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
3 | |||
| get_file_field_max_files | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| get_file_field_max_files_limit | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| get_file_field_max_upload_size | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| get_file_field_accepted_mime_types | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
1 | |||
| enqueue_file_field_assets | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
| get_unauth_endpoint_url | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| render_checkbox_multiple_field | |
81.82% |
63 / 77 |
|
0.00% |
0 / 1 |
28.76 | |||
| render_select_field | |
90.91% |
20 / 22 |
|
0.00% |
0 / 1 |
8.05 | |||
| get_date_formats | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| get_date_format | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| render_date_field | |
91.67% |
66 / 72 |
|
0.00% |
0 / 1 |
4.01 | |||
| render_time_field | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| render_image_select_field | |
0.00% |
0 / 128 |
|
0.00% |
0 / 1 |
1722 | |||
| render_number_field | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| render_default_field | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| get_form_variation_style_properties | |
93.10% |
54 / 58 |
|
0.00% |
0 / 1 |
19.12 | |||
| render_outline_label | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
5 | |||
| render_animated_label | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
| render_below_label | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
| render_field | |
90.27% |
102 / 113 |
|
0.00% |
0 / 1 |
35.07 | |||
| get_field_extra | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| maybe_override_type | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
| is_field_renderable | |
78.57% |
11 / 14 |
|
0.00% |
0 / 1 |
5.25 | |||
| get_form_style | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| has_inset_label | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| type_has_inset_label | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| render_rating_field | |
84.29% |
59 / 70 |
|
0.00% |
0 / 1 |
11.47 | |||
| render_slider_field | |
93.10% |
54 / 58 |
|
0.00% |
0 / 1 |
8.02 | |||
| enqueue_slider_field_assets | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
| get_translatable_countries | |
100.00% |
241 / 241 |
|
100.00% |
1 / 1 |
1 | |||
| enqueue_phone_field_assets | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
3 | |||
| trim_image_select_options | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Contact_Form_Field class. |
| 4 | * |
| 5 | * @package automattic/jetpack-forms |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\Forms\ContactForm; |
| 9 | |
| 10 | use Automattic\Jetpack\Assets; |
| 11 | use Automattic\Jetpack\Constants; |
| 12 | use Automattic\Jetpack\Forms\Jetpack_Forms; |
| 13 | |
| 14 | if ( ! 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 | */ |
| 23 | class Contact_Form_Field extends Contact_Form_Shortcode { |
| 24 | |
| 25 | /** |
| 26 | * Number of files a file upload field accepts when its `maxfiles` attribute says nothing. |
| 27 | * |
| 28 | * One, so that a field authored before the setting existed keeps behaving exactly as it did. |
| 29 | * |
| 30 | * @var int |
| 31 | */ |
| 32 | const FILE_FIELD_DEFAULT_MAX_FILES = 1; |
| 33 | |
| 34 | /** |
| 35 | * Highest number of files a filter may raise a field to. |
| 36 | * |
| 37 | * Separate from FILE_FIELD_MAX_FILES_LIMIT, which bounds what an author can choose: a site that |
| 38 | * wants more headroom than the editor offers can filter its way up to here, and no further. The |
| 39 | * endpoint imposes no count limit of its own that refuses anything — its per-site budgets log |
| 40 | * and alert but still store the file — so this is the only thing standing between a filter and |
| 41 | * a single site uploading as much as it likes. |
| 42 | * |
| 43 | * @var int |
| 44 | */ |
| 45 | const FILE_FIELD_MAX_FILES_ABSOLUTE_LIMIT = 25; |
| 46 | |
| 47 | /** |
| 48 | * Highest number of files an author can choose for a field, whatever `maxfiles` asks for. |
| 49 | * |
| 50 | * This bounds how many files can be attached to a response, not how many bytes reach the site. |
| 51 | * Each file is uploaded as soon as the visitor picks it, so by the time anything here runs the |
| 52 | * transfer has already happened and the submission carries only references — and the upload |
| 53 | * endpoint is reachable on its own besides. Read this as a limit on the shape of a response, |
| 54 | * and look to the endpoint's own quotas for anything to do with volume. |
| 55 | * |
| 56 | * Keep in sync with `MAX_FILES_LIMIT` in `blocks/field-file/edit.jsx`, which bounds the editor |
| 57 | * control; `test_file_field_ceiling_matches_the_editor_control` fails if they drift. |
| 58 | * |
| 59 | * @var int |
| 60 | */ |
| 61 | const FILE_FIELD_MAX_FILES_LIMIT = 10; |
| 62 | |
| 63 | /** |
| 64 | * Maximum size, in bytes, of a single uploaded file. |
| 65 | * |
| 66 | * @var int |
| 67 | */ |
| 68 | const FILE_FIELD_MAX_UPLOAD_SIZE = 20 * 1024 * 1024; |
| 69 | |
| 70 | /** |
| 71 | * The shortcode name. |
| 72 | * |
| 73 | * @var string |
| 74 | */ |
| 75 | public $shortcode_name = 'contact-field'; |
| 76 | |
| 77 | /** |
| 78 | * Field types that never render a style-specific label. |
| 79 | * |
| 80 | * These have no single labellable input — they are groups of inputs, or a |
| 81 | * control with its own labelling — so every non-default form style falls back |
| 82 | * to the plain default label. |
| 83 | * |
| 84 | * @var string[] |
| 85 | */ |
| 86 | const TYPES_WITHOUT_STYLED_LABEL = array( 'checkbox', 'checkbox-multiple', 'radio', 'consent', 'file' ); |
| 87 | |
| 88 | /** |
| 89 | * Field types that never render an *inset* (Outlined / Animated) label. |
| 90 | * |
| 91 | * Their input is not a single text-like box for a label to sit inside, so an |
| 92 | * inset label would overlap the control. Styles that render the label outside |
| 93 | * the field, such as 'below', still apply — unlike TYPES_WITHOUT_STYLED_LABEL, |
| 94 | * this only opts out of the inset styles. |
| 95 | * |
| 96 | * Keep in sync with the editor (`blocks/label/edit.jsx`) and the wrapper |
| 97 | * exclusion list in `contact-form/css/grunion.scss`. |
| 98 | * |
| 99 | * @var string[] |
| 100 | */ |
| 101 | const TYPES_WITHOUT_INSET_LABEL = array( 'slider' ); |
| 102 | |
| 103 | /** |
| 104 | * The parent form. |
| 105 | * |
| 106 | * @var Contact_Form |
| 107 | */ |
| 108 | public $form; |
| 109 | |
| 110 | /** |
| 111 | * Default or POSTed value. |
| 112 | * |
| 113 | * @var string|string[] |
| 114 | */ |
| 115 | public $value; |
| 116 | |
| 117 | /** |
| 118 | * Is the input valid? |
| 119 | * |
| 120 | * @var bool |
| 121 | */ |
| 122 | public $error = false; |
| 123 | |
| 124 | /** |
| 125 | * Styles to be applied to the field |
| 126 | * |
| 127 | * @var string |
| 128 | */ |
| 129 | public $block_styles = ''; |
| 130 | |
| 131 | /** |
| 132 | * Classes to be applied to the field |
| 133 | * |
| 134 | * @var string |
| 135 | */ |
| 136 | public $field_classes = ''; |
| 137 | |
| 138 | /** |
| 139 | * Styles to be applied to the field |
| 140 | * |
| 141 | * @var string |
| 142 | */ |
| 143 | public $field_styles = ''; |
| 144 | |
| 145 | /** |
| 146 | * Classes to be applied to the field option |
| 147 | * |
| 148 | * @var string |
| 149 | */ |
| 150 | public $option_classes = ''; |
| 151 | |
| 152 | /** |
| 153 | * Styles to be applied to the field option |
| 154 | * |
| 155 | * @var string |
| 156 | */ |
| 157 | public $option_styles = ''; |
| 158 | |
| 159 | /** |
| 160 | * Classes to be applied to the field |
| 161 | * |
| 162 | * @var string |
| 163 | */ |
| 164 | public $label_classes = ''; |
| 165 | |
| 166 | /** |
| 167 | * Styles to be applied to the field |
| 168 | * |
| 169 | * @var string |
| 170 | */ |
| 171 | public $label_styles = ''; |
| 172 | |
| 173 | /** |
| 174 | * Input tuple used for the cached hidden-field filter result. |
| 175 | * |
| 176 | * @var array|null |
| 177 | */ |
| 178 | private $hidden_field_filter_input; |
| 179 | |
| 180 | /** |
| 181 | * Cached hidden-field filter result. |
| 182 | * |
| 183 | * @var mixed |
| 184 | */ |
| 185 | private $hidden_field_filter_value; |
| 186 | |
| 187 | /** |
| 188 | * Description markup (help text, format hint, error div) built at the input |
| 189 | * site but held back so an inset-label wrapper can emit it outside the |
| 190 | * field div. Null when nothing has been deferred. |
| 191 | * |
| 192 | * Building it once, where the input's aria-describedby is built, is what |
| 193 | * keeps the emitted ids and the referenced ids identical. |
| 194 | * |
| 195 | * @since 8.0.0 |
| 196 | * |
| 197 | * @var string|null |
| 198 | */ |
| 199 | private $deferred_descriptions = null; |
| 200 | |
| 201 | /** |
| 202 | * Constructor function. |
| 203 | * |
| 204 | * @param array $attributes An associative array of shortcode attributes. @see shortcode_atts(). |
| 205 | * @param null|string $content Null for selfclosing shortcodes. The inner content otherwise. |
| 206 | * @param Contact_Form $form The parent form. |
| 207 | */ |
| 208 | public function __construct( $attributes, $content = null, $form = null ) { |
| 209 | $attributes = shortcode_atts( |
| 210 | array( |
| 211 | 'label' => null, |
| 212 | 'togglelabel' => null, |
| 213 | 'type' => 'text', |
| 214 | 'required' => false, |
| 215 | 'requiredtext' => null, |
| 216 | 'requiredindicator' => true, |
| 217 | 'options' => array(), |
| 218 | 'optionsdata' => array(), |
| 219 | 'allowother' => null, |
| 220 | 'isother' => null, |
| 221 | 'otherplaceholder' => null, |
| 222 | 'id' => null, |
| 223 | 'style' => null, |
| 224 | 'fieldbackgroundcolor' => null, |
| 225 | 'buttonbackgroundcolor' => null, |
| 226 | 'buttonborderradius' => null, |
| 227 | 'buttonborderwidth' => null, |
| 228 | 'textcolor' => null, |
| 229 | 'default' => null, |
| 230 | 'values' => null, |
| 231 | 'placeholder' => null, |
| 232 | 'class' => null, |
| 233 | 'width' => null, |
| 234 | 'consenttype' => null, |
| 235 | 'dateformat' => null, |
| 236 | 'helptext' => null, |
| 237 | 'implicitconsentmessage' => null, |
| 238 | 'explicitconsentmessage' => null, |
| 239 | 'borderradius' => null, |
| 240 | 'borderwidth' => null, |
| 241 | 'lineheight' => null, |
| 242 | 'labellineheight' => null, |
| 243 | 'bordercolor' => null, |
| 244 | 'inputcolor' => null, |
| 245 | 'labelcolor' => null, |
| 246 | 'labelfontsize' => null, |
| 247 | 'fieldfontsize' => null, |
| 248 | 'labelclasses' => null, |
| 249 | 'labelstyles' => null, |
| 250 | 'inputclasses' => null, |
| 251 | 'inputstyles' => null, |
| 252 | 'optionclasses' => null, |
| 253 | 'optionstyles' => null, |
| 254 | 'min' => null, |
| 255 | 'max' => null, |
| 256 | 'minlabel' => null, |
| 257 | 'maxlabel' => null, |
| 258 | 'step' => null, |
| 259 | 'maxfiles' => null, |
| 260 | 'fieldwrapperclasses' => null, |
| 261 | 'stylevariationattributes' => array(), |
| 262 | 'stylevariationclasses' => null, |
| 263 | 'stylevariationstyles' => null, |
| 264 | 'optionsclasses' => null, |
| 265 | 'optionsstyles' => null, |
| 266 | 'align' => null, |
| 267 | 'variation' => null, |
| 268 | 'iconstyle' => null, // For rating field icon style (lowercase for shortcode compatibility) |
| 269 | // full phone field attributes, might become a standalone country list input block |
| 270 | 'showcountryselector' => false, |
| 271 | 'searchplaceholder' => false, |
| 272 | // Image select field attributes |
| 273 | 'ismultiple' => null, |
| 274 | 'showlabels' => null, |
| 275 | 'issupersized' => null, |
| 276 | 'randomizeoptions' => null, |
| 277 | 'showotheroption' => null, |
| 278 | // derived from block metadata for blockVisibility support |
| 279 | 'labelhiddenbyblockvisibility' => null, |
| 280 | // JSON-encoded conditional logic config; decoded below. |
| 281 | 'conditionallogic' => null, |
| 282 | ), |
| 283 | $attributes, |
| 284 | 'contact-field' |
| 285 | ); |
| 286 | |
| 287 | if ( ! empty( $attributes['conditionallogic'] ) && is_string( $attributes['conditionallogic'] ) ) { |
| 288 | $decoded = json_decode( html_entity_decode( $attributes['conditionallogic'], ENT_COMPAT ), true ); |
| 289 | $attributes['conditionallogic'] = is_array( $decoded ) ? $decoded : null; |
| 290 | } elseif ( ! is_array( $attributes['conditionallogic'] ) ) { |
| 291 | $attributes['conditionallogic'] = null; |
| 292 | } |
| 293 | |
| 294 | // special default for subject field |
| 295 | if ( 'subject' === $attributes['type'] && $attributes['default'] === null && $form !== null ) { |
| 296 | $attributes['default'] = $form->get_attribute( 'subject' ); |
| 297 | } |
| 298 | |
| 299 | // allow required=1 or required=true |
| 300 | if ( '1' === $attributes['required'] || 'true' === strtolower( $attributes['required'] ) ) { |
| 301 | $attributes['required'] = true; |
| 302 | } else { |
| 303 | $attributes['required'] = false; |
| 304 | } |
| 305 | |
| 306 | if ( $attributes['requiredtext'] === null ) { |
| 307 | $attributes['requiredtext'] = __( '(required)', 'jetpack-forms' ); |
| 308 | } |
| 309 | |
| 310 | // parse out comma-separated options list (for selects, radios, and checkbox-multiples) |
| 311 | if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) { |
| 312 | $attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) ); |
| 313 | |
| 314 | if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) { |
| 315 | $attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) ); |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | if ( ! empty( $attributes['optionsdata'] ) ) { |
| 320 | $attributes['optionsdata'] = json_decode( html_entity_decode( $attributes['optionsdata'], ENT_COMPAT ), true ); |
| 321 | } |
| 322 | |
| 323 | // allow boolean values for showcountryselector, only if it's set so we don't pollute other fields attrs |
| 324 | if ( isset( $attributes['showcountryselector'] ) ) { |
| 325 | if ( true === $attributes['showcountryselector'] || '1' === $attributes['showcountryselector'] || 'true' === strtolower( $attributes['showcountryselector'] ) ) { |
| 326 | $attributes['showcountryselector'] = true; |
| 327 | } else { |
| 328 | $attributes['showcountryselector'] = false; |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | if ( $form ) { |
| 333 | // make a unique field ID based on the label, with an incrementing number if needed to avoid clashes |
| 334 | $form_id = $form->get_attribute( 'id' ); |
| 335 | $id = $attributes['id'] ?? false; |
| 336 | |
| 337 | $unescaped_label = $this->unesc_attr( $attributes['label'] ); |
| 338 | $unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs? |
| 339 | $unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label ); |
| 340 | |
| 341 | if ( empty( $id ) ) { |
| 342 | $id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label ); |
| 343 | $i = 0; |
| 344 | $max_tries = 99; |
| 345 | while ( isset( $form->fields[ $id ] ) ) { |
| 346 | ++$i; |
| 347 | $id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i ); |
| 348 | |
| 349 | if ( $i > $max_tries ) { |
| 350 | break; |
| 351 | } |
| 352 | } |
| 353 | } elseif ( isset( $form->fields[ $id ] ) ) { |
| 354 | // A manually-set field ID (including the Name field's default IDs |
| 355 | // such as "name"/"first-name") is carried verbatim when a field is |
| 356 | // duplicated or copy/pasted, so two fields can end up sharing an ID. |
| 357 | // Duplicate IDs render the same input name: browsers mirror typing |
| 358 | // across the fields and only one value survives submission, dropping |
| 359 | // the rest from stored responses and emails. Suffix the later |
| 360 | // duplicates ("name" -> "name-2" -> "name-3", …) to keep every field |
| 361 | // ID unique, matching generateUniqueFormFieldId() on the editor side. |
| 362 | // See FORMS-724. |
| 363 | $base = $id; |
| 364 | $i = 2; |
| 365 | $id = $base . '-' . $i; |
| 366 | while ( isset( $form->fields[ $id ] ) ) { |
| 367 | ++$i; |
| 368 | $id = $base . '-' . $i; |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | $attributes['id'] = $id; |
| 373 | } |
| 374 | |
| 375 | parent::__construct( $attributes, $content ); |
| 376 | |
| 377 | // Store parent form |
| 378 | $this->form = $form; |
| 379 | } |
| 380 | |
| 381 | /** |
| 382 | * This field's input is invalid. Flag as invalid and add an error to the parent form |
| 383 | * |
| 384 | * @param string $message The error message to display on the form. |
| 385 | */ |
| 386 | public function add_error( $message ) { |
| 387 | $this->error = true; |
| 388 | $this->form->add_error( $this->get_attribute( 'id' ), $message ); |
| 389 | } |
| 390 | |
| 391 | /** |
| 392 | * Is the field input invalid? |
| 393 | * |
| 394 | * @see $error |
| 395 | * |
| 396 | * @return bool |
| 397 | */ |
| 398 | public function is_error() { |
| 399 | return $this->error; |
| 400 | } |
| 401 | |
| 402 | /** |
| 403 | * Check if the field has a value. |
| 404 | * |
| 405 | * This is used to determine if the field has been filled out by the user. |
| 406 | * |
| 407 | * @return bool True if the field has a value, false otherwise. |
| 408 | */ |
| 409 | public function has_value() { |
| 410 | $field_id = $this->get_attribute( 'id' ); |
| 411 | $field_value = isset( $_POST[ $field_id ] ) ? wp_unslash( $_POST[ $field_id ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- no site changes. |
| 412 | |
| 413 | if ( is_array( $field_value ) ) { |
| 414 | if ( empty( $field_value ) ) { |
| 415 | return false; |
| 416 | } |
| 417 | return ! empty( array_filter( $field_value ) ); |
| 418 | } |
| 419 | return ! empty( trim( $field_value ) ); |
| 420 | } |
| 421 | |
| 422 | /** |
| 423 | * Whether this field's visibility is governed by conditional logic. |
| 424 | * |
| 425 | * @return bool True when the field carries an enabled conditional-logic config. |
| 426 | */ |
| 427 | public function has_conditional_logic() { |
| 428 | $logic = $this->get_attribute( 'conditionallogic' ); |
| 429 | |
| 430 | return is_array( $logic ) && ! empty( $logic['enabled'] ); |
| 431 | } |
| 432 | |
| 433 | /** |
| 434 | * Validates the form input |
| 435 | */ |
| 436 | public function validate() { |
| 437 | // If the field is already invalid, don't validate it again. |
| 438 | if ( $this->is_error() ) { |
| 439 | return; |
| 440 | } |
| 441 | |
| 442 | $field_type = $this->maybe_override_type(); |
| 443 | // If it's not required, there's nothing to validate |
| 444 | if ( ! $this->get_attribute( 'required' ) && ! $this->has_value() ) { |
| 445 | return; |
| 446 | } |
| 447 | |
| 448 | if ( ! $this->is_field_renderable( $field_type ) ) { |
| 449 | return; |
| 450 | } |
| 451 | |
| 452 | $field_id = $this->get_attribute( 'id' ); |
| 453 | $field_label = $this->get_attribute( 'label' ); |
| 454 | |
| 455 | if ( isset( $_POST[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes. |
| 456 | if ( is_array( $_POST[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes. |
| 457 | $field_value = array_map( 'sanitize_text_field', wp_unslash( $_POST[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verification should happen in caller. |
| 458 | } else { |
| 459 | $field_value = sanitize_text_field( wp_unslash( $_POST[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verification should happen in caller. |
| 460 | } |
| 461 | } else { |
| 462 | $field_value = ''; |
| 463 | } |
| 464 | |
| 465 | switch ( $field_type ) { |
| 466 | case 'url': |
| 467 | if ( ! is_string( $field_value ) || empty( $field_value ) || ! preg_match( |
| 468 | // 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. |
| 469 | '%^(?:(?: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', |
| 470 | $field_value |
| 471 | ) ) { |
| 472 | /* translators: %s is the name of a form field */ |
| 473 | $this->add_error( sprintf( __( '%s: Please enter a valid URL - https://www.example.com.', 'jetpack-forms' ), $field_label ) ); |
| 474 | } |
| 475 | break; |
| 476 | case 'email': |
| 477 | // Make sure the email address is valid |
| 478 | if ( ! is_string( $field_value ) || ! is_email( $field_value ) ) { |
| 479 | /* translators: %s is the name of a form field */ |
| 480 | $this->add_error( sprintf( __( '%s requires a valid email address.', 'jetpack-forms' ), $field_label ) ); |
| 481 | } |
| 482 | break; |
| 483 | case 'checkbox-multiple': |
| 484 | // Check that there is at least one option selected |
| 485 | if ( empty( $field_value ) ) { |
| 486 | /* translators: %s is the name of a form field */ |
| 487 | $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) ); |
| 488 | } else { |
| 489 | |
| 490 | $options_data = (array) $this->get_attribute( 'optionsdata' ); |
| 491 | $possible_values = array(); |
| 492 | if ( ! empty( $options_data ) ) { |
| 493 | foreach ( $options_data as $option_index => $option ) { |
| 494 | $option_label = isset( $option['label'] ) ? Contact_Form_Plugin::strip_tags( $option['label'] ) : ''; |
| 495 | if ( is_string( $option_label ) && '' !== $option_label ) { |
| 496 | $possible_values[] = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option_label ); |
| 497 | } |
| 498 | } |
| 499 | } else { |
| 500 | foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) { |
| 501 | $option = Contact_Form_Plugin::strip_tags( $option ); |
| 502 | if ( is_string( $option ) && '' !== $option ) { |
| 503 | $possible_values[] = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option ); |
| 504 | } |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | $non_empty_options = array_map( array( $this, 'sanitize_text_field' ), $possible_values ); |
| 509 | |
| 510 | foreach ( $field_value as $field_value_item ) { |
| 511 | if ( ! in_array( $field_value_item, $non_empty_options, true ) ) { |
| 512 | /* translators: %s is the name of a form field */ |
| 513 | $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) ); |
| 514 | break; |
| 515 | } |
| 516 | } |
| 517 | } |
| 518 | break; |
| 519 | case 'radio': |
| 520 | // Check that there is at least one option selected |
| 521 | if ( empty( $field_value ) ) { |
| 522 | /* translators: %s is the name of a form field */ |
| 523 | $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) ); |
| 524 | } else { |
| 525 | // Check that the selected options are valid |
| 526 | $options = (array) $this->get_attribute( 'options' ); |
| 527 | $options_data = (array) $this->get_attribute( 'optionsdata' ); |
| 528 | $allow_other = $this->get_attribute( 'allowother' ); |
| 529 | |
| 530 | if ( ! empty( $options_data ) ) { |
| 531 | $options = array_map( |
| 532 | function ( $option ) { |
| 533 | return $this->sanitize_text_field( trim( $option['label'] ) ); |
| 534 | }, |
| 535 | $options_data |
| 536 | ); |
| 537 | } else { |
| 538 | $options = array_map( array( $this, 'sanitize_text_field' ), $options ); |
| 539 | } |
| 540 | $non_empty_options = array_filter( |
| 541 | $options, |
| 542 | function ( $option ) { |
| 543 | return $option !== ''; |
| 544 | } |
| 545 | ); |
| 546 | |
| 547 | // Validate Other option values, which may include custom user text. |
| 548 | $is_valid_other = false; |
| 549 | if ( $allow_other ) { |
| 550 | $other_label = null; |
| 551 | foreach ( $options_data as $option ) { |
| 552 | if ( ! empty( $option['isOther'] ) ) { |
| 553 | $other_label = Contact_Form_Plugin::strip_tags( $option['label'] ); |
| 554 | break; |
| 555 | } |
| 556 | } |
| 557 | |
| 558 | if ( ! empty( $other_label ) ) { |
| 559 | $other_prefix = $other_label . ': '; |
| 560 | |
| 561 | if ( $field_value === $other_label ) { |
| 562 | $is_valid_other = true; |
| 563 | } elseif ( strpos( $field_value, $other_prefix ) === 0 ) { |
| 564 | $custom_text = trim( substr( $field_value, strlen( $other_prefix ) ) ); |
| 565 | |
| 566 | if ( $this->get_attribute( 'required' ) && empty( $custom_text ) ) { |
| 567 | /* translators: %1$s is the name of a form field, %2$s is the option label */ |
| 568 | $this->add_error( sprintf( __( '%1$s requires a response when "%2$s" is selected.', 'jetpack-forms' ), $field_label, $other_label ) ); |
| 569 | break; |
| 570 | } |
| 571 | |
| 572 | $is_valid_other = true; |
| 573 | } |
| 574 | } |
| 575 | } |
| 576 | |
| 577 | if ( ! in_array( $field_value, $non_empty_options, true ) && ! $is_valid_other ) { |
| 578 | /* translators: %s is the name of a form field */ |
| 579 | $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) ); |
| 580 | break; |
| 581 | } |
| 582 | } |
| 583 | break; |
| 584 | case 'image-select': |
| 585 | // Check that there is at least one option selected |
| 586 | if ( empty( $field_value ) ) { |
| 587 | /* translators: %s is the name of a form field */ |
| 588 | $this->add_error( sprintf( __( '%s requires at least one selection.', 'jetpack-forms' ), $field_label ) ); |
| 589 | } else { |
| 590 | // Check that the selected options are valid |
| 591 | $options = (array) $this->get_attribute( 'options' ); |
| 592 | $options_data = (array) $this->get_attribute( 'optionsdata' ); |
| 593 | |
| 594 | if ( ! empty( $options_data ) ) { |
| 595 | // Extract letters from options_data for validation |
| 596 | $options = array_map( |
| 597 | function ( $option ) { |
| 598 | return sanitize_text_field( trim( $option['letter'] ?? '' ) ); |
| 599 | }, |
| 600 | $options_data |
| 601 | ); |
| 602 | } |
| 603 | |
| 604 | $non_empty_options = array_filter( |
| 605 | $options, |
| 606 | function ( $option ) { |
| 607 | return $option !== ''; |
| 608 | } |
| 609 | ); |
| 610 | |
| 611 | // For single selection (radio), check if the selected value is in the options |
| 612 | if ( ! $this->get_attribute( 'ismultiple' ) ) { |
| 613 | // Decode the JSON response to get the selected value |
| 614 | $decoded_value = json_decode( $field_value, true ); |
| 615 | $selected_value = $decoded_value['selected'] ?? ''; |
| 616 | |
| 617 | if ( ! in_array( $selected_value, $non_empty_options, true ) ) { |
| 618 | /* translators: %s is the name of a form field */ |
| 619 | $this->add_error( sprintf( __( '%s requires a valid selection.', 'jetpack-forms' ), $field_label ) ); |
| 620 | } |
| 621 | } else { |
| 622 | // For multiple selection (checkbox), check each selected value |
| 623 | foreach ( $field_value as $field_value_item ) { |
| 624 | // Decode the JSON response to get the selected value |
| 625 | $decoded_item = json_decode( $field_value_item, true ); |
| 626 | $selected_value = $decoded_item['selected'] ?? ''; |
| 627 | |
| 628 | if ( ! in_array( $selected_value, $non_empty_options, true ) ) { |
| 629 | /* translators: %s is the name of a form field */ |
| 630 | $this->add_error( sprintf( __( '%s requires valid selections.', 'jetpack-forms' ), $field_label ) ); |
| 631 | break; |
| 632 | } |
| 633 | } |
| 634 | } |
| 635 | } |
| 636 | break; |
| 637 | case 'number': |
| 638 | // Make sure the number address is valid |
| 639 | if ( ! is_numeric( $field_value ) ) { |
| 640 | /* translators: %s is the name of a form field */ |
| 641 | $this->add_error( sprintf( __( '%s requires a number.', 'jetpack-forms' ), $field_label ) ); |
| 642 | } |
| 643 | break; |
| 644 | case 'time': |
| 645 | // Make sure the number address is valid |
| 646 | if ( ! preg_match( '/^(?:2[0-3]|[01][0-9]):[0-5][0-9]$/', $field_value ) ) { |
| 647 | /* translators: %s is the name of a form field */ |
| 648 | $this->add_error( sprintf( __( '%s requires a time', 'jetpack-forms' ), $field_label ) ); |
| 649 | } |
| 650 | break; |
| 651 | case 'file': |
| 652 | // Make sure the file field is not empty |
| 653 | if ( ! is_array( $field_value ) || empty( $field_value[0] ) ) { |
| 654 | /* translators: %s is the name of a form field */ |
| 655 | $this->add_error( sprintf( __( '%s requires a file to be uploaded.', 'jetpack-forms' ), $field_label ) ); |
| 656 | break; |
| 657 | } |
| 658 | |
| 659 | /* |
| 660 | * Nothing enforced the count on this side before. The limit lived only in the |
| 661 | * browser, so a submission assembled by hand could carry as many uploaded file |
| 662 | * IDs as it liked and every one of them would be attached to the response. |
| 663 | */ |
| 664 | $max_files = $this->get_file_field_max_files(); |
| 665 | |
| 666 | if ( count( $field_value ) > $max_files ) { |
| 667 | $this->add_error( |
| 668 | sprintf( |
| 669 | /* translators: 1: the name of a form field, 2: the maximum number of files it accepts. */ |
| 670 | _n( |
| 671 | '%1$s accepts at most %2$d file.', |
| 672 | '%1$s accepts at most %2$d files.', |
| 673 | $max_files, |
| 674 | 'jetpack-forms' |
| 675 | ), |
| 676 | $field_label, |
| 677 | $max_files |
| 678 | ) |
| 679 | ); |
| 680 | } |
| 681 | break; |
| 682 | default: |
| 683 | // Just check for presence of any text |
| 684 | if ( ! is_string( $field_value ) || ! strlen( trim( $field_value ) ) ) { |
| 685 | /* translators: %s is the name of a form field */ |
| 686 | $this->add_error( sprintf( __( '%s field is required.', 'jetpack-forms' ), $field_label ) ); |
| 687 | } |
| 688 | } |
| 689 | } |
| 690 | /** |
| 691 | * Sanitize a text field value and html_entity_decode the field. |
| 692 | * |
| 693 | * @param string $field_value The field value to sanitize. |
| 694 | * @return string The sanitized field value. |
| 695 | */ |
| 696 | public function sanitize_text_field( $field_value ) { |
| 697 | return sanitize_text_field( html_entity_decode( $field_value, ENT_COMPAT ) ); |
| 698 | } |
| 699 | |
| 700 | /** |
| 701 | * Check the default value for options field |
| 702 | * |
| 703 | * @param string $value - the value we're checking. |
| 704 | * @param int $index - the index. |
| 705 | * @param string $options - default field option. |
| 706 | * |
| 707 | * @return string |
| 708 | */ |
| 709 | public function get_option_value( $value, $index, $options ) { |
| 710 | if ( empty( $value[ $index ] ) ) { |
| 711 | return $options; |
| 712 | } |
| 713 | return $value[ $index ]; |
| 714 | } |
| 715 | |
| 716 | /** |
| 717 | * Outputs the HTML for this form field |
| 718 | * |
| 719 | * @return string HTML |
| 720 | */ |
| 721 | public function render() { |
| 722 | |
| 723 | $field_id = $this->get_attribute( 'id' ); |
| 724 | $field_type = $this->maybe_override_type(); |
| 725 | $field_label = $this->get_attribute( 'label' ); |
| 726 | $field_required = $this->get_attribute( 'required' ); |
| 727 | $field_required_text = $this->get_attribute( 'requiredtext' ); |
| 728 | $field_required_indicator = (bool) $this->get_attribute( 'requiredindicator' ); |
| 729 | $field_placeholder = $this->get_attribute( 'placeholder' ); |
| 730 | $field_width = $this->get_attribute( 'width' ); |
| 731 | $class = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' ); |
| 732 | |
| 733 | $label_classes = $this->get_attribute( 'labelclasses' ); |
| 734 | $label_styles = $this->get_attribute( 'labelstyles' ); |
| 735 | $input_classes = $this->get_attribute( 'inputclasses' ); |
| 736 | $input_styles = $this->get_attribute( 'inputstyles' ); |
| 737 | $option_classes = $this->get_attribute( 'optionclasses' ); |
| 738 | $option_styles = $this->get_attribute( 'optionstyles' ); |
| 739 | |
| 740 | $has_block_support_styles = ! empty( $label_classes ) || ! empty( $label_styles ) || ! empty( $input_classes ) || ! empty( $input_styles ) || ! empty( $option_classes ) || ! empty( $option_styles ); |
| 741 | |
| 742 | if ( $has_block_support_styles ) { |
| 743 | // 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? |
| 744 | |
| 745 | // Classes. |
| 746 | if ( ! empty( $label_classes ) ) { |
| 747 | $this->label_classes .= esc_attr( $label_classes ); |
| 748 | } |
| 749 | if ( ! empty( $input_classes ) ) { |
| 750 | $class .= $class ? ' ' . esc_attr( $input_classes ) : esc_attr( $input_classes ); |
| 751 | $this->field_classes = $input_classes; |
| 752 | } |
| 753 | if ( ! empty( $option_classes ) ) { |
| 754 | $class .= $class ? ' ' . esc_attr( $option_classes ) : esc_attr( $option_classes ); |
| 755 | $this->option_classes = $option_classes; |
| 756 | } |
| 757 | |
| 758 | // Styles. |
| 759 | if ( ! empty( $label_styles ) ) { |
| 760 | $this->label_styles .= esc_attr( $label_styles ); |
| 761 | } |
| 762 | if ( ! empty( $input_styles ) ) { |
| 763 | $this->field_styles .= esc_attr( $input_styles ); |
| 764 | } |
| 765 | if ( ! empty( $option_styles ) ) { |
| 766 | $this->option_styles .= esc_attr( $option_styles ); |
| 767 | } |
| 768 | |
| 769 | // For Outline style support. |
| 770 | $form_style = $this->get_form_style(); |
| 771 | if ( 'outlined' === $form_style || 'animated' === $form_style ) { |
| 772 | $output_data = $this->get_form_variation_style_properties( $form_style ); |
| 773 | $this->block_styles .= esc_attr( $output_data['css_vars'] ); |
| 774 | } |
| 775 | } else { |
| 776 | if ( is_numeric( $this->get_attribute( 'borderradius' ) ) ) { |
| 777 | $this->block_styles .= '--jetpack--contact-form--border-radius: ' . esc_attr( $this->get_attribute( 'borderradius' ) ) . 'px;'; |
| 778 | $this->field_styles .= 'border-radius: ' . (int) $this->get_attribute( 'borderradius' ) . 'px;'; |
| 779 | } |
| 780 | |
| 781 | if ( is_numeric( $this->get_attribute( 'borderwidth' ) ) ) { |
| 782 | $this->block_styles .= '--jetpack--contact-form--border-size: ' . esc_attr( $this->get_attribute( 'borderwidth' ) ) . 'px;'; |
| 783 | $this->field_styles .= 'border-width: ' . (int) $this->get_attribute( 'borderwidth' ) . 'px;'; |
| 784 | } |
| 785 | |
| 786 | if ( is_numeric( $this->get_attribute( 'lineheight' ) ) ) { |
| 787 | $this->block_styles .= '--jetpack--contact-form--line-height: ' . esc_attr( $this->get_attribute( 'lineheight' ) ) . ';'; |
| 788 | $this->field_styles .= 'line-height: ' . (int) $this->get_attribute( 'lineheight' ) . ';'; |
| 789 | $this->option_styles .= 'line-height: ' . (int) $this->get_attribute( 'lineheight' ) . ';'; |
| 790 | } |
| 791 | |
| 792 | if ( ! empty( $this->get_attribute( 'bordercolor' ) ) ) { |
| 793 | $this->block_styles .= '--jetpack--contact-form--border-color: ' . esc_attr( $this->get_attribute( 'bordercolor' ) ) . ';'; |
| 794 | $this->field_styles .= 'border-color: ' . esc_attr( $this->get_attribute( 'bordercolor' ) ) . ';'; |
| 795 | } |
| 796 | |
| 797 | if ( ! empty( $this->get_attribute( 'inputcolor' ) ) ) { |
| 798 | $this->block_styles .= '--jetpack--contact-form--text-color: ' . esc_attr( $this->get_attribute( 'inputcolor' ) ) . ';'; |
| 799 | $this->block_styles .= '--jetpack--contact-form--button-outline--text-color: ' . esc_attr( $this->get_attribute( 'inputcolor' ) ) . ';'; |
| 800 | $this->field_styles .= 'color: ' . esc_attr( $this->get_attribute( 'inputcolor' ) ) . ';'; |
| 801 | $this->option_styles .= 'color: ' . esc_attr( $this->get_attribute( 'inputcolor' ) ) . ';'; |
| 802 | } |
| 803 | |
| 804 | if ( ! empty( $this->get_attribute( 'fieldbackgroundcolor' ) ) ) { |
| 805 | $this->block_styles .= '--jetpack--contact-form--input-background: ' . esc_attr( $this->get_attribute( 'fieldbackgroundcolor' ) ) . ';'; |
| 806 | $this->field_styles .= 'background-color: ' . esc_attr( $this->get_attribute( 'fieldbackgroundcolor' ) ) . ';'; |
| 807 | } |
| 808 | |
| 809 | if ( ! empty( $this->get_attribute( 'fieldfontsize' ) ) ) { |
| 810 | $this->block_styles .= '--jetpack--contact-form--font-size: ' . esc_attr( $this->get_attribute( 'fieldfontsize' ) ) . ';'; |
| 811 | $this->field_styles .= 'font-size: ' . esc_attr( $this->get_attribute( 'fieldfontsize' ) ) . ';'; |
| 812 | $this->option_styles .= 'font-size: ' . esc_attr( $this->get_attribute( 'fieldfontsize' ) ) . ';'; |
| 813 | } |
| 814 | |
| 815 | if ( ! empty( $this->get_attribute( 'labelcolor' ) ) ) { |
| 816 | $this->label_styles .= 'color: ' . esc_attr( $this->get_attribute( 'labelcolor' ) ) . ';'; |
| 817 | } |
| 818 | |
| 819 | if ( ! empty( $this->get_attribute( 'labelfontsize' ) ) ) { |
| 820 | $this->label_styles .= 'font-size: ' . esc_attr( $this->get_attribute( 'labelfontsize' ) ) . ';'; |
| 821 | } |
| 822 | |
| 823 | if ( is_numeric( $this->get_attribute( 'labellineheight' ) ) ) { |
| 824 | $this->label_styles .= 'line-height: ' . (int) $this->get_attribute( 'labellineheight' ) . ';'; |
| 825 | } |
| 826 | } |
| 827 | |
| 828 | if ( ! empty( $this->get_attribute( 'buttonbackgroundcolor' ) ) ) { |
| 829 | $this->block_styles .= '--jetpack--contact-form--button-outline--background-color: ' . esc_attr( $this->get_attribute( 'buttonbackgroundcolor' ) ) . ';'; |
| 830 | } |
| 831 | if ( is_numeric( $this->get_attribute( 'buttonborderradius' ) ) ) { |
| 832 | $this->block_styles .= '--jetpack--contact-form--button-outline--border-radius: ' . esc_attr( $this->get_attribute( 'buttonborderradius' ) ) . 'px;'; |
| 833 | } |
| 834 | if ( is_numeric( $this->get_attribute( 'buttonborderwidth' ) ) ) { |
| 835 | $this->block_styles .= '--jetpack--contact-form--button-outline--border-size: ' . esc_attr( $this->get_attribute( 'buttonborderwidth' ) ) . 'px;'; |
| 836 | |
| 837 | } |
| 838 | |
| 839 | if ( ! empty( $field_width ) && ! $this->has_inset_label() ) { |
| 840 | $class .= ' grunion-field-width-' . $field_width; |
| 841 | } |
| 842 | |
| 843 | /** |
| 844 | * Filters the "class" attribute of the contact form input |
| 845 | * |
| 846 | * @module contact-form |
| 847 | * |
| 848 | * @since 6.6.0 |
| 849 | * |
| 850 | * @param string $class Additional CSS classes for input class attribute. |
| 851 | */ |
| 852 | $field_class = apply_filters( 'jetpack_contact_form_input_class', $class ); |
| 853 | |
| 854 | $this->value = $this->get_computed_field_value( $field_type, $field_id ); |
| 855 | |
| 856 | $field_value = Contact_Form_Plugin::strip_tags( $this->value ); |
| 857 | $field_label = Contact_Form_Plugin::strip_tags( $field_label ); |
| 858 | |
| 859 | $extra_attrs = array(); |
| 860 | |
| 861 | if ( $field_type === 'number' || $field_type === 'slider' ) { |
| 862 | if ( is_numeric( $this->get_attribute( 'min' ) ) ) { |
| 863 | $extra_attrs['min'] = $this->get_attribute( 'min' ); |
| 864 | } |
| 865 | if ( is_numeric( $this->get_attribute( 'max' ) ) ) { |
| 866 | $extra_attrs['max'] = $this->get_attribute( 'max' ); |
| 867 | } |
| 868 | if ( is_numeric( $this->get_attribute( 'step' ) ) ) { |
| 869 | $extra_attrs['step'] = $this->get_attribute( 'step' ); |
| 870 | } |
| 871 | } |
| 872 | |
| 873 | if ( $field_type === 'slider' ) { |
| 874 | $minlabel = $this->get_attribute( 'minlabel' ); |
| 875 | $maxlabel = $this->get_attribute( 'maxlabel' ); |
| 876 | if ( null !== $minlabel && '' !== $minlabel ) { |
| 877 | $extra_attrs['minLabel'] = $minlabel; |
| 878 | } |
| 879 | if ( null !== $maxlabel && '' !== $maxlabel ) { |
| 880 | $extra_attrs['maxLabel'] = $maxlabel; |
| 881 | } |
| 882 | } |
| 883 | |
| 884 | $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 ); |
| 885 | |
| 886 | /** |
| 887 | * Filter the HTML of the Contact Form. |
| 888 | * |
| 889 | * @module contact-form |
| 890 | * |
| 891 | * @since 2.6.0 |
| 892 | * |
| 893 | * @param string $rendered_field Contact Form HTML output. |
| 894 | * @param string $field_label Field label. |
| 895 | * @param int|null $id Post ID. |
| 896 | */ |
| 897 | return apply_filters( 'grunion_contact_form_field_html', $rendered_field, $field_label, ( in_the_loop() ? get_the_ID() : null ) ); |
| 898 | } |
| 899 | /** |
| 900 | * Returns the computed field value for a field. It uses the POST, GET, Logged in data. |
| 901 | * |
| 902 | * @module contact-form |
| 903 | * |
| 904 | * @param string $field_type The field type. |
| 905 | * @param string $field_id The field id. |
| 906 | * |
| 907 | * @return string |
| 908 | */ |
| 909 | public function get_computed_field_value( $field_type, $field_id ) { |
| 910 | global $current_user, $user_identity; |
| 911 | // Use the POST Field if it is available. |
| 912 | if ( isset( $_POST[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes. |
| 913 | if ( is_array( $_POST[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes. |
| 914 | return array_map( 'sanitize_textarea_field', wp_unslash( $_POST[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes. |
| 915 | } |
| 916 | |
| 917 | return sanitize_textarea_field( wp_unslash( $_POST[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes. |
| 918 | } |
| 919 | |
| 920 | // Explicit consent never renders checked, so a missing POST value must remain empty |
| 921 | // rather than falling through to a query-string or configured default. |
| 922 | if ( 'consent' === $field_type && 'explicit' === $this->get_attribute( 'consenttype' ) ) { |
| 923 | return ''; |
| 924 | } |
| 925 | |
| 926 | // Checkbox controls are omitted from the request when unchecked. During an actual |
| 927 | // submission that absence is the submitted value; falling through to a query-string |
| 928 | // or configured default would silently check the field again. |
| 929 | $is_checkbox_control = in_array( $field_type, array( 'checkbox', 'checkbox-multiple' ), true ); |
| 930 | if ( $is_checkbox_control && $this->form && $this->form->is_current_submission() ) { |
| 931 | return ''; |
| 932 | } |
| 933 | |
| 934 | // Implicit consent renders as a hidden input with this value, so its computed value |
| 935 | // must match what the browser will submit from the first render onward. |
| 936 | if ( 'consent' === $field_type && 'explicit' !== $this->get_attribute( 'consenttype' ) ) { |
| 937 | return __( 'Yes', 'jetpack-forms' ); |
| 938 | } |
| 939 | |
| 940 | // Use the GET Field if it is available. |
| 941 | if ( isset( $_GET[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes. |
| 942 | if ( is_array( $_GET[ $field_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes. |
| 943 | return array_map( 'sanitize_textarea_field', wp_unslash( $_GET[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes. |
| 944 | } |
| 945 | |
| 946 | return sanitize_textarea_field( wp_unslash( $_GET[ $field_id ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes. |
| 947 | } |
| 948 | |
| 949 | if ( ! is_user_logged_in() ) { |
| 950 | return $this->get_attribute( 'default' ); |
| 951 | } |
| 952 | |
| 953 | /** |
| 954 | * Allow third-party tools to prefill the contact form with the user's details when they're logged in. |
| 955 | * |
| 956 | * @module contact-form |
| 957 | * |
| 958 | * @since 3.2.0 |
| 959 | * |
| 960 | * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false. |
| 961 | */ |
| 962 | $filter_value = apply_filters( 'jetpack_auto_fill_logged_in_user', false ); |
| 963 | if ( ( ! current_user_can( 'manage_options' ) && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) || $filter_value ) { |
| 964 | switch ( $field_type ) { |
| 965 | case 'email': |
| 966 | return $current_user->data->user_email; |
| 967 | |
| 968 | case 'name': |
| 969 | return ! empty( $user_identity ) ? $user_identity : $current_user->data->display_name; |
| 970 | |
| 971 | case 'url': |
| 972 | return $current_user->data->user_url; |
| 973 | } |
| 974 | } |
| 975 | |
| 976 | return $this->get_attribute( 'default' ); |
| 977 | } |
| 978 | |
| 979 | /** |
| 980 | * Get the field value used to resolve conditional logic. |
| 981 | * |
| 982 | * Hidden-field filters affect the value rendered into the browser, so apply the same |
| 983 | * filter before the server evaluates a rule that uses a hidden field as its subject. |
| 984 | * |
| 985 | * @return string|array Field value. |
| 986 | */ |
| 987 | public function get_conditional_logic_value() { |
| 988 | $field_type = $this->get_attribute( 'type' ); |
| 989 | $field_id = $this->get_attribute( 'id' ); |
| 990 | $value = $this->get_computed_field_value( $field_type, $field_id ); |
| 991 | |
| 992 | if ( 'hidden' === $field_type && ! $this->is_submitted_hidden_field_value( $field_id ) ) { |
| 993 | $value = $this->get_filtered_hidden_field_value( $value, $this->get_attribute( 'label' ), $field_id ); |
| 994 | } |
| 995 | |
| 996 | return $value; |
| 997 | } |
| 998 | |
| 999 | /** |
| 1000 | * Filter the value of a hidden field once per input tuple. |
| 1001 | * |
| 1002 | * Rendering and conditional logic must use the exact same filtered value. Caching also |
| 1003 | * prevents stateful filters from returning a different value on their second invocation. |
| 1004 | * |
| 1005 | * @param mixed $value The value of the hidden field. |
| 1006 | * @param string $label The label of the hidden field. |
| 1007 | * @param string $id The ID of the hidden field. |
| 1008 | * @return mixed The modified value of the hidden field. |
| 1009 | */ |
| 1010 | private function get_filtered_hidden_field_value( $value, $label, $id ) { |
| 1011 | $input = array( $value, $label, $id ); |
| 1012 | |
| 1013 | if ( $input === $this->hidden_field_filter_input ) { |
| 1014 | return $this->hidden_field_filter_value; |
| 1015 | } |
| 1016 | |
| 1017 | /** |
| 1018 | * Filter the value of the hidden field. |
| 1019 | * |
| 1020 | * @since 6.3.0 |
| 1021 | * |
| 1022 | * @param string $value The value of the hidden field. |
| 1023 | * @param string $label The label of the hidden field. |
| 1024 | * @param string $id The ID of the hidden field. |
| 1025 | */ |
| 1026 | $this->hidden_field_filter_input = $input; |
| 1027 | $this->hidden_field_filter_value = apply_filters( 'jetpack_forms_hidden_field_value', $value, $label, $id ); |
| 1028 | |
| 1029 | return $this->hidden_field_filter_value; |
| 1030 | } |
| 1031 | |
| 1032 | /** |
| 1033 | * Whether a hidden value came from a matching form submission. |
| 1034 | * |
| 1035 | * Hidden values rendered into the browser have already passed through the hidden-field |
| 1036 | * filter, so their submitted values must not be filtered a second time. |
| 1037 | * |
| 1038 | * @param string $id The ID of the hidden field. |
| 1039 | * @return bool Whether the submitted value should be treated as already filtered. |
| 1040 | */ |
| 1041 | private function is_submitted_hidden_field_value( $id ) { |
| 1042 | return isset( $_POST[ $id ] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- no site changes. |
| 1043 | && $this->form |
| 1044 | && $this->form->is_current_submission(); |
| 1045 | } |
| 1046 | |
| 1047 | /** |
| 1048 | * Return the HTML for the label. |
| 1049 | * |
| 1050 | * @param string $type - the field type. |
| 1051 | * @param int $id - the ID. |
| 1052 | * @param string $label - the label. |
| 1053 | * @param bool $required - if the field is marked as required. |
| 1054 | * @param string $required_field_text - the text in the required text field. |
| 1055 | * @param array $extra_attrs Array of key/value pairs to append as attributes to the element. |
| 1056 | * @param bool $always_render - if the label should always be shown. |
| 1057 | * @param bool $required_indicator Whether to display the required indicator. |
| 1058 | * |
| 1059 | * @return string HTML |
| 1060 | */ |
| 1061 | public function render_label( $type, $id, $label, $required, $required_field_text, $extra_attrs = array(), $always_render = false, $required_indicator = true ) { |
| 1062 | if ( $this->attributes['labelhiddenbyblockvisibility'] ) { |
| 1063 | return ''; |
| 1064 | } |
| 1065 | $form_style = $this->get_form_style(); |
| 1066 | |
| 1067 | if ( ! empty( $form_style ) && $form_style !== 'default' ) { |
| 1068 | if ( ! in_array( $type, self::TYPES_WITHOUT_STYLED_LABEL, true ) ) { |
| 1069 | switch ( $form_style ) { |
| 1070 | case 'outlined': |
| 1071 | if ( $this->type_has_inset_label( $type ) ) { |
| 1072 | return $this->render_outline_label( $id, $label, $required, $required_field_text, $required_indicator ); |
| 1073 | } |
| 1074 | break; |
| 1075 | case 'animated': |
| 1076 | if ( $this->type_has_inset_label( $type ) ) { |
| 1077 | return $this->render_animated_label( $id, $label, $required, $required_field_text, $required_indicator ); |
| 1078 | } |
| 1079 | break; |
| 1080 | case 'below': |
| 1081 | return $this->render_below_label( $id, $label, $required, $required_field_text, $required_indicator ); |
| 1082 | } |
| 1083 | } |
| 1084 | |
| 1085 | if ( ! $always_render ) { |
| 1086 | return ''; |
| 1087 | } |
| 1088 | } |
| 1089 | |
| 1090 | if ( ! empty( $this->label_styles ) ) { |
| 1091 | $extra_attrs['style'] = $this->label_styles; |
| 1092 | } |
| 1093 | |
| 1094 | $type_class = $type ? ' ' . $type : ''; |
| 1095 | |
| 1096 | $extra_attrs['class'] = "grunion-field-label{$type_class}" . ( $this->is_error() ? ' form-error' : '' ); |
| 1097 | |
| 1098 | if ( ! empty( $this->label_classes ) ) { |
| 1099 | $extra_attrs['class'] .= ' ' . $this->label_classes; |
| 1100 | } |
| 1101 | |
| 1102 | $extra_attrs_string = ''; |
| 1103 | |
| 1104 | foreach ( $extra_attrs as $attr => $val ) { |
| 1105 | $extra_attrs_string .= sprintf( '%s="%s" ', esc_attr( $attr ), esc_attr( $val ) ); |
| 1106 | } |
| 1107 | |
| 1108 | $type_class = $type ? ' ' . $type : ''; |
| 1109 | return "<label |
| 1110 | for='" . esc_attr( $id ) . "' " |
| 1111 | . $extra_attrs_string |
| 1112 | . '>' |
| 1113 | . wp_kses_post( $label ) |
| 1114 | . ( $required && $required_indicator ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) . |
| 1115 | "</label>\n"; |
| 1116 | } |
| 1117 | |
| 1118 | /** |
| 1119 | * Whether the field's label is hidden by block visibility in any mode. |
| 1120 | * |
| 1121 | * Two modes hide a label: "hide everywhere" (labelhiddenbyblockvisibility, |
| 1122 | * the label is not rendered at all) and per-viewport (the label is rendered |
| 1123 | * but carries a wp-block-hidden-{viewport} class that display:none's it on |
| 1124 | * that viewport). Either way the label is absent from the accessibility tree |
| 1125 | * where hidden, so the field needs an aria-label fallback to keep a name. |
| 1126 | * |
| 1127 | * The visibility control now only survives on the label block (see |
| 1128 | * FORMS-694), so this is the single place that decides "is the label hidden?" |
| 1129 | * |
| 1130 | * @return bool |
| 1131 | */ |
| 1132 | private function is_label_hidden_by_block_visibility() { |
| 1133 | if ( $this->get_attribute( 'labelhiddenbyblockvisibility' ) ) { |
| 1134 | return true; |
| 1135 | } |
| 1136 | |
| 1137 | return strpos( (string) $this->get_attribute( 'labelclasses' ), 'wp-block-hidden-' ) !== false; |
| 1138 | } |
| 1139 | |
| 1140 | /** |
| 1141 | * The accessible name to fall back to when the label is hidden by block |
| 1142 | * visibility. Prefers the label text (it matches what sighted users see on |
| 1143 | * viewports where the label is visible, for the per-viewport case) and only |
| 1144 | * falls back to a secondary name when there is no label. See FORMS-694. |
| 1145 | * |
| 1146 | * @param string|null $fallback Secondary name to use when there is no label. |
| 1147 | * Must be a raw string, not a built attribute |
| 1148 | * fragment. Defaults to the raw placeholder. |
| 1149 | * @return string |
| 1150 | */ |
| 1151 | private function get_block_visibility_aria_label( $fallback = null ) { |
| 1152 | $label = Contact_Form_Plugin::strip_tags( (string) $this->get_attribute( 'label' ) ); |
| 1153 | if ( '' !== $label ) { |
| 1154 | return $label; |
| 1155 | } |
| 1156 | |
| 1157 | if ( null === $fallback ) { |
| 1158 | $fallback = $this->get_attribute( 'placeholder' ); |
| 1159 | } |
| 1160 | |
| 1161 | return Contact_Form_Plugin::strip_tags( (string) $fallback ); |
| 1162 | } |
| 1163 | |
| 1164 | /** |
| 1165 | * Return an ` aria-label='…'` attribute string when the field's label is |
| 1166 | * hidden by block visibility (full or per-viewport), or '' otherwise. |
| 1167 | * |
| 1168 | * A hidden label (removed on full-hide, display:none per-viewport) is absent |
| 1169 | * from the a11y tree, so every field that renders its own label — single |
| 1170 | * inputs and the grouped <fieldset> legend alike — needs this fallback to |
| 1171 | * keep an accessible name. Shared by all paths so none get missed. The |
| 1172 | * attribute is skipped when there is no name to give. See FORMS-694. |
| 1173 | * |
| 1174 | * @param string|null $fallback Secondary name when there is no label (raw |
| 1175 | * string, not a built attribute fragment). |
| 1176 | * @return string |
| 1177 | */ |
| 1178 | private function get_hidden_label_aria_label_attr( $fallback = null ) { |
| 1179 | if ( ! $this->is_label_hidden_by_block_visibility() ) { |
| 1180 | return ''; |
| 1181 | } |
| 1182 | |
| 1183 | $name = $this->get_block_visibility_aria_label( $fallback ); |
| 1184 | if ( '' === $name ) { |
| 1185 | return ''; |
| 1186 | } |
| 1187 | |
| 1188 | return " aria-label='" . esc_attr( $name ) . "'"; |
| 1189 | } |
| 1190 | |
| 1191 | /** |
| 1192 | * Accessible name for the file field's dropzone <div role="button">. |
| 1193 | * |
| 1194 | * The dropzone is the field's interactive control, but its name is otherwise |
| 1195 | * the generic "Select a file to upload.", so two hidden-label upload fields |
| 1196 | * would be announced identically. When the label is hidden (full or |
| 1197 | * per-viewport), prefix the field name — the same fallback the other single |
| 1198 | * inputs use. Falls back to the plain instruction when the label is visible |
| 1199 | * or there is no name to give. See FORMS-694. |
| 1200 | * |
| 1201 | * @param int $max_files How many files the field accepts. |
| 1202 | * |
| 1203 | * @return string |
| 1204 | */ |
| 1205 | private function get_file_dropzone_aria_label( $max_files ) { |
| 1206 | /* |
| 1207 | * The count belongs in the accessible name rather than only in the visible dropzone text, |
| 1208 | * which is an inner block the author writes and may leave saying "a file" for a field that |
| 1209 | * takes several. |
| 1210 | */ |
| 1211 | $select_file_text = $max_files > 1 |
| 1212 | ? sprintf( |
| 1213 | /* translators: %d: the maximum number of files the field accepts. */ |
| 1214 | _n( |
| 1215 | 'Select up to %d file to upload.', |
| 1216 | 'Select up to %d files to upload.', |
| 1217 | $max_files, |
| 1218 | 'jetpack-forms' |
| 1219 | ), |
| 1220 | $max_files |
| 1221 | ) |
| 1222 | : __( 'Select a file to upload.', 'jetpack-forms' ); |
| 1223 | |
| 1224 | if ( ! $this->is_label_hidden_by_block_visibility() ) { |
| 1225 | return $select_file_text; |
| 1226 | } |
| 1227 | |
| 1228 | $hidden_name = $this->get_block_visibility_aria_label(); |
| 1229 | if ( '' === $hidden_name ) { |
| 1230 | return $select_file_text; |
| 1231 | } |
| 1232 | |
| 1233 | return sprintf( |
| 1234 | /* translators: 1: the form field's label, 2: the "Select a file to upload." instruction. */ |
| 1235 | _x( '%1$s: %2$s', 'file upload field accessible name', 'jetpack-forms' ), |
| 1236 | $hidden_name, |
| 1237 | $select_file_text |
| 1238 | ); |
| 1239 | } |
| 1240 | |
| 1241 | /** |
| 1242 | * Return the HTML for a legend that shares the same style as a label. |
| 1243 | * |
| 1244 | * @param string $type - the field type. |
| 1245 | * @param int $id - the ID. |
| 1246 | * @param string $legend - the legend. |
| 1247 | * @param bool $required - if the field is marked as required. |
| 1248 | * @param string $required_field_text - the text in the required text field. |
| 1249 | * @param array $extra_attrs Array of key/value pairs to append as attributes to the element. |
| 1250 | * @param bool $required_indicator Whether to display the required indicator. |
| 1251 | * |
| 1252 | * @return string HTML |
| 1253 | */ |
| 1254 | public function render_legend_as_label( $type, $id, $legend, $required, $required_field_text, $extra_attrs = array(), $required_indicator = true ) { |
| 1255 | // Full-hide via blockVisibility, mirroring render_label(). Grouped fields |
| 1256 | // (radio, checkbox-multiple, image-select, rating) render their label as a |
| 1257 | // legend, so they need the same guard. Per-viewport hiding still works via |
| 1258 | // the label_classes applied below. |
| 1259 | if ( $this->attributes['labelhiddenbyblockvisibility'] ) { |
| 1260 | return ''; |
| 1261 | } |
| 1262 | if ( ! empty( $this->label_styles ) ) { |
| 1263 | $extra_attrs['style'] = $this->label_styles; |
| 1264 | } |
| 1265 | |
| 1266 | $type_class = $type ? ' ' . $type : ''; |
| 1267 | $extra_attrs['class'] = "grunion-field-label{$type_class}" . ( $this->is_error() ? ' form-error' : '' ); |
| 1268 | |
| 1269 | if ( ! empty( $this->label_classes ) ) { |
| 1270 | $extra_attrs['class'] .= ' ' . $this->label_classes; |
| 1271 | } |
| 1272 | |
| 1273 | $extra_attrs_string = ''; |
| 1274 | if ( is_array( $extra_attrs ) ) { |
| 1275 | foreach ( $extra_attrs as $attr => $val ) { |
| 1276 | $extra_attrs_string .= sprintf( '%s="%s" ', esc_attr( $attr ), esc_attr( $val ) ); |
| 1277 | } |
| 1278 | } |
| 1279 | |
| 1280 | return '<legend ' |
| 1281 | . $extra_attrs_string |
| 1282 | . '>' |
| 1283 | . '<span class="grunion-label-text">' . wp_kses_post( $legend ) . '</span>' |
| 1284 | . ( $required && $required_indicator ? '<span class="grunion-label-required">' . $required_field_text . '</span>' : '' ) |
| 1285 | . "</legend>\n"; |
| 1286 | } |
| 1287 | |
| 1288 | /** |
| 1289 | * Return the HTML for the input field. |
| 1290 | * |
| 1291 | * @param string $type - the field type. |
| 1292 | * @param int $id - the ID. |
| 1293 | * @param string $value - the value of the field. |
| 1294 | * @param string $class - the field class. |
| 1295 | * @param string $placeholder - the field placeholder content. |
| 1296 | * @param bool $required - if the field is marked as required. |
| 1297 | * @param array $extra_attrs Array of key/value pairs to append as attributes to the element. |
| 1298 | * |
| 1299 | * @return string HTML |
| 1300 | */ |
| 1301 | public function render_input_field( $type, $id, $value, $class, $placeholder, $required, $extra_attrs = array() ) { |
| 1302 | if ( ! is_string( $value ) ) { |
| 1303 | $value = ''; |
| 1304 | } |
| 1305 | |
| 1306 | $extra_attrs_string = ''; |
| 1307 | |
| 1308 | if ( ! empty( $this->field_styles ) ) { |
| 1309 | $extra_attrs['style'] = $this->field_styles; |
| 1310 | } |
| 1311 | |
| 1312 | if ( is_array( $extra_attrs ) && ! empty( $extra_attrs ) ) { |
| 1313 | foreach ( $extra_attrs as $attr => $val ) { |
| 1314 | $extra_attrs_string .= sprintf( '%s="%s" ', esc_attr( $attr ), esc_attr( $val ) ); |
| 1315 | } |
| 1316 | } |
| 1317 | |
| 1318 | // this is a hack for Firefox to prevent users from falsely entering a something other than a number into a number field. |
| 1319 | if ( $type === 'number' ) { |
| 1320 | $extra_attrs_string .= " data-wp-on--keypress='actions.handleNumberKeyPress' "; |
| 1321 | } |
| 1322 | |
| 1323 | $extra_attrs_string .= $this->get_hidden_label_aria_label_attr(); |
| 1324 | |
| 1325 | return "<input |
| 1326 | type='" . esc_attr( $type ) . "' |
| 1327 | name='" . esc_attr( $id ) . "' |
| 1328 | id='" . esc_attr( $id ) . "' |
| 1329 | value='" . esc_attr( $value ) . "' |
| 1330 | |
| 1331 | data-wp-bind--aria-invalid='state.fieldAriaInvalid' |
| 1332 | data-wp-bind--value='state.getFieldValue' |
| 1333 | aria-describedby='" . esc_attr( $this->get_described_by( $id, $type ) ) . "' |
| 1334 | data-wp-on--input='actions.onFieldChange' |
| 1335 | data-wp-on--blur='actions.onFieldBlur' |
| 1336 | data-wp-class--has-value='state.hasFieldValue' |
| 1337 | |
| 1338 | " . $class . $placeholder . ' |
| 1339 | ' . ( $required ? "required='true' aria-required='true' " : '' ) . |
| 1340 | $extra_attrs_string . |
| 1341 | " />\n " . $this->get_field_descriptions( $id, $type ) . " \n"; |
| 1342 | } |
| 1343 | |
| 1344 | /** |
| 1345 | * Return the HTML for the error div. |
| 1346 | * |
| 1347 | * @param string $id - the field ID. |
| 1348 | * @param string $type - the field type. |
| 1349 | * @param bool $override_render - if the error div should be rendered even if the label is inset. |
| 1350 | * |
| 1351 | * @return string HTML |
| 1352 | */ |
| 1353 | private function get_error_div( $id, $type, $override_render = false ) { |
| 1354 | |
| 1355 | if ( ! $override_render && $this->has_inset_label() ) { |
| 1356 | return ''; |
| 1357 | } |
| 1358 | return ' |
| 1359 | <div id="' . esc_attr( $id ) . '-' . esc_attr( $type ) . '-error" class="contact-form__input-error" data-wp-class--has-errors="state.fieldHasErrors"> |
| 1360 | <span class="contact-form__warning-icon" aria-hidden="true"> |
| 1361 | <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 1362 | <path d="M8.50015 11.6402H7.50015V10.6402H8.50015V11.6402Z" /> |
| 1363 | <path d="M7.50015 9.64018H8.50015V6.30684H7.50015V9.64018Z" /> |
| 1364 | <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" /> |
| 1365 | </svg> |
| 1366 | </span> |
| 1367 | <span data-wp-text="state.errorMessage" id="' . esc_attr( $id ) . '-' . esc_attr( $type ) . '-error-message"></span> |
| 1368 | </div>'; |
| 1369 | } |
| 1370 | |
| 1371 | /** |
| 1372 | * The author-supplied help text for this field, or null when unset. |
| 1373 | * |
| 1374 | * @since 8.0.0 |
| 1375 | * |
| 1376 | * @return string|null |
| 1377 | */ |
| 1378 | private function get_help_text() { |
| 1379 | $help_text = $this->get_attribute( 'helptext' ); |
| 1380 | |
| 1381 | if ( ! is_string( $help_text ) ) { |
| 1382 | return null; |
| 1383 | } |
| 1384 | |
| 1385 | $help_text = trim( $help_text ); |
| 1386 | |
| 1387 | if ( $help_text === '' ) { |
| 1388 | return null; |
| 1389 | } |
| 1390 | |
| 1391 | // Contact_Form::esc_shortcode_val() encodes `,` `[` `]` `\` as decimal |
| 1392 | // entities so they survive the shortcode parser, but unesc_attr() only |
| 1393 | // decodes the hex forms. Other consumers emit through wp_kses_post() or |
| 1394 | // into an attribute, so the browser decodes whatever is left over; this |
| 1395 | // is the first to emit through esc_html() into a text node, where the |
| 1396 | // leftover `,` would be escaped again and shown to the visitor. |
| 1397 | // Safe to decode here: the value has already been through |
| 1398 | // unesc_attr()'s strip_tags(), and output is still esc_html()'d. |
| 1399 | return html_entity_decode( $help_text, ENT_QUOTES ); |
| 1400 | } |
| 1401 | |
| 1402 | /** |
| 1403 | * The format instruction for date fields, or null for every other type. |
| 1404 | * |
| 1405 | * Note this keys off the field's own `type` attribute, not the type passed |
| 1406 | * to the render helpers — the date field renders a `text` input. |
| 1407 | * |
| 1408 | * @since 8.0.0 |
| 1409 | * |
| 1410 | * @return string|null |
| 1411 | */ |
| 1412 | private function get_format_hint() { |
| 1413 | if ( $this->get_attribute( 'type' ) !== 'date' ) { |
| 1414 | return null; |
| 1415 | } |
| 1416 | |
| 1417 | $formats = self::get_date_formats(); |
| 1418 | |
| 1419 | return $formats[ $this->get_date_format() ] ?? null; |
| 1420 | } |
| 1421 | |
| 1422 | /** |
| 1423 | * The aria-describedby value for a field's input. |
| 1424 | * |
| 1425 | * The error message comes first so screen readers announce it before the |
| 1426 | * advisory text; its span is empty until there is an error, so it costs |
| 1427 | * nothing the rest of the time. |
| 1428 | * |
| 1429 | * @since 8.0.0 |
| 1430 | * |
| 1431 | * @param string $id - the field ID. |
| 1432 | * @param string $type - the description type (matches the emitted element ids). |
| 1433 | * |
| 1434 | * @return string |
| 1435 | */ |
| 1436 | private function get_described_by( $id, $type ) { |
| 1437 | $ids = array( $id . '-' . $type . '-error-message' ); |
| 1438 | |
| 1439 | foreach ( $this->get_description_parts( $id, $type ) as $part ) { |
| 1440 | $ids[] = $part['id']; |
| 1441 | } |
| 1442 | |
| 1443 | return implode( ' ', $ids ); |
| 1444 | } |
| 1445 | |
| 1446 | /** |
| 1447 | * The advisory descriptions attached to a field, in DOM order. |
| 1448 | * |
| 1449 | * Single source for both the ids aria-describedby references and the |
| 1450 | * markup that carries them — building those separately is how they drift, |
| 1451 | * and a describedby pointing at an id that was never emitted is exactly |
| 1452 | * the failure this class already fixes elsewhere. |
| 1453 | * |
| 1454 | * @since 8.0.0 |
| 1455 | * |
| 1456 | * @param string $id - the field ID. |
| 1457 | * @param string $type - the description type (matches get_described_by()). |
| 1458 | * |
| 1459 | * @return array List of array( 'id' => string, 'class' => string, 'text' => string ). |
| 1460 | */ |
| 1461 | private function get_description_parts( $id, $type ) { |
| 1462 | $parts = array(); |
| 1463 | $help_text = $this->get_help_text(); |
| 1464 | |
| 1465 | if ( $help_text !== null ) { |
| 1466 | $parts[] = array( |
| 1467 | 'id' => $id . '-' . $type . '-help', |
| 1468 | 'class' => 'contact-form__field-help', |
| 1469 | 'text' => $help_text, |
| 1470 | ); |
| 1471 | } |
| 1472 | |
| 1473 | $format_hint = $this->get_format_hint(); |
| 1474 | |
| 1475 | if ( $format_hint !== null ) { |
| 1476 | $parts[] = array( |
| 1477 | 'id' => $id . '-' . $type . '-format', |
| 1478 | 'class' => 'contact-form__field-format', |
| 1479 | 'text' => $format_hint, |
| 1480 | ); |
| 1481 | } |
| 1482 | |
| 1483 | return $parts; |
| 1484 | } |
| 1485 | |
| 1486 | /** |
| 1487 | * Return the HTML for a field's descriptions: help text, format hint, and |
| 1488 | * the error div, in that DOM order. |
| 1489 | * |
| 1490 | * These travel together because inset-label form styles render them outside |
| 1491 | * the field wrapper. In that case the markup is deferred to |
| 1492 | * $this->deferred_descriptions and render_field() emits it, so it is only |
| 1493 | * ever built here — with the same $type the input used for its |
| 1494 | * aria-describedby. |
| 1495 | * |
| 1496 | * @since 8.0.0 |
| 1497 | * |
| 1498 | * @param string $id - the field ID. |
| 1499 | * @param string $type - the description type (matches get_described_by()). |
| 1500 | * |
| 1501 | * @return string HTML, or an empty string when the markup is deferred. |
| 1502 | */ |
| 1503 | private function get_field_descriptions( $id, $type ) { |
| 1504 | $hints = ''; |
| 1505 | |
| 1506 | // Deliberately spans, not paragraphs: themes style <p> (margins, size, |
| 1507 | // color) and we would have to fight those rules on every theme. |
| 1508 | foreach ( $this->get_description_parts( $id, $type ) as $part ) { |
| 1509 | $hints .= '<span id="' . esc_attr( $part['id'] ) . '" class="' . esc_attr( $part['class'] ) . '">' |
| 1510 | . esc_html( $part['text'] ) . '</span>'; |
| 1511 | } |
| 1512 | |
| 1513 | $descriptions = ''; |
| 1514 | |
| 1515 | if ( $hints !== '' ) { |
| 1516 | $descriptions .= '<div class="contact-form__field-hints">' . $hints . '</div>'; |
| 1517 | } |
| 1518 | |
| 1519 | // Force the error div to build even for inset labels — the deferral |
| 1520 | // below decides where it lands. |
| 1521 | $descriptions .= $this->get_error_div( $id, $type, true ); |
| 1522 | |
| 1523 | if ( $this->has_inset_label() ) { |
| 1524 | $this->deferred_descriptions = $descriptions; |
| 1525 | return ''; |
| 1526 | } |
| 1527 | |
| 1528 | return $descriptions; |
| 1529 | } |
| 1530 | |
| 1531 | /** |
| 1532 | * Set the invalid message for specific field types. |
| 1533 | * |
| 1534 | * @param string $type - the field type. |
| 1535 | * @param string $message - the message to display. |
| 1536 | * |
| 1537 | * @return void |
| 1538 | */ |
| 1539 | private function set_invalid_message( $type, $message ) { |
| 1540 | wp_interactivity_config( |
| 1541 | 'jetpack/form', |
| 1542 | array( |
| 1543 | 'error_types' => array( |
| 1544 | 'invalid_' . $type => $message, |
| 1545 | ), |
| 1546 | ) |
| 1547 | ); |
| 1548 | } |
| 1549 | |
| 1550 | /** |
| 1551 | * Return the HTML for the email field. |
| 1552 | * |
| 1553 | * @param int $id - the ID. |
| 1554 | * @param string $label - the label. |
| 1555 | * @param string $value - the value of the field. |
| 1556 | * @param string $class - the field class. |
| 1557 | * @param bool $required - if the field is marked as required. |
| 1558 | * @param string $required_field_text - the text in the required text field. |
| 1559 | * @param string $placeholder - the field placeholder content. |
| 1560 | * @param bool $required_indicator Whether to display the required indicator. |
| 1561 | * |
| 1562 | * @return string HTML |
| 1563 | */ |
| 1564 | public function render_email_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) { |
| 1565 | $this->set_invalid_message( 'email', __( 'Please enter a valid email address', 'jetpack-forms' ) ); |
| 1566 | $field = $this->render_label( 'email', $id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 1567 | $field .= $this->render_input_field( 'email', $id, $value, $class, $placeholder, $required ); |
| 1568 | return $field; |
| 1569 | } |
| 1570 | |
| 1571 | /** |
| 1572 | * Return the HTML for the telephone field. |
| 1573 | * |
| 1574 | * @param int $id - the ID. |
| 1575 | * @param string $label - the label. |
| 1576 | * @param string $value - the value of the field. |
| 1577 | * @param string $class - the field class. |
| 1578 | * @param bool $required - if the field is marked as required. |
| 1579 | * @param string $required_field_text - the text in the required text field. |
| 1580 | * @param string $placeholder - the field placeholder content. |
| 1581 | * @param bool $required_indicator Whether to display the required indicator. |
| 1582 | * |
| 1583 | * @return string HTML |
| 1584 | */ |
| 1585 | public function render_telephone_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) { |
| 1586 | $show_country_selector = $this->get_attribute( 'showcountryselector' ); |
| 1587 | $default_country = $this->get_attribute( 'default' ); |
| 1588 | $search_placeholder = $this->get_attribute( 'searchplaceholder' ); |
| 1589 | |
| 1590 | if ( ! $show_country_selector ) { |
| 1591 | // old telephone field treatment |
| 1592 | $this->set_invalid_message( 'telephone', __( 'Please enter a valid phone number', 'jetpack-forms' ) ); |
| 1593 | $label = $this->render_label( 'telephone', $id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 1594 | $field = $this->render_input_field( 'tel', $id, $value, $class, $placeholder, $required ); |
| 1595 | return $label . $field; |
| 1596 | } |
| 1597 | |
| 1598 | if ( empty( $search_placeholder ) ) { |
| 1599 | $search_placeholder = __( 'Search countries…', 'jetpack-forms' ); |
| 1600 | } |
| 1601 | |
| 1602 | $this->enqueue_phone_field_assets(); |
| 1603 | |
| 1604 | // $class is ill-formed, so we need to fix it |
| 1605 | // Strip 'class=' and quotes to get just the class names |
| 1606 | $class_names = preg_replace( "/^class=['\"]([^'\"]*)['\"].*$/", '$1', $class ); |
| 1607 | // somehow we are getting the class jetpack-field__input-element on the wrong wrapper |
| 1608 | // .jetpack-field__input-element is meant to be applied just to the input element, not its wrapper. |
| 1609 | // Remove the jetpack-field__input-element class token regardless of its position and normalize whitespace. |
| 1610 | $class_names = preg_replace( '/\s*\bjetpack-field__input-element\b\s*/', ' ', $class_names ); |
| 1611 | $class_names = trim( preg_replace( '/\s+/', ' ', $class_names ) ); |
| 1612 | |
| 1613 | $link_label_id = $id . '-number'; |
| 1614 | |
| 1615 | $this->set_invalid_message( 'phone', __( 'Please enter a valid phone number', 'jetpack-forms' ) ); |
| 1616 | $label = $this->render_label( 'phone', $link_label_id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 1617 | if ( ! is_string( $value ) ) { |
| 1618 | $value = ''; |
| 1619 | } |
| 1620 | |
| 1621 | $translated_countries = $this->get_translatable_countries(); |
| 1622 | $global_config = array( |
| 1623 | 'i18n' => array( |
| 1624 | 'countryNames' => $translated_countries, |
| 1625 | ), |
| 1626 | ); |
| 1627 | wp_interactivity_config( 'jetpack/field-phone', $global_config ); |
| 1628 | ob_start(); |
| 1629 | ?> |
| 1630 | <div |
| 1631 | class="jetpack-field__input-phone-wrapper <?php echo esc_attr( $this->get_attribute( 'stylevariationclasses' ) ); ?> <?php echo esc_attr( $class_names ); ?>" |
| 1632 | style="<?php echo ( ! empty( $this->field_styles ) && is_string( $this->field_styles ) ? esc_attr( $this->field_styles ) : '' ); ?>" |
| 1633 | data-wp-on--jetpack-form-reset='actions.phoneResetHandler' |
| 1634 | data-wp-class--is-combobox-open="context.comboboxOpen" |
| 1635 | <?php |
| 1636 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- function is supposed to work this way |
| 1637 | echo wp_interactivity_data_wp_context( |
| 1638 | array( |
| 1639 | 'fieldId' => $id, |
| 1640 | 'defaultCountry' => $default_country, |
| 1641 | 'showCountrySelector' => $this->get_attribute( 'showcountryselector' ), |
| 1642 | // dynamic |
| 1643 | 'phoneNumber' => '', |
| 1644 | 'phoneCountryCode' => $default_country, |
| 1645 | 'fullPhoneNumber' => '', |
| 1646 | 'countryPrefix' => '', |
| 1647 | // combobox state |
| 1648 | 'useCombobox' => true, |
| 1649 | 'comboboxOpen' => false, |
| 1650 | 'searchTerm' => '', |
| 1651 | 'allCountries' => array(), |
| 1652 | 'filteredCountries' => array(), |
| 1653 | 'selectedCountry' => array(), |
| 1654 | ) |
| 1655 | ); |
| 1656 | ?> |
| 1657 | > |
| 1658 | <div class="jetpack-field__input-prefix" |
| 1659 | data-wp-bind--hidden="!context.showCountrySelector" |
| 1660 | data-wp-on-document--click="actions.phoneComboboxDocumentClickHandler"> |
| 1661 | <div class="jetpack-custom-combobox"> |
| 1662 | |
| 1663 | <button |
| 1664 | class="jetpack-combobox-trigger" |
| 1665 | type="button" |
| 1666 | data-wp-on--click="actions.phoneComboboxToggle" |
| 1667 | data-wp-bind--aria-expanded="context.comboboxOpen"> |
| 1668 | <span |
| 1669 | class="jetpack-combobox-selected wp-exclude-emoji" |
| 1670 | data-wp-watch="callbacks.updateSelectedFlag">​</span> |
| 1671 | <span |
| 1672 | class="jetpack-combobox-trigger-arrow" |
| 1673 | data-wp-class--is-open="context.comboboxOpen"> |
| 1674 | <svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 1675 | <path d="M1 1L5 5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
| 1676 | </svg> |
| 1677 | </span> |
| 1678 | <span |
| 1679 | class="jetpack-combobox-selected" |
| 1680 | data-wp-text="context.selectedCountry.value"></span> |
| 1681 | </button> |
| 1682 | <div |
| 1683 | class="jetpack-combobox-dropdown <?php echo esc_attr( $this->get_attribute( 'stylevariationclasses' ) ); ?>" |
| 1684 | style="<?php echo ( ! empty( $this->field_styles ) && is_string( $this->field_styles ) ? esc_attr( $this->field_styles ) : '' ); ?>" |
| 1685 | data-wp-bind--hidden="!context.comboboxOpen"> |
| 1686 | <input |
| 1687 | class="jetpack-combobox-search" |
| 1688 | type="text" |
| 1689 | placeholder="<?php echo esc_attr( $search_placeholder ); ?>" |
| 1690 | data-wp-on--input="actions.phoneComboboxInputHandler" |
| 1691 | data-wp-on--keydown="actions.phoneComboboxKeydownHandler" |
| 1692 | data-wp-init="callbacks.registerPhoneComboboxSearchInput"> |
| 1693 | <div class="jetpack-combobox-options" data-wp-init="callbacks.registerPhoneComboboxOptionsList"> |
| 1694 | <template |
| 1695 | data-wp-each--filtered="context.filteredCountries" |
| 1696 | data-wp-each-key="context.filtered.code"> |
| 1697 | <div |
| 1698 | class="jetpack-combobox-option" |
| 1699 | data-wp-key="context.filtered.code" |
| 1700 | data-wp-class--jetpack-combobox-option-selected="context.filtered.selected" |
| 1701 | data-wp-on--click="actions.phoneCountryChangeHandler"> |
| 1702 | <span class="jetpack-combobox-option-icon wp-exclude-emoji" data-wp-watch="callbacks.updateOptionFlag">​</span> |
| 1703 | <span class="jetpack-combobox-option-value" data-wp-text="context.filtered.value"></span> |
| 1704 | <span class="jetpack-combobox-option-description" data-wp-text="context.filtered.country"></span> |
| 1705 | </div> |
| 1706 | </template> |
| 1707 | </div> |
| 1708 | </div> |
| 1709 | </div> |
| 1710 | </div> |
| 1711 | <input |
| 1712 | class="jetpack-field__input-element" |
| 1713 | <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- both are escaped in calling function ?> |
| 1714 | <?php echo $placeholder; ?> |
| 1715 | type="tel" |
| 1716 | <?php if ( $required ) { ?> |
| 1717 | required="true" |
| 1718 | aria-required="true" |
| 1719 | <?php } ?> |
| 1720 | id="<?php echo esc_attr( $link_label_id ); ?>" |
| 1721 | name="<?php echo esc_attr( $link_label_id ); ?>" |
| 1722 | data-wp-bind--disabled='state.isSubmitting' |
| 1723 | data-wp-bind--aria-invalid='state.fieldAriaInvalid' |
| 1724 | data-wp-bind--value='context.phoneNumber' |
| 1725 | aria-describedby="<?php echo esc_attr( $this->get_described_by( $id, 'telephone' ) ); ?>" |
| 1726 | data-wp-on--input='actions.phoneNumberInputHandler' |
| 1727 | data-wp-on--blur='actions.onFieldBlur' |
| 1728 | data-wp-on--focus='actions.phoneNumberFocusHandler' |
| 1729 | data-wp-class--has-value='context.phoneNumber' |
| 1730 | data-wp-init="callbacks.registerPhoneInput" |
| 1731 | data-wp-init--phone-field-custom-combobox="callbacks.initializePhoneFieldCustomComboBox" |
| 1732 | <?php echo $this->get_hidden_label_aria_label_attr(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute value is escaped in the helper. ?> |
| 1733 | /> |
| 1734 | <input type="hidden" |
| 1735 | id="<?php echo esc_attr( $id ); ?>" |
| 1736 | name="<?php echo esc_attr( $id ); ?>" |
| 1737 | data-wp-bind--value='context.fullPhoneNumber' /> |
| 1738 | </div> |
| 1739 | <?php |
| 1740 | $input = ob_get_clean(); |
| 1741 | |
| 1742 | $field = $label . $input . $this->get_field_descriptions( $id, 'telephone' ); |
| 1743 | return $field; |
| 1744 | } |
| 1745 | |
| 1746 | /** |
| 1747 | * Return the HTML for the URL field. |
| 1748 | * |
| 1749 | * @param int $id - the ID. |
| 1750 | * @param string $label - the label. |
| 1751 | * @param string $value - the value of the field. |
| 1752 | * @param string $class - the field class. |
| 1753 | * @param bool $required - if the field is marked as required. |
| 1754 | * @param string $required_field_text - the text in the required text field. |
| 1755 | * @param string $placeholder - the field placeholder content. |
| 1756 | * @param bool $required_indicator Whether to display the required indicator. |
| 1757 | * |
| 1758 | * @return string HTML |
| 1759 | */ |
| 1760 | public function render_url_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) { |
| 1761 | $this->set_invalid_message( 'url', __( 'Please enter a valid URL - https://www.example.com', 'jetpack-forms' ) ); |
| 1762 | |
| 1763 | $field = $this->render_label( 'url', $id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 1764 | $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required ); |
| 1765 | return $field; |
| 1766 | } |
| 1767 | |
| 1768 | /** |
| 1769 | * Return the HTML for the text area field. |
| 1770 | * |
| 1771 | * @param int $id - the ID. |
| 1772 | * @param string $label - the label. |
| 1773 | * @param string $value - the value of the field. |
| 1774 | * @param string $class - the field class. |
| 1775 | * @param bool $required - if the field is marked as required. |
| 1776 | * @param string $required_field_text - the text in the required text field. |
| 1777 | * @param string $placeholder - the field placeholder content. |
| 1778 | * @param bool $required_indicator Whether to display the required indicator. |
| 1779 | * |
| 1780 | * @return string HTML |
| 1781 | */ |
| 1782 | public function render_textarea_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) { |
| 1783 | if ( ! is_string( $value ) ) { |
| 1784 | $value = ''; |
| 1785 | } |
| 1786 | |
| 1787 | $field = $this->render_label( 'textarea', 'contact-form-comment-' . $id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 1788 | $field .= "<textarea |
| 1789 | style='" . $this->field_styles . "' |
| 1790 | name='" . esc_attr( $id ) . "' |
| 1791 | id='contact-form-comment-" . esc_attr( $id ) . "' |
| 1792 | rows='20' |
| 1793 | data-wp-text='state.getFieldValue' |
| 1794 | data-wp-on--input='actions.onFieldChange' |
| 1795 | data-wp-on--blur='actions.onFieldBlur' |
| 1796 | data-wp-class--has-value='state.hasFieldValue' |
| 1797 | aria-describedby='" . esc_attr( $this->get_described_by( $id, 'textarea' ) ) . "' |
| 1798 | data-wp-bind--aria-invalid='state.fieldAriaInvalid' |
| 1799 | " |
| 1800 | . $this->get_hidden_label_aria_label_attr() |
| 1801 | . $class |
| 1802 | . $placeholder |
| 1803 | . ' ' . ( $required ? "required aria-required='true'" : '' ) . |
| 1804 | '>' . esc_textarea( $value ) |
| 1805 | . "</textarea>\n " . $this->get_field_descriptions( $id, 'textarea' ) . "\n"; |
| 1806 | return $field; |
| 1807 | } |
| 1808 | |
| 1809 | /** |
| 1810 | * Return the HTML for the radio field. |
| 1811 | * |
| 1812 | * @param string $id - the ID (starts with 'g' - see constructor). |
| 1813 | * @param string $label - the label. |
| 1814 | * @param string $value - the value of the field. |
| 1815 | * @param string $class - the field class. |
| 1816 | * @param bool $required - if the field is marked as required. |
| 1817 | * @param string $required_field_text - the text in the required text field. |
| 1818 | * @param bool $required_indicator Whether to display the required indicator. |
| 1819 | * |
| 1820 | * @return string HTML |
| 1821 | */ |
| 1822 | public function render_radio_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) { |
| 1823 | $this->set_invalid_message( 'radio', __( 'Please select one of the options.', 'jetpack-forms' ) ); |
| 1824 | $options_classes = $this->get_attribute( 'optionsclasses' ); |
| 1825 | $options_styles = $this->get_attribute( 'optionsstyles' ); |
| 1826 | $form_style = $this->get_form_style(); |
| 1827 | $is_outlined_style = 'outlined' === $form_style; |
| 1828 | $fieldset_id = "id='" . esc_attr( "$id-label" ) . "'" . $this->get_hidden_label_aria_label_attr(); |
| 1829 | |
| 1830 | if ( $is_outlined_style ) { |
| 1831 | $style_variation_attributes = $this->get_attribute( 'stylevariationattributes' ); |
| 1832 | |
| 1833 | if ( ! empty( $style_variation_attributes ) ) { |
| 1834 | $style_variation_attributes = json_decode( html_entity_decode( $style_variation_attributes, ENT_COMPAT ), true ); |
| 1835 | } |
| 1836 | |
| 1837 | // When there's an outlined style, and border radius is set, the existing inline border radius is overridden to apply |
| 1838 | // a limit of `100px` to the radius on the x axis. This achieves the same look and feel as other fields |
| 1839 | // that use the notch html (`notched-label__leading` has a max-width of `100px` to prevent it from getting too wide). |
| 1840 | // It prevents large border radius values from disrupting the look and feel of the fields. |
| 1841 | if ( isset( $style_variation_attributes['border']['radius'] ) ) { |
| 1842 | $options_styles ??= ''; |
| 1843 | $radius = $style_variation_attributes['border']['radius']; |
| 1844 | $has_split_radius_values = is_array( $radius ); |
| 1845 | $top_left_radius = $has_split_radius_values ? $radius['topLeft'] : $radius; |
| 1846 | $top_right_radius = $has_split_radius_values ? $radius['topRight'] : $radius; |
| 1847 | $bottom_left_radius = $has_split_radius_values ? $radius['bottomLeft'] : $radius; |
| 1848 | $bottom_right_radius = $has_split_radius_values ? $radius['bottomRight'] : $radius; |
| 1849 | $options_styles .= "border-top-left-radius: min(100px, {$top_left_radius}) {$top_left_radius};"; |
| 1850 | $options_styles .= "border-top-right-radius: min(100px, {$top_right_radius}) {$top_right_radius};"; |
| 1851 | $options_styles .= "border-bottom-left-radius: min(100px, {$bottom_left_radius}) {$bottom_left_radius};"; |
| 1852 | $options_styles .= "border-bottom-right-radius: min(100px, {$bottom_right_radius}) {$bottom_right_radius};"; |
| 1853 | } |
| 1854 | |
| 1855 | /* |
| 1856 | * For the "outlined" style, the styles and classes are applied to the fieldset element. |
| 1857 | */ |
| 1858 | $field = "<fieldset {$fieldset_id} class='grunion-radio-options " . esc_attr( $options_classes ) . "' style='" . esc_attr( $options_styles ) . "' data-wp-bind--aria-invalid='state.fieldAriaInvalid' >"; |
| 1859 | } else { |
| 1860 | $field = "<fieldset {$fieldset_id} class='jetpack-field-multiple__fieldset' data-wp-bind--aria-invalid='state.fieldAriaInvalid' >"; |
| 1861 | } |
| 1862 | |
| 1863 | $field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text, array(), $required_indicator ); |
| 1864 | |
| 1865 | if ( ! $is_outlined_style ) { |
| 1866 | $field .= "<div class='grunion-radio-options " . esc_attr( $options_classes ) . "' style='" . esc_attr( $options_styles ) . "'>"; |
| 1867 | } |
| 1868 | |
| 1869 | $options_data = $this->get_attribute( 'optionsdata' ); |
| 1870 | $used_html_ids = array(); |
| 1871 | |
| 1872 | if ( ! empty( $options_data ) ) { |
| 1873 | foreach ( $options_data as $option_index => $option ) { |
| 1874 | $option_label = Contact_Form_Plugin::strip_tags( $option['label'] ); |
| 1875 | if ( is_string( $option_label ) && '' !== $option_label ) { |
| 1876 | $radio_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option_label ); |
| 1877 | $radio_id = $id . '-' . sanitize_html_class( $radio_value ); |
| 1878 | |
| 1879 | // If exact id was already used in this radio group, append option index. |
| 1880 | // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc. |
| 1881 | if ( isset( $used_html_ids[ $radio_id ] ) ) { |
| 1882 | $radio_id .= '-' . $option_index; |
| 1883 | } |
| 1884 | $used_html_ids[ $radio_id ] = true; |
| 1885 | |
| 1886 | $default_classes = 'contact-form-field'; |
| 1887 | $option_styles = empty( $option['style'] ) ? '' : "style='" . esc_attr( $option['style'] ) . "'"; |
| 1888 | $option_classes = empty( $option['class'] ) ? $default_classes : $default_classes . ' ' . esc_attr( $option['class'] ); |
| 1889 | |
| 1890 | $is_other_option = ! empty( $option['isOther'] ); |
| 1891 | $change_action = $is_other_option ? 'actions.onOtherRadioChange' : 'actions.onFieldChange'; |
| 1892 | $other_label_attr = $is_other_option ? " data-other-label='" . esc_attr( $option_label ) . "'" : ''; |
| 1893 | |
| 1894 | $field .= "<label {$option_styles} class='{$option_classes}'>"; |
| 1895 | $field .= "<input |
| 1896 | id='" . esc_attr( $radio_id ) . "' |
| 1897 | type='radio' |
| 1898 | name='" . esc_attr( $id ) . "' |
| 1899 | value='" . esc_attr( $radio_value ) . "' |
| 1900 | data-wp-on--change='" . $change_action . "'" |
| 1901 | . $other_label_attr . ' ' |
| 1902 | . $class |
| 1903 | . checked( $option_label, $value, false ) . ' ' |
| 1904 | . ( $required ? "required aria-required='true'" : '' ) |
| 1905 | . '/> '; |
| 1906 | $field .= "<span class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>"; |
| 1907 | $field .= "<span class='grunion-field-text'>" . esc_html( $option_label ) . '</span>'; |
| 1908 | $field .= '</span>'; |
| 1909 | $field .= '</label>'; |
| 1910 | |
| 1911 | if ( $is_other_option ) { |
| 1912 | $placeholder = ! empty( $option['otherPlaceholder'] ) ? $option['otherPlaceholder'] : ''; |
| 1913 | $field .= $this->render_other_input_field( $radio_id, $required, $id, $this->field_styles, $placeholder ); |
| 1914 | } |
| 1915 | } |
| 1916 | } |
| 1917 | } else { |
| 1918 | $field_style = 'style="' . $this->option_styles . '"'; |
| 1919 | |
| 1920 | foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) { |
| 1921 | $option = Contact_Form_Plugin::strip_tags( $option ); |
| 1922 | if ( is_string( $option ) && '' !== $option ) { |
| 1923 | $radio_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option ); |
| 1924 | $radio_id = $id . '-' . sanitize_html_class( $radio_value ); |
| 1925 | |
| 1926 | // If exact id was already used in this radio group, append option index. |
| 1927 | // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc. |
| 1928 | if ( isset( $used_html_ids[ $radio_id ] ) ) { |
| 1929 | $radio_id .= '-' . $option_index; |
| 1930 | } |
| 1931 | $used_html_ids[ $radio_id ] = true; |
| 1932 | |
| 1933 | $field .= "<p class='contact-form-field'>"; |
| 1934 | $field .= "<input |
| 1935 | id='" . esc_attr( $radio_id ) . "' |
| 1936 | type='radio' |
| 1937 | name='" . esc_attr( $id ) . "' |
| 1938 | value='" . esc_attr( $radio_value ) . "' |
| 1939 | data-wp-on--change='actions.onFieldChange' " |
| 1940 | . $class |
| 1941 | . checked( $option, $value, false ) . ' ' |
| 1942 | . ( $required ? "required aria-required='true'" : '' ) |
| 1943 | . '/> '; |
| 1944 | $field .= "<label for='" . esc_attr( $radio_id ) . "' {$field_style} class='grunion-radio-label radio" . ( $this->is_error() ? ' form-error' : '' ) . "'>"; |
| 1945 | $field .= "<span class='grunion-field-text'>" . esc_html( $option ) . '</span>'; |
| 1946 | $field .= '</label>'; |
| 1947 | $field .= '</p>'; |
| 1948 | } |
| 1949 | } |
| 1950 | } |
| 1951 | |
| 1952 | if ( ! $is_outlined_style ) { |
| 1953 | $field .= '</div>'; |
| 1954 | } |
| 1955 | $field .= $this->get_error_div( $id, 'radio' ) . '</fieldset>'; |
| 1956 | return $field; |
| 1957 | } |
| 1958 | |
| 1959 | /** |
| 1960 | * Render the "Other" text input field for radio and checkbox fields. |
| 1961 | * |
| 1962 | * @param string $option_id The ID of the "Other" option. |
| 1963 | * @param bool $required Whether the main field is required. |
| 1964 | * @param string $id The base ID of the main field. |
| 1965 | * @param string $field_styles The styles to apply to the text input field. |
| 1966 | * @param string $placeholder Placeholder text for the input field. |
| 1967 | * |
| 1968 | * @return string The HTML for the "Other" text input field. |
| 1969 | */ |
| 1970 | private function render_other_input_field( $option_id, $required, $id, $field_styles, $placeholder ) { |
| 1971 | $other_text_id = esc_attr( $option_id ) . '-other-text'; |
| 1972 | $other_label_id = esc_attr( $option_id ) . '-other-label'; |
| 1973 | $other_label_text = ! empty( $placeholder ) ? $placeholder : __( 'Please specify…', 'jetpack-forms' ); |
| 1974 | $aria_required_attr = $required ? "aria-required='true'" : ''; |
| 1975 | $other_input_styles = ! empty( $field_styles ) ? " style='" . esc_attr( $field_styles ) . "' " : ''; |
| 1976 | |
| 1977 | $field = "<div class='jetpack-other-text-input-wrapper' data-wp-class--is-visible='state.isOtherSelected' role='region'>"; |
| 1978 | $field .= "<label id='" . $other_label_id . "' for='" . $other_text_id . "' class='screen-reader-text'>" . esc_html( $other_label_text ) . '</label>'; |
| 1979 | $field .= "<input |
| 1980 | id='" . $other_text_id . "' |
| 1981 | name='" . esc_attr( $id ) . "-other-text' |
| 1982 | type='text' |
| 1983 | class='grunion-field' |
| 1984 | " . $other_input_styles . " |
| 1985 | placeholder='" . esc_attr( $placeholder ) . "' |
| 1986 | value='' |
| 1987 | aria-labelledby='" . $other_label_id . "' |
| 1988 | " . $aria_required_attr . " |
| 1989 | data-wp-on--input='actions.onOtherTextInput' |
| 1990 | data-wp-bind--disabled='!state.isOtherSelected' |
| 1991 | data-wp-class--has-value='state.hasFieldValue' />"; |
| 1992 | $field .= '</div>'; |
| 1993 | |
| 1994 | return $field; |
| 1995 | } |
| 1996 | |
| 1997 | /** |
| 1998 | * Return the HTML for the checkbox 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_checkbox_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) { |
| 2011 | $label_class = 'grunion-field-label checkbox'; |
| 2012 | $label_class .= $this->is_error() ? ' form-error' : ''; |
| 2013 | $label_class .= $this->label_classes ? ' ' . $this->label_classes : ''; |
| 2014 | $label_class .= $this->option_classes ? ' ' . $this->option_classes : ''; |
| 2015 | $has_inner_block_option_styles = ! empty( $this->get_attribute( 'optionstyles' ) ); |
| 2016 | |
| 2017 | $field = "<div class='contact-form__checkbox-wrap' style='" . ( $has_inner_block_option_styles ? esc_attr( $this->option_styles ) : '' ) . "' >"; |
| 2018 | $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"; |
| 2019 | $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 ) : '' ) . "'>"; |
| 2020 | $field .= wp_kses_post( $label ) . ( $required && $required_indicator ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ); |
| 2021 | $field .= "</label>\n"; |
| 2022 | $field .= "<div class='clear-form'></div>\n"; |
| 2023 | $field .= '</div>'; |
| 2024 | return $field . $this->get_error_div( $id, 'checkbox' ); |
| 2025 | } |
| 2026 | |
| 2027 | /** |
| 2028 | * Return the HTML for the consent field. |
| 2029 | * |
| 2030 | * @param string $id field id. |
| 2031 | * @param string $class html classes (can be set by the admin). |
| 2032 | */ |
| 2033 | private function render_consent_field( $id, $class ) { |
| 2034 | $consent_type = 'explicit' === $this->get_attribute( 'consenttype' ) ? 'explicit' : 'implicit'; |
| 2035 | $consent_message = 'explicit' === $consent_type ? $this->get_attribute( 'explicitconsentmessage' ) : $this->get_attribute( 'implicitconsentmessage' ); |
| 2036 | $label_class = 'grunion-field-label consent consent-' . esc_attr( $consent_type ); |
| 2037 | $label_class .= $this->option_classes ? ' ' . $this->option_classes : ''; |
| 2038 | $label_class .= $this->is_error() ? ' form-error' : ''; |
| 2039 | $has_inner_block_option_styles = ! empty( $this->get_attribute( 'optionstyles' ) ); |
| 2040 | |
| 2041 | $field = "<label class='" . esc_attr( $label_class ) . "' style='" . esc_attr( $this->label_styles ) . ( $has_inner_block_option_styles ? esc_attr( $this->option_styles ) : '' ) . "'>"; |
| 2042 | |
| 2043 | if ( 'implicit' === $consent_type ) { |
| 2044 | $field .= "\t\t<input type='hidden' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack-forms' ) . "' /> \n"; |
| 2045 | } else { |
| 2046 | $field .= "\t\t<input type='checkbox' data-wp-on--change='actions.onFieldChange' name='" . esc_attr( $id ) . "' value='" . esc_attr__( 'Yes', 'jetpack-forms' ) . "' " . $class . "/> \n"; |
| 2047 | } |
| 2048 | $field .= "\t\t" . wp_kses_post( $consent_message ); |
| 2049 | $field .= "</label>\n"; |
| 2050 | $field .= "<div class='clear-form'></div>\n"; |
| 2051 | return $field . $this->get_error_div( $id, 'checkbox' ); |
| 2052 | } |
| 2053 | |
| 2054 | /** |
| 2055 | * Return the HTML for the file field. |
| 2056 | * |
| 2057 | * Renders a file upload field with drag-and-drop functionality. |
| 2058 | * |
| 2059 | * @since 0.45.0 |
| 2060 | * |
| 2061 | * @param string $id - the field ID. |
| 2062 | * @param string $label - the field label. |
| 2063 | * @param string $class - unused. Kept for signature parity with the other render_*_field() |
| 2064 | * methods; this field writes its input inline rather than from an |
| 2065 | * attribute array. |
| 2066 | * @param bool $required - if the field is marked as required. |
| 2067 | * @param string $required_field_text - the text in the required text field. |
| 2068 | * @param bool $required_indicator Whether to display the required indicator. |
| 2069 | * |
| 2070 | * @return string HTML for the file upload field. |
| 2071 | */ |
| 2072 | private function render_file_field( $id, $label, $class, $required, $required_field_text, $required_indicator = true ) { |
| 2073 | // Check if Jetpack is active |
| 2074 | if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) { |
| 2075 | return '<div class="jetpack-form-field-error">' . |
| 2076 | esc_html__( 'File upload field requires Jetpack to be active.', 'jetpack-forms' ) . |
| 2077 | '</div>'; |
| 2078 | } |
| 2079 | |
| 2080 | $this->set_invalid_message( 'file_uploading', __( 'Please wait a moment, file is currently uploading.', 'jetpack-forms' ) ); |
| 2081 | $this->set_invalid_message( 'file_has_errors', __( 'Please remove any file upload errors.', 'jetpack-forms' ) ); |
| 2082 | |
| 2083 | // Enqueue necessary scripts and styles. |
| 2084 | $this->enqueue_file_field_assets(); |
| 2085 | |
| 2086 | $accept_attribute_value = implode( ', ', self::get_file_field_accepted_mime_types() ); |
| 2087 | $max_files = $this->get_file_field_max_files(); |
| 2088 | $max_upload_size = self::get_file_field_max_upload_size(); |
| 2089 | |
| 2090 | $file_size_units = array( |
| 2091 | _x( 'B', 'unit symbol', 'jetpack-forms' ), |
| 2092 | _x( 'KB', 'unit symbol', 'jetpack-forms' ), |
| 2093 | _x( 'MB', 'unit symbol', 'jetpack-forms' ), |
| 2094 | _x( 'GB', 'unit symbol', 'jetpack-forms' ), |
| 2095 | ); |
| 2096 | |
| 2097 | $global_config = array( |
| 2098 | 'i18n' => array( |
| 2099 | 'language' => get_bloginfo( 'language' ), |
| 2100 | 'fileSizeUnits' => $file_size_units, |
| 2101 | 'zeroBytes' => __( '0 Bytes', 'jetpack-forms' ), |
| 2102 | 'uploadError' => __( 'Error uploading file', 'jetpack-forms' ), |
| 2103 | 'folderNotSupported' => __( 'Folder uploads are not supported', 'jetpack-forms' ), |
| 2104 | // translators: %s is the formatted maximum file size. |
| 2105 | 'fileTooLarge' => sprintf( __( 'File is too large. Maximum allowed size is %s.', 'jetpack-forms' ), size_format( $max_upload_size ) ), |
| 2106 | 'invalidType' => __( 'This file type is not allowed.', 'jetpack-forms' ), |
| 2107 | 'maxFiles' => __( 'You have exceeded the number of files that you can upload.', 'jetpack-forms' ), |
| 2108 | 'uploadFailed' => __( 'File upload failed, try again.', 'jetpack-forms' ), |
| 2109 | ), |
| 2110 | 'endpoint' => $this->get_unauth_endpoint_url(), |
| 2111 | 'iconsPath' => Jetpack_Forms::plugin_url() . 'contact-form/images/file-icons/', |
| 2112 | 'maxUploadSize' => $max_upload_size, |
| 2113 | ); |
| 2114 | |
| 2115 | wp_interactivity_config( 'jetpack/field-file', $global_config ); |
| 2116 | |
| 2117 | // Only genuinely dynamic state lives in the context. The field's static configuration |
| 2118 | // (`maxFiles`, `allowedMimeTypes`) travels in `fieldExtra` alongside every other field's |
| 2119 | // config, and `fieldId` already comes from the wrapper that `render_field()` opens. |
| 2120 | $context = array( |
| 2121 | 'isDropping' => false, |
| 2122 | 'files' => array(), |
| 2123 | // Field-level message that belongs to no single file — currently only the result of |
| 2124 | // offering more files than the field accepts. Per-file problems stay on their preview. |
| 2125 | 'fileNotice' => '', |
| 2126 | ); |
| 2127 | |
| 2128 | $field = $this->render_label( 'file', $id, $label, $required, $required_field_text, array(), true, $required_indicator ); |
| 2129 | |
| 2130 | $dropzone_aria_label = $this->get_file_dropzone_aria_label( $max_files ); |
| 2131 | |
| 2132 | ob_start(); |
| 2133 | ?> |
| 2134 | <div |
| 2135 | class="jetpack-form-file-field__container" |
| 2136 | id="<?php echo esc_attr( $id ); ?>" |
| 2137 | name="dropzone-<?php echo esc_attr( $id ); ?>" |
| 2138 | <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output is pre-escaped by method ?> |
| 2139 | <?php echo wp_interactivity_data_wp_context( $context ); ?> |
| 2140 | data-wp-on--dragover="actions.onFileDragOver" |
| 2141 | data-wp-on--dragleave="actions.onFileDragLeave" |
| 2142 | data-wp-on--mouseleave="actions.onFileDragLeave" |
| 2143 | data-wp-on--drop="actions.fileDropped" |
| 2144 | data-wp-on--jetpack-form-reset="actions.resetFiles" |
| 2145 | data-is-required="<?php echo esc_attr( $required ); ?>" |
| 2146 | > |
| 2147 | <div class="jetpack-form-file-field__dropzone" data-wp-class--is-dropping="context.isDropping" data-wp-class--is-hidden="state.isFileFieldFull"> |
| 2148 | <div class="jetpack-form-file-field__dropzone-inner" data-wp-on--click="actions.openFilePicker" data-wp-on--keydown="actions.onFileDropzoneKeyDown" tabindex="0" role="button" aria-label="<?php echo esc_attr( $dropzone_aria_label ); ?>"></div> |
| 2149 | <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Decoded dropzone markup is filtered through an allowlist by sanitize_file_field_content(). ?> |
| 2150 | <?php echo $this->sanitize_file_field_content( $this->content ); ?> |
| 2151 | <input |
| 2152 | type="file" class="jetpack-form-file-field" |
| 2153 | accept="<?php echo esc_attr( $accept_attribute_value ); ?>" |
| 2154 | <?php // Without this the picker returns one file at a time however many the field accepts; a drop already could. ?> |
| 2155 | <?php echo $max_files > 1 ? 'multiple' : ''; ?> |
| 2156 | data-wp-on--change="actions.fileAdded" /> |
| 2157 | </div> |
| 2158 | <div class="jetpack-form-file-field__preview-wrap" name="file-field-<?php echo esc_attr( $id ); ?>" data-wp-class--is-active="state.hasFileFieldFiles"> |
| 2159 | <template data-wp-each--file="context.files" data-wp-key="context.file.id"> |
| 2160 | <div class="jetpack-form-file-field__preview" tabindex="0" data-wp-bind--aria-label="context.file.name" data-wp-init--focus="callbacks.focusFilePreview" data-wp-class--is-error="context.file.hasError" data-wp-class--is-complete="context.file.isUploaded"> |
| 2161 | <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=""> |
| 2162 | <div class="jetpack-form-file-field__image-wrap" data-wp-style----progress="context.file.progress" data-wp-class--has-icon="context.file.hasIcon"> |
| 2163 | <div class="jetpack-form-file-field__image" data-wp-style--background-image="context.file.url" data-wp-style--mask-image="context.file.mask"></div> |
| 2164 | <div class="jetpack-form-file-field__progress-bar" ></div> |
| 2165 | </div> |
| 2166 | |
| 2167 | <div class="jetpack-form-file-field__file-wrap"> |
| 2168 | <strong class="jetpack-form-file-field__file-name" data-wp-text="context.file.name"></strong> |
| 2169 | <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"> |
| 2170 | <span class="jetpack-form-file-field__file-size" data-wp-text="context.file.formattedSize"></span> |
| 2171 | <span class="jetpack-form-file-field__seperator"> · </span> |
| 2172 | <span aria-live="polite"> |
| 2173 | <span class="jetpack-form-file-field__uploading"><?php esc_html_e( 'Uploading…', 'jetpack-forms' ); ?></span> |
| 2174 | <span class="jetpack-form-file-field__success"><?php esc_html_e( 'Uploaded', 'jetpack-forms' ); ?></span> |
| 2175 | <span class="jetpack-form-file-field__error" data-wp-text="context.file.error"></span> |
| 2176 | </span> |
| 2177 | </div> |
| 2178 | </div> |
| 2179 | <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> |
| 2180 | </div> |
| 2181 | </template> |
| 2182 | </div> |
| 2183 | <p class="jetpack-form-file-field__notice" role="status" data-wp-class--is-visible="state.hasFileFieldNotice" data-wp-text="context.fileNotice"></p> |
| 2184 | </div> |
| 2185 | <?php |
| 2186 | return $field . ob_get_clean() . $this->get_error_div( $id, 'file' ); |
| 2187 | } |
| 2188 | |
| 2189 | /** |
| 2190 | * Sanitize the file field's inner (dropzone) content for output. |
| 2191 | * |
| 2192 | * The dropzone's inner blocks are serialized into the [contact-field] shortcode with |
| 2193 | * esc_html() (see Contact_Form::parse_contact_field()), so the stored content has to be |
| 2194 | * entity-decoded before it can render. Decoding is what makes this dangerous: an author |
| 2195 | * who cannot post raw HTML can still store entity-encoded markup in the shortcode's inner |
| 2196 | * content, which post-save KSES treats as plain text and never inspects. Decoding it would |
| 2197 | * turn that text back into live markup, so the decoded result is filtered here instead of |
| 2198 | * being echoed as-is. |
| 2199 | * |
| 2200 | * @since 7.24.0 |
| 2201 | * |
| 2202 | * @param string|null $content The raw (entity-encoded) field content. |
| 2203 | * |
| 2204 | * @return string Safe HTML. |
| 2205 | */ |
| 2206 | private function sanitize_file_field_content( $content ) { |
| 2207 | if ( ! is_string( $content ) || $content === '' ) { |
| 2208 | return ''; |
| 2209 | } |
| 2210 | |
| 2211 | // core/icon serializes its rotation as `rotate: 45deg`, which is not in WordPress's |
| 2212 | // safe_style_css list, so safecss_filter_attr() would strip it and render the icon |
| 2213 | // unrotated. Allow it just for the duration of this call. |
| 2214 | $allow_rotate = static function ( $attrs ) { |
| 2215 | $attrs[] = 'rotate'; |
| 2216 | return $attrs; |
| 2217 | }; |
| 2218 | |
| 2219 | add_filter( 'safe_style_css', $allow_rotate ); |
| 2220 | |
| 2221 | try { |
| 2222 | return wp_kses( |
| 2223 | html_entity_decode( $content, ENT_COMPAT, 'UTF-8' ), |
| 2224 | self::get_file_field_allowed_html() |
| 2225 | ); |
| 2226 | } finally { |
| 2227 | remove_filter( 'safe_style_css', $allow_rotate ); |
| 2228 | } |
| 2229 | } |
| 2230 | |
| 2231 | /** |
| 2232 | * Build the allowlist of HTML permitted inside the file field's dropzone. |
| 2233 | * |
| 2234 | * Starts from the standard post allowlist and adds the things the dropzone's allowed inner |
| 2235 | * blocks legitimately rely on that wp_kses_post() would otherwise drop: |
| 2236 | * |
| 2237 | * - `tabindex`, which Contact_Form_Plugin::gutenblock_render_dropzone() sets to "-1" on inner |
| 2238 | * links and buttons so the dropzone stays a single tab stop. |
| 2239 | * - `srcset`, `sizes` and `decoding` on images, so core/image keeps serving responsive |
| 2240 | * sources instead of silently degrading to the full-size file. |
| 2241 | * - A conservative inline SVG subset, since core/icon is an allowed inner block of the |
| 2242 | * dropzone. Only presentational elements and attributes are listed; because wp_kses() is |
| 2243 | * allowlist-based, event handlers and scripting constructs are dropped automatically. |
| 2244 | * |
| 2245 | * Note that wp_kses_allowed_html( 'post' ) permits `data-*` globally, so Interactivity API |
| 2246 | * directives survive this filter. Their values are store paths rather than executable code, |
| 2247 | * and removing them would also break core/image's lightbox markup, so they are left alone. |
| 2248 | * |
| 2249 | * @since 7.24.0 |
| 2250 | * |
| 2251 | * @return array Allowed HTML, in wp_kses() format. |
| 2252 | */ |
| 2253 | private static function get_file_field_allowed_html() { |
| 2254 | $allowed = wp_kses_allowed_html( 'post' ); |
| 2255 | |
| 2256 | foreach ( array( 'a', 'button' ) as $tag ) { |
| 2257 | if ( isset( $allowed[ $tag ] ) && is_array( $allowed[ $tag ] ) ) { |
| 2258 | $allowed[ $tag ]['tabindex'] = true; |
| 2259 | } |
| 2260 | } |
| 2261 | |
| 2262 | // wp_kses() only runs its protocol check on a fixed set of URL attributes, which does not |
| 2263 | // include `srcset`. That is not a script vector - srcset candidates are fetched as images, |
| 2264 | // so a `javascript:` candidate never executes - and `src` is still protocol-checked. |
| 2265 | if ( isset( $allowed['img'] ) && is_array( $allowed['img'] ) ) { |
| 2266 | $allowed['img'] += array( |
| 2267 | 'srcset' => true, |
| 2268 | 'sizes' => true, |
| 2269 | 'decoding' => true, |
| 2270 | ); |
| 2271 | } |
| 2272 | |
| 2273 | // `style` is deliberately absent here: safecss_filter_attr() strips the SVG presentation |
| 2274 | // properties (fill, stroke) that would justify it, while still permitting things like |
| 2275 | // position:fixed. It is allowed on the root <svg> below, where block supports need it. |
| 2276 | $svg_common = array( |
| 2277 | 'class' => true, |
| 2278 | 'fill' => true, |
| 2279 | 'fill-rule' => true, |
| 2280 | 'clip-rule' => true, |
| 2281 | 'stroke' => true, |
| 2282 | 'stroke-width' => true, |
| 2283 | 'stroke-linecap' => true, |
| 2284 | 'stroke-linejoin' => true, |
| 2285 | 'opacity' => true, |
| 2286 | 'transform' => true, |
| 2287 | ); |
| 2288 | |
| 2289 | $allowed['svg'] = array_merge( |
| 2290 | $svg_common, |
| 2291 | array( |
| 2292 | 'style' => true, |
| 2293 | 'xmlns' => true, |
| 2294 | 'viewbox' => true, |
| 2295 | 'width' => true, |
| 2296 | 'height' => true, |
| 2297 | 'role' => true, |
| 2298 | 'focusable' => true, |
| 2299 | 'aria-hidden' => true, |
| 2300 | 'aria-label' => true, |
| 2301 | ) |
| 2302 | ); |
| 2303 | |
| 2304 | // Child elements: the shared presentation attributes plus each element's own geometry. |
| 2305 | $svg_children = array( |
| 2306 | 'g' => array(), |
| 2307 | 'path' => array( 'd' ), |
| 2308 | 'circle' => array( 'cx', 'cy', 'r' ), |
| 2309 | 'ellipse' => array( 'cx', 'cy', 'rx', 'ry' ), |
| 2310 | 'line' => array( 'x1', 'y1', 'x2', 'y2' ), |
| 2311 | 'polygon' => array( 'points' ), |
| 2312 | 'polyline' => array( 'points' ), |
| 2313 | 'rect' => array( 'x', 'y', 'width', 'height', 'rx', 'ry' ), |
| 2314 | ); |
| 2315 | |
| 2316 | foreach ( $svg_children as $tag => $attrs ) { |
| 2317 | $allowed[ $tag ] = $svg_common + array_fill_keys( $attrs, true ); |
| 2318 | } |
| 2319 | |
| 2320 | return $allowed; |
| 2321 | } |
| 2322 | |
| 2323 | /** |
| 2324 | * Render a hidden field. |
| 2325 | * |
| 2326 | * @param string $id - the field ID. |
| 2327 | * @param string $label - the field label. |
| 2328 | * @param string $value - the value of the field. |
| 2329 | * |
| 2330 | * @return string HTML for the hidden field. |
| 2331 | */ |
| 2332 | private function render_hidden_field( $id, $label, $value ) { |
| 2333 | if ( ! $this->is_submitted_hidden_field_value( $id ) ) { |
| 2334 | $value = $this->get_filtered_hidden_field_value( $value, $label, $id ); |
| 2335 | } |
| 2336 | |
| 2337 | $context = array( |
| 2338 | 'fieldId' => $id, |
| 2339 | 'fieldType' => 'hidden', |
| 2340 | 'fieldLabel' => $label, |
| 2341 | 'fieldValue' => $value, |
| 2342 | 'fieldIsRequired' => false, |
| 2343 | 'fieldExtra' => array(), |
| 2344 | 'formHash' => $this->form ? $this->form->hash : '', |
| 2345 | ); |
| 2346 | |
| 2347 | $interactivity_attributes = "data-jp-field-id='" . esc_attr( $id ) . "' data-wp-interactive='jetpack/form' " |
| 2348 | . wp_interactivity_data_wp_context( $context ) |
| 2349 | . " data-wp-init='callbacks.initializeField' data-wp-on--jetpack-form-reset='callbacks.initializeField'"; |
| 2350 | |
| 2351 | return "<input type='hidden' name='" . esc_attr( $id ) . "' id='" . esc_attr( $id ) |
| 2352 | . "' value='" . esc_attr( $value ) . "' " . $interactivity_attributes . " />\n"; |
| 2353 | } |
| 2354 | |
| 2355 | /** |
| 2356 | * How many files this file upload field accepts. |
| 2357 | * |
| 2358 | * Read from the block's `maxfiles` attribute and clamped rather than trusted: the value reaches |
| 2359 | * PHP as author-supplied shortcode text, and both the rendered `multiple` attribute and the |
| 2360 | * submission-time count check are derived from it. |
| 2361 | * |
| 2362 | * @since 8.0.0 |
| 2363 | * |
| 2364 | * @return int A number between 1 and FILE_FIELD_MAX_FILES_LIMIT. |
| 2365 | */ |
| 2366 | private function get_file_field_max_files() { |
| 2367 | $max_files = $this->get_attribute( 'maxfiles' ); |
| 2368 | |
| 2369 | if ( ! is_numeric( $max_files ) ) { |
| 2370 | return self::FILE_FIELD_DEFAULT_MAX_FILES; |
| 2371 | } |
| 2372 | |
| 2373 | return max( 1, min( (int) $max_files, self::get_file_field_max_files_limit() ) ); |
| 2374 | } |
| 2375 | |
| 2376 | /** |
| 2377 | * The highest number of files an author may set a single file upload field to. |
| 2378 | * |
| 2379 | * @since 8.0.0 |
| 2380 | * |
| 2381 | * @return int A number between 1 and FILE_FIELD_MAX_FILES_ABSOLUTE_LIMIT. |
| 2382 | */ |
| 2383 | public static function get_file_field_max_files_limit() { |
| 2384 | /** |
| 2385 | * Filters the highest number of files an author may set a file upload field to. |
| 2386 | * |
| 2387 | * This raises the ceiling rather than setting the count. A field still takes whatever its |
| 2388 | * author chose, so one deliberately set to a single file keeps taking a single file — which |
| 2389 | * is the whole difference between offering more room and overriding everybody's choice. |
| 2390 | * |
| 2391 | * The editor's control reads the same number, so what an author is offered is what the site |
| 2392 | * will honour. A field already storing a number above the ceiling is clamped down to it, so |
| 2393 | * removing this filter returns those fields to the default rather than leaving them on a |
| 2394 | * value nothing enforces any more. |
| 2395 | * |
| 2396 | * Bounded by FILE_FIELD_MAX_FILES_ABSOLUTE_LIMIT. Nothing at the receiving end refuses a |
| 2397 | * submission for holding too many files — the endpoint's per-site budgets log and alert but |
| 2398 | * store the file regardless — so this ceiling is the only thing standing in the way. |
| 2399 | * |
| 2400 | * @since 8.0.0 |
| 2401 | * |
| 2402 | * @param int $limit The highest number of files an author may choose. |
| 2403 | */ |
| 2404 | $limit = (int) apply_filters( |
| 2405 | 'jetpack_forms_file_field_max_files_limit', |
| 2406 | self::FILE_FIELD_MAX_FILES_LIMIT |
| 2407 | ); |
| 2408 | |
| 2409 | return max( 1, min( $limit, self::FILE_FIELD_MAX_FILES_ABSOLUTE_LIMIT ) ); |
| 2410 | } |
| 2411 | |
| 2412 | /** |
| 2413 | * The largest file a file upload field accepts, in bytes. |
| 2414 | * |
| 2415 | * @since 8.0.0 |
| 2416 | * |
| 2417 | * @return int Size in bytes. |
| 2418 | */ |
| 2419 | private static function get_file_field_max_upload_size() { |
| 2420 | /** |
| 2421 | * Filters the largest file a file upload field accepts, in bytes. |
| 2422 | * |
| 2423 | * Deliberately not bounded by the site's own PHP upload limits. The browser sends each file |
| 2424 | * straight to the upload endpoint, so `upload_max_filesize` and `post_max_size` have nothing |
| 2425 | * to do with this transfer — clamping to them would cap the field at whatever a cheap host |
| 2426 | * allows for files that host never receives. |
| 2427 | * |
| 2428 | * The bound that does matter is the endpoint's own, which rejects anything over |
| 2429 | * FILE_FIELD_MAX_UPLOAD_SIZE. So this filter can only lower the limit, never raise it: a |
| 2430 | * larger value would have the field accept a file, show the visitor a size allowance in the |
| 2431 | * "file is too large" message that nothing can honour, and then fail the upload once they had |
| 2432 | * already waited for it. Raising the ceiling means raising it at the endpoint first. |
| 2433 | * |
| 2434 | * The result reaches the browser as the `maxUploadSize` config value, and the "file is too |
| 2435 | * large" message is built from the same number, so the two cannot disagree. |
| 2436 | * |
| 2437 | * @since 8.0.0 |
| 2438 | * |
| 2439 | * @param int $max_upload_size Maximum size of a single file, in bytes. |
| 2440 | */ |
| 2441 | $max_upload_size = (int) apply_filters( |
| 2442 | 'jetpack_forms_file_field_max_upload_size', |
| 2443 | self::FILE_FIELD_MAX_UPLOAD_SIZE |
| 2444 | ); |
| 2445 | |
| 2446 | return max( 1, min( $max_upload_size, self::FILE_FIELD_MAX_UPLOAD_SIZE ) ); |
| 2447 | } |
| 2448 | |
| 2449 | /** |
| 2450 | * MIME types the file upload field accepts. |
| 2451 | * |
| 2452 | * Shared by the input's `accept` attribute and by the field's `fieldExtra` config, so the |
| 2453 | * browser-side picker filter and the client-side check can never drift apart. |
| 2454 | * |
| 2455 | * @since 8.0.0 |
| 2456 | * |
| 2457 | * @return string[] List of accepted MIME types. |
| 2458 | */ |
| 2459 | private static function get_file_field_accepted_mime_types() { |
| 2460 | return array_values( |
| 2461 | array( |
| 2462 | 'jpg|jpeg|jpe' => 'image/jpeg', |
| 2463 | 'png' => 'image/png', |
| 2464 | 'gif' => 'image/gif', |
| 2465 | 'pdf' => 'application/pdf', |
| 2466 | 'doc' => 'application/msword', |
| 2467 | 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', |
| 2468 | 'docm' => 'application/vnd.ms-word.document.macroEnabled.12', |
| 2469 | 'pot|pps|ppt' => 'application/vnd.ms-powerpoint', |
| 2470 | 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', |
| 2471 | 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', |
| 2472 | 'odt' => 'application/vnd.oasis.opendocument.text', |
| 2473 | 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', |
| 2474 | 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', |
| 2475 | 'csv' => 'text/csv', |
| 2476 | 'xla|xls|xlt|xlw' => 'application/vnd.ms-excel', |
| 2477 | 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', |
| 2478 | 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', |
| 2479 | 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', |
| 2480 | 'key' => 'application/vnd.apple.keynote', |
| 2481 | 'webp' => 'image/webp', |
| 2482 | 'heic' => 'image/heic', |
| 2483 | 'heics' => 'image/heic-sequence', |
| 2484 | 'heif' => 'image/heif', |
| 2485 | 'heifs' => 'image/heif-sequence', |
| 2486 | 'asc' => 'application/pgp-keys', |
| 2487 | ) |
| 2488 | ); |
| 2489 | } |
| 2490 | |
| 2491 | /** |
| 2492 | * Enqueues scripts and styles needed for the file field. |
| 2493 | * |
| 2494 | * @since 0.45.0 |
| 2495 | * |
| 2496 | * @return void |
| 2497 | */ |
| 2498 | private function enqueue_file_field_assets() { |
| 2499 | $version = Constants::get_constant( 'JETPACK__VERSION' ); |
| 2500 | |
| 2501 | // extra cache busting strategy for view.js, seems they are left out of cache clearing on deploys |
| 2502 | $asset_file = plugin_dir_path( __FILE__ ) . '../../dist/modules/file-field/view.asset.php'; |
| 2503 | $asset = file_exists( $asset_file ) ? require $asset_file : null; |
| 2504 | $version .= $asset['version'] ?? ''; |
| 2505 | |
| 2506 | \wp_enqueue_script_module( |
| 2507 | 'jetpack-form-file-field', |
| 2508 | plugins_url( '../../dist/modules/file-field/view.js', __FILE__ ), |
| 2509 | // `jp-forms-view` is not decoration: this module contributes `state.validators.file`, |
| 2510 | // which `registerField()` reads at `data-wp-init` time. Loading after hydration would |
| 2511 | // leave a required file field registering as valid, since the shared `validateField()` |
| 2512 | // no longer has a `file` branch to fall back on. Same reason `field-phone` declares it. |
| 2513 | array( '@wordpress/interactivity', 'jp-forms-view' ), |
| 2514 | $version |
| 2515 | ); |
| 2516 | |
| 2517 | \wp_enqueue_style( |
| 2518 | 'jetpack-form-file-field', |
| 2519 | plugins_url( '../../dist/contact-form/css/file-field.css', __FILE__ ), |
| 2520 | array(), |
| 2521 | $version |
| 2522 | ); |
| 2523 | } |
| 2524 | |
| 2525 | /** |
| 2526 | * Returns the URL for the unauthenticated file upload endpoint. |
| 2527 | * |
| 2528 | * @return string |
| 2529 | */ |
| 2530 | private function get_unauth_endpoint_url() { |
| 2531 | // Return a placeholder URL if Jetpack is not active |
| 2532 | if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) { |
| 2533 | return '#jetpack-not-active'; |
| 2534 | } |
| 2535 | |
| 2536 | return sprintf( 'https://public-api.wordpress.com/wpcom/v2/sites/%d/unauth-file-upload', \Jetpack_Options::get_option( 'id' ) ); |
| 2537 | } |
| 2538 | |
| 2539 | /** |
| 2540 | * Return the HTML for the multiple checkbox field. |
| 2541 | * |
| 2542 | * @param string $id - the ID (starts with 'g' - see constructor). |
| 2543 | * @param string $label - the label. |
| 2544 | * @param string $value - the value of the field. |
| 2545 | * @param string $class - the field class. |
| 2546 | * @param bool $required - if the field is marked as required. |
| 2547 | * @param string $required_field_text - the text in the required text field. |
| 2548 | * @param bool $required_indicator Whether to display the required indicator. |
| 2549 | * |
| 2550 | * @return string HTML |
| 2551 | */ |
| 2552 | public function render_checkbox_multiple_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) { |
| 2553 | $options_classes = $this->get_attribute( 'optionsclasses' ); |
| 2554 | $options_styles = $this->get_attribute( 'optionsstyles' ); |
| 2555 | $form_style = $this->get_form_style(); |
| 2556 | $is_outlined_style = 'outlined' === $form_style; |
| 2557 | |
| 2558 | /* |
| 2559 | * The `data-required` attribute is used in `accessible-form.js` to ensure at least one |
| 2560 | * checkbox is checked. Unlike radio buttons, for which the required attribute is satisfied if |
| 2561 | * any of the radio buttons in the group is selected, adding a required attribute directly to |
| 2562 | * a checkbox means that this specific checkbox must be checked. |
| 2563 | */ |
| 2564 | $fieldset_id = "id='" . esc_attr( "$id-label" ) . "'" . $this->get_hidden_label_aria_label_attr(); |
| 2565 | |
| 2566 | if ( $is_outlined_style ) { |
| 2567 | $style_variation_attributes = $this->get_attribute( 'stylevariationattributes' ); |
| 2568 | |
| 2569 | if ( ! empty( $style_variation_attributes ) ) { |
| 2570 | $style_variation_attributes = json_decode( html_entity_decode( $style_variation_attributes, ENT_COMPAT ), true ); |
| 2571 | } |
| 2572 | |
| 2573 | /* |
| 2574 | * When there's an outlined style, and border radius is set, the existing inline border radius is overridden to apply |
| 2575 | * a limit of `100px` to the radius on the x axis. This achieves the same look and feel as other fields |
| 2576 | * that use the notch html (`notched-label__leading` has a max-width of `100px` to prevent it from getting too wide). |
| 2577 | * It prevents large border radius values from disrupting the look and feel of the fields. |
| 2578 | */ |
| 2579 | if ( isset( $style_variation_attributes['border']['radius'] ) ) { |
| 2580 | $options_styles ??= ''; |
| 2581 | $radius = $style_variation_attributes['border']['radius']; |
| 2582 | $has_split_radius_values = is_array( $radius ); |
| 2583 | $top_left_radius = $has_split_radius_values ? $radius['topLeft'] : $radius; |
| 2584 | $top_right_radius = $has_split_radius_values ? $radius['topRight'] : $radius; |
| 2585 | $bottom_left_radius = $has_split_radius_values ? $radius['bottomLeft'] : $radius; |
| 2586 | $bottom_right_radius = $has_split_radius_values ? $radius['bottomRight'] : $radius; |
| 2587 | $options_styles .= "border-top-left-radius: min(100px, {$top_left_radius}) {$top_left_radius};"; |
| 2588 | $options_styles .= "border-top-right-radius: min(100px, {$top_right_radius}) {$top_right_radius};"; |
| 2589 | $options_styles .= "border-bottom-left-radius: min(100px, {$bottom_left_radius}) {$bottom_left_radius};"; |
| 2590 | $options_styles .= "border-bottom-right-radius: min(100px, {$bottom_right_radius}) {$bottom_right_radius};"; |
| 2591 | } |
| 2592 | |
| 2593 | /* |
| 2594 | * For the "outlined" style, the styles and classes are applied to the fieldset element. |
| 2595 | */ |
| 2596 | $field = "<fieldset {$fieldset_id} class='grunion-checkbox-multiple-options " . esc_attr( $options_classes ) . "' style='" . esc_attr( $options_styles ) . "' " . ( $required ? 'data-required' : '' ) . ' data-wp-bind--aria-invalid="state.fieldAriaInvalid">'; |
| 2597 | } else { |
| 2598 | $field = "<fieldset {$fieldset_id} class='jetpack-field-multiple__fieldset'" . ( $required ? 'data-required' : '' ) . ' data-wp-bind--aria-invalid="state.fieldAriaInvalid">'; |
| 2599 | } |
| 2600 | |
| 2601 | $field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text, array(), $required_indicator ); |
| 2602 | |
| 2603 | if ( ! $is_outlined_style ) { |
| 2604 | $field .= "<div class='grunion-checkbox-multiple-options " . esc_attr( $options_classes ) . "' style='" . esc_attr( $options_styles ) . "' " . '>'; |
| 2605 | } |
| 2606 | |
| 2607 | $options_data = $this->get_attribute( 'optionsdata' ); |
| 2608 | $used_html_ids = array(); |
| 2609 | |
| 2610 | if ( ! empty( $options_data ) ) { |
| 2611 | foreach ( $options_data as $option_index => $option ) { |
| 2612 | $option_label = Contact_Form_Plugin::strip_tags( $option['label'] ); |
| 2613 | if ( is_string( $option_label ) && '' !== $option_label ) { |
| 2614 | $checkbox_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option_label ); |
| 2615 | $checkbox_id = $id . '-' . sanitize_html_class( $checkbox_value ); |
| 2616 | |
| 2617 | // If exact id was already used in this checkbox group, append option index. |
| 2618 | // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc. |
| 2619 | if ( isset( $used_html_ids[ $checkbox_id ] ) ) { |
| 2620 | $checkbox_id .= '-' . $option_index; |
| 2621 | } |
| 2622 | $used_html_ids[ $checkbox_id ] = true; |
| 2623 | |
| 2624 | $default_classes = 'contact-form-field'; |
| 2625 | $option_styles = empty( $option['style'] ) ? '' : "style='" . esc_attr( $option['style'] ) . "'"; |
| 2626 | $option_classes = empty( $option['class'] ) ? $default_classes : $default_classes . ' ' . esc_attr( $option['class'] ); |
| 2627 | |
| 2628 | $field .= "<label {$option_styles} class='{$option_classes}'>"; |
| 2629 | $field .= "<input |
| 2630 | id='" . esc_attr( $checkbox_id ) . "' |
| 2631 | type='checkbox' |
| 2632 | data-wp-on--change='actions.onMultipleFieldChange' |
| 2633 | name='" . esc_attr( $id ) . "[]' |
| 2634 | value='" . esc_attr( $checkbox_value ) . "' " |
| 2635 | . $class |
| 2636 | . checked( in_array( $option_label, (array) $value, true ), true, false ) |
| 2637 | . ' /> '; |
| 2638 | $field .= "<span class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>"; |
| 2639 | $field .= "<span class='grunion-field-text'>" . esc_html( $option_label ) . '</span>'; |
| 2640 | $field .= '</span>'; |
| 2641 | $field .= '</label>'; |
| 2642 | } |
| 2643 | } |
| 2644 | } else { |
| 2645 | $field_style = 'style="' . $this->option_styles . '"'; |
| 2646 | |
| 2647 | foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) { |
| 2648 | $option = Contact_Form_Plugin::strip_tags( $option ); |
| 2649 | if ( is_string( $option ) && '' !== $option ) { |
| 2650 | $checkbox_value = $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option ); |
| 2651 | $checkbox_id = $id . '-' . sanitize_html_class( $checkbox_value ); |
| 2652 | |
| 2653 | // If exact id was already used in this checkbox group, append option index. |
| 2654 | // Multiple 'blue' options would give id-blue, id-blue-1, id-blue-2, etc. |
| 2655 | if ( isset( $used_html_ids[ $checkbox_id ] ) ) { |
| 2656 | $checkbox_id .= '-' . $option_index; |
| 2657 | } |
| 2658 | $used_html_ids[ $checkbox_id ] = true; |
| 2659 | |
| 2660 | $field .= "<label class='contact-form-field'>"; |
| 2661 | $field .= "<input |
| 2662 | id='" . esc_attr( $checkbox_id ) . "' |
| 2663 | data-wp-on--change='actions.onMultipleFieldChange' |
| 2664 | type='checkbox' |
| 2665 | name='" . esc_attr( $id ) . "[]' |
| 2666 | value='" . esc_attr( $checkbox_value ) . "' " |
| 2667 | . $class |
| 2668 | . checked( in_array( $option, (array) $value, true ), true, false ) |
| 2669 | . ' /> '; |
| 2670 | $field .= "<span {$field_style} class='grunion-checkbox-multiple-label checkbox-multiple" . ( $this->is_error() ? ' form-error' : '' ) . "'>"; |
| 2671 | $field .= "<span class='grunion-field-text'>" . esc_html( $option ) . '</span>'; |
| 2672 | $field .= '</span>'; |
| 2673 | $field .= '</label>'; |
| 2674 | } |
| 2675 | } |
| 2676 | } |
| 2677 | if ( ! $is_outlined_style ) { |
| 2678 | $field .= '</div>'; |
| 2679 | } |
| 2680 | $field .= $this->get_error_div( $id, 'select' ) . '</fieldset>'; |
| 2681 | return $field; |
| 2682 | } |
| 2683 | |
| 2684 | /** |
| 2685 | * Return the HTML for the select field. |
| 2686 | * |
| 2687 | * @param int $id - the ID. |
| 2688 | * @param string $label - the label. |
| 2689 | * @param string $value - the value of the field. |
| 2690 | * @param string $class - the field class. |
| 2691 | * @param bool $required - if the field is marked as required. |
| 2692 | * @param string $required_field_text - the text in the required text field. |
| 2693 | * @param bool $required_indicator Whether to display the required indicator. |
| 2694 | * |
| 2695 | * @return string HTML |
| 2696 | */ |
| 2697 | public function render_select_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) { |
| 2698 | $field = $this->render_label( 'select', $id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 2699 | $class = preg_replace( "/class=['\"]([^'\"]*)['\"]/", 'class="contact-form__select-wrapper $1"', $class ); |
| 2700 | $field .= "<div {$class} style='" . esc_attr( $this->field_styles ) . "'>"; |
| 2701 | $aria_label = ! empty( $this->get_attribute( 'togglelabel' ) ) |
| 2702 | ? Contact_Form_Plugin::strip_tags( $this->get_attribute( 'togglelabel' ) ) |
| 2703 | : __( 'Select an option', 'jetpack-forms' ); // selects don't have a default label |
| 2704 | $field .= "\t<span class='contact-form__select-element-wrapper'><select name='" . esc_attr( $id ) . "' id='" . esc_attr( $id ) . "' " . ( $required ? "required aria-required='true'" : '' ) . " data-wp-on--change='actions.onFieldChange' data-wp-bind--aria-invalid='state.fieldAriaInvalid' aria-describedby='" . esc_attr( $this->get_described_by( $id, 'select' ) ) . "' " . $this->get_hidden_label_aria_label_attr( $aria_label ) . ">\n"; |
| 2705 | |
| 2706 | if ( $this->get_attribute( 'togglelabel' ) ) { |
| 2707 | $field .= "\t\t<option value=''>" . $this->get_attribute( 'togglelabel' ) . "</option>\n"; |
| 2708 | } elseif ( ! $this->get_attribute( 'default' ) ) { |
| 2709 | // For select fields without an explicit togglelabel or default value (e.g., shortcode-based forms), |
| 2710 | // add a placeholder option to ensure users must make an explicit selection. |
| 2711 | $field .= "\t\t<option value=''>" . esc_html__( 'Select an option', 'jetpack-forms' ) . "</option>\n"; |
| 2712 | } |
| 2713 | |
| 2714 | foreach ( (array) $this->get_attribute( 'options' ) as $option_index => $option ) { |
| 2715 | $option = Contact_Form_Plugin::strip_tags( $option ); |
| 2716 | if ( is_string( $option ) && $option !== '' ) { |
| 2717 | $field .= "\t\t<option" |
| 2718 | . selected( $option, $value, false ) |
| 2719 | . " value='" . esc_attr( $this->get_option_value( $this->get_attribute( 'values' ), $option_index, $option ) ) |
| 2720 | . "'>" . esc_html( $option ) |
| 2721 | . "</option>\n"; |
| 2722 | } |
| 2723 | } |
| 2724 | $field .= "\t</select><span class='jetpack-field-dropdown__icon'></span></span>\n"; |
| 2725 | $field .= "</div>\n"; |
| 2726 | |
| 2727 | return $field . $this->get_field_descriptions( $id, 'select' ); |
| 2728 | } |
| 2729 | |
| 2730 | /** |
| 2731 | * The date formats the date field supports, keyed by the jQuery-style |
| 2732 | * format string stored in the `dateformat` attribute. |
| 2733 | * |
| 2734 | * WARNING: sync data with DATE_FORMATS in src/blocks/shared/util/constants.js |
| 2735 | * |
| 2736 | * @since 8.0.0 |
| 2737 | * |
| 2738 | * @return array |
| 2739 | */ |
| 2740 | private static function get_date_formats() { |
| 2741 | return array( |
| 2742 | /* translators: date format. DD is the day of the month, MM the month, and YYYY the year (e.g., 12/31/2023). */ |
| 2743 | 'mm/dd/yy' => __( 'MM/DD/YYYY', 'jetpack-forms' ), |
| 2744 | /* translators: date format. DD is the day of the month, MM the month, and YYYY the year (e.g., 31/12/2023). */ |
| 2745 | 'dd/mm/yy' => __( 'DD/MM/YYYY', 'jetpack-forms' ), |
| 2746 | /* translators: date format. DD is the day of the month, MM the month, and YYYY the year (e.g., 2023-12-31). */ |
| 2747 | 'yy-mm-dd' => __( 'YYYY-MM-DD', 'jetpack-forms' ), |
| 2748 | ); |
| 2749 | } |
| 2750 | |
| 2751 | /** |
| 2752 | * The field's date format key, falling back to the default. |
| 2753 | * |
| 2754 | * Shared by the visible hint and the `data-format` attribute that drives |
| 2755 | * the picker, so the two cannot disagree about which format is in effect. |
| 2756 | * |
| 2757 | * @since 8.0.0 |
| 2758 | * |
| 2759 | * @return string |
| 2760 | */ |
| 2761 | private function get_date_format() { |
| 2762 | $date_format = $this->get_attribute( 'dateformat' ); |
| 2763 | |
| 2764 | return ! empty( $date_format ) ? $date_format : 'yy-mm-dd'; |
| 2765 | } |
| 2766 | |
| 2767 | /** |
| 2768 | * Return the HTML for the date field. |
| 2769 | * |
| 2770 | * @param int $id - the ID. |
| 2771 | * @param string $label - the label. |
| 2772 | * @param string $value - the value of the field. |
| 2773 | * @param string $class - the field class. |
| 2774 | * @param bool $required - if the field is marked as required. |
| 2775 | * @param string $required_field_text - the text in the required text field. |
| 2776 | * @param string $placeholder - the field placeholder content. |
| 2777 | * @param bool $required_indicator Whether to display the required indicator. |
| 2778 | * |
| 2779 | * @return string HTML |
| 2780 | */ |
| 2781 | public function render_date_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) { |
| 2782 | static $is_loaded = false; |
| 2783 | $this->set_invalid_message( 'date', __( 'Please enter a valid date.', 'jetpack-forms' ) ); |
| 2784 | |
| 2785 | $date_format = $this->get_date_format(); |
| 2786 | $extra_attrs = array( 'data-format' => $date_format ); |
| 2787 | |
| 2788 | $field = $this->render_label( 'date', $id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 2789 | $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required, $extra_attrs ); |
| 2790 | |
| 2791 | /* For AMP requests, use amp-date-picker element: https://amp.dev/documentation/components/amp-date-picker */ |
| 2792 | if ( class_exists( 'Jetpack_AMP_Support' ) && \Jetpack_AMP_Support::is_amp_request() ) { |
| 2793 | return sprintf( |
| 2794 | '<%1$s mode="overlay" layout="container" type="single" input-selector="[name=%2$s]">%3$s</%1$s>', |
| 2795 | 'amp-date-picker', |
| 2796 | esc_attr( $id ), |
| 2797 | $field |
| 2798 | ); |
| 2799 | } |
| 2800 | |
| 2801 | Assets::register_script( |
| 2802 | 'jp-forms-date-picker', |
| 2803 | '../../dist/contact-form/js/date-picker.js', |
| 2804 | __FILE__, |
| 2805 | array( |
| 2806 | 'enqueue' => true, |
| 2807 | 'dependencies' => array(), |
| 2808 | 'version' => Constants::get_constant( 'JETPACK__VERSION' ), |
| 2809 | ) |
| 2810 | ); |
| 2811 | |
| 2812 | /** |
| 2813 | * Filter the localized date picker script. |
| 2814 | */ |
| 2815 | if ( ! $is_loaded ) { |
| 2816 | \wp_localize_script( |
| 2817 | 'jp-forms-date-picker', |
| 2818 | 'jpDatePicker', |
| 2819 | array( |
| 2820 | 'offset' => intval( get_option( 'start_of_week', 1 ) ), |
| 2821 | 'lang' => array( |
| 2822 | // translators: These are the two letter abbreviated name of the week. |
| 2823 | 'days' => array( |
| 2824 | __( 'Su', 'jetpack-forms' ), |
| 2825 | __( 'Mo', 'jetpack-forms' ), |
| 2826 | __( 'Tu', 'jetpack-forms' ), |
| 2827 | __( 'We', 'jetpack-forms' ), |
| 2828 | __( 'Th', 'jetpack-forms' ), |
| 2829 | __( 'Fr', 'jetpack-forms' ), |
| 2830 | __( 'Sa', 'jetpack-forms' ), |
| 2831 | ), |
| 2832 | 'months' => array( |
| 2833 | __( 'January', 'jetpack-forms' ), |
| 2834 | __( 'February', 'jetpack-forms' ), |
| 2835 | __( 'March', 'jetpack-forms' ), |
| 2836 | __( 'April', 'jetpack-forms' ), |
| 2837 | __( 'May', 'jetpack-forms' ), |
| 2838 | __( 'June', 'jetpack-forms' ), |
| 2839 | __( 'July', 'jetpack-forms' ), |
| 2840 | __( 'August', 'jetpack-forms' ), |
| 2841 | __( 'September', 'jetpack-forms' ), |
| 2842 | __( 'October', 'jetpack-forms' ), |
| 2843 | __( 'November', 'jetpack-forms' ), |
| 2844 | __( 'December', 'jetpack-forms' ), |
| 2845 | ), |
| 2846 | 'today' => __( 'Today', 'jetpack-forms' ), |
| 2847 | 'clear' => __( 'Clear', 'jetpack-forms' ), |
| 2848 | 'close' => __( 'Close', 'jetpack-forms' ), |
| 2849 | 'ariaLabel' => array( |
| 2850 | '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' ), |
| 2851 | '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' ), |
| 2852 | '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' ), |
| 2853 | 'monthPickerButton' => __( 'Month picker. Use the space key to enter the month picker.', 'jetpack-forms' ), |
| 2854 | 'yearPickerButton' => __( 'Year picker. Use the space key to enter the month picker.', 'jetpack-forms' ), |
| 2855 | 'dayButton' => __( 'Use the space key to select the date.', 'jetpack-forms' ), |
| 2856 | 'todayButton' => __( 'Today button. Use the space key to select the current date.', 'jetpack-forms' ), |
| 2857 | 'clearButton' => __( 'Clear button. Use the space key to clear the date picker.', 'jetpack-forms' ), |
| 2858 | 'closeButton' => __( 'Close button. Use the space key to close the date picker.', 'jetpack-forms' ), |
| 2859 | ), |
| 2860 | ), |
| 2861 | ) |
| 2862 | ); |
| 2863 | $is_loaded = true; |
| 2864 | } |
| 2865 | |
| 2866 | return $field; |
| 2867 | } |
| 2868 | |
| 2869 | /** |
| 2870 | * Return the HTML for the time field. |
| 2871 | * |
| 2872 | * @param int $id - the ID. |
| 2873 | * @param string $label - the label. |
| 2874 | * @param string $value - the value of the field. |
| 2875 | * @param string $class - the field class. |
| 2876 | * @param bool $required - if the field is marked as required. |
| 2877 | * @param string $required_field_text - the text in the required text field. |
| 2878 | * @param string $placeholder - the field placeholder content. |
| 2879 | * @param bool $required_indicator Whether to display the required indicator. |
| 2880 | * |
| 2881 | * @return string HTML |
| 2882 | */ |
| 2883 | public function render_time_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $required_indicator = true ) { |
| 2884 | $this->set_invalid_message( 'time', __( 'Please enter a valid time.', 'jetpack-forms' ) ); |
| 2885 | |
| 2886 | $field = $this->render_label( 'time', $id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 2887 | $field .= $this->render_input_field( 'time', $id, $value, $class, $placeholder, $required ); |
| 2888 | |
| 2889 | return $field; |
| 2890 | } |
| 2891 | |
| 2892 | /** |
| 2893 | * Return the HTML for the image select field. |
| 2894 | * |
| 2895 | * @param int $id - the ID. |
| 2896 | * @param string $label - the label. |
| 2897 | * @param string $value - the value of the field. |
| 2898 | * @param string $class - the field class. |
| 2899 | * @param bool $required - if the field is marked as required. |
| 2900 | * @param string $required_field_text - the text in the required text field. |
| 2901 | * @param bool $required_indicator Whether to display the required indicator. |
| 2902 | * |
| 2903 | * @return string HTML |
| 2904 | */ |
| 2905 | public function render_image_select_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) { |
| 2906 | 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' ) ); |
| 2907 | |
| 2908 | $is_multiple = $this->get_attribute( 'ismultiple' ); |
| 2909 | $show_labels = $this->get_attribute( 'showlabels' ); |
| 2910 | $randomize_options = $this->get_attribute( 'randomizeoptions' ); |
| 2911 | $is_supersized = $this->get_attribute( 'issupersized' ); |
| 2912 | |
| 2913 | $input_type = $is_multiple ? 'checkbox' : 'radio'; |
| 2914 | $input_name = $is_multiple ? $id . '[]' : $id; |
| 2915 | |
| 2916 | $field = "<div class='jetpack-field jetpack-field-image-select'>"; |
| 2917 | |
| 2918 | $fieldset_id = "id='" . esc_attr( "$id-label" ) . "'" . $this->get_hidden_label_aria_label_attr(); |
| 2919 | |
| 2920 | $field .= "<fieldset {$fieldset_id} data-wp-bind--aria-invalid='state.fieldAriaInvalid' >"; |
| 2921 | |
| 2922 | $field .= $this->render_legend_as_label( '', $id, $label, $required, $required_field_text, array(), $required_indicator ); |
| 2923 | |
| 2924 | $options_classes = $this->get_attribute( 'optionsclasses' ); |
| 2925 | $options_styles = $this->get_attribute( 'optionsstyles' ); |
| 2926 | |
| 2927 | $field .= "<div class='" . esc_attr( $options_classes ) . " jetpack-field jetpack-fieldset-image-options' style='" . esc_attr( $options_styles ) . "'>"; |
| 2928 | $field .= "<div class='jetpack-fieldset-image-options__wrapper'>"; |
| 2929 | |
| 2930 | $options_data = $this->get_attribute( 'optionsdata' ); |
| 2931 | |
| 2932 | // Filter out empty options from the end |
| 2933 | $options_data = $this->trim_image_select_options( $options_data ); |
| 2934 | |
| 2935 | $used_html_ids = array(); |
| 2936 | |
| 2937 | // Create a separate array of original letters in sequence (A, B, C...) |
| 2938 | $perceived_letters = array(); |
| 2939 | |
| 2940 | foreach ( $options_data as $option ) { |
| 2941 | $perceived_letters[] = Contact_Form_Plugin::strip_tags( $option['letter'] ); |
| 2942 | } |
| 2943 | |
| 2944 | // Create a working copy of options for potential randomization |
| 2945 | $working_options = $options_data; |
| 2946 | |
| 2947 | // Randomize options if requested, but preserve original letter values |
| 2948 | if ( $randomize_options ) { |
| 2949 | shuffle( $working_options ); |
| 2950 | |
| 2951 | // Trims options after randomization to ensure the last option has a label or image. |
| 2952 | $working_options = $this->trim_image_select_options( $working_options ); |
| 2953 | } |
| 2954 | |
| 2955 | // Calculate row options count for CSS variable |
| 2956 | $total_options_count = count( $working_options ); |
| 2957 | // Those values are halved on mobile via CSS media query |
| 2958 | $max_images_per_row = $is_supersized ? 2 : 4; |
| 2959 | $row_options_count = min( $total_options_count, $max_images_per_row ); |
| 2960 | |
| 2961 | foreach ( $working_options as $option_index => $option ) { |
| 2962 | $option_label = Contact_Form_Plugin::strip_tags( $option['label'] ); |
| 2963 | $option_letter = Contact_Form_Plugin::strip_tags( $option['letter'] ); |
| 2964 | $image_block = $option['image'] ?? null; |
| 2965 | $image_id = ( is_array( $image_block ) && isset( $image_block['attrs'] ) && is_array( $image_block['attrs'] ) ) ? ( $image_block['attrs']['id'] ?? null ) : null; |
| 2966 | |
| 2967 | $rendered_image_block = is_array( $image_block ) ? render_block( $image_block ) : ''; |
| 2968 | // Remove any links from the rendered block |
| 2969 | $rendered_image_block = preg_replace( '/<a[^>]*>(.*?)<\/a>/s', '$1', $rendered_image_block ); |
| 2970 | |
| 2971 | // Extract image src from rendered block |
| 2972 | $image_src = ''; |
| 2973 | |
| 2974 | if ( ! empty( $rendered_image_block ) ) { |
| 2975 | if ( preg_match( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $rendered_image_block, $matches ) ) { |
| 2976 | $extracted_src = $matches[1]; |
| 2977 | |
| 2978 | if ( filter_var( $extracted_src, FILTER_VALIDATE_URL ) || str_starts_with( $extracted_src, 'data:' ) ) { |
| 2979 | $image_src = $extracted_src; |
| 2980 | } |
| 2981 | } |
| 2982 | } else { |
| 2983 | $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>'; |
| 2984 | } |
| 2985 | |
| 2986 | $option_value = wp_json_encode( |
| 2987 | array( |
| 2988 | 'perceived' => $perceived_letters[ $option_index ], |
| 2989 | 'selected' => $option_letter, |
| 2990 | 'label' => $option_label, |
| 2991 | 'showLabels' => $show_labels, |
| 2992 | 'image' => array( |
| 2993 | 'id' => $image_id, |
| 2994 | 'src' => $image_src ?? null, |
| 2995 | ), |
| 2996 | ), |
| 2997 | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES |
| 2998 | ); |
| 2999 | $option_id = $id . '-' . $option_letter; |
| 3000 | $used_html_ids[ $option_id ] = true; |
| 3001 | |
| 3002 | $figcaption_id = esc_attr( $option_id . '-figcaption' ); |
| 3003 | |
| 3004 | // Add id attribute to figcaption for accessibility |
| 3005 | if ( ! empty( $rendered_image_block ) && strpos( $rendered_image_block, '<figcaption' ) !== false ) { |
| 3006 | $rendered_image_block = preg_replace( '/(<figcaption[^>]*)(>)/', '$1 id="' . $figcaption_id . '"$2', $rendered_image_block ); |
| 3007 | } |
| 3008 | |
| 3009 | // To be able to apply the backdrop-filter for the hover effect, we need to separate the background into an outer div. |
| 3010 | // This outer div needs the color styles separately, and also the border radius to match the inner div without sticking out. |
| 3011 | $option_outer_classes = 'jetpack-input-image-option__outer ' . ( $option['classcolor'] ?? '' ); |
| 3012 | |
| 3013 | if ( $is_supersized ) { |
| 3014 | $option_outer_classes .= ' is-supersized'; |
| 3015 | } |
| 3016 | |
| 3017 | $border_styles = ''; |
| 3018 | if ( ! empty( $option['style'] ) ) { |
| 3019 | preg_match( '/border-radius:([^;]+)/', $option['style'], $radius_match ); |
| 3020 | preg_match( '/border-width:([^;]+)/', $option['style'], $width_match ); |
| 3021 | |
| 3022 | if ( ! empty( $radius_match[1] ) ) { |
| 3023 | $radius_value = trim( $radius_match[1] ); |
| 3024 | |
| 3025 | if ( ! empty( $width_match[1] ) ) { |
| 3026 | $width_value = trim( $width_match[1] ); |
| 3027 | $border_styles = "border-radius:calc({$radius_value} + {$width_value});"; |
| 3028 | } else { |
| 3029 | $border_styles = "border-radius:{$radius_value};"; |
| 3030 | } |
| 3031 | } else { |
| 3032 | // Handle individual border radius properties when border-radius is not present |
| 3033 | preg_match( '/border-top-left-radius:([^;]+)/', $option['style'], $top_left_match ); |
| 3034 | preg_match( '/border-top-right-radius:([^;]+)/', $option['style'], $top_right_match ); |
| 3035 | preg_match( '/border-bottom-right-radius:([^;]+)/', $option['style'], $bottom_right_match ); |
| 3036 | preg_match( '/border-bottom-left-radius:([^;]+)/', $option['style'], $bottom_left_match ); |
| 3037 | |
| 3038 | if ( ! empty( $top_left_match[1] ) || ! empty( $top_right_match[1] ) || ! empty( $bottom_right_match[1] ) || ! empty( $bottom_left_match[1] ) ) { |
| 3039 | $width_value = ! empty( $width_match[1] ) ? trim( $width_match[1] ) : '1px'; |
| 3040 | |
| 3041 | $top_left = ! empty( $top_left_match[1] ) ? trim( $top_left_match[1] ) : '4px'; |
| 3042 | $top_right = ! empty( $top_right_match[1] ) ? trim( $top_right_match[1] ) : '4px'; |
| 3043 | $bottom_right = ! empty( $bottom_right_match[1] ) ? trim( $bottom_right_match[1] ) : '4px'; |
| 3044 | $bottom_left = ! empty( $bottom_left_match[1] ) ? trim( $bottom_left_match[1] ) : '4px'; |
| 3045 | |
| 3046 | $border_styles = "border-radius:calc({$top_left} + {$width_value}) calc({$top_right} + {$width_value}) calc({$bottom_right} + {$width_value}) calc({$bottom_left} + {$width_value});"; |
| 3047 | } |
| 3048 | } |
| 3049 | } |
| 3050 | |
| 3051 | $option_outer_styles = ( empty( $option['stylecolor'] ) ? '' : $option['stylecolor'] ) . $border_styles; |
| 3052 | $option_outer_styles .= "--row-options-count: {$row_options_count};"; |
| 3053 | $option_outer_styles = empty( $option_outer_styles ) ? '' : "style='" . esc_attr( $option_outer_styles ) . "'"; |
| 3054 | |
| 3055 | $field .= "<div class='{$option_outer_classes}' {$option_outer_styles}>"; |
| 3056 | |
| 3057 | $default_classes = 'jetpack-field jetpack-input-image-option'; |
| 3058 | $option_styles = empty( $option['style'] ) ? '' : "style='" . esc_attr( $option['style'] ) . "'"; |
| 3059 | $option_classes = "class='" . ( empty( $option['class'] ) ? $default_classes : $default_classes . ' ' . esc_attr( $option['class'] ) ) . "'"; |
| 3060 | |
| 3061 | $field .= "<div {$option_classes} {$option_styles} data-wp-on--click='actions.onImageOptionClick' data-wp-init='callbacks.setImageOptionOutlineColor'>"; |
| 3062 | |
| 3063 | $input_id = esc_attr( $option_id ); |
| 3064 | $label_id = esc_attr( $option_id . '-label' ); |
| 3065 | |
| 3066 | /* translators: %s is the letter associated with the option, e.g. "Option A" */ |
| 3067 | $aria_label_parts = array( sprintf( __( 'Option %s', 'jetpack-forms' ), $perceived_letters[ $option_index ] ) ); |
| 3068 | |
| 3069 | if ( ! empty( $option_label ) ) { |
| 3070 | $aria_label_parts[] = $option_label; |
| 3071 | } |
| 3072 | |
| 3073 | $aria_label = implode( ': ', $aria_label_parts ); |
| 3074 | |
| 3075 | // Build aria-describedby to reference label and figcaption |
| 3076 | $aria_describedby_parts = array( $label_id ); |
| 3077 | |
| 3078 | if ( ! empty( $rendered_image_block ) && strpos( $rendered_image_block, '<figcaption' ) !== false ) { |
| 3079 | $aria_describedby_parts[] = $figcaption_id; |
| 3080 | } |
| 3081 | |
| 3082 | $aria_describedby = implode( ' ', $aria_describedby_parts ); |
| 3083 | |
| 3084 | $field .= "<div class='jetpack-input-image-option__wrapper'>"; |
| 3085 | $field .= "<input |
| 3086 | id='" . $input_id . "' |
| 3087 | class='jetpack-input-image-option__input' |
| 3088 | type='" . esc_attr( $input_type ) . "' |
| 3089 | name='" . esc_attr( $input_name ) . "' |
| 3090 | value='" . esc_attr( $option_value ) . "' |
| 3091 | aria-label='" . esc_attr( $aria_label ) . "' |
| 3092 | aria-describedby='" . esc_attr( $aria_describedby ) . "' |
| 3093 | data-wp-init='callbacks.setImageOptionCheckColor' |
| 3094 | data-wp-on--keydown='actions.onKeyDownImageOption' |
| 3095 | data-wp-on--change='" . ( $is_multiple ? 'actions.onMultipleFieldChange' : 'actions.onFieldChange' ) . "' " |
| 3096 | . $class |
| 3097 | . ( $is_multiple ? checked( in_array( $option_value, (array) $value, true ), true, false ) : checked( $option_value, $value, false ) ) . ' ' |
| 3098 | . ( $required ? "required aria-required='true'" : '' ) |
| 3099 | . '/> '; |
| 3100 | |
| 3101 | $field .= $rendered_image_block; |
| 3102 | $field .= '</div>'; |
| 3103 | |
| 3104 | $field .= "<div class='jetpack-input-image-option__label-wrapper'>"; |
| 3105 | $field .= "<div class='jetpack-input-image-option__label-code'>" . esc_html( $perceived_letters[ $option_index ] ) . '</div>'; |
| 3106 | |
| 3107 | $label_classes = 'jetpack-input-image-option__label'; |
| 3108 | $label_classes .= $show_labels ? '' : ' visually-hidden'; |
| 3109 | $field .= "<span id='{$label_id}' class='{$label_classes}'>" . esc_html( $option_label ) . '</span>'; |
| 3110 | $field .= '</div></div></div>'; |
| 3111 | } |
| 3112 | |
| 3113 | $field .= '</div></div>'; |
| 3114 | |
| 3115 | $field .= $this->get_error_div( $id, 'image-select' ); |
| 3116 | |
| 3117 | $field .= '</fieldset>'; |
| 3118 | |
| 3119 | $field .= '</div>'; |
| 3120 | |
| 3121 | return $field; |
| 3122 | } |
| 3123 | |
| 3124 | /** |
| 3125 | * Return the HTML for the number field. |
| 3126 | * |
| 3127 | * @param int $id - the ID. |
| 3128 | * @param string $label - the label. |
| 3129 | * @param string $value - the value of the field. |
| 3130 | * @param string $class - the field class. |
| 3131 | * @param bool $required - if the field is marked as required. |
| 3132 | * @param string $required_field_text - the text in the required text field. |
| 3133 | * @param string $placeholder - the field placeholder content. |
| 3134 | * @param array $extra_attrs - Extra attributes used in number field, namely `min` and `max`. |
| 3135 | * @param bool $required_indicator Whether to display the required indicator. |
| 3136 | * |
| 3137 | * @return string HTML |
| 3138 | */ |
| 3139 | public function render_number_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $extra_attrs = array(), $required_indicator = true ) { |
| 3140 | $this->set_invalid_message( 'number', __( 'Please enter a valid number', 'jetpack-forms' ) ); |
| 3141 | if ( isset( $extra_attrs['min'] ) ) { |
| 3142 | // translators: %d is the minimum value. |
| 3143 | $this->set_invalid_message( 'min_number', __( 'Please select a value that is no less than %d.', 'jetpack-forms' ) ); |
| 3144 | } |
| 3145 | if ( isset( $extra_attrs['max'] ) ) { |
| 3146 | // translators: %d is the maximum value. |
| 3147 | $this->set_invalid_message( 'max_number', __( 'Please select a value that is no more than %d.', 'jetpack-forms' ) ); |
| 3148 | } |
| 3149 | $field = $this->render_label( 'number', $id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 3150 | $field .= $this->render_input_field( 'number', $id, $value, $class, $placeholder, $required, $extra_attrs ); |
| 3151 | return $field; |
| 3152 | } |
| 3153 | |
| 3154 | /** |
| 3155 | * Return the HTML for the default field. |
| 3156 | * |
| 3157 | * @param int $id - the ID. |
| 3158 | * @param string $label - the label. |
| 3159 | * @param string $value - the value of the field. |
| 3160 | * @param string $class - the field class. |
| 3161 | * @param bool $required - if the field is marked as required. |
| 3162 | * @param string $required_field_text - the text in the required text field. |
| 3163 | * @param string $placeholder - the field placeholder content. |
| 3164 | * @param string $type - the type. |
| 3165 | * @param bool $required_indicator Whether to display the required indicator. |
| 3166 | * |
| 3167 | * @return string HTML |
| 3168 | */ |
| 3169 | public function render_default_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $type, $required_indicator = true ) { |
| 3170 | $field = $this->render_label( $type, $id, $label, $required, $required_field_text, array(), false, $required_indicator ); |
| 3171 | $field .= $this->render_input_field( 'text', $id, $value, $class, $placeholder, $required ); |
| 3172 | return $field; |
| 3173 | } |
| 3174 | |
| 3175 | /** |
| 3176 | * Returns the styles, classes and CSS vars necessary to render fields in the "Outlined" style. |
| 3177 | * The "Animated" style variation shares the CSS vars, which require similar calculations for the left offset and label left position. |
| 3178 | * At the block level, the styles are extracted and added to the shortcode attributes in |
| 3179 | * Contact_Form_Plugin::get_outlined_style_attributes(). |
| 3180 | * This function extracts those styles and applies them to the field, |
| 3181 | * and ensures any global or theme styles are applied. |
| 3182 | * |
| 3183 | * @param string $form_style (optional) The form style. |
| 3184 | * |
| 3185 | * @return array { |
| 3186 | * @type string $style_attrs The style attributes. |
| 3187 | * @type string $css_vars The CSS variables. |
| 3188 | * @type string $class_name The class name. |
| 3189 | * } |
| 3190 | */ |
| 3191 | private function get_form_variation_style_properties( $form_style = 'outlined' ) { |
| 3192 | $css_vars = ''; |
| 3193 | $variation_attributes = $this->get_attribute( 'stylevariationattributes' ); |
| 3194 | $variation_attributes = ! empty( $variation_attributes ) ? json_decode( html_entity_decode( $variation_attributes, ENT_COMPAT ), true ) : array(); |
| 3195 | $variation_classes = $this->get_attribute( 'stylevariationclasses' ); |
| 3196 | $variation_style = $this->get_attribute( 'stylevariationstyles' ); |
| 3197 | $block_name = 'jetpack/input'; |
| 3198 | |
| 3199 | if ( $this->maybe_override_type() === 'radio' || $this->maybe_override_type() === 'checkbox-multiple' ) { |
| 3200 | $block_name = 'jetpack/options'; |
| 3201 | } |
| 3202 | |
| 3203 | $global_styles = wp_get_global_styles( |
| 3204 | array( 'border' ), |
| 3205 | array( |
| 3206 | 'block_name' => $block_name, |
| 3207 | 'transforms' => array( 'resolve-variables' ), |
| 3208 | ) |
| 3209 | ); |
| 3210 | |
| 3211 | /* |
| 3212 | * The `borderwidth` attribute contains the border value that forms used before the migration to global styles. |
| 3213 | * Any old forms saved in a post will still use this attribute, so it needs to be factored into the css vars for border |
| 3214 | * 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. |
| 3215 | * For newer forms that use global styles or the block supports styles, this value will be empty and is ignored. |
| 3216 | */ |
| 3217 | $border_width_attribute = $this->get_attribute( 'borderwidth' ); |
| 3218 | $legacy_border_size = ! empty( $border_width_attribute ) || $border_width_attribute === '0' ? $border_width_attribute . 'px' : null; |
| 3219 | |
| 3220 | $border_radius_attribute = $this->get_attribute( 'borderradius' ); |
| 3221 | $legacy_border_radius = ! empty( $border_radius_attribute ) || $border_radius_attribute === '0' ? $border_radius_attribute . 'px' : $variation_attributes['border']['radius'] ?? null; |
| 3222 | |
| 3223 | $border_top_size = $legacy_border_size ?? |
| 3224 | $variation_attributes['border']['width'] ?? |
| 3225 | $variation_attributes['border']['top']['width'] ?? |
| 3226 | $global_styles['width'] ?? |
| 3227 | $global_styles['top']['width'] ?? null; |
| 3228 | |
| 3229 | $border_right_size = $legacy_border_size ?? |
| 3230 | |
| 3231 | $variation_attributes['border']['right']['width'] ?? |
| 3232 | $global_styles['width'] ?? |
| 3233 | $global_styles['right']['width'] ?? null; |
| 3234 | |
| 3235 | $border_bottom_size = $legacy_border_size ?? |
| 3236 | $variation_attributes['border']['width'] ?? |
| 3237 | $variation_attributes['border']['bottom']['width'] ?? |
| 3238 | $global_styles['width'] ?? |
| 3239 | $global_styles['bottom']['width'] ?? null; |
| 3240 | |
| 3241 | $border_left_size = $legacy_border_size ?? |
| 3242 | $variation_attributes['border']['width'] ?? |
| 3243 | $variation_attributes['border']['left']['width'] ?? |
| 3244 | $global_styles['width'] ?? |
| 3245 | $global_styles['left']['width'] ?? null; |
| 3246 | |
| 3247 | $border_radius = $legacy_border_radius ?? |
| 3248 | $global_styles['radius'] ?? null; |
| 3249 | |
| 3250 | // Border size to accommodate legacy border width attribute. |
| 3251 | $css_vars = $legacy_border_size ? '--jetpack--contact-form--border-size: ' . $legacy_border_size . ';' : ''; |
| 3252 | |
| 3253 | // Border side sizes to accommodate global styles split values. |
| 3254 | $css_vars .= $border_top_size ? '--jetpack--contact-form--border-top-size: ' . $border_top_size . ';' : ''; |
| 3255 | $css_vars .= $border_right_size ? '--jetpack--contact-form--border-right-size: ' . $border_right_size . ';' : ''; |
| 3256 | $css_vars .= $border_bottom_size ? '--jetpack--contact-form--border-bottom-size: ' . $border_bottom_size . ';' : ''; |
| 3257 | $css_vars .= $border_left_size ? '--jetpack--contact-form--border-left-size: ' . $border_left_size . ';' : ''; |
| 3258 | |
| 3259 | // Check if border radius is split or a single value. |
| 3260 | if ( is_array( $border_radius ) ) { |
| 3261 | // If corner radii are set on the top-left or bottom-left of the block, take the maximum of the two. |
| 3262 | // We check the left side due to writing direction—this variable is used to offset text. |
| 3263 | // TODO: this should factor in RTL languages. |
| 3264 | $css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: max(' . ( $border_radius['topLeft'] ?? '0' ) . ',' . ( $border_radius['bottomLeft'] ?? '0' ) . ');' : ''; |
| 3265 | } elseif ( isset( $border_radius ) ) { |
| 3266 | $css_vars .= $border_radius ? '--jetpack--contact-form--border-radius: ' . $border_radius . ';' : ''; |
| 3267 | } |
| 3268 | |
| 3269 | if ( 'outlined' === $form_style ) { |
| 3270 | $css_vars .= '--jetpack--contact-form--notch-width: max(var(--jetpack--contact-form--input-padding-left, 16px), var(--jetpack--contact-form--border-radius));'; |
| 3271 | } elseif ( 'animated' === $form_style ) { |
| 3272 | $css_vars .= '--jetpack--contact-form--animated-left-offset: 16px;'; |
| 3273 | } |
| 3274 | |
| 3275 | return array( |
| 3276 | 'style' => $variation_style, |
| 3277 | 'css_vars' => $css_vars, |
| 3278 | 'class_name' => $variation_classes, |
| 3279 | ); |
| 3280 | } |
| 3281 | |
| 3282 | /** |
| 3283 | * Return the HTML for the outlined label. |
| 3284 | * |
| 3285 | * @param int $id - the ID. |
| 3286 | * @param string $label - the label. |
| 3287 | * @param bool $required - if the field is marked as required. |
| 3288 | * @param string $required_field_text - the text in the required text field. |
| 3289 | * @param bool $required_indicator Whether to display the required indicator. |
| 3290 | * |
| 3291 | * @return string HTML |
| 3292 | */ |
| 3293 | public function render_outline_label( $id, $label, $required, $required_field_text, $required_indicator = true ) { |
| 3294 | $classes = 'notched-label__label'; |
| 3295 | $classes .= $this->is_error() ? ' form-error' : ''; |
| 3296 | $classes .= $this->label_classes ? ' ' . $this->label_classes : ''; |
| 3297 | |
| 3298 | $output_data = $this->get_form_variation_style_properties(); |
| 3299 | |
| 3300 | return ' |
| 3301 | <div class="notched-label"> |
| 3302 | <div class="notched-label__leading' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div> |
| 3303 | <div class="notched-label__notch' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"> |
| 3304 | <label |
| 3305 | for="' . esc_attr( $id ) . '" |
| 3306 | class=" ' . $classes . '" |
| 3307 | style="' . $this->label_styles . esc_attr( $output_data['css_vars'] ) . '" |
| 3308 | > |
| 3309 | <span class="grunion-label-text">' . esc_html( $label ) . '</span>' |
| 3310 | . ( $required && $required_indicator ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) . |
| 3311 | '</label> |
| 3312 | </div> |
| 3313 | <div class="notched-label__filler' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div> |
| 3314 | <div class="notched-label__trailing' . esc_attr( $output_data['class_name'] ) . '" style="' . esc_attr( $output_data['style'] ) . '"></div> |
| 3315 | </div>'; |
| 3316 | } |
| 3317 | |
| 3318 | /** |
| 3319 | * Return the HTML for the animated label. |
| 3320 | * |
| 3321 | * @param int $id - the ID. |
| 3322 | * @param string $label - the label. |
| 3323 | * @param bool $required - if the field is marked as required. |
| 3324 | * @param string $required_field_text - the text in the required text field. |
| 3325 | * @param bool $required_indicator Whether to display the required indicator. |
| 3326 | * |
| 3327 | * @return string HTML |
| 3328 | */ |
| 3329 | public function render_animated_label( $id, $label, $required, $required_field_text, $required_indicator = true ) { |
| 3330 | $classes = 'animated-label__label'; |
| 3331 | $classes .= $this->is_error() ? ' form-error' : ''; |
| 3332 | $classes .= $this->label_classes ? ' ' . $this->label_classes : ''; |
| 3333 | |
| 3334 | return ' |
| 3335 | <label |
| 3336 | for="' . esc_attr( $id ) . '" |
| 3337 | class="' . $classes . '" |
| 3338 | style="' . $this->label_styles . '" |
| 3339 | > |
| 3340 | <span class="grunion-label-text">' . wp_kses_post( $label ) . '</span>' |
| 3341 | . ( $required && $required_indicator !== 'hidden' ? '<span class="grunion-label-required" aria-hidden="true">' . $required_field_text . '</span>' : '' ) . |
| 3342 | '</label>'; |
| 3343 | } |
| 3344 | |
| 3345 | /** |
| 3346 | * Return the HTML for the below label. |
| 3347 | * |
| 3348 | * @param int $id - the ID. |
| 3349 | * @param string $label - the label. |
| 3350 | * @param bool $required - if the field is marked as required. |
| 3351 | * @param string $required_field_text - the text in the required text field. |
| 3352 | * @param bool $required_indicator Whether to display the required indicator. |
| 3353 | * |
| 3354 | * @return string HTML |
| 3355 | */ |
| 3356 | public function render_below_label( $id, $label, $required, $required_field_text, $required_indicator = true ) { |
| 3357 | return ' |
| 3358 | <label |
| 3359 | for="' . esc_attr( $id ) . '" |
| 3360 | class="below-label__label ' . ( $this->is_error() ? ' form-error' : '' ) . ( $this->label_classes ? ' ' . esc_attr( $this->label_classes ) : '' ) . '" |
| 3361 | >' |
| 3362 | . esc_html( $label ) |
| 3363 | . ( $required && $required_indicator ? '<span>' . $required_field_text . '</span>' : '' ) . |
| 3364 | '</label>'; |
| 3365 | } |
| 3366 | |
| 3367 | /** |
| 3368 | * Return the HTML for the email field. |
| 3369 | * |
| 3370 | * @param string $type - the type. |
| 3371 | * @param int $id - the ID. |
| 3372 | * @param string $label - the label. |
| 3373 | * @param string $value - the value of the field. |
| 3374 | * @param string $class - the field class. |
| 3375 | * @param string $placeholder - the field placeholder content. |
| 3376 | * @param bool $required - if the field is marked as required. |
| 3377 | * @param string $required_field_text - the text for a field marked as required. |
| 3378 | * @param array $extra_attrs - extra attributes to be passed to render functions. |
| 3379 | * @param bool $required_indicator Whether to display the required indicator. |
| 3380 | * |
| 3381 | * @return string HTML |
| 3382 | */ |
| 3383 | public function render_field( $type, $id, $label, $value, $class, $placeholder, $required, $required_field_text, $extra_attrs = array(), $required_indicator = true ) { |
| 3384 | if ( ! $this->is_field_renderable( $type ) ) { |
| 3385 | return ''; |
| 3386 | } |
| 3387 | |
| 3388 | if ( $type === 'hidden' ) { |
| 3389 | // For hidden fields, we don't need to render the label or any other HTML. |
| 3390 | return $this->render_hidden_field( $id, $label, $value ); |
| 3391 | } |
| 3392 | |
| 3393 | $trimmed_type = trim( esc_attr( $type ) ); |
| 3394 | $class .= ' grunion-field'; |
| 3395 | |
| 3396 | $form_style = $this->get_form_style(); |
| 3397 | if ( ! empty( $form_style ) && $form_style !== 'default' ) { |
| 3398 | if ( isset( $placeholder ) && '' !== $placeholder ) { |
| 3399 | $class .= ' has-placeholder'; |
| 3400 | } |
| 3401 | } |
| 3402 | |
| 3403 | // Field classes. |
| 3404 | $field_class = "class='" . $trimmed_type . ' ' . esc_attr( $class ) . "' "; |
| 3405 | |
| 3406 | // Shell wrapper classes. Add -wrap to each class. |
| 3407 | $wrap_classes = empty( $class ) ? '' : implode( '-wrap ', array_filter( explode( ' ', $class ) ) ) . '-wrap'; |
| 3408 | $field_wrapper_classes = $this->get_attribute( 'fieldwrapperclasses' ) ? $this->get_attribute( 'fieldwrapperclasses' ) . ' ' : ''; |
| 3409 | |
| 3410 | if ( empty( $label ) && ! $required ) { |
| 3411 | $wrap_classes .= ' no-label'; |
| 3412 | } |
| 3413 | |
| 3414 | $shell_field_class = "class='" . $field_wrapper_classes . 'grunion-field-' . $trimmed_type . '-wrap ' . esc_attr( $wrap_classes ) . "' "; |
| 3415 | |
| 3416 | /** |
| 3417 | * Filter the Contact Form required field text |
| 3418 | * |
| 3419 | * @module contact-form |
| 3420 | * |
| 3421 | * @since 3.8.0 |
| 3422 | * |
| 3423 | * @param string $var Required field text. Default is "(required)". |
| 3424 | */ |
| 3425 | $required_field_text = wp_kses_post( apply_filters( 'jetpack_required_field_text', $required_field_text ) ); |
| 3426 | |
| 3427 | $block_style = 'style="' . $this->block_styles . '"'; |
| 3428 | $has_inset_label = $this->has_inset_label(); |
| 3429 | $field = ''; |
| 3430 | $field_placeholder = ! empty( $placeholder ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : ''; |
| 3431 | |
| 3432 | $context = array( |
| 3433 | 'fieldId' => $id, |
| 3434 | 'fieldType' => $type, |
| 3435 | 'fieldLabel' => $label, |
| 3436 | 'fieldValue' => $value, |
| 3437 | 'fieldPlaceholder' => $placeholder, |
| 3438 | 'fieldIsRequired' => $required, |
| 3439 | 'fieldErrorMessage' => '', |
| 3440 | 'fieldExtra' => $this->get_field_extra( $type, $extra_attrs ), |
| 3441 | 'formHash' => $this->form->hash, |
| 3442 | ); |
| 3443 | |
| 3444 | // Conditional logic is not emitted per field: it cascades, so resolving it needs every |
| 3445 | // field's logic and type at once. The form block emits that map once as |
| 3446 | // `conditionalLogic`, and this field's wrapper reads its own entry from there. |
| 3447 | $interactivity_attrs = ' data-wp-interactive="jetpack/form" ' . wp_interactivity_data_wp_context( $context ) . ' '; |
| 3448 | |
| 3449 | // Hiding a field has to hide whichever element occupies the slot in the row, or the |
| 3450 | // field disappears and leaves a hole behind it. For an inset label that is the outer |
| 3451 | // wrapper, which is where the width class lives -- the inner div is inside it and |
| 3452 | // carries no width of its own. |
| 3453 | // data-jp-visibility-root names the element the initial server-side render stamps, so |
| 3454 | // the first paint and the runtime always hide the same element. Matching on |
| 3455 | // data-jp-field-id instead would stamp the inner div even when the wrapper is the one |
| 3456 | // the runtime hides. |
| 3457 | $visibility_attrs = " data-jp-visibility-root='" . esc_attr( $id ) . "'" |
| 3458 | . ( $this->has_conditional_logic() ? " data-jp-conditional='1'" : '' ) |
| 3459 | . ' data-wp-class--jetpack-field--conditionally-hidden="state.isFieldHidden"' |
| 3460 | // Runs whenever the field's visibility changes, so focus does not fall to <body> |
| 3461 | // when the field the visitor is filling in disappears under them. |
| 3462 | . ' data-wp-watch--conditional-focus="callbacks.manageConditionalFocus"'; |
| 3463 | |
| 3464 | // Fields with an inset label need an extra wrapper to show the error message below the input. |
| 3465 | if ( $has_inset_label ) { |
| 3466 | $field_width = $this->get_attribute( 'width' ); |
| 3467 | $inset_label_class = array( 'contact-form__inset-label-wrap' ); |
| 3468 | |
| 3469 | if ( ! empty( $field_width ) ) { |
| 3470 | array_push( $inset_label_class, 'grunion-field-width-' . $field_width . '-wrap' ); |
| 3471 | } |
| 3472 | |
| 3473 | $field .= "\n<div class='" . implode( ' ', $inset_label_class ) . "' {$interactivity_attrs} {$visibility_attrs} >\n"; |
| 3474 | $interactivity_attrs = ''; // Reset interactivity attributes for the field wrapper. |
| 3475 | $visibility_attrs = ''; // The outer wrapper owns visibility for this layout. |
| 3476 | } |
| 3477 | |
| 3478 | $field .= "\n<div {$block_style} {$interactivity_attrs} {$shell_field_class} data-jp-field-id='" . esc_attr( $id ) . "'{$visibility_attrs} data-wp-init='callbacks.initializeField' data-wp-on--jetpack-form-reset='callbacks.initializeField' >\n"; // new in Jetpack 6.8.0 |
| 3479 | |
| 3480 | switch ( $type ) { |
| 3481 | case 'email': |
| 3482 | $field .= $this->render_email_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator ); |
| 3483 | break; |
| 3484 | case 'phone': |
| 3485 | case 'telephone': |
| 3486 | $field .= $this->render_telephone_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator ); |
| 3487 | break; |
| 3488 | case 'url': |
| 3489 | $field .= $this->render_url_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator ); |
| 3490 | break; |
| 3491 | case 'textarea': |
| 3492 | $field .= $this->render_textarea_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator ); |
| 3493 | break; |
| 3494 | case 'radio': |
| 3495 | $field .= $this->render_radio_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator ); |
| 3496 | break; |
| 3497 | case 'checkbox': |
| 3498 | $field .= $this->render_checkbox_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator ); |
| 3499 | break; |
| 3500 | case 'checkbox-multiple': |
| 3501 | $field .= $this->render_checkbox_multiple_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator ); |
| 3502 | break; |
| 3503 | case 'select': |
| 3504 | $field .= $this->render_select_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator ); |
| 3505 | break; |
| 3506 | case 'date': |
| 3507 | $field .= $this->render_date_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator ); |
| 3508 | break; |
| 3509 | case 'consent': |
| 3510 | $field .= $this->render_consent_field( $id, $field_class ); |
| 3511 | break; |
| 3512 | case 'number': |
| 3513 | $field .= $this->render_number_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $extra_attrs, $required_indicator ); |
| 3514 | break; |
| 3515 | case 'slider': |
| 3516 | $field .= $this->render_slider_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $extra_attrs, $required_indicator ); |
| 3517 | break; |
| 3518 | case 'file': |
| 3519 | $field .= $this->render_file_field( $id, $label, $field_class, $required, $required_field_text, $required_indicator ); |
| 3520 | break; |
| 3521 | case 'rating': |
| 3522 | $field .= $this->render_rating_field( |
| 3523 | $id, |
| 3524 | $label, |
| 3525 | $value, |
| 3526 | $field_class, |
| 3527 | $required, |
| 3528 | $required_field_text, |
| 3529 | $required_indicator |
| 3530 | ); |
| 3531 | break; |
| 3532 | case 'time': |
| 3533 | $field .= $this->render_time_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $required_indicator ); |
| 3534 | break; |
| 3535 | case 'image-select': |
| 3536 | $field .= $this->render_image_select_field( $id, $label, $value, $field_class, $required, $required_field_text, $required_indicator ); |
| 3537 | break; |
| 3538 | default: // text field |
| 3539 | $field .= $this->render_default_field( $id, $label, $value, $field_class, $required, $required_field_text, $field_placeholder, $type, $required_indicator ); |
| 3540 | break; |
| 3541 | } |
| 3542 | |
| 3543 | $field .= "\t</div>\n"; |
| 3544 | |
| 3545 | if ( $has_inset_label ) { |
| 3546 | // Description-aware renderers build their markup at the input site, |
| 3547 | // where the correct type is known, and defer it here. Field types |
| 3548 | // that do not yet do that fall back to the plain error div. Several |
| 3549 | // fields render an input whose type differs from the field type |
| 3550 | // (date and url render `text`, simple telephone renders `tel`), so |
| 3551 | // rebuilding it here from $type would not match the ids the input's |
| 3552 | // aria-describedby references. |
| 3553 | $field .= $this->deferred_descriptions ?? $this->get_error_div( $id, $type, true ); |
| 3554 | // Consume it, so a second render cannot replay this one's markup. |
| 3555 | $this->deferred_descriptions = null; |
| 3556 | // Close the extra wrapper for inset labels. |
| 3557 | $field .= "\t</div>\n"; |
| 3558 | } |
| 3559 | |
| 3560 | return $field; |
| 3561 | } |
| 3562 | |
| 3563 | /** |
| 3564 | * Returns the extra attributes for the field. |
| 3565 | * That are used in field validation. |
| 3566 | * |
| 3567 | * @param string $type - the field type. |
| 3568 | * @param array $extra_attrs - the extra attributes. |
| 3569 | * |
| 3570 | * @return string|array The extra attributes. |
| 3571 | */ |
| 3572 | private function get_field_extra( $type, $extra_attrs ) { |
| 3573 | if ( 'date' === $type ) { |
| 3574 | return $this->get_date_format(); |
| 3575 | } |
| 3576 | |
| 3577 | if ( 'file' === $type ) { |
| 3578 | return array( |
| 3579 | 'maxFiles' => $this->get_file_field_max_files(), |
| 3580 | 'allowedMimeTypes' => self::get_file_field_accepted_mime_types(), |
| 3581 | ); |
| 3582 | } |
| 3583 | |
| 3584 | return $extra_attrs; |
| 3585 | } |
| 3586 | |
| 3587 | /** |
| 3588 | * Overrides input type (maybe). |
| 3589 | * |
| 3590 | * @module contact-form |
| 3591 | * |
| 3592 | * Custom input types, like URL, will rely on browser's implementation to validate |
| 3593 | * the value. If the input carries a data-type-override, we allow to override |
| 3594 | * the type at render/submit so it can be validated with custom patterns. |
| 3595 | * This method will try to match the input's type to a custom data-type-override |
| 3596 | * attribute and return it. Defaults to input's type. |
| 3597 | * |
| 3598 | * @return string The input's type attribute or the overriden type. |
| 3599 | */ |
| 3600 | private function maybe_override_type() { |
| 3601 | // Define overridables-to-custom-type, extend as needed. |
| 3602 | $overridable_types = array( 'text' => array( 'url' ) ); |
| 3603 | $type = $this->get_attribute( 'type' ); |
| 3604 | |
| 3605 | if ( ! array_key_exists( $type, $overridable_types ) ) { |
| 3606 | return $type; |
| 3607 | } |
| 3608 | |
| 3609 | $override_type = $this->get_attribute( 'data-type-override' ); |
| 3610 | |
| 3611 | if ( in_array( $override_type, $overridable_types[ $type ], true ) ) { |
| 3612 | return $override_type; |
| 3613 | } |
| 3614 | |
| 3615 | return $type; |
| 3616 | } |
| 3617 | |
| 3618 | /** |
| 3619 | * Determines if a form field is valid. |
| 3620 | * |
| 3621 | * Add checks here to confirm if any given form field |
| 3622 | * is configured correctly and thus should be rendered |
| 3623 | * on the frontend. |
| 3624 | * |
| 3625 | * @param string $type - the field type. |
| 3626 | * |
| 3627 | * @return bool |
| 3628 | */ |
| 3629 | public function is_field_renderable( $type ) { |
| 3630 | // Check that radio, select, multiple choice, and image select |
| 3631 | // fields have at least one valid option. |
| 3632 | if ( $type === 'radio' || $type === 'checkbox-multiple' || $type === 'select' ) { |
| 3633 | $options = (array) $this->get_attribute( 'options' ); |
| 3634 | $non_empty_options = array_filter( |
| 3635 | $options, |
| 3636 | function ( $option ) { |
| 3637 | return $option !== ''; |
| 3638 | } |
| 3639 | ); |
| 3640 | return count( $non_empty_options ) > 0; |
| 3641 | } |
| 3642 | |
| 3643 | if ( $type === 'image-select' ) { |
| 3644 | $options_data = (array) $this->get_attribute( 'optionsdata' ); |
| 3645 | $trimmed_options_data = $this->trim_image_select_options( $options_data ); |
| 3646 | |
| 3647 | return ! empty( $trimmed_options_data ); |
| 3648 | } |
| 3649 | |
| 3650 | return true; |
| 3651 | } |
| 3652 | |
| 3653 | /** |
| 3654 | * Gets the form style based on its CSS class. |
| 3655 | * |
| 3656 | * @return string The form style type. |
| 3657 | */ |
| 3658 | private function get_form_style() { |
| 3659 | $class_name = $this->form->get_attribute( 'className' ); |
| 3660 | preg_match( '/is-style-([^\s]+)/i', $class_name, $matches ); |
| 3661 | return count( $matches ) >= 2 ? $matches[1] : null; |
| 3662 | } |
| 3663 | |
| 3664 | /** |
| 3665 | * Checks if the form style renders labels inside the field rather than above it. |
| 3666 | * |
| 3667 | * This is a property of the *style* alone: it stays true for a field type that |
| 3668 | * opts out of the inset label itself, because it also governs the extra |
| 3669 | * `.contact-form__inset-label-wrap` wrapper, which those fields still need (it |
| 3670 | * carries the field width). Use type_has_inset_label() to decide whether a given |
| 3671 | * field actually renders an inset label. |
| 3672 | * |
| 3673 | * @return boolean |
| 3674 | */ |
| 3675 | private function has_inset_label() { |
| 3676 | $form_style = $this->get_form_style(); |
| 3677 | |
| 3678 | return in_array( $form_style, array( 'outlined', 'animated' ), true ); |
| 3679 | } |
| 3680 | |
| 3681 | /** |
| 3682 | * Checks whether a field type renders an inset label under the current form style. |
| 3683 | * |
| 3684 | * @param string $type The field type. |
| 3685 | * |
| 3686 | * @return boolean |
| 3687 | */ |
| 3688 | private function type_has_inset_label( $type ) { |
| 3689 | return $this->has_inset_label() |
| 3690 | && ! in_array( $type, self::TYPES_WITHOUT_STYLED_LABEL, true ) |
| 3691 | && ! in_array( $type, self::TYPES_WITHOUT_INSET_LABEL, true ); |
| 3692 | } |
| 3693 | |
| 3694 | /** |
| 3695 | * Return the HTML for the rating (stars/hearts/etc.) field. |
| 3696 | * |
| 3697 | * This field is purely decorative (spans acting as buttons) and stores the |
| 3698 | * selected rating in a hidden input so it is handled by existing form |
| 3699 | * validation/submission logic. |
| 3700 | * |
| 3701 | * @since 0.46.0 |
| 3702 | * |
| 3703 | * @param string $id Field ID. |
| 3704 | * @param string $label Field label. |
| 3705 | * @param string $value Current value. |
| 3706 | * @param string $class Additional CSS classes. |
| 3707 | * @param bool $required Whether field is required. |
| 3708 | * @param string $required_field_text Required label text. |
| 3709 | * @param bool $required_indicator Whether to display the required indicator. |
| 3710 | * @return string HTML markup. |
| 3711 | */ |
| 3712 | private function render_rating_field( $id, $label, $value, $class, $required, $required_field_text, $required_indicator = true ) { |
| 3713 | // Enqueue stylesheet for rating field. |
| 3714 | wp_enqueue_style( 'jetpack-form-field-rating-style', plugins_url( '../../dist/blocks/field-rating/style.css', __FILE__ ), array(), Constants::get_constant( 'JETPACK__VERSION' ) ); |
| 3715 | |
| 3716 | // Read block attributes needed for rendering. |
| 3717 | $max_attr = $this->get_attribute( 'max' ); |
| 3718 | $max_rating = is_numeric( $max_attr ) && (int) $max_attr > 0 ? (int) $max_attr : 5; |
| 3719 | // The editor caps this, but a hand-edited block or shortcode attribute does not. |
| 3720 | $max_rating = min( $max_rating, Feedback_Field::MAX_RATING_ICONS ); |
| 3721 | |
| 3722 | $initial_rating = (int) $value ? (int) $value : 0; |
| 3723 | |
| 3724 | $label_html = $this->render_legend_as_label( 'rating', $id, $label, $required, $required_field_text, array(), $required_indicator ); |
| 3725 | |
| 3726 | /* |
| 3727 | * Determine which icon SVG to use based on the 'iconstyle' attribute. |
| 3728 | * Note: attribute name is lowercase due to WordPress shortcode processing |
| 3729 | */ |
| 3730 | $icon_style = $this->get_attribute( 'iconstyle' ); |
| 3731 | $has_hearts_style = ( 'hearts' === $icon_style ); |
| 3732 | |
| 3733 | // SVG icon definitions - keep in sync with JavaScript icons.js |
| 3734 | $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>'; |
| 3735 | $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>'; |
| 3736 | |
| 3737 | $icon_svg = $has_hearts_style ? $heart_svg : $star_svg; |
| 3738 | |
| 3739 | $options = ''; |
| 3740 | for ( $i = 1; $i <= $max_rating; $i++ ) { |
| 3741 | $radio_id = $id . '-' . $i; |
| 3742 | $options .= sprintf( |
| 3743 | '<div class="jetpack-field-rating__option"> |
| 3744 | <input |
| 3745 | id="%1$s" |
| 3746 | type="radio" |
| 3747 | name="%2$s" |
| 3748 | value="%3$s/%4$s" |
| 3749 | data-wp-on--change="actions.onFieldChange" |
| 3750 | class="jetpack-field-rating__input visually-hidden" |
| 3751 | %5$s |
| 3752 | %6$s /> |
| 3753 | <label for="%1$s" class="jetpack-field-rating__label"> |
| 3754 | %7$s |
| 3755 | </label> |
| 3756 | </div>', |
| 3757 | esc_attr( $radio_id ), // %1$s: id and label for |
| 3758 | esc_attr( $id ), // %2$s: name |
| 3759 | esc_attr( $i ), // %3$s: value (current rating) |
| 3760 | esc_attr( $max_rating ), // %4$s: value (max rating) |
| 3761 | checked( $i, $initial_rating, false ), // %5$s: checked attribute |
| 3762 | $required ? 'required' : '', // %6$s: required attribute |
| 3763 | $icon_svg // %7$s: icon SVG |
| 3764 | ); |
| 3765 | } |
| 3766 | |
| 3767 | $style_attr = ''; |
| 3768 | |
| 3769 | $css_styles = array_filter( array_map( 'trim', explode( ';', $this->field_styles ) ) ); |
| 3770 | |
| 3771 | $css_key_value_pairs = array_reduce( |
| 3772 | $css_styles, |
| 3773 | function ( $pairs, $style ) { |
| 3774 | list( $key, $value ) = explode( ':', $style ); |
| 3775 | $pairs[ trim( $key ) ] = trim( $value ); |
| 3776 | return $pairs; |
| 3777 | }, |
| 3778 | array() |
| 3779 | ); |
| 3780 | |
| 3781 | // The rating input overwrites the text color, so we are using a custom logic to set the star color as a CSS variable. |
| 3782 | $has_star_color = isset( $css_key_value_pairs['color'] ); |
| 3783 | |
| 3784 | if ( $has_star_color ) { |
| 3785 | $color_value = $css_key_value_pairs['color']; |
| 3786 | $style_attr = 'style="--jetpack--contact-form--rating-star-color: ' . esc_attr( $color_value ) . ';'; |
| 3787 | unset( $css_key_value_pairs['color'] ); |
| 3788 | } else { |
| 3789 | // Theme colors are set in the field_classes attribute |
| 3790 | $preset_colors = array( |
| 3791 | 'has-base-color' => '--wp--preset--color--base', |
| 3792 | 'has-contrast-color' => '--wp--preset--color--contrast', |
| 3793 | ); |
| 3794 | |
| 3795 | if ( preg_match( '/has-accent-(\d+)-color/', $this->field_classes, $matches ) ) { |
| 3796 | $accent_number = $matches[1]; |
| 3797 | $preset_colors[ 'has-accent-' . $accent_number . '-color' ] = '--wp--preset--color--accent-' . $accent_number; |
| 3798 | } |
| 3799 | |
| 3800 | foreach ( $preset_colors as $class => $css_var ) { |
| 3801 | if ( strpos( $this->field_classes, $class ) !== false ) { |
| 3802 | $style_attr = 'style="--jetpack--contact-form--rating-star-color: var(' . esc_attr( $css_var ) . ');'; |
| 3803 | |
| 3804 | break; |
| 3805 | } |
| 3806 | } |
| 3807 | } |
| 3808 | |
| 3809 | $remaining_styles = array_map( |
| 3810 | function ( $key, $value ) { |
| 3811 | return $key . ': ' . $value; |
| 3812 | }, |
| 3813 | array_keys( $css_key_value_pairs ), |
| 3814 | array_values( $css_key_value_pairs ) |
| 3815 | ); |
| 3816 | |
| 3817 | $style_attr .= ' ' . implode( ';', $remaining_styles ) . '"'; |
| 3818 | |
| 3819 | return sprintf( |
| 3820 | '<fieldset id="%4$s-label" class="jetpack-field-multiple__fieldset jetpack-field-rating" %1$s%6$s> |
| 3821 | %5$s |
| 3822 | <div class="jetpack-field-rating__options %3$s">%2$s</div> |
| 3823 | </fieldset>', |
| 3824 | $style_attr, |
| 3825 | $options, |
| 3826 | esc_attr( $this->field_classes ), |
| 3827 | esc_attr( $id ), |
| 3828 | $label_html, |
| 3829 | $this->get_hidden_label_aria_label_attr() |
| 3830 | ) . $this->get_error_div( $id, 'rating' ); |
| 3831 | } |
| 3832 | |
| 3833 | /** |
| 3834 | * Return the HTML for the slider field. |
| 3835 | * |
| 3836 | * @since 5.1.0 |
| 3837 | * |
| 3838 | * @param int $id The field ID. |
| 3839 | * @param string $label The field label. |
| 3840 | * @param string $value The field value. |
| 3841 | * @param string $class The field class. |
| 3842 | * @param bool $required Whether the field is required. |
| 3843 | * @param string $required_field_text The required field text. |
| 3844 | * @param string $placeholder The field placeholder. |
| 3845 | * @param array $extra_attrs Extra attributes (e.g., min, max). |
| 3846 | * @param bool $required_indicator Whether to display the required indicator. |
| 3847 | * |
| 3848 | * @return string HTML for the slider field. |
| 3849 | */ |
| 3850 | public function render_slider_field( $id, $label, $value, $class, $required, $required_field_text, $placeholder, $extra_attrs = array(), $required_indicator = true ) { |
| 3851 | $this->enqueue_slider_field_assets(); |
| 3852 | $this->set_invalid_message( 'slider', __( 'Please select a valid value', 'jetpack-forms' ) ); |
| 3853 | if ( isset( $extra_attrs['min'] ) ) { |
| 3854 | // translators: %d is the minimum value. |
| 3855 | $this->set_invalid_message( 'min_slider', __( 'Please select a value that is no less than %d.', 'jetpack-forms' ) ); |
| 3856 | } |
| 3857 | if ( isset( $extra_attrs['max'] ) ) { |
| 3858 | // translators: %d is the maximum value. |
| 3859 | $this->set_invalid_message( 'max_slider', __( 'Please select a value that is no more than %d.', 'jetpack-forms' ) ); |
| 3860 | } |
| 3861 | $min = $extra_attrs['min'] ?? 0; |
| 3862 | $max = $extra_attrs['max'] ?? 100; |
| 3863 | $starting_value = $extra_attrs['default'] ?? 0; |
| 3864 | $step = $extra_attrs['step'] ?? 1; |
| 3865 | $current_value = ( $value !== '' && $value !== null ) ? $value : $starting_value; |
| 3866 | $min_text_label = $extra_attrs['minLabel'] ?? ''; |
| 3867 | $max_text_label = $extra_attrs['maxLabel'] ?? ''; |
| 3868 | |
| 3869 | // $always_render must be true: 'slider' is excluded from inset labels in |
| 3870 | // render_label(), so without it the label would be dropped entirely when the |
| 3871 | // form uses the Outlined or Animated style. |
| 3872 | $field = $this->render_label( 'slider', $id, $label, $required, $required_field_text, array(), true, $required_indicator ); |
| 3873 | |
| 3874 | ob_start(); |
| 3875 | ?> |
| 3876 | <div class="jetpack-field-slider__input-row <?php echo esc_attr( $this->field_classes ); ?>" |
| 3877 | data-wp-context=' |
| 3878 | <?php |
| 3879 | echo esc_attr( |
| 3880 | wp_json_encode( |
| 3881 | array( |
| 3882 | 'min' => $min, |
| 3883 | 'max' => $max, |
| 3884 | 'default' => $starting_value, |
| 3885 | 'step' => $step, |
| 3886 | ), |
| 3887 | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES |
| 3888 | ) |
| 3889 | ); |
| 3890 | ?> |
| 3891 | '> |
| 3892 | <span class="jetpack-field-slider__min-label"><?php echo esc_html( $min ); ?></span> |
| 3893 | <div class="jetpack-field-slider__input-container"> |
| 3894 | <input |
| 3895 | type="range" |
| 3896 | name="<?php echo esc_attr( $id ); ?>" |
| 3897 | id="<?php echo esc_attr( $id ); ?>" |
| 3898 | value="<?php echo esc_attr( $current_value ); ?>" |
| 3899 | min="<?php echo esc_attr( $min ); ?>" |
| 3900 | max="<?php echo esc_attr( $max ); ?>" |
| 3901 | step="<?php echo esc_attr( $step ); ?>" |
| 3902 | class="<?php echo esc_attr( trim( $class . ' jetpack-field-slider__range' ) ); ?>" |
| 3903 | placeholder="<?php echo esc_attr( $placeholder ); ?>" |
| 3904 | <?php |
| 3905 | if ( $required ) : |
| 3906 | ?> |
| 3907 | required<?php endif; ?> |
| 3908 | data-wp-bind--value="state.getSliderValue" |
| 3909 | data-wp-on--input="actions.onSliderChange" |
| 3910 | data-wp-bind--aria-invalid="state.fieldAriaInvalid" |
| 3911 | aria-describedby="<?php echo esc_attr( $this->get_described_by( $id, 'slider' ) ); ?>" |
| 3912 | <?php echo $this->get_hidden_label_aria_label_attr(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attribute value is escaped in the helper. ?> |
| 3913 | /> |
| 3914 | <div |
| 3915 | class="jetpack-field-slider__value-indicator" |
| 3916 | data-wp-text="state.getSliderValue" |
| 3917 | data-wp-style--left="state.getSliderPosition" |
| 3918 | ><?php echo esc_html( $current_value ); ?></div> |
| 3919 | </div> |
| 3920 | <span class="jetpack-field-slider__max-label"><?php echo esc_html( $max ); ?></span> |
| 3921 | </div> |
| 3922 | <?php if ( '' !== $min_text_label || '' !== $max_text_label ) : ?> |
| 3923 | <div class="jetpack-field-slider__text-labels <?php echo esc_attr( $this->field_classes ); ?>" aria-hidden="true"> |
| 3924 | <span class="jetpack-field-slider__min-text-label"><?php echo esc_html( $min_text_label ); ?></span> |
| 3925 | <span class="jetpack-field-slider__max-text-label"><?php echo esc_html( $max_text_label ); ?></span> |
| 3926 | </div> |
| 3927 | <?php endif; ?> |
| 3928 | <?php |
| 3929 | $field .= ob_get_clean(); |
| 3930 | return $field . $this->get_field_descriptions( $id, 'slider' ); |
| 3931 | } |
| 3932 | |
| 3933 | /** |
| 3934 | * Enqueues scripts and styles needed for the slider field. |
| 3935 | * |
| 3936 | * @since 5.1.0 |
| 3937 | * |
| 3938 | * @return void |
| 3939 | */ |
| 3940 | private function enqueue_slider_field_assets() { |
| 3941 | $version = defined( 'JETPACK__VERSION' ) ? \JETPACK__VERSION : '0.1'; |
| 3942 | |
| 3943 | \wp_enqueue_style( |
| 3944 | 'jetpack-form-slider-field', |
| 3945 | plugins_url( '../../dist/blocks/input-range/style.css', __FILE__ ), |
| 3946 | array(), |
| 3947 | $version |
| 3948 | ); |
| 3949 | |
| 3950 | \wp_enqueue_script_module( |
| 3951 | 'jetpack-form-slider-field', |
| 3952 | plugins_url( '../../dist/modules/slider-field/view.js', __FILE__ ), |
| 3953 | array( '@wordpress/interactivity' ), |
| 3954 | $version |
| 3955 | ); |
| 3956 | } |
| 3957 | |
| 3958 | /** |
| 3959 | * Gets an array of translatable country names indexed by their two-letter country codes. |
| 3960 | * |
| 3961 | * @since 6.2.1 |
| 3962 | * |
| 3963 | * @return array Array of country names with two-letter country codes as keys. |
| 3964 | */ |
| 3965 | public function get_translatable_countries() { |
| 3966 | return array( |
| 3967 | 'AF' => __( 'Afghanistan', 'jetpack-forms' ), |
| 3968 | 'AL' => __( 'Albania', 'jetpack-forms' ), |
| 3969 | 'DZ' => __( 'Algeria', 'jetpack-forms' ), |
| 3970 | 'AS' => __( 'American Samoa', 'jetpack-forms' ), |
| 3971 | 'AD' => __( 'Andorra', 'jetpack-forms' ), |
| 3972 | 'AO' => __( 'Angola', 'jetpack-forms' ), |
| 3973 | 'AI' => __( 'Anguilla', 'jetpack-forms' ), |
| 3974 | 'AG' => __( 'Antigua and Barbuda', 'jetpack-forms' ), |
| 3975 | 'AR' => __( 'Argentina', 'jetpack-forms' ), |
| 3976 | 'AM' => __( 'Armenia', 'jetpack-forms' ), |
| 3977 | 'AW' => __( 'Aruba', 'jetpack-forms' ), |
| 3978 | 'AU' => __( 'Australia', 'jetpack-forms' ), |
| 3979 | 'AT' => __( 'Austria', 'jetpack-forms' ), |
| 3980 | 'AZ' => __( 'Azerbaijan', 'jetpack-forms' ), |
| 3981 | 'BS' => __( 'Bahamas', 'jetpack-forms' ), |
| 3982 | 'BH' => __( 'Bahrain', 'jetpack-forms' ), |
| 3983 | 'BD' => __( 'Bangladesh', 'jetpack-forms' ), |
| 3984 | 'BB' => __( 'Barbados', 'jetpack-forms' ), |
| 3985 | 'BY' => __( 'Belarus', 'jetpack-forms' ), |
| 3986 | 'BE' => __( 'Belgium', 'jetpack-forms' ), |
| 3987 | 'BZ' => __( 'Belize', 'jetpack-forms' ), |
| 3988 | 'BJ' => __( 'Benin', 'jetpack-forms' ), |
| 3989 | 'BM' => __( 'Bermuda', 'jetpack-forms' ), |
| 3990 | 'BT' => __( 'Bhutan', 'jetpack-forms' ), |
| 3991 | 'BO' => __( 'Bolivia', 'jetpack-forms' ), |
| 3992 | 'BA' => __( 'Bosnia and Herzegovina', 'jetpack-forms' ), |
| 3993 | 'BW' => __( 'Botswana', 'jetpack-forms' ), |
| 3994 | 'BR' => __( 'Brazil', 'jetpack-forms' ), |
| 3995 | 'IO' => __( 'British Indian Ocean Territory', 'jetpack-forms' ), |
| 3996 | 'VG' => __( 'British Virgin Islands', 'jetpack-forms' ), |
| 3997 | 'BN' => __( 'Brunei', 'jetpack-forms' ), |
| 3998 | 'BG' => __( 'Bulgaria', 'jetpack-forms' ), |
| 3999 | 'BF' => __( 'Burkina Faso', 'jetpack-forms' ), |
| 4000 | 'BI' => __( 'Burundi', 'jetpack-forms' ), |
| 4001 | 'KH' => __( 'Cambodia', 'jetpack-forms' ), |
| 4002 | 'CM' => __( 'Cameroon', 'jetpack-forms' ), |
| 4003 | 'CA' => __( 'Canada', 'jetpack-forms' ), |
| 4004 | 'CV' => __( 'Cape Verde', 'jetpack-forms' ), |
| 4005 | 'KY' => __( 'Cayman Islands', 'jetpack-forms' ), |
| 4006 | 'CF' => __( 'Central African Republic', 'jetpack-forms' ), |
| 4007 | 'TD' => __( 'Chad', 'jetpack-forms' ), |
| 4008 | 'CL' => __( 'Chile', 'jetpack-forms' ), |
| 4009 | 'CN' => __( 'China', 'jetpack-forms' ), |
| 4010 | 'CX' => __( 'Christmas Island', 'jetpack-forms' ), |
| 4011 | 'CC' => __( 'Cocos (Keeling) Islands', 'jetpack-forms' ), |
| 4012 | 'CO' => __( 'Colombia', 'jetpack-forms' ), |
| 4013 | 'KM' => __( 'Comoros', 'jetpack-forms' ), |
| 4014 | 'CG' => __( 'Congo - Brazzaville', 'jetpack-forms' ), |
| 4015 | 'CD' => __( 'Congo - Kinshasa', 'jetpack-forms' ), |
| 4016 | 'CK' => __( 'Cook Islands', 'jetpack-forms' ), |
| 4017 | 'CR' => __( 'Costa Rica', 'jetpack-forms' ), |
| 4018 | 'HR' => __( 'Croatia', 'jetpack-forms' ), |
| 4019 | 'CU' => __( 'Cuba', 'jetpack-forms' ), |
| 4020 | 'CY' => __( 'Cyprus', 'jetpack-forms' ), |
| 4021 | 'CZ' => __( 'Czech Republic', 'jetpack-forms' ), |
| 4022 | 'CI' => __( "Côte d'Ivoire", 'jetpack-forms' ), |
| 4023 | 'DK' => __( 'Denmark', 'jetpack-forms' ), |
| 4024 | 'DJ' => __( 'Djibouti', 'jetpack-forms' ), |
| 4025 | 'DM' => __( 'Dominica', 'jetpack-forms' ), |
| 4026 | 'DO' => __( 'Dominican Republic', 'jetpack-forms' ), |
| 4027 | 'EC' => __( 'Ecuador', 'jetpack-forms' ), |
| 4028 | 'EG' => __( 'Egypt', 'jetpack-forms' ), |
| 4029 | 'SV' => __( 'El Salvador', 'jetpack-forms' ), |
| 4030 | 'GQ' => __( 'Equatorial Guinea', 'jetpack-forms' ), |
| 4031 | 'ER' => __( 'Eritrea', 'jetpack-forms' ), |
| 4032 | 'EE' => __( 'Estonia', 'jetpack-forms' ), |
| 4033 | 'SZ' => __( 'Eswatini', 'jetpack-forms' ), |
| 4034 | 'ET' => __( 'Ethiopia', 'jetpack-forms' ), |
| 4035 | 'FK' => __( 'Falkland Islands', 'jetpack-forms' ), |
| 4036 | 'FO' => __( 'Faroe Islands', 'jetpack-forms' ), |
| 4037 | 'FJ' => __( 'Fiji', 'jetpack-forms' ), |
| 4038 | 'FI' => __( 'Finland', 'jetpack-forms' ), |
| 4039 | 'FR' => __( 'France', 'jetpack-forms' ), |
| 4040 | 'GF' => __( 'French Guiana', 'jetpack-forms' ), |
| 4041 | 'PF' => __( 'French Polynesia', 'jetpack-forms' ), |
| 4042 | 'GA' => __( 'Gabon', 'jetpack-forms' ), |
| 4043 | 'GM' => __( 'Gambia', 'jetpack-forms' ), |
| 4044 | 'GE' => __( 'Georgia', 'jetpack-forms' ), |
| 4045 | 'DE' => __( 'Germany', 'jetpack-forms' ), |
| 4046 | 'GH' => __( 'Ghana', 'jetpack-forms' ), |
| 4047 | 'GI' => __( 'Gibraltar', 'jetpack-forms' ), |
| 4048 | 'GR' => __( 'Greece', 'jetpack-forms' ), |
| 4049 | 'GL' => __( 'Greenland', 'jetpack-forms' ), |
| 4050 | 'GD' => __( 'Grenada', 'jetpack-forms' ), |
| 4051 | 'GP' => __( 'Guadeloupe', 'jetpack-forms' ), |
| 4052 | 'GU' => __( 'Guam', 'jetpack-forms' ), |
| 4053 | 'GT' => __( 'Guatemala', 'jetpack-forms' ), |
| 4054 | 'GG' => __( 'Guernsey', 'jetpack-forms' ), |
| 4055 | 'GN' => __( 'Guinea', 'jetpack-forms' ), |
| 4056 | 'GW' => __( 'Guinea-Bissau', 'jetpack-forms' ), |
| 4057 | 'GY' => __( 'Guyana', 'jetpack-forms' ), |
| 4058 | 'HT' => __( 'Haiti', 'jetpack-forms' ), |
| 4059 | 'HN' => __( 'Honduras', 'jetpack-forms' ), |
| 4060 | 'HK' => __( 'Hong Kong', 'jetpack-forms' ), |
| 4061 | 'HU' => __( 'Hungary', 'jetpack-forms' ), |
| 4062 | 'IS' => __( 'Iceland', 'jetpack-forms' ), |
| 4063 | 'IN' => __( 'India', 'jetpack-forms' ), |
| 4064 | 'ID' => __( 'Indonesia', 'jetpack-forms' ), |
| 4065 | 'IR' => __( 'Iran', 'jetpack-forms' ), |
| 4066 | 'IQ' => __( 'Iraq', 'jetpack-forms' ), |
| 4067 | 'IE' => __( 'Ireland', 'jetpack-forms' ), |
| 4068 | 'IM' => __( 'Isle of Man', 'jetpack-forms' ), |
| 4069 | 'IL' => __( 'Israel', 'jetpack-forms' ), |
| 4070 | 'IT' => __( 'Italy', 'jetpack-forms' ), |
| 4071 | 'JM' => __( 'Jamaica', 'jetpack-forms' ), |
| 4072 | 'JP' => __( 'Japan', 'jetpack-forms' ), |
| 4073 | 'JE' => __( 'Jersey', 'jetpack-forms' ), |
| 4074 | 'JO' => __( 'Jordan', 'jetpack-forms' ), |
| 4075 | 'KZ' => __( 'Kazakhstan', 'jetpack-forms' ), |
| 4076 | 'KE' => __( 'Kenya', 'jetpack-forms' ), |
| 4077 | 'KI' => __( 'Kiribati', 'jetpack-forms' ), |
| 4078 | 'XK' => __( 'Kosovo', 'jetpack-forms' ), |
| 4079 | 'KW' => __( 'Kuwait', 'jetpack-forms' ), |
| 4080 | 'KG' => __( 'Kyrgyzstan', 'jetpack-forms' ), |
| 4081 | 'LA' => __( 'Laos', 'jetpack-forms' ), |
| 4082 | 'LV' => __( 'Latvia', 'jetpack-forms' ), |
| 4083 | 'LB' => __( 'Lebanon', 'jetpack-forms' ), |
| 4084 | 'LS' => __( 'Lesotho', 'jetpack-forms' ), |
| 4085 | 'LR' => __( 'Liberia', 'jetpack-forms' ), |
| 4086 | 'LY' => __( 'Libya', 'jetpack-forms' ), |
| 4087 | 'LI' => __( 'Liechtenstein', 'jetpack-forms' ), |
| 4088 | 'LT' => __( 'Lithuania', 'jetpack-forms' ), |
| 4089 | 'LU' => __( 'Luxembourg', 'jetpack-forms' ), |
| 4090 | 'MO' => __( 'Macao', 'jetpack-forms' ), |
| 4091 | 'MG' => __( 'Madagascar', 'jetpack-forms' ), |
| 4092 | 'MW' => __( 'Malawi', 'jetpack-forms' ), |
| 4093 | 'MY' => __( 'Malaysia', 'jetpack-forms' ), |
| 4094 | 'MV' => __( 'Maldives', 'jetpack-forms' ), |
| 4095 | 'ML' => __( 'Mali', 'jetpack-forms' ), |
| 4096 | 'MT' => __( 'Malta', 'jetpack-forms' ), |
| 4097 | 'MH' => __( 'Marshall Islands', 'jetpack-forms' ), |
| 4098 | 'MQ' => __( 'Martinique', 'jetpack-forms' ), |
| 4099 | 'MR' => __( 'Mauritania', 'jetpack-forms' ), |
| 4100 | 'MU' => __( 'Mauritius', 'jetpack-forms' ), |
| 4101 | 'YT' => __( 'Mayotte', 'jetpack-forms' ), |
| 4102 | 'MX' => __( 'Mexico', 'jetpack-forms' ), |
| 4103 | 'FM' => __( 'Micronesia', 'jetpack-forms' ), |
| 4104 | 'MD' => __( 'Moldova', 'jetpack-forms' ), |
| 4105 | 'MC' => __( 'Monaco', 'jetpack-forms' ), |
| 4106 | 'MN' => __( 'Mongolia', 'jetpack-forms' ), |
| 4107 | 'ME' => __( 'Montenegro', 'jetpack-forms' ), |
| 4108 | 'MS' => __( 'Montserrat', 'jetpack-forms' ), |
| 4109 | 'MA' => __( 'Morocco', 'jetpack-forms' ), |
| 4110 | 'MZ' => __( 'Mozambique', 'jetpack-forms' ), |
| 4111 | 'MM' => __( 'Myanmar', 'jetpack-forms' ), |
| 4112 | 'NA' => __( 'Namibia', 'jetpack-forms' ), |
| 4113 | 'NR' => __( 'Nauru', 'jetpack-forms' ), |
| 4114 | 'NP' => __( 'Nepal', 'jetpack-forms' ), |
| 4115 | 'NL' => __( 'Netherlands', 'jetpack-forms' ), |
| 4116 | 'NC' => __( 'New Caledonia', 'jetpack-forms' ), |
| 4117 | 'NZ' => __( 'New Zealand', 'jetpack-forms' ), |
| 4118 | 'NI' => __( 'Nicaragua', 'jetpack-forms' ), |
| 4119 | 'NE' => __( 'Niger', 'jetpack-forms' ), |
| 4120 | 'NG' => __( 'Nigeria', 'jetpack-forms' ), |
| 4121 | 'NU' => __( 'Niue', 'jetpack-forms' ), |
| 4122 | 'NF' => __( 'Norfolk Island', 'jetpack-forms' ), |
| 4123 | 'KP' => __( 'North Korea', 'jetpack-forms' ), |
| 4124 | 'MK' => __( 'North Macedonia', 'jetpack-forms' ), |
| 4125 | 'MP' => __( 'Northern Mariana Islands', 'jetpack-forms' ), |
| 4126 | 'NO' => __( 'Norway', 'jetpack-forms' ), |
| 4127 | 'OM' => __( 'Oman', 'jetpack-forms' ), |
| 4128 | 'PK' => __( 'Pakistan', 'jetpack-forms' ), |
| 4129 | 'PW' => __( 'Palau', 'jetpack-forms' ), |
| 4130 | 'PS' => __( 'Palestine', 'jetpack-forms' ), |
| 4131 | 'PA' => __( 'Panama', 'jetpack-forms' ), |
| 4132 | 'PG' => __( 'Papua New Guinea', 'jetpack-forms' ), |
| 4133 | 'PY' => __( 'Paraguay', 'jetpack-forms' ), |
| 4134 | 'PE' => __( 'Peru', 'jetpack-forms' ), |
| 4135 | 'PH' => __( 'Philippines', 'jetpack-forms' ), |
| 4136 | 'PN' => __( 'Pitcairn Islands', 'jetpack-forms' ), |
| 4137 | 'PL' => __( 'Poland', 'jetpack-forms' ), |
| 4138 | 'PT' => __( 'Portugal', 'jetpack-forms' ), |
| 4139 | 'PR' => __( 'Puerto Rico', 'jetpack-forms' ), |
| 4140 | 'QA' => __( 'Qatar', 'jetpack-forms' ), |
| 4141 | 'RO' => __( 'Romania', 'jetpack-forms' ), |
| 4142 | 'RU' => __( 'Russia', 'jetpack-forms' ), |
| 4143 | 'RW' => __( 'Rwanda', 'jetpack-forms' ), |
| 4144 | 'RE' => __( 'Réunion', 'jetpack-forms' ), |
| 4145 | 'BL' => __( 'Saint Barthélemy', 'jetpack-forms' ), |
| 4146 | 'SH' => __( 'Saint Helena', 'jetpack-forms' ), |
| 4147 | 'KN' => __( 'Saint Kitts and Nevis', 'jetpack-forms' ), |
| 4148 | 'LC' => __( 'Saint Lucia', 'jetpack-forms' ), |
| 4149 | 'MF' => __( 'Saint Martin', 'jetpack-forms' ), |
| 4150 | 'PM' => __( 'Saint Pierre and Miquelon', 'jetpack-forms' ), |
| 4151 | 'VC' => __( 'Saint Vincent and the Grenadines', 'jetpack-forms' ), |
| 4152 | 'WS' => __( 'Samoa', 'jetpack-forms' ), |
| 4153 | 'SM' => __( 'San Marino', 'jetpack-forms' ), |
| 4154 | 'SA' => __( 'Saudi Arabia', 'jetpack-forms' ), |
| 4155 | 'SN' => __( 'Senegal', 'jetpack-forms' ), |
| 4156 | 'RS' => __( 'Serbia', 'jetpack-forms' ), |
| 4157 | 'SC' => __( 'Seychelles', 'jetpack-forms' ), |
| 4158 | 'SL' => __( 'Sierra Leone', 'jetpack-forms' ), |
| 4159 | 'SG' => __( 'Singapore', 'jetpack-forms' ), |
| 4160 | 'SK' => __( 'Slovakia', 'jetpack-forms' ), |
| 4161 | 'SI' => __( 'Slovenia', 'jetpack-forms' ), |
| 4162 | 'SB' => __( 'Solomon Islands', 'jetpack-forms' ), |
| 4163 | 'SO' => __( 'Somalia', 'jetpack-forms' ), |
| 4164 | 'ZA' => __( 'South Africa', 'jetpack-forms' ), |
| 4165 | 'GS' => __( 'South Georgia and the South Sandwich Islands', 'jetpack-forms' ), |
| 4166 | 'KR' => __( 'South Korea', 'jetpack-forms' ), |
| 4167 | 'ES' => __( 'Spain', 'jetpack-forms' ), |
| 4168 | 'LK' => __( 'Sri Lanka', 'jetpack-forms' ), |
| 4169 | 'SD' => __( 'Sudan', 'jetpack-forms' ), |
| 4170 | 'SR' => __( 'Suriname', 'jetpack-forms' ), |
| 4171 | 'SJ' => __( 'Svalbard and Jan Mayen', 'jetpack-forms' ), |
| 4172 | 'SE' => __( 'Sweden', 'jetpack-forms' ), |
| 4173 | 'CH' => __( 'Switzerland', 'jetpack-forms' ), |
| 4174 | 'SY' => __( 'Syria', 'jetpack-forms' ), |
| 4175 | 'ST' => __( 'São Tomé and PrÃncipe', 'jetpack-forms' ), |
| 4176 | 'TW' => __( 'Taiwan', 'jetpack-forms' ), |
| 4177 | 'TJ' => __( 'Tajikistan', 'jetpack-forms' ), |
| 4178 | 'TZ' => __( 'Tanzania', 'jetpack-forms' ), |
| 4179 | 'TH' => __( 'Thailand', 'jetpack-forms' ), |
| 4180 | 'TL' => __( 'Timor-Leste', 'jetpack-forms' ), |
| 4181 | 'TG' => __( 'Togo', 'jetpack-forms' ), |
| 4182 | 'TK' => __( 'Tokelau', 'jetpack-forms' ), |
| 4183 | 'TO' => __( 'Tonga', 'jetpack-forms' ), |
| 4184 | 'TT' => __( 'Trinidad and Tobago', 'jetpack-forms' ), |
| 4185 | 'TN' => __( 'Tunisia', 'jetpack-forms' ), |
| 4186 | 'TR' => __( 'Turkey', 'jetpack-forms' ), |
| 4187 | 'TM' => __( 'Turkmenistan', 'jetpack-forms' ), |
| 4188 | 'TC' => __( 'Turks and Caicos Islands', 'jetpack-forms' ), |
| 4189 | 'TV' => __( 'Tuvalu', 'jetpack-forms' ), |
| 4190 | 'VI' => __( 'U.S. Virgin Islands', 'jetpack-forms' ), |
| 4191 | 'UG' => __( 'Uganda', 'jetpack-forms' ), |
| 4192 | 'UA' => __( 'Ukraine', 'jetpack-forms' ), |
| 4193 | 'AE' => __( 'United Arab Emirates', 'jetpack-forms' ), |
| 4194 | 'GB' => __( 'United Kingdom', 'jetpack-forms' ), |
| 4195 | 'US' => __( 'United States', 'jetpack-forms' ), |
| 4196 | 'UY' => __( 'Uruguay', 'jetpack-forms' ), |
| 4197 | 'UZ' => __( 'Uzbekistan', 'jetpack-forms' ), |
| 4198 | 'VU' => __( 'Vanuatu', 'jetpack-forms' ), |
| 4199 | 'VA' => __( 'Vatican City', 'jetpack-forms' ), |
| 4200 | 'VE' => __( 'Venezuela', 'jetpack-forms' ), |
| 4201 | 'VN' => __( 'Vietnam', 'jetpack-forms' ), |
| 4202 | 'WF' => __( 'Wallis and Futuna', 'jetpack-forms' ), |
| 4203 | 'YE' => __( 'Yemen', 'jetpack-forms' ), |
| 4204 | 'ZM' => __( 'Zambia', 'jetpack-forms' ), |
| 4205 | 'ZW' => __( 'Zimbabwe', 'jetpack-forms' ), |
| 4206 | ); |
| 4207 | } |
| 4208 | |
| 4209 | /** |
| 4210 | * Enqueues scripts and styles needed for the slider field. |
| 4211 | * |
| 4212 | * @since 6.2.1 |
| 4213 | * |
| 4214 | * @return void |
| 4215 | */ |
| 4216 | private function enqueue_phone_field_assets() { |
| 4217 | $version = defined( 'JETPACK__VERSION' ) ? \JETPACK__VERSION : '0.1'; |
| 4218 | |
| 4219 | // extra cache busting strategy for view.js, seems they are left out of cache clearing on deploys |
| 4220 | $asset_file = plugin_dir_path( __FILE__ ) . '../../dist/modules/field-phone/view.asset.php'; |
| 4221 | $asset = file_exists( $asset_file ) ? require $asset_file : null; |
| 4222 | $version .= $asset['version'] ?? ''; |
| 4223 | |
| 4224 | // combobox styles |
| 4225 | \wp_enqueue_style( |
| 4226 | 'jetpack-form-combobox', |
| 4227 | plugins_url( '../../dist/contact-form/css/combobox.css', __FILE__ ), |
| 4228 | array(), |
| 4229 | $version |
| 4230 | ); |
| 4231 | |
| 4232 | \wp_enqueue_style( |
| 4233 | 'jetpack-form-phone-field', |
| 4234 | plugins_url( '../../dist/contact-form/css/phone-field.css', __FILE__ ), |
| 4235 | array(), |
| 4236 | $version |
| 4237 | ); |
| 4238 | |
| 4239 | \wp_enqueue_script_module( |
| 4240 | 'jetpack-form-phone-field', |
| 4241 | plugins_url( '../../dist/modules/field-phone/view.js', __FILE__ ), |
| 4242 | array( '@wordpress/interactivity', 'jp-forms-view' ), |
| 4243 | $version |
| 4244 | ); |
| 4245 | } |
| 4246 | |
| 4247 | /** |
| 4248 | * Trims the image select options from the end of the array if they are empty. |
| 4249 | * |
| 4250 | * @param array $options The options to trim. |
| 4251 | * |
| 4252 | * @return array The trimmed options array. |
| 4253 | */ |
| 4254 | private function trim_image_select_options( $options ) { |
| 4255 | if ( empty( $options ) ) { |
| 4256 | return $options; |
| 4257 | } |
| 4258 | |
| 4259 | // Work backwards through the array to find the last valid option |
| 4260 | $last_valid_index = -1; |
| 4261 | |
| 4262 | for ( $i = count( $options ) - 1; $i >= 0; $i-- ) { |
| 4263 | $option = $options[ $i ]; |
| 4264 | |
| 4265 | // Check if option has a label |
| 4266 | $has_label = ! empty( $option['label'] ); |
| 4267 | |
| 4268 | // Check if option has an image with src |
| 4269 | $has_image = false; |
| 4270 | |
| 4271 | if ( isset( $option['image']['innerHTML'] ) ) { |
| 4272 | // Extract src from innerHTML using regex |
| 4273 | preg_match( '/src="([^"]*)"/', $option['image']['innerHTML'], $matches ); |
| 4274 | $has_image = ! empty( $matches[1] ); |
| 4275 | } |
| 4276 | |
| 4277 | // If this option has either a label or an image, it's valid |
| 4278 | if ( $has_label || $has_image ) { |
| 4279 | $last_valid_index = $i; |
| 4280 | break; |
| 4281 | } |
| 4282 | } |
| 4283 | |
| 4284 | return array_slice( $options, 0, $last_valid_index + 1 ); |
| 4285 | } |
| 4286 | } |