| @@ -11,47 +11,53 @@ | ||
| 11 | 11 | * @var FrmFieldSelectionData|null |
| 12 | 12 | */ |
| 13 | 13 | private static $field_selection_data; |
| 14 | 14 | |
| 15 | + /** | |
| 16 | + * Render the fields the form builder asked for over ajax. | |
| 17 | + * | |
| 18 | + * The browser sends field ids only. The fields themselves are read from the form, which is one | |
| 19 | + * indexed query shared with the rest of the request through the field cache, and cheaper than | |
| 20 | + * shipping every field's data down to the page and straight back up again. | |
| 21 | + * | |
| 22 | + * @return void | |
| 23 | + */ | |
| 15 | 24 | public static function load_field() { |
| 16 | 25 | FrmAppHelper::permission_check( 'frm_edit_forms' ); |
| 17 | 26 | check_ajax_referer( 'frm_ajax', 'nonce' ); |
| 18 | 27 | |
| 19 | - // Javascript may be included in some field settings. | |
| 20 | - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 21 | - $fields = isset( $_POST['field'] ) ? wp_unslash( $_POST['field'] ) : array(); | |
| 28 | + $field_ids = FrmAppHelper::get_post_param( 'field_ids', array(), 'absint' ); | |
| 29 | + $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' ); | |
| 22 | 30 | |
| 23 | - if ( ! $fields ) { | |
| 31 | + if ( ! $form_id || ! is_array( $field_ids ) || ! $field_ids ) { | |
| 24 | 32 | wp_die(); |
| 25 | 33 | } |
| 26 | 34 | |
| 27 | 35 | $_GET['page'] = 'formidable'; |
| 28 | - | |
| 29 | - $values = array( | |
| 30 | - 'id' => FrmAppHelper::get_post_param( 'form_id', '', 'absint' ), | |
| 36 | + $fields = self::get_builder_fields_by_id( $form_id ); | |
| 37 | + $values = array( | |
| 38 | + 'id' => $form_id, | |
| 31 | 39 | 'doing_ajax' => true, |
| 32 | 40 | ); |
| 33 | - $field_html = array(); | |
| 41 | + $field_html = array(); | |
| 34 | 42 | |
| 35 | - foreach ( $fields as $field ) { | |
| 36 | - $field = htmlspecialchars_decode( nl2br( $field ) ); | |
| 37 | - $field = json_decode( $field ); | |
| 38 | - | |
| 39 | - if ( ! isset( $field->id ) || ! is_numeric( $field->id ) ) { | |
| 40 | - // This field may have already been loaded | |
| 43 | + foreach ( $field_ids as $field_id ) { | |
| 44 | + if ( ! isset( $fields[ $field_id ] ) ) { | |
| 45 | + // This field may have already been loaded, or is no longer in the form. | |
| 41 | 46 | continue; |
| 42 | 47 | } |
| 43 | 48 | |
| 44 | - if ( ! isset( $field->value ) ) { | |
| 45 | - $field->value = ''; | |
| 46 | - } | |
| 47 | - $field->field_options = json_decode( json_encode( $field->field_options ), true ); | |
| 48 | - $field->options = json_decode( json_encode( $field->options ), true ); | |
| 49 | - $field->default_value = json_decode( json_encode( $field->default_value ), true ); | |
| 49 | + $field = $fields[ $field_id ]; | |
| 50 | 50 | |
| 51 | 51 | ob_start(); |
| 52 | 52 | self::load_single_field( $field, $values ); |
| 53 | - $field_html[ absint( $field->id ) ] = ob_get_clean(); | |
| 53 | + | |
| 54 | + $field_html[ $field_id ] = array( | |
| 55 | + // The type travels with the html so the js can report it to frm_ajax_loaded_field | |
| 56 | + // listeners without a copy of the field. | |
| 57 | + 'type' => $field->type, | |
| 58 | + 'html' => ob_get_clean(), | |
| 59 | + ); | |
| 54 | 60 | }//end foreach |
| 55 | 61 | |
| 56 | 62 | echo json_encode( $field_html ); |
| 57 | 63 | |
| @@ -58,8 +64,40 @@ | ||
| 58 | 64 | wp_die(); |
| 59 | 65 | } |
| 60 | 66 | |
| 61 | 67 | /** |
| 68 | + * Get a form's fields, as the form builder sees them, indexed by field id. | |
| 69 | + * | |
| 70 | + * @since 6.35 | |
| 71 | + * | |
| 72 | + * @param int $form_id | |
| 73 | + * | |
| 74 | + * @return array Field objects keyed by field id. Empty if the form is gone. | |
| 75 | + */ | |
| 76 | + private static function get_builder_fields_by_id( $form_id ) { | |
| 77 | + $form = FrmForm::getOne( $form_id ); | |
| 78 | + | |
| 79 | + if ( ! $form ) { | |
| 80 | + return array(); | |
| 81 | + } | |
| 82 | + | |
| 83 | + $fields = FrmField::get_all_for_form( $form_id ); | |
| 84 | + | |
| 85 | + /** This filter is documented in classes/controllers/FrmFormsController.php */ | |
| 86 | + $fields = apply_filters( 'frm_fields_in_form_builder', $fields, compact( 'form' ) ); | |
| 87 | + | |
| 88 | + $fields_by_id = array(); | |
| 89 | + | |
| 90 | + foreach ( (array) $fields as $field ) { | |
| 91 | + if ( is_object( $field ) && ! empty( $field->id ) ) { | |
| 92 | + $fields_by_id[ (int) $field->id ] = $field; | |
| 93 | + } | |
| 94 | + } | |
| 95 | + | |
| 96 | + return $fields_by_id; | |
| 97 | + } | |
| 98 | + | |
| 99 | + /** | |
| 62 | 100 | * Create a new field with ajax |
| 63 | 101 | */ |
| 64 | 102 | public static function create() { |
| 65 | 103 | FrmAppHelper::permission_check( 'frm_edit_forms' ); |
| @@ -133,9 +171,9 @@ | ||
| 133 | 171 | $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' ); |
| 134 | 172 | $new_field = FrmField::duplicate_single_field( $field_id, $form_id ); |
| 135 | 173 | |
| 136 | 174 | if ( is_array( $new_field ) && ! empty( $new_field['field_id'] ) ) { |
| 137 | - self::load_single_field( $new_field['field_id'], $new_field['values'] ); | |
| 175 | + self::load_single_field( $new_field['field_id'], $new_field['values'], $form_id ); | |
| 138 | 176 | } |
| 139 | 177 | |
| 140 | 178 | wp_die(); |
| 141 | 179 | } |
| @@ -183,9 +221,12 @@ | ||
| 183 | 221 | return; |
| 184 | 222 | } |
| 185 | 223 | |
| 186 | 224 | if ( ! isset( $field ) && is_object( $field_object ) ) { |
| 187 | - $field_object->parent_form_id = $values['id'] ?? $field_object->form_id; | |
| 225 | + // Prefer the explicit form id. $values['id'] is not always a form id (for example when | |
| 226 | + // duplicating a field it is the copied field's id), so trusting it would set parent_form_id | |
| 227 | + // to a field id and break settings that resolve fields against the parent form. | |
| 228 | + $field_object->parent_form_id = $form_id ? $form_id : ( $values['id'] ?? $field_object->form_id ); | |
| 188 | 229 | $field = FrmFieldsHelper::setup_edit_vars( $field_object ); |
| 189 | 230 | } |
| 190 | 231 | |
| 191 | 232 | /** |
| @@ -217,8 +258,14 @@ | ||
| 217 | 258 | private static function get_classes_for_builder_field( $field, $display, $field_info ) { |
| 218 | 259 | $li_classes = $field_info->form_builder_classes( $display['type'] ); |
| 219 | 260 | $li_classes .= ' frm_form_field frmstart '; |
| 220 | 261 | |
| 262 | + $style_align_class = self::get_builder_field_style_align_class( $field, $field_info ); | |
| 263 | + | |
| 264 | + if ( $style_align_class ) { | |
| 265 | + $li_classes .= $style_align_class . ' '; | |
| 266 | + } | |
| 267 | + | |
| 221 | 268 | if ( isset( $field['classes'] ) ) { |
| 222 | 269 | $li_classes .= trim( $field['classes'] ) . ' '; |
| 223 | 270 | } |
| 224 | 271 | |
| @@ -230,8 +277,39 @@ | ||
| 230 | 277 | |
| 231 | 278 | return $li_classes; |
| 232 | 279 | } |
| 233 | 280 | |
| 281 | + /** | |
| 282 | + * Apply the active style alignment to a radio or checkbox builder preview on load. | |
| 283 | + * | |
| 284 | + * The per-field alignment setting only exists in Pro, so on load the style setting is | |
| 285 | + * used. When the field has an explicit alignment (Pro), the frm_build_field_class filter | |
| 286 | + * applies it instead. When Pro is not installed, any saved alignment is treated as empty. | |
| 287 | + * | |
| 288 | + * @since 6.32 | |
| 289 | + * | |
| 290 | + * @param array $field | |
| 291 | + * @param FrmFieldType $field_info | |
| 292 | + * | |
| 293 | + * @return string | |
| 294 | + */ | |
| 295 | + private static function get_builder_field_style_align_class( $field, $field_info ) { | |
| 296 | + if ( ! $field || ! is_array( $field ) || ( ! FrmField::is_radio( $field ) && ! FrmField::is_checkbox( $field ) ) ) { | |
| 297 | + return ''; | |
| 298 | + } | |
| 299 | + | |
| 300 | + $align = FrmAppHelper::pro_is_installed() ? FrmField::get_option( $field, 'align' ) : ''; | |
| 301 | + | |
| 302 | + if ( $align ) { | |
| 303 | + return ''; | |
| 304 | + } | |
| 305 | + | |
| 306 | + $align = FrmStylesHelper::get_align_from_active_style( $field ); | |
| 307 | + $field_info->prepare_align_class( $align ); | |
| 308 | + | |
| 309 | + return $align; | |
| 310 | + } | |
| 311 | + | |
| 234 | 312 | public static function destroy() { |
| 235 | 313 | FrmAppHelper::permission_check( 'frm_edit_forms' ); |
| 236 | 314 | check_ajax_referer( 'frm_ajax', 'nonce' ); |
| 237 | 315 | |
| @@ -368,8 +446,79 @@ | ||
| 368 | 446 | if ( $display['clear_on_focus'] && is_array( $field['placeholder'] ) ) { |
| 369 | 447 | $field['placeholder'] = implode( ', ', $field['placeholder'] ); |
| 370 | 448 | } |
| 371 | 449 | |
| 450 | + $pro_is_installed = FrmAppHelper::pro_is_installed(); | |
| 451 | + | |
| 452 | + $unique_values_label_atts = array( | |
| 453 | + 'for' => 'frm_uniq_field_' . $field['id'], | |
| 454 | + 'class' => 'frm_help frm-mb-0', | |
| 455 | + 'title' => __( | |
| 456 | + 'Unique: Do not allow the same response multiple times. For example, if one user enters \'Joe\', then no one else will be allowed to enter the same name.', | |
| 457 | + 'formidable' | |
| 458 | + ), | |
| 459 | + 'data-trigger' => 'hover', | |
| 460 | + ); | |
| 461 | + | |
| 462 | + $read_only_label_atts = array( | |
| 463 | + 'for' => 'frm_read_only_field_' . $field['id'], | |
| 464 | + 'class' => 'frm_help frm-mb-0', | |
| 465 | + 'title' => __( 'Read Only: Show this field but do not allow the field value to be edited from the front-end.', 'formidable' ), | |
| 466 | + 'data-trigger' => 'hover', | |
| 467 | + ); | |
| 468 | + | |
| 469 | + if ( ! $pro_is_installed ) { | |
| 470 | + $visibility_upsell_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts( | |
| 471 | + array( | |
| 472 | + 'id' => 'field_options_admin_only_' . $field['id'], | |
| 473 | + 'readonly' => '1', | |
| 474 | + ), | |
| 475 | + 'field_visibility', | |
| 476 | + __( 'Visibility options', 'formidable' ), | |
| 477 | + '/field-options/#kb-visibility' | |
| 478 | + ); | |
| 479 | + | |
| 480 | + $autocomplete_upsell_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts( | |
| 481 | + array( 'id' => 'field_options_autocomplete_' . $field['id'] ), | |
| 482 | + 'autocomplete', | |
| 483 | + __( 'Autocomplete options', 'formidable' ), | |
| 484 | + '/email-address/#kb-autocomplete-attribute' | |
| 485 | + ); | |
| 486 | + | |
| 487 | + $before_after_content_upsell_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts( | |
| 488 | + array( | |
| 489 | + 'type' => 'text', | |
| 490 | + 'readonly' => '1', | |
| 491 | + ), | |
| 492 | + 'before_after_contents', | |
| 493 | + __( 'Before and after field contents', 'formidable' ), | |
| 494 | + '/field-options/#kb-before-after-input' | |
| 495 | + ); | |
| 496 | + | |
| 497 | + $show_upsell_for_unique_value = in_array( | |
| 498 | + $field['type'], | |
| 499 | + array( 'checkbox', 'email', 'name', 'number', 'phone', 'radio', 'text', 'textarea', 'url' ), | |
| 500 | + true | |
| 501 | + ); | |
| 502 | + $show_upsell_for_read_only = in_array( $field['type'], array( 'address', 'email', 'hidden', 'number', 'phone', 'radio', 'text', 'textarea', 'url' ), true ); | |
| 503 | + $show_upsell_for_before_after_contents = in_array( $field['type'], array( 'email', 'number', 'phone', 'quantity', 'select', 'tag', 'text', 'total', 'url' ), true ); | |
| 504 | + $show_upsell_for_autocomplete = in_array( $field['type'], array( 'text', 'email', 'number' ), true ); | |
| 505 | + $show_upsell_for_visibility = $field['type'] !== 'hidden'; | |
| 506 | + | |
| 507 | + $unique_values_label_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts( | |
| 508 | + $unique_values_label_atts, | |
| 509 | + 'unique_values', | |
| 510 | + __( 'Unique Values', 'formidable' ), | |
| 511 | + '/field-options/#kb-unique' | |
| 512 | + ); | |
| 513 | + $read_only_label_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts( | |
| 514 | + $read_only_label_atts, | |
| 515 | + 'read_only', | |
| 516 | + __( 'Read Only Values', 'formidable' ), | |
| 517 | + '/field-options/#kb-read-only' | |
| 518 | + ); | |
| 519 | + }//end if | |
| 520 | + | |
| 372 | 521 | include FrmAppHelper::plugin_path() . '/classes/views/frm-fields/back-end/settings.php'; |
| 373 | 522 | } |
| 374 | 523 | |
| 375 | 524 | /** |
| @@ -467,9 +616,9 @@ | ||
| 467 | 616 | |
| 468 | 617 | $pro_fields = FrmField::pro_field_selection(); |
| 469 | 618 | // We want to keep credit_card types as credit card types for Stripe Lite. |
| 470 | 619 | // The credit_card key is set for backward compatibility. |
| 471 | - unset( $pro_fields['credit_card'] ); | |
| 620 | + FrmField::remove_moved_field_types_from_pro( $pro_fields ); | |
| 472 | 621 | |
| 473 | 622 | return array_key_exists( $type, $pro_fields ) ? 'text' : $type; |
| 474 | 623 | } |
| 475 | 624 | |
| @@ -534,8 +683,9 @@ | ||
| 534 | 683 | FrmFormsHelper::add_html_attr( $class, 'class', $add_html ); |
| 535 | 684 | |
| 536 | 685 | self::add_shortcodes_to_html( $field, $add_html ); |
| 537 | 686 | self::add_pattern_attribute( $field, $add_html ); |
| 687 | + self::add_currency_field_attributes( $field, $add_html ); | |
| 538 | 688 | |
| 539 | 689 | $add_html = apply_filters( 'frm_field_extra_html', $add_html, $field ); |
| 540 | 690 | $add_html = ' ' . implode( ' ', $add_html ) . ' '; |
| 541 | 691 | |
| @@ -1046,8 +1196,60 @@ | ||
| 1046 | 1196 | $format = FrmEntryValidate::phone_format( $field ); |
| 1047 | 1197 | $format = substr( $format, 2, - 1 ); |
| 1048 | 1198 | |
| 1049 | 1199 | $add_html['pattern'] = 'pattern="' . esc_attr( $format ) . '"'; |
| 1200 | + } | |
| 1201 | + | |
| 1202 | + /** | |
| 1203 | + * Add product attributes to fields in multi-paged forms. | |
| 1204 | + * | |
| 1205 | + * @param array $field | |
| 1206 | + * @param array $add_html | |
| 1207 | + * | |
| 1208 | + * @return void | |
| 1209 | + */ | |
| 1210 | + private static function add_currency_field_attributes( $field, &$add_html ) { | |
| 1211 | + $type = $field['original_type'] ?? $field['type']; | |
| 1212 | + $is_product_field = in_array( $type, array( 'total', 'quantity', 'product' ), true ); | |
| 1213 | + | |
| 1214 | + if ( ! $is_product_field ) { | |
| 1215 | + return; | |
| 1216 | + } | |
| 1217 | + | |
| 1218 | + if ( $type === 'total' ) { | |
| 1219 | + $add_html['data-frmtotal'] = 'data-frmtotal'; | |
| 1220 | + } elseif ( $type === 'quantity' ) { | |
| 1221 | + $product_field = FrmField::get_option( $field, 'product_field' ); | |
| 1222 | + $add_html['data-frmproduct'] = 'data-frmproduct="' . esc_attr( json_encode( $product_field ) ) . '"'; | |
| 1223 | + } elseif ( $type === 'product' && 'hidden' === $field['type'] ) { | |
| 1224 | + // We want to do this only for fields that are hidden because it's | |
| 1225 | + // not their page, hence the check : 'hidden' === $field['type']. | |
| 1226 | + $price = empty( $field['value'] ) ? 0 : self::get_product_price( $field ); | |
| 1227 | + | |
| 1228 | + $add_html['data-frmprice'] = 'data-frmprice="' . esc_attr( $price ) . '"'; | |
| 1229 | + } | |
| 1230 | + } | |
| 1231 | + | |
| 1232 | + /** | |
| 1233 | + * @param array $field | |
| 1234 | + */ | |
| 1235 | + private static function get_product_price( $field ) { | |
| 1236 | + if ( is_array( $field['value'] ) ) { | |
| 1237 | + // '' is unlikely though, let's just do it to prevent warnings | |
| 1238 | + $value = isset( $field['opt_key'] ) && isset( $field['value'][ $field['opt_key'] ] ) | |
| 1239 | + ? $field['value'][ $field['opt_key'] ] | |
| 1240 | + : ''; | |
| 1241 | + } else { | |
| 1242 | + $value = $field['value']; | |
| 1243 | + } | |
| 1244 | + | |
| 1245 | + $field_obj = FrmFieldFactory::get_field_object( $field['id'] ); | |
| 1246 | + | |
| 1247 | + if ( method_exists( $field_obj, 'get_posted_price' ) ) { | |
| 1248 | + return $field_obj->get_posted_price( $value ); | |
| 1249 | + } | |
| 1250 | + | |
| 1251 | + return $value; | |
| 1050 | 1252 | } |
| 1051 | 1253 | |
| 1052 | 1254 | /** |
| 1053 | 1255 | * @param mixed $opt |