This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,395 @@
<?php
/**
* Handles the Form Field wrapper block.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Form field wrapper block class.
*/
class GenerateBlocks_Block_Form_Field extends GenerateBlocks_Block {
/**
* Keep track of all blocks of this type on the page.
*
* @var array
*/
protected static $block_ids = [];
/**
* Block name.
*
* @var string
*/
public static $block_name = 'generateblocks-pro/form-field';
/**
* All allowed field types.
*
* @var array
*/
private static $allowed_types = [ 'text', 'email', 'url', 'tel', 'number', 'hidden', 'textarea', 'select', 'checkbox', 'radio', 'checkbox-group' ];
/**
* Field types that support same-value match validation.
*
* @var array
*/
private static $matchable_types = [ 'text', 'email', 'url', 'tel', 'number', 'textarea' ];
/**
* Allowed display condition operators.
*
* @var array
*/
private static $allowed_condition_operators = [ 'is', 'isnot', 'isempty', 'isnotempty' ];
/**
* Per-render instance number, set by GenerateBlocks_Pro_Form_Render before
* rendering each form.
*
* @var int
*/
private static $render_instance = 0;
/**
* Set the current render instance number.
*
* @param int $instance Instance number (1+) or 0 to reset.
*/
public static function set_render_instance( $instance ) {
self::$render_instance = (int) $instance;
}
/**
* Get the current render instance number.
*
* @return int
*/
public static function get_render_instance() {
return (int) self::$render_instance;
}
/**
* Suffix appended to every field ID for the current render.
*
* @return string
*/
public static function id_suffix() {
return self::$render_instance > 0 ? '-' . self::$render_instance : '';
}
/**
* Normalize a field type.
*
* @param mixed $field_type Raw field type.
* @return string
*/
public static function normalize_field_type( $field_type ) {
$field_type = is_string( $field_type ) ? $field_type : '';
if ( '' === $field_type ) {
return 'text';
}
if ( ! in_array( $field_type, self::$allowed_types, true ) ) {
return 'text';
}
return $field_type;
}
/**
* Whether a field type uses the wrapper as a fieldset.
*
* @param string $field_type Field type.
* @return bool
*/
public static function is_group_type( $field_type ) {
return in_array( self::normalize_field_type( $field_type ), [ 'radio', 'checkbox-group' ], true );
}
/**
* Get a field ID shared by label/control children.
*
* @param string $unique_id Wrapper unique ID.
* @param string $field_name Field name fallback.
* @return string
*/
public static function get_field_id( $unique_id, $field_name = '' ) {
$key = $unique_id ? $unique_id : sanitize_key( $field_name );
if ( '' === $key ) {
$key = 'field';
}
return 'gb-field-' . sanitize_html_class( $key ) . self::id_suffix();
}
/**
* Get a value from a WP_Block context object.
*
* @param mixed $block Block object.
* @param string $key Context key suffix.
* @param mixed $fallback Fallback.
* @return mixed
*/
public static function get_context_value( $block, $key, $fallback = '' ) {
$context_key = 'generateblocks-pro/form-field/' . $key;
if ( is_object( $block ) && isset( $block->context ) && is_array( $block->context ) && array_key_exists( $context_key, $block->context ) ) {
return $block->context[ $context_key ];
}
return $fallback;
}
/**
* Build classes for a styles-builder-aware block.
*
* @param string $base_class Base class.
* @param array $attributes Block attributes.
* @param array $extra Extra classes.
* @return string
*/
public static function get_block_classes( $base_class, $attributes, $extra = [] ) {
$classes = [ $base_class ];
if ( ! empty( $attributes['globalClasses'] ) && is_array( $attributes['globalClasses'] ) ) {
foreach ( $attributes['globalClasses'] as $class_name ) {
$class_name = sanitize_html_class( $class_name );
if ( $class_name ) {
$classes[] = $class_name;
}
}
}
if ( ! empty( $attributes['styles'] ) && is_array( $attributes['styles'] ) && ! empty( $attributes['uniqueId'] ) ) {
$classes[] = $base_class . '-' . sanitize_html_class( $attributes['uniqueId'] );
}
foreach ( $extra as $class_name ) {
$class_name = sanitize_html_class( $class_name );
if ( $class_name ) {
$classes[] = $class_name;
}
}
return implode( ' ', array_unique( array_filter( $classes ) ) );
}
/**
* Format an associative attribute array.
*
* @param array $attributes Attribute name/value pairs.
* @return string
*/
public static function format_attributes( $attributes ) {
$out = [];
foreach ( $attributes as $name => $value ) {
if ( true === $value ) {
$out[] = esc_attr( $name );
continue;
}
if ( false === $value || null === $value || is_array( $value ) || is_object( $value ) ) {
continue;
}
$out[] = sprintf( '%s="%s"', esc_attr( $name ), esc_attr( (string) $value ) );
}
return implode( ' ', $out );
}
/**
* Normalize a choice option for rendering.
*
* @param mixed $option Raw option attribute.
* @return array|null Normalized label/value pair or null for empty rows.
*/
public static function normalize_option( $option ) {
if ( ! is_array( $option ) ) {
return null;
}
$label = isset( $option['label'] ) ? (string) $option['label'] : '';
$value = isset( $option['value'] ) ? (string) $option['value'] : '';
if ( '' === $value && '' !== $label ) {
$value = $label;
}
if ( '' === $label && '' !== $value ) {
$label = $value;
}
if ( '' === $label && '' === $value ) {
return null;
}
return [
'label' => $label,
'value' => $value,
];
}
/**
* Sanitize display conditions before rendering them as frontend data attrs.
*
* Incomplete rows can be left behind while editing, especially if a trigger
* field is removed before the empty row is deleted. Those must not create
* `data-gb-conditions`, because an empty field name evaluates as empty on the
* frontend and causes a hidden-then-revealed layout shift.
*
* @param mixed $conditions Raw conditions attribute.
* @return array
*/
private static function sanitize_conditions( $conditions ) {
if ( ! is_array( $conditions ) ) {
return [];
}
$sanitized = [];
foreach ( $conditions as $condition ) {
if ( ! is_array( $condition ) ) {
continue;
}
$field = sanitize_key( $condition['field'] ?? '' );
$operator = sanitize_key( $condition['operator'] ?? '' );
if ( '' === $field || ! in_array( $operator, self::$allowed_condition_operators, true ) ) {
continue;
}
$sanitized[] = [
'field' => $field,
'operator' => $operator,
'value' => (string) ( $condition['value'] ?? '' ),
];
}
return $sanitized;
}
/**
* Resolve dynamic tags in a default value.
*
* @param string $default_value Raw default value.
* @param mixed $block Block instance.
* @return string
*/
public static function resolve_default_value( $default_value, $block ) {
$default_value = (string) $default_value;
if ( '' !== $default_value && false !== strpos( $default_value, '{{' ) && class_exists( 'GenerateBlocks_Register_Dynamic_Tag' ) ) {
$default_value = GenerateBlocks_Register_Dynamic_Tag::replace_tags(
$default_value,
[],
$block
);
}
return (string) $default_value;
}
/**
* Render the Form Field wrapper block.
*
* @param array $attributes The block attributes.
* @param string $block_content The block content.
* @param WP_Block $block The block instance.
* @return string The modified block content.
*/
public static function render_block( $attributes, $block_content, $block ) {
unset( $block );
$block_content = generateblocks_maybe_add_block_css(
$block_content,
[
'class_name' => __CLASS__,
'attributes' => $attributes,
'block_ids' => self::$block_ids,
]
);
$field_type = self::normalize_field_type( $attributes['fieldType'] ?? '' );
if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) {
return $block_content;
}
$processor = new WP_HTML_Tag_Processor( $block_content );
$tag_name = self::is_group_type( $field_type ) ? 'fieldset' : 'div';
if ( ! $processor->next_tag( $tag_name ) ) {
return $block_content;
}
$modifier_map = [
'checkbox' => 'gb-form-field--checkbox',
'radio' => 'gb-form-field--radio',
'checkbox-group' => 'gb-form-field--checkbox-group',
];
if ( isset( $modifier_map[ $field_type ] ) ) {
$processor->add_class( $modifier_map[ $field_type ] );
}
// The wrapper is a native <fieldset>, whose implicit "group" role does not
// support aria-required (Lighthouse/axe: aria-allowed-attr). The required
// state is carried by the native `required` attribute on the radio inputs;
// checkbox groups have no native per-input required, so they validate via
// data-gb-required + JS. Either way the <fieldset> needs no aria-required.
if ( 'checkbox-group' === $field_type && ! empty( $attributes['isRequired'] ) ) {
$processor->set_attribute( 'data-gb-required', 'true' );
}
$field_name = sanitize_key( $attributes['fieldName'] ?? '' );
$matches_field = sanitize_key( $attributes['matchesField'] ?? '' );
if (
$field_name &&
$matches_field &&
$field_name !== $matches_field &&
in_array( $field_type, self::$matchable_types, true )
) {
$processor->set_attribute( 'data-gb-field-name', $field_name );
$processor->set_attribute( 'data-gb-matches-field', $matches_field );
$processor->set_attribute(
'data-gb-match-message',
__( 'The matching fields must have the same value.', 'generateblocks-pro' )
);
$processor->set_attribute(
'data-gb-match-message-with-label',
/* translators: %1$s: source field label, %2$s: target field label. */
__( 'Please make sure "%1$s" matches "%2$s".', 'generateblocks-pro' )
);
$processor->set_attribute(
'data-gb-match-message-with-target',
/* translators: %s: target field label. */
__( 'This field must match "%s".', 'generateblocks-pro' )
);
}
$conditions = self::sanitize_conditions( $attributes['conditions'] ?? [] );
if ( ! empty( $conditions ) ) {
$processor->set_attribute( 'data-gb-conditions', wp_json_encode( $conditions ) );
$processor->set_attribute( 'hidden', true );
}
return $processor->get_updated_html();
}
}
@@ -0,0 +1,12 @@
<?php
/**
* Form field block loader.
*
* @package GenerateBlocksPro
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/class-form-field.php';