| @@ -8,10 +8,13 @@ | ||
| 8 | 8 | |
| 9 | 9 | namespace SRFM\Inc\Fields; |
| 10 | 10 | |
| 11 | 11 | require SRFM_DIR . 'modules/gutenberg/classes/class-spec-gb-helper.php'; |
| 12 | + | |
| 12 | 13 | use Spec_Gb_Helper; |
| 14 | +use SRFM\Inc\Compatibility\Multilingual\String_Translator; | |
| 13 | 15 | use SRFM\Inc\Helper; |
| 16 | +use SRFM\Inc\Smart_Tags; | |
| 14 | 17 | |
| 15 | 18 | if ( ! defined( 'ABSPATH' ) ) { |
| 16 | 19 | exit; // Exit if accessed directly. |
| 17 | 20 | } |
| @@ -21,8 +24,15 @@ | ||
| 21 | 24 | * |
| 22 | 25 | * @since 0.0.1 |
| 23 | 26 | */ |
| 24 | 27 | class Dropdown_Markup extends Base { |
| 28 | + /** | |
| 29 | + * Unique instance identifier for this dropdown. | |
| 30 | + * | |
| 31 | + * @var string | |
| 32 | + * @since 1.10.0 | |
| 33 | + */ | |
| 34 | + protected $unique_slug; | |
| 25 | 35 | |
| 26 | 36 | /** |
| 27 | 37 | * Stores the multi select attribute value. |
| 28 | 38 | * |
| @@ -39,8 +49,32 @@ | ||
| 39 | 49 | */ |
| 40 | 50 | protected $search_attr; |
| 41 | 51 | |
| 42 | 52 | /** |
| 53 | + * Flag indicating if the value should be shown. | |
| 54 | + * | |
| 55 | + * @var bool | |
| 56 | + * @since 1.5.0 | |
| 57 | + */ | |
| 58 | + protected $show_values; | |
| 59 | + | |
| 60 | + /** | |
| 61 | + * Array of preselected option indices. | |
| 62 | + * | |
| 63 | + * @var array | |
| 64 | + * @since 2.3.0 | |
| 65 | + */ | |
| 66 | + protected $preselected_options; | |
| 67 | + | |
| 68 | + /** | |
| 69 | + * Static counter for generating unique dropdown instances. | |
| 70 | + * | |
| 71 | + * @var int | |
| 72 | + * @since 1.10.0 | |
| 73 | + */ | |
| 74 | + private static $instance_counter = 0; | |
| 75 | + | |
| 76 | + /** | |
| 43 | 77 | * Initialize the properties based on block attributes. |
| 44 | 78 | * |
| 45 | 79 | * @param array<mixed> $attributes Block attributes. |
| 46 | 80 | * @since 0.0.2 |
| @@ -45,19 +79,45 @@ | ||
| 45 | 79 | * @param array<mixed> $attributes Block attributes. |
| 46 | 80 | * @since 0.0.2 |
| 47 | 81 | */ |
| 48 | 82 | public function __construct( $attributes ) { |
| 83 | + $this->slug = 'dropdown'; | |
| 49 | 84 | $this->set_properties( $attributes ); |
| 50 | 85 | $this->set_input_label( __( 'Dropdown', 'sureforms' ) ); |
| 51 | 86 | $this->set_error_msg( $attributes, 'srfm_dropdown_block_required_text' ); |
| 52 | - $this->slug = 'dropdown'; | |
| 53 | - $this->multi_select_attr = ! empty( $attributes['multiSelect'] ) ? 'true' : 'false'; | |
| 54 | - $this->search_attr = ! empty( $attributes['searchable'] ) ? 'true' : 'false'; | |
| 87 | + $this->multi_select_attr = ! empty( $attributes['multiSelect'] ) ? 'true' : 'false'; | |
| 88 | + $this->search_attr = ! empty( $attributes['searchable'] ) ? 'true' : 'false'; | |
| 89 | + $this->preselected_options = $attributes['preselectedOptions'] ?? []; | |
| 90 | + $this->resolve_dynamic_default( $attributes ); | |
| 55 | 91 | $this->set_markup_properties(); |
| 56 | 92 | $this->set_aria_described_by(); |
| 93 | + | |
| 94 | + // Store the original placeholder before set_label_as_placeholder modifies placeholder_attr. | |
| 95 | + $original_placeholder = $this->placeholder; | |
| 57 | 96 | $this->set_label_as_placeholder( $this->input_label ); |
| 58 | - $this->placeholder = ! empty( $this->placeholder_attr ) ? $this->label : __( 'Select an option', 'sureforms' ); | |
| 59 | 97 | |
| 98 | + // For dropdowns, use the appropriate placeholder text: | |
| 99 | + // 1. If label is being used as placeholder (label_markup is empty), use the label. | |
| 100 | + // 2. Otherwise, use the block's placeholder value. | |
| 101 | + if ( empty( $this->label_markup ) ) { | |
| 102 | + $this->placeholder = $this->label; | |
| 103 | + } elseif ( ! empty( $original_placeholder ) ) { | |
| 104 | + $this->placeholder = $original_placeholder; | |
| 105 | + } | |
| 106 | + | |
| 107 | + // Translate the default placeholder text for frontend display. Routed through | |
| 108 | + // the multilingual provider (not just the textdomain) so it is translated on | |
| 109 | + // WPML/Polylang sites too — the default placeholder is not serialized into | |
| 110 | + // block markup, so it never reaches the per-form string package. | |
| 111 | + if ( 'Select an option' === $this->placeholder ) { | |
| 112 | + $this->placeholder = String_Translator::get_instance()->translate_validation_message( 'srfm_dropdown_placeholder', __( 'Select an option', 'sureforms' ) ); | |
| 113 | + } | |
| 114 | + | |
| 115 | + $this->show_values = apply_filters( 'srfm_show_options_values', false, $attributes['showValues'] ?? false ); | |
| 116 | + | |
| 117 | + // Generate unique instance identifier. | |
| 118 | + self::$instance_counter++; | |
| 119 | + $this->unique_slug = $this->slug . '-' . self::$instance_counter; | |
| 60 | 120 | } |
| 61 | 121 | |
| 62 | 122 | /** |
| 63 | 123 | * Render the sureforms dropdown classic styling |
| @@ -62,33 +122,51 @@ | ||
| 62 | 122 | /** |
| 63 | 123 | * Render the sureforms dropdown classic styling |
| 64 | 124 | * |
| 65 | 125 | * @since 0.0.2 |
| 66 | - * @return string|boolean | |
| 126 | + * @return string|bool | |
| 67 | 127 | */ |
| 68 | 128 | public function markup() { |
| 129 | + $this->class_name = $this->get_field_classes(); | |
| 130 | + | |
| 69 | 131 | ob_start(); ?> |
| 70 | - <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>" class="srfm-block-single srfm-block srfm-<?php echo esc_attr( $this->slug ); ?>-block srf-<?php echo esc_attr( $this->slug ); ?>-<?php echo esc_attr( $this->block_id ); ?>-block<?php echo esc_attr( $this->block_width ); ?><?php echo esc_attr( $this->class_name ); ?> <?php echo esc_attr( $this->conditional_class ); ?>"> | |
| 132 | + <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>" class="<?php echo esc_attr( $this->class_name ); ?>"> | |
| 71 | 133 | <fieldset> |
| 72 | - <input class="srfm-input-<?php echo esc_attr( $this->slug ); ?>-hidden" aria-required="<?php echo esc_attr( $this->aria_require_attr ); ?>" name="srfm-<?php echo esc_attr( $this->slug ); ?>-<?php echo esc_attr( $this->block_id ); ?><?php echo esc_attr( $this->field_name ); ?>" type="hidden" value=""/> | |
| 73 | - <legend> | |
| 134 | + <input class="srfm-input-<?php echo esc_attr( $this->slug ); ?>-hidden" data-required="<?php echo esc_attr( $this->data_require_attr ); ?>" aria-required="<?php echo esc_attr( $this->data_require_attr ); ?>" <?php echo wp_kses_post( $this->data_attribute_markup() ); ?> name="srfm-<?php echo esc_attr( $this->unique_slug ); ?>-<?php echo esc_attr( $this->block_id ); ?><?php echo esc_attr( $this->field_name ); ?>" type="hidden" value=""/> | |
| 135 | + <legend class="srfm-block-legend"> | |
| 74 | 136 | <?php echo wp_kses_post( $this->label_markup ); ?> |
| 75 | - <?php echo wp_kses_post( $this->help_markup ); ?> | |
| 76 | 137 | </legend> |
| 138 | + <?php echo wp_kses_post( $this->help_markup ); ?> | |
| 77 | 139 | <div class="srfm-block-wrap srfm-dropdown-common-wrap"> |
| 78 | 140 | <?php |
| 79 | 141 | if ( is_array( $this->options ) ) { |
| 80 | 142 | ?> |
| 81 | - <select class="srfm-dropdown-common srfm-<?php echo esc_attr( $this->slug ); ?>-input" | |
| 143 | + <select | |
| 144 | + class="srfm-dropdown-common srfm-<?php echo esc_attr( $this->slug ); ?>-input" | |
| 82 | 145 | <?php echo ! empty( $this->aria_described_by ) ? "aria-describedby='" . esc_attr( trim( $this->aria_described_by ) ) . "'" : ''; ?> |
| 83 | - aria-required="<?php echo esc_attr( $this->aria_require_attr ); ?>" name="srfm-<?php echo esc_attr( $this->slug ); ?>-<?php echo esc_attr( $this->block_id ); ?><?php echo esc_attr( $this->field_name ); ?>" data-multiple="<?php echo esc_attr( $this->multi_select_attr ); ?>" data-searchable="<?php echo esc_attr( $this->search_attr ); ?>" tabindex="0" aria-hidden="true"> | |
| 84 | - <option class="srfm-dropdown-placeholder" value="" disabled selected><?php echo esc_html( $this->placeholder ); ?></option> | |
| 85 | - <?php foreach ( $this->options as $option ) { ?> | |
| 146 | + data-required="<?php echo esc_attr( $this->data_require_attr ); ?>" aria-required="<?php echo esc_attr( $this->data_require_attr ); ?>" <?php echo wp_kses_post( $this->data_attribute_markup() ); ?> name="srfm-<?php echo esc_attr( $this->unique_slug ); ?>-<?php echo esc_attr( $this->block_id ); ?><?php echo esc_attr( $this->field_name ); ?>" data-multiple="<?php echo esc_attr( $this->multi_select_attr ); ?>" data-searchable="<?php echo esc_attr( $this->search_attr ); ?>" | |
| 147 | + <?php | |
| 148 | + if ( ! empty( $this->preselected_options ) ) { | |
| 149 | + $preselected_labels = array_map( | |
| 150 | + function( $i ) { | |
| 151 | + $option = $this->options[ $i ] ?? []; | |
| 152 | + return is_array( $option ) ? ( $option['label'] ?? '' ) : ''; | |
| 153 | + }, | |
| 154 | + $this->preselected_options | |
| 155 | + ); | |
| 156 | + $json_encoded = wp_json_encode( $preselected_labels ); | |
| 157 | + echo 'data-preselected="' . esc_attr( false !== $json_encoded ? $json_encoded : '' ) . '"'; | |
| 158 | + } | |
| 159 | + ?> | |
| 160 | + tabindex="0" aria-hidden="true"> | |
| 161 | + <option class="srfm-dropdown-placeholder" value="" disabled <?php echo empty( $this->preselected_options ) ? 'selected' : ''; ?>><?php echo esc_html( $this->placeholder ); ?></option> | |
| 162 | + <?php foreach ( $this->options as $i => $option ) { ?> | |
| 86 | 163 | <?php |
| 87 | - $icon_svg = Spec_Gb_Helper::render_svg_html( isset( $option['icon'] ) ? $option['icon'] : '', true ); | |
| 164 | + $icon_svg = Spec_Gb_Helper::render_svg_html( $option['icon'] ?? '', true ); | |
| 88 | 165 | $escaped_icon_svg = htmlspecialchars( Helper::get_string_value( $icon_svg ), ENT_QUOTES, 'UTF-8' ); |
| 166 | + $is_preselected = is_array( $this->preselected_options ) && in_array( $i, $this->preselected_options, true ); | |
| 89 | 167 | ?> |
| 90 | - <option value="<?php echo isset( $option['label'] ) ? esc_html( $option['label'] ) : ''; ?>" data-icon="<?php echo ! empty( $escaped_icon_svg ) ? esc_attr( $escaped_icon_svg ) : ''; ?>"><?php echo isset( $option['label'] ) ? esc_html( $option['label'] ) : ''; ?></option> | |
| 168 | + <option value="<?php echo isset( $option['label'] ) ? esc_html( $option['label'] ) : ''; ?>" data-icon="<?php echo ! empty( $escaped_icon_svg ) ? esc_attr( $escaped_icon_svg ) : ''; ?>" <?php echo $this->show_values && isset( $option['value'] ) ? 'option-value="' . esc_attr( $option['value'] ) . '"' : ''; ?> <?php echo $is_preselected ? 'selected' : ''; ?>><?php echo isset( $option['label'] ) ? esc_html( $option['label'] ) : ''; ?></option> | |
| 91 | 169 | <?php |
| 92 | 170 | } |
| 93 | 171 | ?> |
| 94 | 172 | </select> |
| @@ -99,7 +177,132 @@ | ||
| 99 | 177 | </div> |
| 100 | 178 | </fieldset> |
| 101 | 179 | </div> |
| 102 | 180 | <?php |
| 103 | - return ob_get_clean(); | |
| 181 | + $markup = ob_get_clean(); | |
| 182 | + | |
| 183 | + return apply_filters( | |
| 184 | + 'srfm_block_field_markup', | |
| 185 | + $markup, | |
| 186 | + [ | |
| 187 | + 'slug' => $this->slug, | |
| 188 | + 'field_name' => $this->field_name, | |
| 189 | + 'is_editing' => $this->is_editing, | |
| 190 | + 'attributes' => $this->attributes, | |
| 191 | + ] | |
| 192 | + ); | |
| 193 | + } | |
| 194 | + | |
| 195 | + /** | |
| 196 | + * Resolve dynamic default value from smart tags and override preselected options. | |
| 197 | + * | |
| 198 | + * Single-select dropdowns match the resolved value as a single trimmed | |
| 199 | + * string, so labels that legitimately contain delimiter characters | |
| 200 | + * (commas, pipes, etc.) still match. | |
| 201 | + * | |
| 202 | + * Multi-select dropdowns split the resolved value on the pipe character | |
| 203 | + * (`|`) and match every segment (for example "{get_input:a}|{get_input:b}" | |
| 204 | + * resolving to "Red|Blue", or a single `{get_input:colors}` smart tag with | |
| 205 | + * URL `?colors=Red|Blue`). The same pipe split applies to multi-character | |
| 206 | + * resolved values from `{get_input:...}` and `{get_cookie:...}`, so cookie | |
| 207 | + * payloads that contain pipes are treated as multi-value in multi-select | |
| 208 | + * mode. | |
| 209 | + * | |
| 210 | + * When the `Add Numeric Values to Options` (showValues) setting is on, | |
| 211 | + * each segment is also compared against the option's `value`. | |
| 212 | + * | |
| 213 | + * @param array<mixed> $attributes Block attributes. | |
| 214 | + * @since 2.8.1 | |
| 215 | + * @since 2.10.0 Added multi-select (pipe-delimited) value matching. | |
| 216 | + * @return void | |
| 217 | + */ | |
| 218 | + protected function resolve_dynamic_default( $attributes ) { | |
| 219 | + // Coalesce first so blocks saved before this attribute existed (pre-2.10.0) | |
| 220 | + // don't trigger an "Undefined array key" warning on every frontend render. | |
| 221 | + $raw_default = $attributes['dynamicDefaultValue'] ?? ''; | |
| 222 | + $dynamic_default = is_string( $raw_default ) ? $raw_default : ''; | |
| 223 | + if ( empty( $dynamic_default ) ) { | |
| 224 | + return; | |
| 225 | + } | |
| 226 | + | |
| 227 | + $smart_tags = new Smart_Tags(); | |
| 228 | + $resolved = $smart_tags->process_smart_tags( $dynamic_default ); | |
| 229 | + | |
| 230 | + if ( empty( $resolved ) || ! is_string( $resolved ) || ! is_array( $this->options ) ) { | |
| 231 | + return; | |
| 232 | + } | |
| 233 | + | |
| 234 | + // START: multi-value dynamic default resolution. | |
| 235 | + $is_multi_select = ! empty( $attributes['multiSelect'] ); | |
| 236 | + | |
| 237 | + if ( $is_multi_select ) { | |
| 238 | + $segments = array_filter( | |
| 239 | + array_map( 'trim', explode( '|', $resolved ) ), | |
| 240 | + static function ( $segment ) { | |
| 241 | + return '' !== $segment; | |
| 242 | + } | |
| 243 | + ); | |
| 244 | + // Cap segments to avoid pathological URLs (e.g. ?colors=|||||...) producing thousands of comparisons. | |
| 245 | + $segments = array_slice( $segments, 0, 50 ); | |
| 246 | + } else { | |
| 247 | + $trimmed = trim( $resolved ); | |
| 248 | + $segments = '' === $trimmed ? [] : [ $trimmed ]; | |
| 249 | + } | |
| 250 | + | |
| 251 | + if ( empty( $segments ) ) { | |
| 252 | + return; | |
| 253 | + } | |
| 254 | + | |
| 255 | + $match_value_too = ! empty( $attributes['showValues'] ); | |
| 256 | + $matching_indices = []; | |
| 257 | + | |
| 258 | + foreach ( $segments as $segment ) { | |
| 259 | + foreach ( $this->options as $i => $option ) { | |
| 260 | + if ( ! is_array( $option ) || in_array( $i, $matching_indices, true ) ) { | |
| 261 | + continue; | |
| 262 | + } | |
| 263 | + | |
| 264 | + $option_label = isset( $option['label'] ) && is_scalar( $option['label'] ) ? (string) $option['label'] : ''; | |
| 265 | + $raw_value = $option['value'] ?? ''; | |
| 266 | + $option_value = $match_value_too && is_scalar( $raw_value ) ? (string) $raw_value : ''; | |
| 267 | + | |
| 268 | + $is_match = ( '' !== $option_label && strcasecmp( $option_label, $segment ) === 0 ) | |
| 269 | + || ( $match_value_too && '' !== $option_value && strcasecmp( $option_value, $segment ) === 0 ); | |
| 270 | + | |
| 271 | + if ( $is_match ) { | |
| 272 | + $matching_indices[] = $i; | |
| 273 | + if ( ! $is_multi_select ) { | |
| 274 | + break 2; | |
| 275 | + } | |
| 276 | + break; | |
| 277 | + } | |
| 278 | + } | |
| 279 | + } | |
| 280 | + | |
| 281 | + if ( ! empty( $matching_indices ) ) { | |
| 282 | + $this->preselected_options = $is_multi_select ? $matching_indices : [ $matching_indices[0] ]; | |
| 283 | + } | |
| 284 | + // END: multi-value dynamic default resolution. | |
| 285 | + } | |
| 286 | + | |
| 287 | + /** | |
| 288 | + * Data attribute markup for min and max value | |
| 289 | + * | |
| 290 | + * @since 0.0.13 | |
| 291 | + * @return string | |
| 292 | + */ | |
| 293 | + protected function data_attribute_markup() { | |
| 294 | + $data_attr = ''; | |
| 295 | + if ( 'false' === $this->multi_select_attr ) { | |
| 296 | + return ''; | |
| 297 | + } | |
| 298 | + | |
| 299 | + if ( $this->min_selection ) { | |
| 300 | + $data_attr .= 'data-min-selection="' . esc_attr( $this->min_selection ) . '"'; | |
| 301 | + } | |
| 302 | + if ( $this->max_selection ) { | |
| 303 | + $data_attr .= 'data-max-selection="' . esc_attr( $this->max_selection ) . '"'; | |
| 304 | + } | |
| 305 | + | |
| 306 | + return $data_attr; | |
| 104 | 307 | } |
| 105 | 308 | } |