register_sub_fields( self::get_part_labels() ); } /** * Gets the label for each part of an address, keyed by the value the part is * shown with in a shortcode, like [25 show=city]. * * @since 6.35 * * @return array */ public static function get_part_labels() { return array( 'line1' => __( 'Line 1', 'formidable' ), 'line2' => __( 'Line 2', 'formidable' ), 'city' => __( 'City', 'formidable' ), 'state' => __( 'State/Province', 'formidable' ), 'zip' => __( 'Zip/Postal', 'formidable' ), 'country' => __( 'Country', 'formidable' ), ); } protected function field_settings_for_type() { return array( 'clear_on_focus' => false, 'description' => false, 'default' => false, ); } protected function extra_field_opts() { $options = parent::extra_field_opts(); $options['address_type'] = 'international'; // Default descriptions for sub-fields (line1 and line2 are empty). $options['line1_desc'] = ''; $options['line2_desc'] = ''; $options['city_desc'] = __( 'City', 'formidable' ); $options['state_desc'] = __( 'State/Province', 'formidable' ); $options['zip_desc'] = __( 'Zip/Postal', 'formidable' ); $options['country_desc'] = __( 'Country', 'formidable' ); return $options; } /** * Get processed sub-fields based on address type. * * @since 6.34 * * @return array */ protected function get_processed_sub_fields() { $address_type = FrmField::get_option( $this->field, 'address_type' ); $sub_fields = $this->sub_fields; // Set default classes and attributes $default_atts = array( 'line1' => array( 'autocomplete' => 'address-line1' ), 'line2' => array( 'autocomplete' => 'address-line2', 'optional' => true, ), 'city' => array( 'autocomplete' => 'address-level2', 'wrapper_classes' => 'frm_third frm_first', ), 'state' => array( 'autocomplete' => 'address-level1', 'wrapper_classes' => 'frm_third', ), 'zip' => array( 'autocomplete' => 'postal-code', 'wrapper_classes' => 'frm_third', ), 'country' => array( 'autocomplete' => 'country-name' ), ); foreach ( $sub_fields as $name => &$sub_field ) { if ( isset( $default_atts[ $name ] ) ) { $sub_field = array_merge( $sub_field, $default_atts[ $name ] ); } // Ensure label is preserved from registered sub-fields. if ( ! isset( $sub_field['label'] ) ) { $sub_field['label'] = $this->sub_fields[ $name ]['label'] ?? ''; } } // Handle Europe address type (no state field, zip shown before city) if ( 'europe' === $address_type ) { $city_field = $sub_fields['city']; $country_field = $sub_fields['country']; unset( $sub_fields['state'], $sub_fields['city'], $sub_fields['country'] ); $sub_fields['city'] = $city_field; $sub_fields['country'] = $country_field; $sub_fields['city']['wrapper_classes'] = 'frm_third'; $sub_fields['zip']['wrapper_classes'] = 'frm_third frm_first'; } // Handle US address type (state dropdown, no country field) if ( 'us' === $address_type ) { $sub_fields['state']['type'] = 'select'; $sub_fields['state']['options'] = FrmFieldsHelper::get_us_states(); unset( $sub_fields['country'] ); } // International and Europe address types use a country dropdown if ( in_array( $address_type, array( 'international', 'europe' ), true ) ) { $sub_fields['country']['type'] = 'select'; $sub_fields['country']['options'] = FrmFieldsHelper::get_countries(); } // Generic type has no country dropdown if ( 'generic' === $address_type ) { unset( $sub_fields['country'] ); } return $sub_fields; } /** * @since 6.34 * * @param array $args - Includes 'field', 'display', and 'values'. */ public function show_primary_options( $args ) { $field = $args['field']; include FrmAppHelper::plugin_path() . '/classes/views/combo-fields/addresses/back-end-field-opts.php'; parent::show_primary_options( $args ); } /** * @param array $atts * * @return void */ protected function fill_default_atts( &$atts ) { parent::fill_default_atts( $atts ); $defaults = array( 'line_sep' => '
', 'force_array' => false, ); $atts = wp_parse_args( $atts, $defaults ); } protected function prepare_display_value( $value, $atts ) { if ( ! is_array( $value ) ) { return $value; } $keys_to_check = array( 'line1', 'city', 'state', 'zip', 'country' ); $at_least_one_field_populated = false; foreach ( $keys_to_check as $key ) { if ( ! empty( $value[ $key ] ) ) { $at_least_one_field_populated = true; break; } } if ( ! $at_least_one_field_populated ) { return ''; } if ( ! isset( $value['line1'] ) ) { $value['line1'] = ''; } return $this->format_address_for_display( $value, $atts ); } /** * @since 6.34 * * @param array $value * @param array $atts * * @return array|string */ public function format_address_for_display( $value, $atts = array() ) { $defaults = $this->empty_value_array(); $this->fill_values( $value, $defaults ); $this->fill_default_atts( $atts ); if ( $atts['force_array'] ) { return $value; } $format = $this->address_format_for_display( $atts, $value ); foreach ( $defaults as $k => $part ) { $format = str_replace( '[' . $k . ']', $value[ $k ], $format ); } return str_replace( $atts['line_sep'] . $atts['line_sep'], $atts['line_sep'], $format ); } /** * @since 6.34 * * @param array $atts * @param array $value * * @return string */ private function address_format_for_display( $atts, $value ) { if ( ! empty( $atts['show'] ) ) { return '[' . $atts['show'] . ']'; } $line_sep = $atts['line_sep']; $address_type = FrmField::get_option( $this->field, 'address_type' ); $address_format = ''; if ( ! empty( $value['line1'] ) ) { $address_format = '[line1]' . $line_sep . '[line2]' . $line_sep; } if ( 'europe' === $address_type ) { $address_format .= '[zip] [city]'; } else { $address_format .= '[city], [state] [zip]'; } $address_format .= $line_sep . '[country]'; /** * Change the format of a displayed address * * @since 6.34 * * @param string $address_format * @param array $args */ $result = apply_filters( 'frm_address_format', $address_format, array( 'field' => $this->field ) ); return is_string( $result ) ? $result : $address_format; } /** * @since 6.34 * * @param mixed $value * * @return array */ public function address_string_to_array( $value ) { if ( is_array( $value ) ) { return $value; } if ( ! is_string( $value ) ) { $empty_array = array_keys( $this->empty_value_array() ); return array_combine( $empty_array, array_fill( 0, count( $empty_array ), '' ) ); } $value = array_map( 'trim', explode( ',', $value ) ); $empty_array = array_keys( $this->empty_value_array() ); $array_length_diff = count( $empty_array ) - count( $value ); if ( $array_length_diff === 1 ) { $value[] = ''; } elseif ( $array_length_diff > 1 ) { $filler_array = array_fill( 0, $array_length_diff, '' ); $value = array_merge( $value, $filler_array ); } elseif ( $array_length_diff < 0 ) { $value = array_slice( $value, 0, count( $empty_array ) ); } return array_combine( $empty_array, $value ); } /** * Get empty value array for address. * * @since 6.34 * * @return array */ private function empty_value_array() { /** * @since 6.34 * * @param array $empty_value_array array of empty address data. */ /** @var array */ $result = apply_filters( 'frm_address_empty_value_array', array( 'line1' => '', 'line2' => '', 'city' => '', 'state' => '', 'zip' => '', 'country' => '', ) ); return is_array( $result ) ? $result : array( 'line1' => '', 'line2' => '', 'city' => '', 'state' => '', 'zip' => '', 'country' => '', ); } /** * Convert comma-separated address values to an associative array * * @since 6.34 * * @param array|string $value * @param array $atts * * @return array */ protected function prepare_import_value( $value, $atts ) { if ( is_array( $value ) ) { return $value; } $sep = apply_filters( 'frm_csv_sep', ', ' ); $sep = is_string( $sep ) && $sep !== '' ? $sep : ', '; $value = explode( $sep, $value ); $count = count( $value ); if ( $count < 4 || $count > 6 ) { return $value; } $new_value = array( 'line1' => '', 'line2' => '', 'city' => '', 'state' => '', 'zip' => '', 'country' => '', ); $new_value['line1'] = $value[0]; $last_item = end( $value ); if ( $count === 6 || ( $count === 5 && is_numeric( $last_item ) ) ) { $new_value['line2'] = $value[1]; $new_value['city'] = $value[2]; $new_value['state'] = $value[3]; $new_value['zip'] = $value[4]; if ( $count === 6 ) { $new_value['country'] = $value[5]; } } else { $new_value['city'] = $value[1]; $new_value['state'] = $value[2]; $new_value['zip'] = $value[3]; if ( $count === 5 ) { $new_value['country'] = $value[4]; } } return $new_value; } /** * Validate field. * * @since 6.34 * * @param array $args Arguments. Includes `errors`, `value`. * * @return array Errors array. */ public function validate( $args ) { $errors = parent::validate( $args ); // Add zip validation for US addresses $values = $args['value']; if ( empty( $values['zip'] ) ) { return $errors; } $address_type = FrmField::get_option( $this->field, 'address_type' ); $format = ''; if ( 'us' === $address_type ) { $format = '/^[0-9]{5}(?:-[0-9]{4})?$/'; } $format = apply_filters( 'frm_zip_format', $format, array( 'field' => $this->field ) ); if ( $format && ! preg_match( $format, $values['zip'] ) ) { $errors[ 'field' . $args['id'] . '-zip' ] = __( 'This value is invalid', 'formidable' ); } return $errors; } /** * @since 6.34 * * @param mixed $value */ public function sanitize_value( &$value ) { FrmAppHelper::sanitize_value( 'sanitize_text_field', $value ); } /** * Get export headings for address field. * * @since 6.34 * * @return array */ public function get_export_headings() { $field_id = is_object( $this->field ) ? $this->field->id : $this->field['id']; $field_name = is_object( $this->field ) ? $this->field->name : $this->field['name']; // Include main field heading at the front. $headings = array( $field_id => $field_name, ); // Include sub-field headings. Use the union operator instead of array_merge // so the numeric main field key (e.g. 10) is preserved and not re-indexed. return $headings + parent::get_export_headings(); } }