| 1 |
<?php |
| 2 |
/** |
| 3 |
* Merchant Settings Renderer. |
| 4 |
* |
| 5 |
* Handles all rendering of module settings panels, including |
| 6 |
* field wrappers, titles, descriptions, and inner field content. |
| 7 |
* Extracted from {@see Merchant_Admin_Options}. |
| 8 |
* |
| 9 |
* @package Merchant |
| 10 |
* @since 1.9.3 |
| 11 |
*/ |
| 12 |
|
| 13 |
if ( ! defined( 'ABSPATH' ) ) { |
| 14 |
exit; |
| 15 |
} |
| 16 |
|
| 17 |
/** |
| 18 |
* Merchant_Settings_Renderer |
| 19 |
* |
| 20 |
* Renders module settings panels and their individual fields. |
| 21 |
* Delegates field-type rendering to the {@see Merchant_Field_Registry}. |
| 22 |
* |
| 23 |
* @since 1.9.3 |
| 24 |
*/ |
| 25 |
class Merchant_Settings_Renderer { |
| 26 |
|
| 27 |
/** |
| 28 |
* Create and render a module settings panel. |
| 29 |
* |
| 30 |
* Processes the settings definition array, saves any submitted |
| 31 |
* form data, and renders the full settings panel HTML including |
| 32 |
* title, field wrappers, and individual fields. |
| 33 |
* |
| 34 |
* @since 1.0 |
| 35 |
* |
| 36 |
* @param array<string, mixed> $settings { |
| 37 |
* Module settings configuration. |
| 38 |
* |
| 39 |
* @type string $module Module ID. |
| 40 |
* @type string $title Panel title. |
| 41 |
* @type string $subtitle Panel subtitle. |
| 42 |
* @type array $fields Array of field definition arrays. |
| 43 |
* } |
| 44 |
* |
| 45 |
* @return void |
| 46 |
*/ |
| 47 |
public static function create( $settings ) { |
| 48 |
$module_id = ( isset( $_GET['module'] ) ) ? sanitize_text_field( wp_unslash( $_GET['module'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 49 |
|
| 50 |
/** |
| 51 |
* Hook: merchant_module_settings |
| 52 |
* |
| 53 |
* @param array $settings Module settings. |
| 54 |
* @param string $module_id Module ID. |
| 55 |
* |
| 56 |
* @since 1.0 |
| 57 |
*/ |
| 58 |
$settings = apply_filters( 'merchant_module_settings', $settings, $module_id ); |
| 59 |
|
| 60 |
Merchant_Settings_Saver::save_options( $settings ); |
| 61 |
|
| 62 |
$options = get_option( 'merchant', array() ); |
| 63 |
?> |
| 64 |
<div class="merchant-module-page-settings"> |
| 65 |
<div class="merchant-module-page-setting-box"> |
| 66 |
<?php if ( ! empty( $settings['title'] ) ) : ?> |
| 67 |
<div class="merchant-module-page-setting-title"> |
| 68 |
<?php echo esc_html( $settings['title'] ); ?> |
| 69 |
<?php if ( ! empty( $settings['subtitle'] ) ) : ?> |
| 70 |
<div class="merchant-module-page-setting-subtitle"><?php echo esc_html( $settings['subtitle'] ); ?></div> |
| 71 |
<?php endif; ?> |
| 72 |
</div> |
| 73 |
<?php endif; ?> |
| 74 |
<div class="merchant-module-page-setting-fields"> |
| 75 |
<?php self::render_fields( $settings, $options, $module_id ); ?> |
| 76 |
</div> |
| 77 |
</div> |
| 78 |
</div> |
| 79 |
<?php |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* Render all fields in a module settings panel. |
| 84 |
* |
| 85 |
* Iterates the field definitions, resolves each field's saved |
| 86 |
* value, and delegates rendering to either {@see field()} or |
| 87 |
* {@see disabled_field()} depending on Pro status. |
| 88 |
* |
| 89 |
* @since 1.9.3 |
| 90 |
* |
| 91 |
* @param array<string, mixed> $settings Module settings with 'module' and 'fields' keys. |
| 92 |
* @param array<string, mixed> $options All saved merchant options. |
| 93 |
* @param string $module_id The current module ID. |
| 94 |
* |
| 95 |
* @return void |
| 96 |
*/ |
| 97 |
private static function render_fields( $settings, $options, $module_id ) { |
| 98 |
if ( empty( $settings['fields'] ) ) { |
| 99 |
return; |
| 100 |
} |
| 101 |
|
| 102 |
$current_module = Merchant_Admin_Modules::get_module_info( $settings['module'] ); |
| 103 |
$is_pro_module = ! merchant_is_pro_active() && isset( $current_module['pro'] ) && $current_module['pro'] === true; |
| 104 |
|
| 105 |
foreach ( $settings['fields'] as $field ) { |
| 106 |
$value = $field['default'] ?? null; |
| 107 |
|
| 108 |
if ( isset( $field['id'] ) && isset( $options[ $settings['module'] ][ $field['id'] ] ) ) { |
| 109 |
$value = $options[ $settings['module'] ][ $field['id'] ]; |
| 110 |
} |
| 111 |
|
| 112 |
$is_pro_field = ! merchant_is_pro_active() && isset( $field['pro'] ) && $field['pro'] === true; |
| 113 |
|
| 114 |
// A locked section or field is disabled without the Pro badge: the |
| 115 |
// gate behind it is a plan tier, not the free-vs-pro split. |
| 116 |
$is_locked = ! empty( $settings['locked'] ) || ! empty( $field['locked'] ); |
| 117 |
|
| 118 |
if ( $is_pro_module || $is_pro_field || $is_locked ) { |
| 119 |
self::disabled_field( $field, $value ); |
| 120 |
} else { |
| 121 |
self::field( $field, $value, $module_id ); |
| 122 |
} |
| 123 |
} |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Render a single field. |
| 128 |
* |
| 129 |
* Outputs the full field markup including the wrapper div, |
| 130 |
* title, description, and the field's inner HTML delegated |
| 131 |
* to {@see Merchant_Field_Registry}. |
| 132 |
* |
| 133 |
* @since 1.0 |
| 134 |
* |
| 135 |
* @param array<string, mixed> $settings The field configuration array. |
| 136 |
* @param mixed $value The current saved value for this field. |
| 137 |
* @param string $module_id The module ID (used for nested field contexts). |
| 138 |
* |
| 139 |
* @return void |
| 140 |
*/ |
| 141 |
public static function field( $settings, $value, $module_id = '' ) { |
| 142 |
if ( empty( $settings['type'] ) ) { |
| 143 |
return; |
| 144 |
} |
| 145 |
|
| 146 |
$type = $settings['type']; |
| 147 |
$id = $settings['id'] ?? ''; |
| 148 |
$is_upsell = ! merchant_is_pro_active() && isset( $settings['pro'] ) && $settings['pro'] === true; |
| 149 |
$value = self::resolve_field_value( $settings, $value, $module_id ); |
| 150 |
|
| 151 |
self::render_field_wrapper_open( $settings, $value, $module_id, $type, $id ); |
| 152 |
self::render_field_title( $settings, $id, $is_upsell ); |
| 153 |
|
| 154 |
echo '<div class="merchant-module-page-setting-field-inner merchant-field-' . esc_attr( $id ) . '">'; |
| 155 |
self::render_field_inner( $type, $settings, $value, $module_id ); |
| 156 |
echo '</div>'; |
| 157 |
|
| 158 |
self::render_field_description( $settings, $value, $module_id ); |
| 159 |
echo '</div>'; |
| 160 |
} |
| 161 |
|
| 162 |
/** |
| 163 |
* Resolve the default value for a field based on its type. |
| 164 |
* |
| 165 |
* Handles type-specific fallback logic for checkbox_multiple, |
| 166 |
* text, and url field types. |
| 167 |
* |
| 168 |
* @since 1.9.3 |
| 169 |
* |
| 170 |
* @param array<string, mixed> $settings The field configuration array. |
| 171 |
* @param mixed $value The current raw value. |
| 172 |
* @param string $module_id The module ID. |
| 173 |
* |
| 174 |
* @return mixed The resolved value. |
| 175 |
*/ |
| 176 |
private static function resolve_field_value( $settings, $value, $module_id ) { |
| 177 |
$type = $settings['type']; |
| 178 |
$id = $settings['id'] ?? ''; |
| 179 |
$default = $settings['default'] ?? null; |
| 180 |
|
| 181 |
if ( $value || 0 === $value || '0' === $value ) { |
| 182 |
return $value; |
| 183 |
} |
| 184 |
|
| 185 |
if ( $type === 'checkbox_multiple' ) { |
| 186 |
return (array) $default; |
| 187 |
} |
| 188 |
|
| 189 |
if ( in_array( $type, array( 'text', 'url' ), true ) && ! empty( $module_id ) ) { |
| 190 |
return Merchant_Option::get( $module_id, $id ); |
| 191 |
} |
| 192 |
|
| 193 |
return $default; |
| 194 |
} |
| 195 |
|
| 196 |
/** |
| 197 |
* Render the opening wrapper div for a field. |
| 198 |
* |
| 199 |
* Builds CSS classes and data attributes, then outputs the |
| 200 |
* opening div tag. |
| 201 |
* |
| 202 |
* @since 1.9.3 |
| 203 |
* |
| 204 |
* @param array<string, mixed> $settings The field configuration array. |
| 205 |
* @param mixed $value The field value. |
| 206 |
* @param string $module_id The module ID. |
| 207 |
* @param string $type The field type. |
| 208 |
* @param string $id The field ID. |
| 209 |
* |
| 210 |
* @return void |
| 211 |
*/ |
| 212 |
private static function render_field_wrapper_open( $settings, $value, $module_id, $type, $id ) { |
| 213 |
$class = ! empty( $settings['class'] ) ? ' ' . $settings['class'] : ''; |
| 214 |
$condition = $settings['condition'] ?? array(); |
| 215 |
$conditions = $settings['conditions'] ?? ''; |
| 216 |
|
| 217 |
$wrapper_classes = array( 'merchant-module-page-setting-field' ); |
| 218 |
$wrapper_classes[] = 'merchant-module-page-setting-field-' . $type; |
| 219 |
|
| 220 |
if ( ! empty( $class ) ) { |
| 221 |
$wrapper_classes[] = $class; |
| 222 |
} |
| 223 |
|
| 224 |
/** |
| 225 |
* Hook 'merchant_admin_module_field_wrapper_classes' |
| 226 |
* |
| 227 |
* @since 1.9.3 |
| 228 |
*/ |
| 229 |
$wrapper_classes = apply_filters( 'merchant_admin_module_field_wrapper_classes', $wrapper_classes, $settings, $value, $module_id ); |
| 230 |
|
| 231 |
echo '<div class="' . esc_attr( implode( ' ', $wrapper_classes ) ) . '" data-id="' |
| 232 |
. esc_attr( $id ) . '" data-type="' . esc_attr( $type ) . '" data-condition="' . esc_attr( (string) wp_json_encode( $condition ) ) |
| 233 |
. '" data-conditions="' . ( $conditions ? esc_attr( (string) wp_json_encode( $conditions ) ) : "" ) . '">'; |
| 234 |
} |
| 235 |
|
| 236 |
/** |
| 237 |
* Render the title bar for a field, including Pro upsell badge. |
| 238 |
* |
| 239 |
* @since 1.9.3 |
| 240 |
* |
| 241 |
* @param array<string, mixed> $settings The field configuration array. |
| 242 |
* @param string $id The field ID. |
| 243 |
* @param bool $is_upsell Whether to show the Pro upsell badge. |
| 244 |
* |
| 245 |
* @return void |
| 246 |
*/ |
| 247 |
private static function render_field_title( $settings, $id, $is_upsell ) { |
| 248 |
if ( empty( $settings['title'] ) ) { |
| 249 |
return; |
| 250 |
} |
| 251 |
?> |
| 252 |
<div class="merchant-module-page-setting-field-title<?php echo esc_attr( $is_upsell ? ' merchant-module-page-setting-field-title__has-upsell' : '' ); ?>"> |
| 253 |
<?php echo esc_html( $settings['title'] ); ?> |
| 254 |
|
| 255 |
<?php if ( $is_upsell ) : ?> |
| 256 |
<a href="https://athemes.com/merchant-upgrade?utm_source=inner_module_settings_field&utm_content=<?php echo esc_attr( $id ); ?>&utm_medium=merchant_dashboard&utm_campaign=Merchant" class="merchant-module-pro-upsell" target="_blank"> |
| 257 |
<span class="merchant-pro-badge merchant-pro-tooltip" data-tooltip-message="<?php echo esc_attr__( 'This option is only available on Merchant Pro', 'merchant' ); ?>"> |
| 258 |
<svg width="28" height="16" viewBox="0 0 28 16" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 259 |
<path d="M7.41309 8.90723H5.58203V7.85254H7.41309C7.71257 7.85254 7.95508 7.80371 8.14062 7.70605C8.32943 7.60514 8.46777 7.46842 8.55566 7.2959C8.64355 7.12012 8.6875 6.91992 8.6875 6.69531C8.6875 6.47721 8.64355 6.27376 8.55566 6.08496C8.46777 5.89616 8.32943 5.74316 8.14062 5.62598C7.95508 5.50879 7.71257 5.4502 7.41309 5.4502H6.02148V11.5H4.67871V4.39062H7.41309C7.96647 4.39062 8.43848 4.48991 8.8291 4.68848C9.22298 4.88379 9.52246 5.1556 9.72754 5.50391C9.93587 5.84896 10.04 6.24284 10.04 6.68555C10.04 7.14453 9.93587 7.54004 9.72754 7.87207C9.52246 8.2041 9.22298 8.45964 8.8291 8.63867C8.43848 8.81771 7.96647 8.90723 7.41309 8.90723ZM11.0947 4.39062H13.6777C14.2181 4.39062 14.682 4.47201 15.0693 4.63477C15.4567 4.79753 15.7546 5.03841 15.9629 5.35742C16.1712 5.67643 16.2754 6.06868 16.2754 6.53418C16.2754 6.90202 16.2103 7.22103 16.0801 7.49121C15.9499 7.76139 15.766 7.98763 15.5283 8.16992C15.2939 8.35221 15.0173 8.49544 14.6982 8.59961L14.2783 8.81445H11.998L11.9883 7.75488H13.6924C13.9691 7.75488 14.1986 7.70605 14.3809 7.6084C14.5632 7.51074 14.6999 7.37565 14.791 7.20312C14.8854 7.0306 14.9326 6.83366 14.9326 6.6123C14.9326 6.37467 14.887 6.1696 14.7959 5.99707C14.7048 5.82129 14.5664 5.6862 14.3809 5.5918C14.1953 5.4974 13.9609 5.4502 13.6777 5.4502H12.4375V11.5H11.0947V4.39062ZM15.1084 11.5L13.4629 8.31641L14.8838 8.31152L16.5488 11.4316V11.5H15.1084ZM23.209 7.76465V8.13086C23.209 8.66797 23.1374 9.15137 22.9941 9.58105C22.8509 10.0075 22.6475 10.3704 22.3838 10.6699C22.1201 10.9694 21.806 11.1989 21.4414 11.3584C21.0768 11.5179 20.6715 11.5977 20.2256 11.5977C19.7861 11.5977 19.3825 11.5179 19.0146 11.3584C18.6501 11.1989 18.3343 10.9694 18.0674 10.6699C17.8005 10.3704 17.5938 10.0075 17.4473 9.58105C17.3008 9.15137 17.2275 8.66797 17.2275 8.13086V7.76465C17.2275 7.22428 17.3008 6.74089 17.4473 6.31445C17.5938 5.88802 17.7988 5.52507 18.0625 5.22559C18.3262 4.92285 18.6403 4.69173 19.0049 4.53223C19.3727 4.37272 19.7764 4.29297 20.2158 4.29297C20.6618 4.29297 21.0671 4.37272 21.4316 4.53223C21.7962 4.69173 22.1104 4.92285 22.374 5.22559C22.641 5.52507 22.846 5.88802 22.9893 6.31445C23.1357 6.74089 23.209 7.22428 23.209 7.76465ZM21.8516 8.13086V7.75488C21.8516 7.36751 21.8158 7.02734 21.7441 6.73438C21.6725 6.43815 21.5667 6.18913 21.4268 5.9873C21.2868 5.78548 21.1143 5.63411 20.9092 5.5332C20.7041 5.42904 20.473 5.37695 20.2158 5.37695C19.9554 5.37695 19.7243 5.42904 19.5225 5.5332C19.3239 5.63411 19.1546 5.78548 19.0146 5.9873C18.8747 6.18913 18.7673 6.43815 18.6924 6.73438C18.6208 7.02734 18.585 7.36751 18.585 7.75488V8.13086C18.585 8.51497 18.6208 8.85514 18.6924 9.15137C18.7673 9.44759 18.8747 9.69824 19.0146 9.90332C19.1579 10.1051 19.3304 10.2581 19.5322 10.3623C19.734 10.4665 19.9652 10.5186 20.2256 10.5186C20.486 10.5186 20.7171 10.4665 20.9189 10.3623C21.1208 10.2581 21.29 10.1051 21.4268 9.90332C21.5667 9.69824 21.6725 9.44759 21.7441 9.15137C21.8158 8.85514 21.8516 8.51497 21.8516 8.13086Z" fill="#3858E9"/> |
| 260 |
<rect x="0.5" y="1" width="27" height="14" rx="1.5" stroke="#3858E9"/> |
| 261 |
</svg> |
| 262 |
</span> |
| 263 |
</a> |
| 264 |
<?php endif; ?> |
| 265 |
</div> |
| 266 |
<?php |
| 267 |
} |
| 268 |
|
| 269 |
/** |
| 270 |
* Render the inner field content via the field registry. |
| 271 |
* |
| 272 |
* Delegates rendering to the registered field class. Logs an |
| 273 |
* error if the field type is unknown. |
| 274 |
* |
| 275 |
* @since 1.9.3 |
| 276 |
* |
| 277 |
* @param string $type The field type. |
| 278 |
* @param array<string, mixed> $settings The field configuration array. |
| 279 |
* @param mixed $value The field value. |
| 280 |
* @param string $module_id The module ID. |
| 281 |
* |
| 282 |
* @return void |
| 283 |
*/ |
| 284 |
private static function render_field_inner( $type, $settings, $value, $module_id ) { |
| 285 |
$registry = Merchant_Field_Registry::instance(); |
| 286 |
|
| 287 |
if ( $registry->has( $type ) ) { |
| 288 |
try { |
| 289 |
$field_instance = $registry->create( $type, $settings, $value, $module_id ); |
| 290 |
if ( $field_instance !== null ) { |
| 291 |
$field_instance->render(); |
| 292 |
} |
| 293 |
} catch ( \Exception $e ) { |
| 294 |
wp_trigger_error( __METHOD__, 'Merchant field render error (' . $type . '): ' . $e->getMessage() ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped |
| 295 |
} |
| 296 |
} else { |
| 297 |
printf( |
| 298 |
'<div class="merchant-field-error"><strong>%s</strong> %s</div>', |
| 299 |
esc_html__( 'Merchant field error:', 'merchant' ), |
| 300 |
esc_html( |
| 301 |
sprintf( |
| 302 |
/* translators: %s: the unsupported field type slug */ |
| 303 |
__( 'Unknown field type "%s". Register it via the merchant_field_types filter.', 'merchant' ), |
| 304 |
$type |
| 305 |
) |
| 306 |
) |
| 307 |
); |
| 308 |
_doing_it_wrong( |
| 309 |
__METHOD__, |
| 310 |
sprintf( 'Unknown field type "%s". Register it via the merchant_field_types filter.', esc_html( $type ) ), |
| 311 |
'2.2.5' |
| 312 |
); |
| 313 |
} |
| 314 |
} |
| 315 |
|
| 316 |
/** |
| 317 |
* Render the description and hidden description for a field. |
| 318 |
* |
| 319 |
* @since 1.9.3 |
| 320 |
* |
| 321 |
* @param array<string, mixed> $settings The field configuration array. |
| 322 |
* @param mixed $value The field value. |
| 323 |
* @param string $module_id The module ID. |
| 324 |
* |
| 325 |
* @return void |
| 326 |
*/ |
| 327 |
private static function render_field_description( $settings, $value, $module_id ) { |
| 328 |
$hidden_desc = $settings['hidden_desc'] ?? ''; |
| 329 |
|
| 330 |
/** |
| 331 |
* Hook 'merchant_admin_module_field_hidden_description' |
| 332 |
* |
| 333 |
* @since 1.9.3 |
| 334 |
*/ |
| 335 |
$hidden_desc = apply_filters( 'merchant_admin_module_field_hidden_description', $hidden_desc, $settings, $value, $module_id ); |
| 336 |
|
| 337 |
$desc = $settings['desc'] ?? ''; |
| 338 |
|
| 339 |
/** |
| 340 |
* Hook 'merchant_admin_module_field_description' |
| 341 |
* |
| 342 |
* @since 1.9.3 |
| 343 |
*/ |
| 344 |
$desc = apply_filters( 'merchant_admin_module_field_description', $desc, $settings, $value, $module_id ); |
| 345 |
|
| 346 |
if ( ! empty( $desc ) ) { |
| 347 |
$hidden_desc_html = ''; |
| 348 |
if ( ! empty( $hidden_desc ) ) { |
| 349 |
$hidden_desc_html = '<div class="merchant-module-page-setting-field-hidden-desc-trigger" data-show-text="' . esc_html__( 'Show more', 'merchant' ) . '" data-hidden-text="' . esc_html__( 'Show less', 'merchant' ) . '"><span>' . esc_html__( 'Show more', 'merchant' ) . '</span>'; |
| 350 |
$hidden_desc_html .= '<img src="' . esc_url( MERCHANT_URI . '/assets/images/arrow-down.svg' ) . '" alt="Merchant" />'; |
| 351 |
$hidden_desc_html .= '</div>'; |
| 352 |
} |
| 353 |
|
| 354 |
$desc_class = 'merchant-module-page-setting-field-desc' |
| 355 |
. ( $hidden_desc ? ' merchant-module-page-setting-field-desc-has-hidden-desc' : '' ); |
| 356 |
|
| 357 |
printf( '<div class="%s">%s%s</div>', esc_attr( $desc_class ), wp_kses_post( $desc ), wp_kses_post( $hidden_desc_html ) ); |
| 358 |
} |
| 359 |
|
| 360 |
if ( ! empty( $hidden_desc ) ) { |
| 361 |
printf( '<div class="merchant-module-page-setting-field-hidden-desc">%s</div>', wp_kses_post( nl2br( $hidden_desc ) ) ); |
| 362 |
} |
| 363 |
} |
| 364 |
|
| 365 |
/** |
| 366 |
* Render a disabled (pro-gated) field. |
| 367 |
* |
| 368 |
* Renders the field via the normal registry path, then disables all |
| 369 |
* interactive elements so the user can see the control but not interact. |
| 370 |
* |
| 371 |
* @since 1.0 |
| 372 |
* |
| 373 |
* @param array<string, mixed> $settings Field settings. |
| 374 |
* @param mixed $value Field value. |
| 375 |
* @param string $module_id Module ID. |
| 376 |
* |
| 377 |
* @return void |
| 378 |
*/ |
| 379 |
public static function disabled_field( $settings, $value, $module_id = '' ) { |
| 380 |
ob_start(); |
| 381 |
self::field( $settings, $value, $module_id ); |
| 382 |
$field_html = (string) ob_get_clean(); |
| 383 |
|
| 384 |
// Matched on the tag name, not on "<input " — templates break their |
| 385 |
// attributes across lines however they like, and a newline right after the |
| 386 |
// tag name used to slip past this and leave the control editable. |
| 387 |
$field_html = (string) preg_replace( |
| 388 |
'/<(input|select|textarea|button)(?=[\s>])/i', |
| 389 |
'<$1 disabled', |
| 390 |
$field_html |
| 391 |
); |
| 392 |
|
| 393 |
$field_html = str_replace( |
| 394 |
'merchant-module-page-setting-field-inner', |
| 395 |
'merchant-module-page-setting-field-inner disabled', |
| 396 |
$field_html |
| 397 |
); |
| 398 |
|
| 399 |
// The HTML is generated by self::field() — trusted admin output. |
| 400 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All values already escaped in field(). |
| 401 |
echo $field_html; |
| 402 |
} |
| 403 |
} |
| 404 |
|