PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.11.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.11.0
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
sureforms / inc / page-builders / bricks / elements / form-widget.php

form-widget.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.11.0, at inc/page-builders/bricks/elements/form-widget.php

762 lines 21.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Bricks SureForms form element.
4 *
5 * @package sureforms.
6 * @since 0.0.5
7 */
8
9 namespace SRFM\Inc\Page_Builders\Bricks\Elements;
10
11 use Spec_Gb_Helper;
12 use SRFM\Inc\Generate_Form_Markup;
13 use SRFM\Inc\Helper;
14 use SRFM\Inc\Page_Builders\Page_Builders;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit; // Exit if accessed directly.
18 }
19
20 /**
21 * SureForms Bricks element.
22 */
23 class Form_Widget extends \Bricks\Element {
24 /**
25 * Element category.
26 *
27 * @var string
28 */
29 public $category = 'sureforms';
30
31 /**
32 * Element name.
33 *
34 * @var string
35 */
36 public $name = 'sureforms';
37
38 /**
39 * Element icon.
40 *
41 * @var string
42 */
43 public $icon = 'ti-layout-accordion-separated';
44
45 /**
46 * Constructor.
47 *
48 * @param array<mixed> $element Element data.
49 */
50 public function __construct( $element = null ) {
51
52 if ( bricks_is_builder() ) {
53 // call the js functions to handle form submission, load page break, phone, dropdown.
54 $this->scripts = [ 'handleBricksPreviewFormSubmission', 'srfmLoadPageBreak', 'srfmInitializePhoneField', 'srfmInitializeDropdown' ];
55 }
56
57 parent::__construct( $element );
58 }
59
60 /**
61 * Get element name.
62 *
63 * @since 0.0.5
64 * @return string element name.
65 */
66 public function get_label() {
67 return __( 'SureForms', 'sureforms' );
68 }
69
70 /**
71 * Get element keywords.
72 *
73 * @since 0.0.5
74 * @return array<string> element keywords.
75 */
76 public function get_keywords() {
77 return [
78 'sureforms',
79 'contact form',
80 'form',
81 'bricks form',
82 ];
83 }
84
85 /**
86 * Set control groups for the Style tab accordion sections.
87 *
88 * @since 2.7.0
89 * @return void
90 */
91 public function set_control_groups() {
92 $this->control_groups['srfm_form_styling'] = [
93 'title' => __( 'Form Styling', 'sureforms' ),
94 'tab' => 'style',
95 ];
96
97 $styling_required = [
98 [ 'form-id', '!=', '' ],
99 [ 'formTheme', '!=', 'inherit' ],
100 ];
101
102 $this->control_groups['srfm_layout'] = [
103 'title' => __( 'Layout', 'sureforms' ),
104 'tab' => 'style',
105 'required' => $styling_required,
106 ];
107
108 $this->control_groups['srfm_button'] = [
109 'title' => __( 'Button', 'sureforms' ),
110 'tab' => 'style',
111 'required' => $styling_required,
112 ];
113
114 $this->control_groups['srfm_fields'] = [
115 'title' => __( 'Fields', 'sureforms' ),
116 'tab' => 'style',
117 'required' => $styling_required,
118 ];
119 }
120
121 /**
122 * Set element controls.
123 *
124 * @since 0.0.5
125 * @return void
126 */
127 public function set_controls() {
128
129 // === CONTENT TAB ===
130
131 // Select Form.
132 $this->controls['form-id'] = [
133
134 'tab' => 'content',
135 'label' => __( 'Form', 'sureforms' ),
136 'type' => 'select',
137 'options' => Helper::get_sureforms_title_with_ids(),
138 'placeholder' => __( 'Select Form', 'sureforms' ),
139 ];
140
141 // Show Form Title Toggle.
142 $this->controls['form-title'] = [
143 'tab' => 'content',
144 'label' => __( 'Show Form Title', 'sureforms' ),
145 'type' => 'checkbox',
146 'info' => __( 'Enable this to show form title.', 'sureforms' ),
147 'required' => [ 'form-id', '!=', '' ],
148 ];
149
150 $this->controls['srfm_form_submission_info'] = [
151 'tab' => 'content',
152 'content' => __( 'Form submission will be possible on the frontend.', 'sureforms' ),
153 'type' => 'info',
154 'required' => [ 'form-id', '!=', '' ],
155 ];
156
157 // === STYLE TAB — Form Styling Group ===
158
159 $form_required = [
160 [ 'form-id', '!=', '' ],
161 [ 'formTheme', '!=', 'inherit' ],
162 ];
163
164 // Form Theme Select.
165 $this->controls['formTheme'] = [
166 'group' => 'srfm_form_styling',
167 'label' => __( 'Form Theme', 'sureforms' ),
168 'type' => 'select',
169 'options' => [
170 'inherit' => __( "Inherit Form's Original Style", 'sureforms' ),
171 'default' => __( 'Default', 'sureforms' ),
172 ],
173 'default' => 'inherit',
174 ];
175
176 /**
177 * Hook for Pro to add form theme control after inherit toggle.
178 *
179 * @param Form_Widget $element The element instance.
180 * @since 2.7.0
181 */
182 do_action( 'srfm_bricks_after_basic_styling_controls', $this );
183
184 // Primary Color.
185 $this->controls['primaryColor'] = [
186 'group' => 'srfm_form_styling',
187 'label' => __( 'Primary Color', 'sureforms' ),
188 'type' => 'color',
189 'default' => '#111C44',
190 'required' => $form_required,
191 ];
192
193 // Text Color.
194 $this->controls['textColor'] = [
195 'group' => 'srfm_form_styling',
196 'label' => __( 'Text Color', 'sureforms' ),
197 'type' => 'color',
198 'default' => '#1E1E1E',
199 'required' => $form_required,
200 ];
201
202 // Text on Primary Color.
203 $this->controls['textOnPrimaryColor'] = [
204 'group' => 'srfm_form_styling',
205 'label' => __( 'Text on Primary', 'sureforms' ),
206 'type' => 'color',
207 'default' => '#FFFFFF',
208 'required' => $form_required,
209 ];
210
211 // Background Separator.
212 $this->controls['bgSeparator'] = [
213 'group' => 'srfm_form_styling',
214 'label' => __( 'Background', 'sureforms' ),
215 'type' => 'separator',
216 'required' => $form_required,
217 ];
218
219 // Background Type.
220 $this->controls['bgType'] = [
221 'group' => 'srfm_form_styling',
222 'label' => __( 'Type', 'sureforms' ),
223 'type' => 'select',
224 'options' => [
225 'color' => __( 'Color', 'sureforms' ),
226 'gradient' => __( 'Gradient', 'sureforms' ),
227 'image' => __( 'Image', 'sureforms' ),
228 ],
229 'default' => 'color',
230 'required' => $form_required,
231 ];
232
233 // Background Color (when bgType=color).
234 $this->controls['bgColor'] = [
235 'group' => 'srfm_form_styling',
236 'label' => __( 'Background Color', 'sureforms' ),
237 'type' => 'color',
238 'default' => '#FFFFFF',
239 'required' => array_merge( $form_required, [ [ 'bgType', '=', 'color' ] ] ),
240 ];
241
242 // --- Gradient Controls (when bgType=gradient) ---
243 $gradient_required = array_merge( $form_required, [ [ 'bgType', '=', 'gradient' ] ] );
244
245 $this->controls['bgGradientColor1'] = [
246 'group' => 'srfm_form_styling',
247 'label' => __( 'Gradient Color 1', 'sureforms' ),
248 'type' => 'color',
249 'required' => $gradient_required,
250 ];
251
252 $this->controls['bgGradientColor1Stop'] = [
253 'group' => 'srfm_form_styling',
254 'label' => __( 'Color 1 Location (%)', 'sureforms' ),
255 'type' => 'number',
256 'default' => 0,
257 'min' => 0,
258 'max' => 100,
259 'unit' => '%',
260 'required' => $gradient_required,
261 ];
262
263 $this->controls['bgGradientColor2'] = [
264 'group' => 'srfm_form_styling',
265 'label' => __( 'Gradient Color 2', 'sureforms' ),
266 'type' => 'color',
267 'required' => $gradient_required,
268 ];
269
270 $this->controls['bgGradientColor2Stop'] = [
271 'group' => 'srfm_form_styling',
272 'label' => __( 'Color 2 Location (%)', 'sureforms' ),
273 'type' => 'number',
274 'default' => 100,
275 'min' => 0,
276 'max' => 100,
277 'unit' => '%',
278 'required' => $gradient_required,
279 ];
280
281 $this->controls['bgGradientType'] = [
282 'group' => 'srfm_form_styling',
283 'label' => __( 'Gradient Type', 'sureforms' ),
284 'type' => 'select',
285 'options' => [
286 'linear' => __( 'Linear', 'sureforms' ),
287 'radial' => __( 'Radial', 'sureforms' ),
288 ],
289 'default' => 'linear',
290 'required' => $gradient_required,
291 ];
292
293 $this->controls['bgGradientAngle'] = [
294 'group' => 'srfm_form_styling',
295 'label' => __( 'Angle', 'sureforms' ),
296 'type' => 'number',
297 'default' => 90,
298 'min' => 0,
299 'max' => 360,
300 'unit' => 'deg',
301 'required' => array_merge( $gradient_required, [ [ 'bgGradientType', '=', 'linear' ] ] ),
302 ];
303
304 // --- Background Image Controls (when bgType=image) ---
305 $image_required = array_merge( $form_required, [ [ 'bgType', '=', 'image' ] ] );
306
307 $this->controls['bgImage'] = [
308 'group' => 'srfm_form_styling',
309 'label' => __( 'Image', 'sureforms' ),
310 'type' => 'image',
311 'required' => $image_required,
312 ];
313
314 $this->controls['bgImageSize'] = [
315 'group' => 'srfm_form_styling',
316 'label' => __( 'Size', 'sureforms' ),
317 'type' => 'select',
318 'options' => [
319 'cover' => __( 'Cover', 'sureforms' ),
320 'contain' => __( 'Contain', 'sureforms' ),
321 'auto' => __( 'Auto', 'sureforms' ),
322 ],
323 'default' => 'cover',
324 'required' => $image_required,
325 ];
326
327 $this->controls['bgImagePosition'] = [
328 'group' => 'srfm_form_styling',
329 'label' => __( 'Position', 'sureforms' ),
330 'type' => 'select',
331 'options' => [
332 'left top' => __( 'Left Top', 'sureforms' ),
333 'left center' => __( 'Left Center', 'sureforms' ),
334 'left bottom' => __( 'Left Bottom', 'sureforms' ),
335 'center top' => __( 'Center Top', 'sureforms' ),
336 'center center' => __( 'Center Center', 'sureforms' ),
337 'center bottom' => __( 'Center Bottom', 'sureforms' ),
338 'right top' => __( 'Right Top', 'sureforms' ),
339 'right center' => __( 'Right Center', 'sureforms' ),
340 'right bottom' => __( 'Right Bottom', 'sureforms' ),
341 ],
342 'default' => 'center center',
343 'required' => $image_required,
344 ];
345
346 $this->controls['bgImageRepeat'] = [
347 'group' => 'srfm_form_styling',
348 'label' => __( 'Repeat', 'sureforms' ),
349 'type' => 'select',
350 'options' => [
351 'no-repeat' => __( 'No Repeat', 'sureforms' ),
352 'repeat' => __( 'Repeat', 'sureforms' ),
353 'repeat-x' => __( 'Repeat X', 'sureforms' ),
354 'repeat-y' => __( 'Repeat Y', 'sureforms' ),
355 ],
356 'default' => 'no-repeat',
357 'required' => $image_required,
358 ];
359
360 $this->controls['bgImageAttachment'] = [
361 'group' => 'srfm_form_styling',
362 'label' => __( 'Attachment', 'sureforms' ),
363 'type' => 'select',
364 'options' => [
365 'scroll' => __( 'Scroll', 'sureforms' ),
366 'fixed' => __( 'Fixed', 'sureforms' ),
367 ],
368 'default' => 'scroll',
369 'required' => $image_required,
370 ];
371
372 // === STYLE TAB — Layout Group ===
373
374 // Form Padding.
375 $this->controls['formPadding'] = [
376 'group' => 'srfm_layout',
377 'label' => __( 'Form Padding', 'sureforms' ),
378 'type' => 'spacing',
379 'css' => [
380 [
381 'property' => '--srfm-form-padding-{key}',
382 'selector' => '.srfm-form-container',
383 ],
384 ],
385 'default' => [
386 'top' => '0',
387 'right' => '0',
388 'bottom' => '0',
389 'left' => '0',
390 'unit' => 'px',
391 ],
392 'required' => $form_required,
393 ];
394
395 // Form Border Radius.
396 $this->controls['formBorderRadius'] = [
397 'group' => 'srfm_layout',
398 'label' => __( 'Form Border Radius', 'sureforms' ),
399 'type' => 'spacing',
400 'css' => [
401 [
402 'property' => '--srfm-form-border-radius-{key}',
403 'selector' => '.srfm-form-container',
404 ],
405 ],
406 'default' => [
407 'top' => '0',
408 'right' => '0',
409 'bottom' => '0',
410 'left' => '0',
411 'unit' => 'px',
412 ],
413 'required' => $form_required,
414 ];
415
416 /**
417 * Hook for Pro to add additional layout controls (e.g., row/column gap).
418 *
419 * @param Form_Widget $element The element instance.
420 * @since 2.7.0
421 */
422 do_action( 'srfm_bricks_layout_controls', $this );
423
424 // === STYLE TAB — Button Group ===
425
426 // Button Alignment.
427 $this->controls['buttonAlignment'] = [
428 'group' => 'srfm_button',
429 'label' => __( 'Alignment', 'sureforms' ),
430 'type' => 'select',
431 'options' => [
432 'left' => __( 'Left', 'sureforms' ),
433 'center' => __( 'Center', 'sureforms' ),
434 'right' => __( 'Right', 'sureforms' ),
435 'justify' => __( 'Full Width', 'sureforms' ),
436 ],
437 'default' => 'left',
438 'required' => $form_required,
439 ];
440
441 /**
442 * Hook for Pro to add additional button controls.
443 *
444 * @param Form_Widget $element The element instance.
445 * @since 2.7.0
446 */
447 do_action( 'srfm_bricks_button_controls', $this );
448
449 // === STYLE TAB — Fields Group ===
450
451 // Field Spacing.
452 // Hidden when Pro's custom theme is selected (replaced by Row/Column Gap).
453 $this->controls['fieldSpacing'] = [
454 'group' => 'srfm_fields',
455 'label' => __( 'Field Spacing', 'sureforms' ),
456 'type' => 'select',
457 'options' => [
458 'small' => __( 'Small', 'sureforms' ),
459 'medium' => __( 'Medium', 'sureforms' ),
460 'large' => __( 'Large', 'sureforms' ),
461 ],
462 'default' => 'medium',
463 'required' => array_merge( $form_required, [ [ 'formTheme', '!=', 'custom' ] ] ),
464 ];
465
466 /**
467 * Hook for Pro to add additional field controls.
468 *
469 * @param Form_Widget $element The element instance.
470 * @since 2.7.0
471 */
472 do_action( 'srfm_bricks_field_controls', $this );
473
474 /**
475 * Hook for Pro to add additional style sections (e.g., Messages).
476 *
477 * @param Form_Widget $element The element instance.
478 * @since 2.7.0
479 */
480 do_action( 'srfm_bricks_after_styling_section', $this );
481 }
482
483 /**
484 * Enqueue scripts for phone and dropdown field in the Bricks editor.
485 *
486 * @since 0.0.10
487 * @return void
488 */
489 public function enqueue_scripts() {
490 // enqueue common fields assets for the dropdown and phone fields.
491 Page_Builders::enqueue_common_fields_assets();
492 }
493
494 /**
495 * Render element.
496 *
497 * @since 0.0.5
498 * @return void
499 */
500 public function render() {
501 $settings = $this->settings;
502 $form_id = absint( $settings['form-id'] ?? 0 );
503
504 if ( $form_id > 0 ) {
505 $form = get_post( $form_id );
506 // 'protected' is a custom SureForms post status for password-protected forms.
507 if ( ! $form || ! in_array( $form->post_status, [ 'publish', 'protected' ], true ) ) {
508 echo esc_html__( 'This form has been deleted or is unavailable.', 'sureforms' );
509 return;
510 }
511
512 $form_title = isset( $settings['form-title'] );
513 $block_attrs = $this->get_block_attrs( $settings );
514
515 // Get spectra blocks CSS/JS.
516 $blocks = parse_blocks( get_post_field( 'post_content', $form_id ) );
517 $styles = Spec_Gb_Helper::get_instance()->get_assets( $blocks );
518 ?>
519 <div <?php echo $this->render_attributes( '_root' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
520 <?php
521 // Bypass shortcode — call Generate_Form_Markup directly with block_attrs.
522 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in Generate_Form_Markup.
523 echo Generate_Form_Markup::get_form_markup(
524 $form_id,
525 $form_title,
526 '',
527 'post',
528 true,
529 $block_attrs
530 );
531 ?>
532 <style><?php echo $styles['css']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></style>
533 <script><?php echo $styles['js']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></script>
534 </div>
535 <?php
536 } else {
537 // Show placeholder when no form is selected.
538 // phpcs:ignore -- WordPress.Security.EscapeOutput.OutputNotEscaped - Escaping not required.
539 echo $this->render_element_placeholder(
540 [
541 'icon-class' => $this->icon,
542 'description' => esc_html__( 'Select the form that you wish to add here.', 'sureforms' ),
543 ]
544 );
545 }
546 }
547
548 /**
549 * Resolve a Bricks color value to a CSS-safe color string.
550 * Bricks color controls may return a string or an array with 'hex'/'raw' keys.
551 * Since Bricks 2.x, global palette colors are stored as CSS variable references
552 * (e.g., var(--bricks-color-xxx)) instead of raw hex values.
553 *
554 * @param mixed $color_value Raw value from Bricks color control.
555 * @return string|null CSS-safe color string or null.
556 * @since 2.7.0
557 */
558 public static function resolve_bricks_color( $color_value ) {
559 if ( empty( $color_value ) ) {
560 return null;
561 }
562
563 $raw = null;
564
565 if ( is_string( $color_value ) ) {
566 $raw = $color_value;
567 } elseif ( is_array( $color_value ) ) {
568 // Use Bricks' native color resolution for global palette colors (Bricks 2.x+).
569 // This correctly handles palette IDs, light/dark mode, and CSS variable references.
570 if ( method_exists( '\Bricks\Assets', 'generate_css_color' ) ) {
571 $raw = \Bricks\Assets::generate_css_color( $color_value );
572 } else {
573 $raw = $color_value['hex'] ?? $color_value['raw'] ?? null;
574 }
575 }
576
577 if ( null === $raw || '' === $raw ) {
578 return null;
579 }
580
581 // Validate as a CSS-safe color value.
582 $hex = sanitize_hex_color( $raw );
583 if ( $hex ) {
584 return $hex;
585 }
586
587 // Allow rgb/rgba/hsl/hsla functional notation — restrict to safe characters only.
588 if ( preg_match( '/^(rgb|rgba|hsl|hsla)\s*\([0-9.,\s\/%]+\)$/i', $raw ) ) {
589 return $raw;
590 }
591
592 // Allow CSS custom property references (e.g., var(--bricks-color-xxx)) for Bricks 2.x global palette colors.
593 if ( preg_match( '/^var\(\s*--[a-zA-Z0-9_$-]+\s*\)$/', $raw ) ) {
594 return $raw;
595 }
596
597 return null;
598 }
599
600 /**
601 * Map Bricks 'spacing' control to individual camelCase block_attrs keys.
602 * Bricks spacing returns: ['top' => '10', 'right' => '10', ..., 'unit' => 'px']
603 *
604 * @param array<string, mixed> $settings Bricks settings.
605 * @param string $setting_key Key in settings for the spacing control.
606 * @param string $attr_prefix Prefix for output block_attrs keys.
607 * @return array<string, string> Mapped attributes.
608 * @since 2.7.0
609 */
610 public static function map_bricks_spacing( $settings, $setting_key, $attr_prefix ) {
611 $attrs = [];
612
613 if ( empty( $settings[ $setting_key ] ) || ! is_array( $settings[ $setting_key ] ) ) {
614 return $attrs;
615 }
616
617 $dims = $settings[ $setting_key ];
618 $allowed_units = [ 'px', 'em', 'rem', '%', 'vw', 'vh' ];
619 $raw_unit = $dims['unit'] ?? 'px';
620 $unit = in_array( $raw_unit, $allowed_units, true ) ? $raw_unit : 'px';
621
622 foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
623 if ( isset( $dims[ $side ] ) && '' !== (string) $dims[ $side ] ) {
624 $attrs[ $attr_prefix . ucfirst( $side ) ] = floatval( $dims[ $side ] ) . $unit;
625 }
626 }
627
628 return $attrs;
629 }
630
631 /**
632 * Build CSS gradient string from individual Bricks controls.
633 * Since Bricks has no built-in gradient group control, we use individual controls.
634 *
635 * @param array<string, mixed> $settings Bricks settings.
636 * @param string $prefix Control name prefix (e.g., 'bg' for bgGradientColor1).
637 * @return string|null CSS gradient string or null.
638 * @since 2.7.0
639 */
640 public static function build_bricks_gradient_css( $settings, $prefix ) {
641 $color_1 = self::resolve_bricks_color( $settings[ $prefix . 'GradientColor1' ] ?? null );
642 $color_2 = self::resolve_bricks_color( $settings[ $prefix . 'GradientColor2' ] ?? null );
643
644 // Both colors are required.
645 if ( ! $color_1 || ! $color_2 ) {
646 return null;
647 }
648
649 $type = 'radial' === ( $settings[ $prefix . 'GradientType' ] ?? 'linear' ) ? 'radial' : 'linear';
650 $angle = absint( Helper::get_string_value( $settings[ $prefix . 'GradientAngle' ] ?? 90 ) );
651 $color1_stop = absint( Helper::get_string_value( $settings[ $prefix . 'GradientColor1Stop' ] ?? 0 ) );
652 $color2_stop = absint( Helper::get_string_value( $settings[ $prefix . 'GradientColor2Stop' ] ?? 100 ) );
653
654 if ( 'radial' === $type ) {
655 return sprintf(
656 'radial-gradient(at center center, %s %s%%, %s %s%%)',
657 $color_1,
658 $color1_stop,
659 $color_2,
660 $color2_stop
661 );
662 }
663
664 return sprintf(
665 'linear-gradient(%sdeg, %s %s%%, %s %s%%)',
666 $angle,
667 $color_1,
668 $color1_stop,
669 $color_2,
670 $color2_stop
671 );
672 }
673
674 /**
675 * Convert Bricks settings to block_attrs array.
676 * Uses same camelCase keys as Gutenberg for code reuse with Form_Styling.
677 *
678 * @param array<string, mixed> $settings Bricks element settings.
679 * @return array<string, mixed> Block attributes.
680 * @since 2.7.0
681 */
682 protected function get_block_attrs( $settings ) {
683 $block_attrs = [
684 'blockId' => 'bricks-' . ( $this->id ?? wp_unique_id() ),
685 ];
686
687 // Check form theme — if inheriting, don't pass any custom styling attributes.
688 $form_theme = $settings['formTheme'] ?? 'inherit';
689 $block_attrs['formTheme'] = $form_theme;
690
691 if ( 'inherit' === $form_theme ) {
692 return $block_attrs;
693 }
694
695 // Color controls.
696 $color_keys = [
697 'primaryColor',
698 'textColor',
699 'textOnPrimaryColor',
700 'bgColor',
701 ];
702
703 foreach ( $color_keys as $key ) {
704 $color = self::resolve_bricks_color( $settings[ $key ] ?? null );
705 if ( $color ) {
706 $block_attrs[ $key ] = $color;
707 }
708 }
709
710 // Pass-through keys with allowed values for server-side validation.
711 $passthrough_keys = [
712 'fieldSpacing' => [ 'small', 'medium', 'large' ],
713 'buttonAlignment' => [ 'left', 'center', 'right', 'justify' ],
714 'bgType' => [ 'color', 'gradient', 'image' ],
715 'bgImageSize' => [ 'cover', 'contain', 'auto' ],
716 'bgImagePosition' => [ 'left top', 'left center', 'left bottom', 'center top', 'center center', 'center bottom', 'right top', 'right center', 'right bottom' ],
717 'bgImageRepeat' => [ 'no-repeat', 'repeat', 'repeat-x', 'repeat-y' ],
718 'bgImageAttachment' => [ 'scroll', 'fixed' ],
719 ];
720
721 foreach ( $passthrough_keys as $key => $allowed ) {
722 if ( isset( $settings[ $key ] ) && in_array( $settings[ $key ], $allowed, true ) ) {
723 $block_attrs[ $key ] = $settings[ $key ];
724 }
725 }
726
727 // Build gradient CSS string from individual Bricks controls.
728 if ( 'gradient' === ( $settings['bgType'] ?? '' ) ) {
729 $gradient_css = self::build_bricks_gradient_css( $settings, 'bg' );
730 if ( $gradient_css ) {
731 $block_attrs['bgGradient'] = $gradient_css;
732 }
733 }
734
735 // Handle spacing controls (4-sided) → individual camelCase keys with unit appended.
736 $block_attrs = array_merge( $block_attrs, self::map_bricks_spacing( $settings, 'formPadding', 'formPadding' ) );
737 $block_attrs = array_merge( $block_attrs, self::map_bricks_spacing( $settings, 'formBorderRadius', 'formBorderRadius' ) );
738
739 // Handle bgImage — Bricks image control returns ['url', 'id', ...].
740 if ( ! empty( $settings['bgImage'] ) && is_array( $settings['bgImage'] ) ) {
741 if ( ! empty( $settings['bgImage']['url'] ) ) {
742 $raw_url = esc_url_raw( $settings['bgImage']['url'] );
743 // Encode parentheses to prevent CSS injection in url() context.
744 $block_attrs['bgImage'] = str_replace( [ '(', ')' ], [ '%28', '%29' ], $raw_url );
745 }
746 }
747
748 /**
749 * Filters the Bricks block attributes after sanitization.
750 *
751 * Third-party code hooking this filter is responsible for sanitizing
752 * any values it adds or modifies. Unsanitized values may be output
753 * directly into CSS custom properties.
754 *
755 * @param array<string, mixed> $block_attrs Sanitized block attributes.
756 * @param array<string, mixed> $settings Raw Bricks element settings.
757 * @since 2.7.0
758 */
759 return apply_filters( 'srfm_bricks_block_attrs', $block_attrs, $settings );
760 }
761 }
762