PluginProbe
Stripe Payment Forms by WP Simple Pay – Accept Credit Card Payments + Subscriptions with Stripe / trunk
Stripe Payment Forms by WP Simple Pay – Accept Credit Card Payments + Subscriptions with Stripe vtrunk
4.17.3 trunk 2.2.0 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.6.1 2.6.2 2.6.3 4.10.0 4.11.1 4.12.2 4.14.1 4.14.2 4.14.3 4.15.0 4.16.0 All 59 releases
stripe / src / Admin / FormBuilder / FormStyle / Frontend.php

Frontend.php in Stripe Payment Forms by WP Simple Pay – Accept Credit Card Payments + Subscriptions with Stripe trunk, at src/Admin/FormBuilder/FormStyle/Frontend.php

986 lines 56.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles frontend filtering and style application.
4 *
5 * @package SimplePay\Core\Admin\FormBuilder\FormStyle
6 */
7
8 namespace SimplePay\Core\Admin\FormBuilder\FormStyle;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Frontend handler class.
17 *
18 * Handles frontend filtering and style application for WP Simple Pay forms.
19 * Modifies Stripe Elements configuration and generates custom CSS for forms.
20 *
21 * @since 1.0.0
22 */
23 class Frontend {
24
25 /**
26 * The single instance of the class.
27 *
28 * @var Frontend
29 * @since 1.0.0
30 */
31 private static $instance = null;
32
33 /**
34 * Stores the IDs of forms rendered on the current page.
35 *
36 * @since 1.0.0
37 * @var array<int>
38 */
39 private static $rendered_form_ids = array();
40
41 /**
42 * Get the singleton instance.
43 *
44 * Ensures only one instance of Frontend is loaded or can be loaded.
45 *
46 * @since 1.0.0
47 * @static
48 *
49 * @return Frontend The single instance of this class.
50 */
51 public static function get_instance() {
52 if ( is_null( self::$instance ) ) {
53 self::$instance = new self();
54 }
55 return self::$instance;
56 }
57
58 /**
59 * Constructor.
60 *
61 * Initializes the class and sets up hooks.
62 *
63 * @since 1.0.0
64 * @access private
65 */
66 private function __construct() {
67 $this->hooks();
68 }
69
70 /**
71 * Initialize hooks.
72 *
73 * @since 1.0.0
74 * @access private
75 */
76 private function hooks(): void {
77 // Hook unconditionally to modify Elements config.
78 add_filter( 'simpay_elements_config', array( $this, 'modify_elements_config' ), 10, 2 );
79
80 // Hook to capture rendered form IDs (for inline CSS and for Elements config).
81 add_action( 'simpay_form_before_form_top', array( $this, 'capture_rendered_form_id' ), 10, 1 ); // Only need form ID.
82
83 // Hook late to print inline CSS for non-Stripe elements.
84 add_action( 'wp_print_footer_scripts', array( $this, 'print_late_frontend_styles' ), 100 ); // Using a late hook.
85
86 // Hook to add data attribute to embedded heading via JavaScript.
87 add_action( 'wp_print_footer_scripts', array( $this, 'inject_heading_data_attributes' ), 99 );
88 }
89
90 /**
91 * Filters the Stripe Elements configuration array based on saved settings.
92 *
93 * Modifies the Stripe Elements appearance configuration to apply custom styles
94 * based on the saved settings for the current form.
95 *
96 * @since 1.0.0
97 *
98 * @param array $config The original Elements configuration.
99 * @param int $form_id The form ID (optional, for backwards compatibility).
100 * @return array The modified Elements configuration.
101 */
102 public function modify_elements_config( $config, $form_id = null ) {
103 // Use the provided form ID if available, otherwise fall back to the last rendered form ID.
104 if ( null === $form_id ) {
105 // Try to get the relevant form ID from the rendered list (backwards compatibility).
106 if ( empty( self::$rendered_form_ids ) ) {
107 return $config;
108 }
109 $form_id = end( self::$rendered_form_ids );
110 if ( ! $form_id ) {
111 return $config;
112 }
113 }
114
115 // Check if styling is applicable for this form type.
116 $display_type = get_post_meta( $form_id, '_form_display_type', true );
117 if ( ! in_array( $display_type, array( 'embedded', 'overlay' ), true ) ) {
118 return $config;
119 }
120
121 // --- Apply styles ---
122 // Ensure appearance key exists.
123 if ( ! isset( $config['appearance'] ) ) {
124 $config['appearance'] = array();
125 }
126 if ( ! isset( $config['appearance']['variables'] ) ) {
127 $config['appearance']['variables'] = array();
128 }
129 if ( ! isset( $config['appearance']['rules'] ) ) {
130 $config['appearance']['rules'] = array();
131 }
132
133 // Apply saved styles (Logic remains the same as before).
134 $primary_color = Settings::get_setting( $form_id, 'primary_color' );
135 if ( $primary_color ) {
136 $config['appearance']['variables']['colorPrimary'] = $primary_color;
137 $config['appearance']['rules']['.Tab:focus']['boxShadow'] = 'inset 0 -4px ' . $primary_color;
138 $config['appearance']['rules']['.Tab:hover']['boxShadow'] = 'inset 0 -4px ' . $primary_color;
139 $config['appearance']['rules']['.Tab--selected']['boxShadow'] = 'inset 0 -2px ' . $primary_color;
140 $config['appearance']['rules']['.Tab--selected:focus']['boxShadow'] = 'inset 0 -4px ' . $primary_color;
141 $config['appearance']['rules']['.Input:focus']['boxShadow'] = sprintf(
142 '0 0 0 1px %1$s, 0 0 0 3px %2$s, 0 1px 2px rgba(0, 0, 0, 0.05)',
143 $primary_color,
144 self::hex_to_rgba( $primary_color, 0.15 )
145 );
146 $config['appearance']['rules']['.CodeInput:focus']['boxShadow'] = $config['appearance']['rules']['.Input:focus']['boxShadow'];
147 $config['appearance']['rules']['.CheckboxInput:focus']['boxShadow'] = $config['appearance']['rules']['.Input:focus']['boxShadow'];
148 $config['appearance']['rules']['.PickerItem--selected']['boxShadow'] = $config['appearance']['rules']['.Input:focus']['boxShadow'];
149
150 // Add focus styling for dropdown selects.
151 $config['appearance']['rules']['.p-Select-select:focus']['boxShadow'] = $config['appearance']['rules']['.Input:focus']['boxShadow'];
152 $config['appearance']['rules']['.p-Select-select:focus']['borderColor'] = $primary_color;
153 }
154
155 $background_color = Settings::get_setting( $form_id, 'background_color' );
156 if ( $background_color ) {
157 // Don't set colorBackground variable as it affects all elements including labels.
158 // Only apply background color to text input fields (text, email, number, etc.), not selects or checkboxes.
159
160 // Add explicit background color for text input types only.
161 $config['appearance']['rules']['.Input']['backgroundColor'] = $background_color;
162 $config['appearance']['rules']['.CodeInput']['backgroundColor'] = $background_color;
163
164 // Explicitly ensure labels, selects, checkboxes, and other non-text inputs don't get the input background color.
165 $config['appearance']['rules']['.Label']['backgroundColor'] = 'transparent';
166 $config['appearance']['rules']['.TabLabel']['backgroundColor'] = 'transparent';
167 $config['appearance']['rules']['.CheckboxInput']['backgroundColor'] = 'transparent';
168 $config['appearance']['rules']['.p-Select-select']['backgroundColor'] = 'transparent';
169 $config['appearance']['rules']['.p-Select-option']['backgroundColor'] = 'transparent';
170 $config['appearance']['rules']['.p-FauxInput']['backgroundColor'] = 'transparent';
171 }
172
173 $text_color = Settings::get_setting( $form_id, 'text_color' );
174 if ( $text_color ) {
175 $config['appearance']['variables']['colorText'] = $text_color;
176 $config['appearance']['rules']['.TabLabel']['color'] = $text_color;
177 $config['appearance']['rules']['.Label']['color'] = $text_color;
178 $config['appearance']['rules']['.Input']['color'] = $text_color;
179 $config['appearance']['rules']['.CodeInput']['color'] = $text_color;
180 $config['appearance']['rules']['.PickerItem']['color'] = $text_color;
181 $config['appearance']['rules']['.DropdownItem']['color'] = $text_color;
182 $config['appearance']['rules']['.TabIcon--selected']['fill'] = $text_color;
183
184 // Add text color for select dropdown and options.
185 $config['appearance']['rules']['.p-Select-select']['color'] = $text_color;
186 $config['appearance']['rules']['.p-Select-option']['color'] = $text_color;
187 $config['appearance']['rules']['option']['color'] = $text_color;
188 }
189
190 // Apply label text color (overrides general text color for labels).
191 $label_text_color = Settings::get_setting( $form_id, 'label_text_color' );
192 if ( $label_text_color ) {
193 $config['appearance']['rules']['.Label']['color'] = $label_text_color;
194 $config['appearance']['rules']['.TabLabel']['color'] = $label_text_color;
195 }
196
197 // Apply input text color (overrides general text color for inputs).
198 $input_text_color = Settings::get_setting( $form_id, 'input_text_color' );
199 if ( $input_text_color ) {
200 $config['appearance']['rules']['.Input']['color'] = $input_text_color;
201 $config['appearance']['rules']['.CodeInput']['color'] = $input_text_color;
202 $config['appearance']['rules']['.PickerItem']['color'] = $input_text_color;
203 $config['appearance']['rules']['.DropdownItem']['color'] = $input_text_color;
204 $config['appearance']['rules']['.p-Select-select']['color'] = $input_text_color;
205 $config['appearance']['rules']['.p-Select-option']['color'] = $input_text_color;
206 $config['appearance']['rules']['option']['color'] = $input_text_color;
207 $config['appearance']['rules']['.p-FauxInput']['color'] = $input_text_color;
208 }
209
210 // Apply border color.
211 $border_color = Settings::get_setting( $form_id, 'border_color' );
212 if ( $border_color ) {
213 // Add border color to inputs.
214 $config['appearance']['rules']['.Input']['boxShadow'] = '0 0 0 1px ' . $border_color . ', 0 1px 2px rgba(0, 0, 0, 0.05)';
215 $config['appearance']['rules']['.CodeInput']['boxShadow'] = $config['appearance']['rules']['.Input']['boxShadow'];
216 $config['appearance']['rules']['.CheckboxInput']['boxShadow'] = $config['appearance']['rules']['.Input']['boxShadow'];
217 $config['appearance']['rules']['.p-Select-select']['boxShadow'] = $config['appearance']['rules']['.Input']['boxShadow'];
218 $config['appearance']['rules']['.p-Select-select']['borderColor'] = $border_color;
219 $config['appearance']['rules']['.Input']['borderColor'] = $border_color;
220 }
221
222 // Apply error border color.
223 $error_border_color = Settings::get_setting( $form_id, 'error_border_color' );
224 if ( $error_border_color ) {
225 $config['appearance']['rules']['.Input--invalid']['boxShadow'] = '0 0 0 1px ' . $error_border_color . ', 0 1px 2px rgba(0, 0, 0, 0.05)';
226 $config['appearance']['rules']['.Input--invalid']['borderColor'] = $error_border_color;
227 }
228
229 // Apply error text color.
230 $error_text_color = Settings::get_setting( $form_id, 'error_text_color' );
231 if ( $error_text_color ) {
232 $config['appearance']['rules']['.Error']['color'] = $error_text_color;
233 $config['appearance']['rules']['.ErrorMessage']['color'] = $error_text_color;
234 }
235
236 $border_radius = Settings::get_setting( $form_id, 'border_radius', '' );
237 // Only apply border radius if it has been explicitly set.
238 if ( '' !== $border_radius && Settings::setting_exists( $form_id, 'border_radius' ) ) {
239 $config['appearance']['variables']['borderRadius'] = $border_radius . 'px';
240
241 // Add explicit border radius for select elements.
242 $config['appearance']['rules']['.p-Select-select']['borderRadius'] = $border_radius . 'px';
243 }
244
245 $input_font_size = Settings::get_setting( $form_id, 'input_font_size' );
246 if ( $input_font_size ) {
247 $config['appearance']['rules']['.Input']['fontSize'] = $input_font_size . 'px';
248 $config['appearance']['rules']['.CodeInput']['fontSize'] = $input_font_size . 'px';
249 $config['appearance']['rules']['.PickerItem']['fontSize'] = $input_font_size . 'px';
250
251 // Add font size for select elements.
252 $config['appearance']['rules']['.p-Select-select']['fontSize'] = $input_font_size . 'px';
253 $config['appearance']['rules']['.p-Select-option']['fontSize'] = $input_font_size . 'px';
254 $config['appearance']['rules']['option']['fontSize'] = $input_font_size . 'px';
255 }
256
257 $label_font_size = Settings::get_setting( $form_id, 'label_font_size' );
258 if ( $label_font_size ) {
259 $config['appearance']['rules']['.Label']['fontSize'] = $label_font_size . 'px';
260 $config['appearance']['rules']['.TabLabel']['fontSize'] = $label_font_size . 'px';
261 }
262
263 $label_font_weight = Settings::get_setting( $form_id, 'label_font_weight' );
264 if ( $label_font_weight ) {
265 $config['appearance']['rules']['.Label']['fontWeight'] = $label_font_weight;
266 $config['appearance']['rules']['.TabLabel']['fontWeight'] = $label_font_weight;
267 }
268 // --- End Apply styles. ---
269
270 // Remove empty appearance sub-keys so they don't serialize as JSON
271 // arrays ([]) instead of objects ({}), and so the JS auto-detection
272 // fallback in elements.js can run when no custom styles are configured.
273 if ( empty( $config['appearance']['variables'] ) ) {
274 unset( $config['appearance']['variables'] );
275 }
276
277 if ( empty( $config['appearance']['rules'] ) ) {
278 unset( $config['appearance']['rules'] );
279 }
280
281 if ( empty( $config['appearance'] ) ) {
282 unset( $config['appearance'] );
283 }
284
285 return $config;
286 }
287
288 /**
289 * Capture the ID of a form being rendered on the page.
290 *
291 * Stores the form ID in a static array to be used later for styling.
292 *
293 * @since 1.0.0
294 *
295 * @param int $form_id The ID of the form being rendered.
296 * @return void
297 */
298 public function capture_rendered_form_id( $form_id ) {
299 $form_id = absint( $form_id );
300 if ( $form_id > 0 && ! in_array( $form_id, self::$rendered_form_ids, true ) ) {
301 self::$rendered_form_ids[] = $form_id;
302 }
303 }
304
305 /**
306 * Prints inline CSS late in the footer for non-Elements form parts.
307 *
308 * Generates and outputs custom CSS for all rendered forms on the current page.
309 * This CSS targets non-Stripe Elements parts of the form.
310 *
311 * @since 1.0.0
312 *
313 * @return void
314 */
315 public function print_late_frontend_styles() {
316 if ( empty( self::$rendered_form_ids ) ) {
317 return;
318 }
319
320 $css = '';
321 $preview_css = ''; // Separate CSS for preview wrapper.
322
323 foreach ( self::$rendered_form_ids as $form_id ) {
324 $display_type = get_post_meta( $form_id, '_form_display_type', true );
325 if ( ! in_array( $display_type, array( 'embedded', 'overlay' ), true ) ) {
326 continue;
327 }
328
329 // Get the base CSS from our generator method.
330 $form_css = $this->generate_custom_css( $form_id );
331 $css .= $form_css;
332
333 // Add legacy CSS selectors for backwards compatibility.
334 $live_wrapper_selector = "#simpay-embedded-form-wrap-{$form_id}"; // Target the outer wrapper div by ID.
335
336 // --- Form Container Background Color & Padding
337 $form_bg_color = Settings::get_setting( $form_id, 'form_container_background_color' );
338 if ( $form_bg_color ) {
339 // Apply background to the specific live wrapper ID (no padding).
340 $css .= "{$live_wrapper_selector} {
341 background-color: " . esc_attr( $form_bg_color ) . " !important;
342 margin: 0 auto !important;
343 }\n";
344 // Apply background only to preview wrapper.
345 $preview_css .= 'body.simpay-form-preview .simpay-form-preview-wrap { background: ' . esc_attr( $form_bg_color ) . " !important; }\n";
346 }
347
348 // --- Form Container Border Radius for preview wrapper.
349 $form_border_radius = Settings::get_setting( $form_id, 'form_border_radius', '' );
350 if ( '' !== $form_border_radius && Settings::setting_exists( $form_id, 'form_border_radius' ) ) {
351 $preview_css .= 'body.simpay-form-preview .simpay-form-preview-wrap { border-radius: ' . absint( $form_border_radius ) . "px !important; }\n";
352 }
353 }
354
355 // Combine live CSS and preview CSS.
356 $final_css = $preview_css . $css;
357
358 // Output the combined CSS.
359 if ( ! empty( $final_css ) ) {
360 // Directly print the CSS in the footer.
361 echo "\n<style id=\"wpsp-admin-inline-styles-late\">\n";
362 echo $final_css; // WPCS: XSS okay.
363 echo "</style>\n";
364 }
365 }
366
367 /**
368 * Get the custom CSS for a specific form.
369 *
370 * Public accessor for generate_custom_css(), used by the Gutenberg block
371 * preview where wp_print_footer_scripts does not fire.
372 *
373 * @since 4.17.0
374 *
375 * @param int $form_id The form ID.
376 * @return string The generated CSS, or empty string if not applicable.
377 */
378 public function get_form_css( $form_id ) {
379 $display_type = get_post_meta( $form_id, '_form_display_type', true );
380 if ( ! in_array( $display_type, array( 'embedded', 'overlay' ), true ) ) {
381 return '';
382 }
383
384 return $this->generate_custom_css( $form_id );
385 }
386
387 /**
388 * Helper function to convert hex color to rgba.
389 *
390 * Converts a hexadecimal color value to an rgba color value with the specified opacity.
391 *
392 * @since 1.0.0
393 * @access private
394 * @static
395 *
396 * @param string $color The hexadecimal color value.
397 * @param float $opacity The opacity value (0-1).
398 * @return string The rgba color value.
399 */
400 private static function hex_to_rgba( $color, $opacity = 1 ) {
401 // If the color is already in rgba/rgb format, return it directly.
402 if ( preg_match( '/^rgba?\(/', $color ) ) {
403 return $color;
404 }
405
406 $color = ltrim( $color, '#' );
407 if ( strlen( $color ) === 3 ) {
408 $r = hexdec( substr( $color, 0, 1 ) . substr( $color, 0, 1 ) );
409 $g = hexdec( substr( $color, 1, 1 ) . substr( $color, 1, 1 ) );
410 $b = hexdec( substr( $color, 2, 1 ) . substr( $color, 2, 1 ) );
411 } elseif ( strlen( $color ) === 6 ) {
412 $r = hexdec( substr( $color, 0, 2 ) );
413 $g = hexdec( substr( $color, 2, 2 ) );
414 $b = hexdec( substr( $color, 4, 2 ) );
415 } else {
416 return 'rgba(0,0,0,0)'; // Invalid color.
417 }
418 $opacity = max( 0, min( 1, $opacity ) );
419 return sprintf( 'rgba(%d,%d,%d,%.2f)', $r, $g, $b, $opacity );
420 }
421
422 /**
423 * Build the box-shadow CSS value used to render a border on form inputs.
424 *
425 * Native HTML inputs and Stripe Elements both use a `box-shadow` outline
426 * to paint borders (the base CSS sets `border: 0`). Centralizing the
427 * shadow tuple here keeps the rendering consistent and prevents drift
428 * between the regular border, focus, and error states.
429 *
430 * @since 4.17.3
431 * @access private
432 * @static
433 *
434 * @param string $color The (already-sanitized) color value to use for the inner ring.
435 * @return string The CSS `box-shadow` value (without the `box-shadow:` property name or trailing `!important`).
436 */
437 private static function build_box_shadow_border( $color ) {
438 return '0 0 0 1px ' . $color . ', 0 1px 2px rgba(0, 0, 0, 0.05)';
439 }
440
441 /**
442 * Sanitize a color value for safe inclusion in generated CSS.
443 *
444 * Defense-in-depth against legacy or imported postmeta that may have
445 * bypassed save-time sanitization in {@see Settings::sanitize_setting()}.
446 * Accepts hex colors and `rgb()`/`rgba()`/`hsl()`/`hsla()` notation.
447 * Anything else (including `javascript:`, escape sequences, or arbitrary
448 * strings that could break out of the CSS context) is rejected.
449 *
450 * @since 4.17.3
451 * @access private
452 * @static
453 *
454 * @param string $color The raw color value from settings.
455 * @return string A safe color value, or empty string if the input is unsafe.
456 */
457 private static function sanitize_color_for_css( $color ) {
458 if ( ! is_string( $color ) || '' === $color ) {
459 return '';
460 }
461
462 // Hex color (#fff, #ffffff). sanitize_hex_color returns null/empty on bad input.
463 $hex = sanitize_hex_color( $color );
464 if ( ! empty( $hex ) ) {
465 return $hex;
466 }
467
468 // Functional notation: rgb(), rgba(), hsl(), hsla(). Match the same
469 // shape Settings::sanitize_setting() accepts.
470 if ( preg_match( '/^(rgba?|hsla?)\(\s*[\d.,\s%]+\)$/', $color ) ) {
471 return $color;
472 }
473
474 // Anything else is rejected to prevent CSS injection.
475 return '';
476 }
477
478 /**
479 * Generate the custom CSS for a form based on its style settings.
480 *
481 * Creates CSS rules for a specific form based on its saved style settings.
482 * This CSS targets standard HTML elements that are not controlled by Stripe Elements.
483 *
484 * @since 1.0.0
485 * @access private
486 *
487 * @param int $form_id The form ID.
488 * @return string The generated CSS.
489 */
490 private function generate_custom_css( $form_id ) {
491 $css = '';
492
493 // Only add container width to make sure the form doesn't get squeezed.
494 $css .= "
495 /* Form container width */
496 #simpay-embedded-form-wrap-{$form_id} {
497 max-width: none !important;
498 }";
499
500 // Now add the theme-specific styles
501 // Add form container background color.
502 $form_bg_color = Settings::get_setting( $form_id, 'form_container_background_color' );
503 if ( ! empty( $form_bg_color ) ) {
504 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] { background-color: {$form_bg_color} !important; }\n";
505 $css .= "#simpay-form-{$form_id} { background-color: {$form_bg_color} !important; }\n";
506 $css .= "#simpay-embedded-form-wrap-{$form_id} { background-color: {$form_bg_color} !important; }\n";
507 $css .= ".simpay-checkout-form--embedded[data-form-id=\"{$form_id}\"] { background-color: {$form_bg_color} !important; }\n";
508 // Apply to overlay modal content.
509 $css .= ".simpay-modal__content[data-form-id=\"{$form_id}\"] { background-color: {$form_bg_color} !important; }\n";
510 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content { background-color: {$form_bg_color} !important; }\n";
511 $css .= ".simpay-checkout-form--overlay[data-form-id=\"{$form_id}\"] { background-color: {$form_bg_color} !important; }\n";
512 }
513
514 // Add form padding.
515 $form_padding = Settings::get_setting( $form_id, 'form_padding', '' );
516 if ( '' !== $form_padding && Settings::setting_exists( $form_id, 'form_padding' ) ) {
517 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] { padding: {$form_padding}px !important; }\n";
518 $css .= "#simpay-form-{$form_id} { padding: {$form_padding}px !important; }\n";
519 $css .= "#simpay-embedded-form-wrap-{$form_id} { padding: {$form_padding}px !important; }\n";
520 $css .= ".simpay-checkout-form--embedded[data-form-id=\"{$form_id}\"] { padding: {$form_padding}px !important; }\n";
521 // Apply to overlay modal content.
522 $css .= ".simpay-modal__content[data-form-id=\"{$form_id}\"] { padding: {$form_padding}px !important; }\n";
523 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content { padding: {$form_padding}px !important; }\n";
524 $css .= ".simpay-checkout-form--overlay[data-form-id=\"{$form_id}\"] { padding: {$form_padding}px !important; }\n";
525 }
526
527 // Add form background color (input fields - text inputs only).
528 $bg_color = Settings::get_setting( $form_id, 'background_color' );
529 if ( ! empty( $bg_color ) ) {
530 // Target only text input types (text, email, number, tel, password), NOT selects, checkboxes, or textareas.
531 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='text'],\n";
532 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='email'],\n";
533 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='tel'],\n";
534 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='number'],\n";
535 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='password'],\n";
536 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='url'],\n";
537 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='search'] { background-color: {$bg_color} !important; }\n";
538
539 $css .= "#simpay-form-{$form_id} input[type='text'],\n";
540 $css .= "#simpay-form-{$form_id} input[type='email'],\n";
541 $css .= "#simpay-form-{$form_id} input[type='tel'],\n";
542 $css .= "#simpay-form-{$form_id} input[type='number'],\n";
543 $css .= "#simpay-form-{$form_id} input[type='password'],\n";
544 $css .= "#simpay-form-{$form_id} input[type='url'],\n";
545 $css .= "#simpay-form-{$form_id} input[type='search'] { background-color: {$bg_color} !important; }\n";
546
547 // Target Stripe Elements text inputs only (not selects or checkboxes).
548 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { background-color: {$bg_color} !important; }\n";
549 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .CodeInput { background-color: {$bg_color} !important; }\n";
550
551 // Explicitly ensure selects, checkboxes, and other non-text inputs don't get the background color.
552 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] select { background-color: transparent !important; }\n";
553 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] textarea { background-color: transparent !important; }\n";
554 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='checkbox'] { background-color: transparent !important; }\n";
555 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='radio'] { background-color: transparent !important; }\n";
556 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { background-color: transparent !important; }\n";
557 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .CheckboxInput { background-color: transparent !important; }\n";
558
559 $css .= "#simpay-form-{$form_id} select { background-color: transparent !important; }\n";
560 $css .= "#simpay-form-{$form_id} textarea { background-color: transparent !important; }\n";
561 $css .= "#simpay-form-{$form_id} input[type='checkbox'] { background-color: transparent !important; }\n";
562 $css .= "#simpay-form-{$form_id} input[type='radio'] { background-color: transparent !important; }\n";
563 }
564
565 // Add text color (general text color - applies to all text elements).
566 $text_color = Settings::get_setting( $form_id, 'text_color' );
567 if ( ! empty( $text_color ) ) {
568 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] { color: {$text_color} !important; }\n";
569 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] * { color: {$text_color} !important; }\n";
570 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-form-control { color: {$text_color} !important; }\n";
571 $css .= "#simpay-form-{$form_id} { color: {$text_color} !important; }\n";
572 $css .= "#simpay-form-{$form_id} * { color: {$text_color} !important; }\n";
573 $css .= "#simpay-form-{$form_id} .simpay-form-control { color: {$text_color} !important; }\n";
574
575 // Target Stripe Elements text colors - including dropdowns, inputs and labels.
576 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { color: {$text_color} !important; }\n";
577 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement select.Input { color: {$text_color} !important; }\n";
578 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { color: {$text_color} !important; }\n";
579 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Label { color: {$text_color} !important; }\n";
580 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement input { color: {$text_color} !important; }\n";
581 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement option { color: {$text_color} !important; }\n";
582 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement p { color: {$text_color} !important; }\n";
583 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement span { color: {$text_color} !important; }\n";
584 }
585
586 // Add label text color (overrides text color for labels).
587 $label_text_color = Settings::get_setting( $form_id, 'label_text_color' );
588 if ( ! empty( $label_text_color ) ) {
589 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] label { color: {$label_text_color} !important; }\n";
590 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-label { color: {$label_text_color} !important; }\n";
591 $css .= "#simpay-form-{$form_id} label { color: {$label_text_color} !important; }\n";
592 $css .= "#simpay-form-{$form_id} .simpay-label { color: {$label_text_color} !important; }\n";
593 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Label { color: {$label_text_color} !important; }\n";
594 }
595
596 // Add input text color (overrides text color for inputs).
597 $input_text_color = Settings::get_setting( $form_id, 'input_text_color' );
598 if ( ! empty( $input_text_color ) ) {
599 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input { color: {$input_text_color} !important; }\n";
600 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] select { color: {$input_text_color} !important; }\n";
601 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] textarea { color: {$input_text_color} !important; }\n";
602 $css .= "#simpay-form-{$form_id} input { color: {$input_text_color} !important; }\n";
603 $css .= "#simpay-form-{$form_id} select { color: {$input_text_color} !important; }\n";
604 $css .= "#simpay-form-{$form_id} textarea { color: {$input_text_color} !important; }\n";
605 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { color: {$input_text_color} !important; }\n";
606 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement select.Input { color: {$input_text_color} !important; }\n";
607 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { color: {$input_text_color} !important; }\n";
608 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement input { color: {$input_text_color} !important; }\n";
609 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement option { color: {$input_text_color} !important; }\n";
610 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-FauxInput { color: {$input_text_color} !important; }\n";
611 }
612
613 // Add border color.
614 $border_color = Settings::get_setting( $form_id, 'border_color' );
615 if ( ! empty( $border_color ) ) {
616 // Re-sanitize at output time as defense-in-depth against
617 // legacy/imported postmeta that may have bypassed save-time sanitization.
618 $border_color = self::sanitize_color_for_css( $border_color );
619 }
620 if ( ! empty( $border_color ) ) {
621 // Native HTML inputs use box-shadow for borders (base CSS sets border: 0).
622 $box_shadow_border = 'box-shadow: ' . self::build_box_shadow_border( $border_color ) . ' !important;';
623
624 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-form-control { border-color: {$border_color} !important; }\n";
625 $css .= "#simpay-form-{$form_id} .simpay-form-control { border-color: {$border_color} !important; }\n";
626 $css .= "#simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='text'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='email'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='tel'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='number'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='password'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='url'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='search'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control select, #simpay-form-{$form_id}.simpay-styled .simpay-form-control textarea { {$box_shadow_border} }\n";
627
628 // Add specific style for Stripe elements.
629 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement { border-color: {$border_color} !important; }\n";
630 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { {$box_shadow_border} border-color: {$border_color} !important; }\n";
631 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .CodeInput { {$box_shadow_border} border-color: {$border_color} !important; }\n";
632 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { border-color: {$border_color} !important; }\n";
633 }
634
635 // Add error border color.
636 $error_border_color = Settings::get_setting( $form_id, 'error_border_color' );
637 if ( ! empty( $error_border_color ) ) {
638 // Re-sanitize at output time as defense-in-depth against
639 // legacy/imported postmeta that may have bypassed save-time sanitization.
640 $error_border_color = self::sanitize_color_for_css( $error_border_color );
641 }
642 if ( ! empty( $error_border_color ) ) {
643 $error_box_shadow = 'box-shadow: ' . self::build_box_shadow_border( $error_border_color ) . ' !important;';
644 // Native inputs use box-shadow for border rendering (matches .simpay-input-error in SCSS).
645 // Also emit `border-color` so themes that paint real borders pick up the error color.
646 //
647 // Note on specificity: the base `border_color` rule above emits
648 // `box-shadow !important` under a (1,3,1)-specificity selector
649 // (`#simpay-form-{id}.simpay-styled .simpay-form-control input[type='text'], ...`).
650 // The lower-specificity error variants below would lose the cascade
651 // when `border_color` is also set, so we additionally emit a rule
652 // using the same (1,3,1) chain so the error color reliably wins
653 // (later rule of equal specificity wins under `!important`).
654 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input.simpay-input-error { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
655 $css .= "#simpay-form-{$form_id} input.simpay-input-error { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
656 $css .= "#simpay-form-{$form_id}.simpay-styled .simpay-form-control input.simpay-input-error { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
657 // Native HTML5 validation fallback. `:user-invalid` is preferred over
658 // `:invalid` because it only matches after user interaction, avoiding
659 // red borders on initial render. Scoped to this form to avoid clobbering
660 // globals or other forms on the page. The third rule matches the base
661 // border specificity (see note above).
662 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input:user-invalid, .simpay-form-wrap[data-form-id=\"{$form_id}\"] select:user-invalid, .simpay-form-wrap[data-form-id=\"{$form_id}\"] textarea:user-invalid { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
663 $css .= "#simpay-form-{$form_id} input:user-invalid, #simpay-form-{$form_id} select:user-invalid, #simpay-form-{$form_id} textarea:user-invalid { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
664 $css .= "#simpay-form-{$form_id}.simpay-styled .simpay-form-control input:user-invalid, #simpay-form-{$form_id}.simpay-styled .simpay-form-control select:user-invalid, #simpay-form-{$form_id}.simpay-styled .simpay-form-control textarea:user-invalid { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
665 // Stripe Elements invalid state.
666 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input--invalid { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
667 }
668
669 // Add error text color.
670 $error_text_color = Settings::get_setting( $form_id, 'error_text_color' );
671 if ( ! empty( $error_text_color ) ) {
672 // Re-sanitize at output time as defense-in-depth against
673 // legacy/imported postmeta that may have bypassed save-time sanitization.
674 $error_text_color = self::sanitize_color_for_css( $error_text_color );
675 }
676 if ( ! empty( $error_text_color ) ) {
677 // .simpay-errors is the class used on all inline field error message elements.
678 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-errors { color: {$error_text_color} !important; }\n";
679 $css .= "#simpay-form-{$form_id} .simpay-errors { color: {$error_text_color} !important; }\n";
680 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Error { color: {$error_text_color} !important; }\n";
681 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .ErrorMessage { color: {$error_text_color} !important; }\n";
682 }
683
684 // Add primary color (accent color - used for focus states, links, etc.).
685 $primary_color = Settings::get_setting( $form_id, 'primary_color' );
686 if ( ! empty( $primary_color ) ) {
687 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-form-control:focus { border-color: {$primary_color} !important; }\n";
688 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] a { color: {$primary_color} !important; }\n";
689 $css .= "#simpay-form-{$form_id} .simpay-form-control:focus { border-color: {$primary_color} !important; }\n";
690 $css .= "#simpay-form-{$form_id} a { color: {$primary_color} !important; }\n";
691
692 // Target Stripe Elements focus states.
693 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input:focus { border-color: {$primary_color} !important; }\n";
694 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement select.Input:focus { border-color: {$primary_color} !important; }\n";
695 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select:focus { border-color: {$primary_color} !important; }\n";
696 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input:focus { box-shadow: 0 0 0 1px {$primary_color}, 0 0 0 3px " . self::hex_to_rgba( $primary_color, 0.15 ) . ", 0 1px 2px rgba(0, 0, 0, 0.05) !important; }\n";
697 }
698
699 // Add form container border radius (separate from input/button border radius).
700 $form_border_radius = Settings::get_setting( $form_id, 'form_border_radius', '' );
701 if ( '' !== $form_border_radius && Settings::setting_exists( $form_id, 'form_border_radius' ) ) {
702 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] { border-radius: {$form_border_radius}px !important; }\n";
703 $css .= "#simpay-form-{$form_id} { border-radius: {$form_border_radius}px !important; }\n";
704 $css .= "#simpay-embedded-form-wrap-{$form_id} { border-radius: {$form_border_radius}px !important; }\n";
705 $css .= ".simpay-checkout-form--embedded[data-form-id=\"{$form_id}\"] { border-radius: {$form_border_radius}px !important; }\n";
706 // Apply to overlay form modal content.
707 $css .= ".simpay-modal__content[data-form-id=\"{$form_id}\"] { border-radius: {$form_border_radius}px !important; }\n";
708 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content { border-radius: {$form_border_radius}px !important; }\n";
709 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] { border-radius: {$form_border_radius}px !important; }\n";
710 }
711
712 // Add border radius for inputs, buttons, and form controls.
713 $border_radius = Settings::get_setting( $form_id, 'border_radius', '' );
714 if ( '' !== $border_radius && Settings::setting_exists( $form_id, 'border_radius' ) ) {
715 // For form controls.
716 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-form-control { border-radius: {$border_radius}px !important; }\n";
717 $css .= "#simpay-form-{$form_id} .simpay-form-control { border-radius: {$border_radius}px !important; }\n";
718 // Override .simpay-styled .simpay-form-control input/select/textarea border-radius, scoped to this form.
719 $css .= "#simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='text'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='email'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='tel'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='number'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='password'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='url'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='search'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control select, #simpay-form-{$form_id}.simpay-styled .simpay-form-control textarea { border-radius: {$border_radius}px !important; }\n";
720
721 // Target Stripe Elements border radius.
722 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { border-radius: {$border_radius}px !important; }\n";
723 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement select.Input { border-radius: {$border_radius}px !important; }\n";
724 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { border-radius: {$border_radius}px !important; }\n";
725
726 // Apply to buttons.
727 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-payment-btn { border-radius: {$border_radius}px !important; }\n";
728 $css .= "#simpay-form-{$form_id} .simpay-payment-btn { border-radius: {$border_radius}px !important; }\n";
729 $css .= "#simpay-form-{$form_id} .simpay-checkout-btn { border-radius: {$border_radius}px !important; }\n";
730 $css .= "#simpay-form-{$form_id} .simpay-apply-coupon { border-radius: {$border_radius}px !important; }\n";
731
732 // Target buttons with higher specificity to override WP Simple Pay defaults.
733 $css .= "body .simpay-form-wrap[data-form-id=\"{$form_id}\"] button.simpay-payment-btn { border-radius: {$border_radius}px !important; }\n";
734 $css .= "body #simpay-form-{$form_id} button.simpay-checkout-btn { border-radius: {$border_radius}px !important; }\n";
735 $css .= "body #simpay-form-{$form_id} button.simpay-apply-coupon { border-radius: {$border_radius}px !important; }\n";
736 }
737
738 // Add button background color.
739 $button_bg_color = Settings::get_raw_setting( $form_id, 'button_background_color' );
740 if ( ! empty( $button_bg_color ) ) {
741 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-payment-btn { background-color: {$button_bg_color} !important; border-color: {$button_bg_color} !important; }\n";
742 $css .= "#simpay-form-{$form_id} .simpay-payment-btn { background-color: {$button_bg_color} !important; border-color: {$button_bg_color} !important; }\n";
743 $css .= "#simpay-form-{$form_id} .simpay-checkout-btn { background-color: {$button_bg_color} !important; border-color: {$button_bg_color} !important; }\n";
744 $css .= "#simpay-form-{$form_id} .simpay-apply-coupon { background-color: {$button_bg_color} !important; border-color: {$button_bg_color} !important; }\n";
745 }
746
747 // Add button text color.
748 // Default to white when text_color is set, since the text_color wildcard
749 // (* selector) would otherwise override the button's white text from SCSS.
750 $button_text_color = Settings::get_raw_setting( $form_id, 'button_text_color' );
751 if ( empty( $button_text_color ) && ! empty( $text_color ) ) {
752 $button_text_color = '#ffffff';
753 }
754 if ( ! empty( $button_text_color ) ) {
755 // Target the button and all children (e.g. <span> inside <button>)
756 // to override the text_color wildcard (* selector).
757 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-payment-btn, .simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-payment-btn * { color: {$button_text_color} !important; }\n";
758 $css .= "#simpay-form-{$form_id} .simpay-payment-btn, #simpay-form-{$form_id} .simpay-payment-btn * { color: {$button_text_color} !important; }\n";
759 $css .= "#simpay-form-{$form_id} .simpay-checkout-btn, #simpay-form-{$form_id} .simpay-checkout-btn * { color: {$button_text_color} !important; }\n";
760 $css .= "#simpay-form-{$form_id} .simpay-apply-coupon, #simpay-form-{$form_id} .simpay-apply-coupon * { color: {$button_text_color} !important; }\n";
761 }
762
763 // Add button hover background color.
764 $button_hover_bg_color = Settings::get_raw_setting( $form_id, 'button_hover_background_color' );
765 if ( ! empty( $button_hover_bg_color ) ) {
766 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-payment-btn:hover { background-color: {$button_hover_bg_color} !important; border-color: {$button_hover_bg_color} !important; }\n";
767 $css .= "#simpay-form-{$form_id} .simpay-payment-btn:hover { background-color: {$button_hover_bg_color} !important; border-color: {$button_hover_bg_color} !important; }\n";
768 $css .= "#simpay-form-{$form_id} .simpay-checkout-btn:hover { background-color: {$button_hover_bg_color} !important; border-color: {$button_hover_bg_color} !important; }\n";
769 $css .= "#simpay-form-{$form_id} .simpay-apply-coupon:hover { background-color: {$button_hover_bg_color} !important; border-color: {$button_hover_bg_color} !important; }\n";
770 }
771
772 // Add font sizes.
773 $label_font_size = Settings::get_setting( $form_id, 'label_font_size' );
774 if ( ! empty( $label_font_size ) ) {
775 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] label { font-size: {$label_font_size}px !important; }\n";
776 $css .= "#simpay-form-{$form_id} label { font-size: {$label_font_size}px !important; }\n";
777 $css .= "#simpay-form-{$form_id} .simpay-label-wrap { font-size: {$label_font_size}px !important; }\n";
778 }
779
780 $input_font_size = Settings::get_setting( $form_id, 'input_font_size' );
781 if ( ! empty( $input_font_size ) ) {
782 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-form-control { font-size: {$input_font_size}px !important; }\n";
783 $css .= "#simpay-form-{$form_id} .simpay-form-control { font-size: {$input_font_size}px !important; }\n";
784 }
785
786 // Add font weights.
787 $label_font_weight = Settings::get_setting( $form_id, 'label_font_weight' );
788 if ( ! empty( $label_font_weight ) ) {
789 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] label.simpay-form-control { font-weight: {$label_font_weight} !important; }\n";
790 $css .= "#simpay-form-{$form_id} label.simpay-form-control { font-weight: {$label_font_weight} !important; }\n";
791 $css .= "#simpay-form-{$form_id} .simpay-label { font-weight: {$label_font_weight} !important; }\n";
792 $css .= "#simpay-form-{$form_id} .simpay-label-wrap { font-weight: {$label_font_weight} !important; }\n";
793 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-label-wrap { font-weight: {$label_font_weight} !important; }\n";
794 $css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] label { font-weight: {$label_font_weight} !important; }\n";
795 // Override .simpay-styled .simpay-form-control legend/label (font-weight: 600), scoped to this form.
796 $css .= "#simpay-form-{$form_id}.simpay-styled .simpay-form-control label, #simpay-form-{$form_id}.simpay-styled .simpay-form-control legend { font-weight: {$label_font_weight} !important; }\n";
797 }
798
799 // Add form title styling.
800 // Fall back to text_color if title_color is not explicitly set,
801 // since .simpay-embedded-heading is a sibling of .simpay-form-wrap
802 // and won't inherit the text_color wildcard rule.
803 $title_color = Settings::get_setting( $form_id, 'title_color' );
804 if ( empty( $title_color ) && ! empty( $text_color ) ) {
805 $title_color = $text_color;
806 }
807 if ( ! empty( $title_color ) ) {
808 // Embedded form selectors - direct heading with data attribute (added via JS).
809 $css .= ".simpay-embedded-heading[data-form-id=\"{$form_id}\"] .simpay-form-title { color: {$title_color} !important; }\n";
810 // Embedded form selectors - wrapper-based.
811 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-form-title { color: {$title_color} !important; }\n";
812 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-embedded-heading .simpay-form-title { color: {$title_color} !important; }\n";
813 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-form-title { color: {$title_color} !important; }\n";
814 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-embedded-heading .simpay-form-title { color: {$title_color} !important; }\n";
815 // Form element selectors.
816 $css .= "#simpay-form-{$form_id} .simpay-form-title { color: {$title_color} !important; }\n";
817 $css .= "[data-simpay-form-id=\"{$form_id}\"] .simpay-form-title { color: {$title_color} !important; }\n";
818 // Overlay/modal selectors.
819 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-form-title { color: {$title_color} !important; }\n";
820 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content .simpay-form-title { color: {$title_color} !important; }\n";
821 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__body .simpay-modal__content .simpay-form-title { color: {$title_color} !important; }\n";
822 }
823
824 $title_font_size = Settings::get_setting( $form_id, 'title_font_size' );
825 if ( ! empty( $title_font_size ) ) {
826 // Embedded form selectors - direct heading with data attribute (added via JS).
827 $css .= ".simpay-embedded-heading[data-form-id=\"{$form_id}\"] .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
828 // Embedded form selectors - wrapper-based.
829 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
830 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-embedded-heading .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
831 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
832 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-embedded-heading .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
833 // Form element selectors.
834 $css .= "#simpay-form-{$form_id} .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
835 $css .= "[data-simpay-form-id=\"{$form_id}\"] .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
836 // Overlay/modal selectors.
837 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
838 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
839 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__body .simpay-modal__content .simpay-form-title { font-size: {$title_font_size}px !important; }\n";
840 }
841
842 $title_font_weight = Settings::get_setting( $form_id, 'title_font_weight' );
843 if ( ! empty( $title_font_weight ) ) {
844 // Embedded form selectors - direct heading with data attribute (added via JS).
845 $css .= ".simpay-embedded-heading[data-form-id=\"{$form_id}\"] .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
846 // Embedded form selectors - wrapper-based.
847 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
848 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-embedded-heading .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
849 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
850 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-embedded-heading .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
851 // Form element selectors.
852 $css .= "#simpay-form-{$form_id} .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
853 $css .= "[data-simpay-form-id=\"{$form_id}\"] .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
854 // Overlay/modal selectors.
855 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
856 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
857 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__body .simpay-modal__content .simpay-form-title { font-weight: {$title_font_weight} !important; }\n";
858 }
859
860 // Add form description styling.
861 // Fall back to text_color if description_color is not explicitly set,
862 // since .simpay-embedded-heading is a sibling of .simpay-form-wrap.
863 $description_color = Settings::get_setting( $form_id, 'description_color' );
864 if ( empty( $description_color ) && ! empty( $text_color ) ) {
865 $description_color = $text_color;
866 }
867 if ( ! empty( $description_color ) ) {
868 // Embedded form selectors - direct heading with data attribute (added via JS).
869 $css .= ".simpay-embedded-heading[data-form-id=\"{$form_id}\"] .simpay-form-description { color: {$description_color} !important; }\n";
870 // Embedded form selectors - wrapper-based.
871 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-form-description { color: {$description_color} !important; }\n";
872 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-embedded-heading .simpay-form-description { color: {$description_color} !important; }\n";
873 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-form-description { color: {$description_color} !important; }\n";
874 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-embedded-heading .simpay-form-description { color: {$description_color} !important; }\n";
875 // Form element selectors.
876 $css .= "#simpay-form-{$form_id} .simpay-form-description { color: {$description_color} !important; }\n";
877 $css .= "[data-simpay-form-id=\"{$form_id}\"] .simpay-form-description { color: {$description_color} !important; }\n";
878 // Overlay/modal selectors.
879 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-form-description { color: {$description_color} !important; }\n";
880 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content .simpay-form-description { color: {$description_color} !important; }\n";
881 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__body .simpay-modal__content .simpay-form-description { color: {$description_color} !important; }\n";
882 }
883
884 $description_font_size = Settings::get_setting( $form_id, 'description_font_size' );
885 if ( ! empty( $description_font_size ) ) {
886 // Embedded form selectors - direct heading with data attribute (added via JS).
887 $css .= ".simpay-embedded-heading[data-form-id=\"{$form_id}\"] .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
888 // Embedded form selectors - wrapper-based.
889 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
890 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-embedded-heading .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
891 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
892 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-embedded-heading .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
893 // Form element selectors.
894 $css .= "#simpay-form-{$form_id} .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
895 $css .= "[data-simpay-form-id=\"{$form_id}\"] .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
896 // Overlay/modal selectors.
897 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
898 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
899 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__body .simpay-modal__content .simpay-form-description { font-size: {$description_font_size}px !important; }\n";
900 }
901
902 $description_font_weight = Settings::get_setting( $form_id, 'description_font_weight' );
903 if ( ! empty( $description_font_weight ) ) {
904 // Embedded form selectors - direct heading with data attribute (added via JS).
905 $css .= ".simpay-embedded-heading[data-form-id=\"{$form_id}\"] .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
906 // Embedded form selectors - wrapper-based.
907 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
908 $css .= "#simpay-embedded-form-wrap-{$form_id} .simpay-embedded-heading .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
909 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
910 $css .= "[data-id=\"simpay-form-{$form_id}-wrap\"] .simpay-embedded-heading .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
911 // Form element selectors.
912 $css .= "#simpay-form-{$form_id} .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
913 $css .= "[data-simpay-form-id=\"{$form_id}\"] .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
914 // Overlay/modal selectors.
915 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
916 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
917 $css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__body .simpay-modal__content .simpay-form-description { font-weight: {$description_font_weight} !important; }\n";
918 }
919
920 return $css;
921 }
922
923 /**
924 * Injects JavaScript to add data-form-id attribute to embedded headings and form wraps.
925 *
926 * This allows CSS selectors to target the heading and form wrap by form ID.
927 *
928 * @since 1.0.0
929 * @return void
930 */
931 public function inject_heading_data_attributes() {
932 if ( empty( self::$rendered_form_ids ) ) {
933 return;
934 }
935
936 $form_ids = array_unique( self::$rendered_form_ids );
937 $js = '<script type="text/javascript">';
938 $js .= '(function() {';
939
940 foreach ( $form_ids as $form_id ) {
941 // Add data-form-id to the form wrap.
942 $js .= sprintf(
943 '
944 var formWrap%d = document.getElementById("simpay-embedded-form-wrap-%d");
945 if (formWrap%d) {
946 formWrap%d.setAttribute("data-form-id", "%d");
947 var heading%d = formWrap%d.previousElementSibling;
948 if (heading%d && heading%d.classList && heading%d.classList.contains("simpay-embedded-heading")) {
949 heading%d.setAttribute("data-form-id", "%d");
950 }
951 }
952 // Also add to form wrap by class selector for other display types.
953 var formWraps%d = document.querySelectorAll(".simpay-form-wrap");
954 formWraps%d.forEach(function(wrap) {
955 var formId = wrap.getAttribute("data-id");
956 if (formId && formId.indexOf("simpay-form-%d-wrap") !== -1) {
957 wrap.setAttribute("data-form-id", "%d");
958 }
959 });
960 ',
961 $form_id,
962 $form_id,
963 $form_id,
964 $form_id,
965 $form_id,
966 $form_id,
967 $form_id,
968 $form_id,
969 $form_id,
970 $form_id,
971 $form_id,
972 $form_id,
973 $form_id,
974 $form_id,
975 $form_id,
976 $form_id
977 );
978 }
979
980 $js .= '})();';
981 $js .= '</script>';
982
983 echo $js; // WPCS: XSS okay.
984 }
985 }
986