1043 lines
32 KiB
PHP
1043 lines
32 KiB
PHP
<?php
|
|
|
|
if ( ! class_exists( 'GFForms' ) ) {
|
|
die();
|
|
}
|
|
|
|
/**
|
|
* The Repeater field.
|
|
*
|
|
* 2.4
|
|
*
|
|
* Class GF_Field_Repeater
|
|
*/
|
|
class GF_Field_Repeater extends GF_Field {
|
|
|
|
public $type = 'repeater';
|
|
|
|
/**
|
|
* Returns the field title for the form editor.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_form_editor_field_title() {
|
|
return esc_attr__( 'Repeater', 'gravityforms' );
|
|
}
|
|
|
|
/**
|
|
* Returns the field settings for the form editor.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @return array
|
|
*/
|
|
function get_form_editor_field_settings() {
|
|
return array(
|
|
'conditional_logic_field_setting',
|
|
'error_message_setting',
|
|
'label_setting',
|
|
'label_placement_setting',
|
|
'admin_label_setting',
|
|
'rules_setting',
|
|
'visibility_setting',
|
|
'description_setting',
|
|
'css_class_setting',
|
|
);
|
|
}
|
|
|
|
public function get_form_editor_button() {
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Used to determine the required validation result.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param int $form_id The ID of the form currently being processed.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_value_submission_empty( $form_id ) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Validates each sub-field.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param string|array $items The field values from get_value_submission().
|
|
* @param array $form The Form Object currently being processed.
|
|
*/
|
|
public function validate( $items, $form ) {
|
|
|
|
if ( empty( $items ) ) {
|
|
return;
|
|
}
|
|
|
|
/* @var GF_Field[] $fields */
|
|
$fields = $this->fields;
|
|
|
|
$context = GFFormDisplay::get_submission_context();
|
|
|
|
foreach ( $items as $i => $item ) {
|
|
foreach ( $fields as $field ) {
|
|
|
|
$field->set_context_property( 'itemIndex', $i );
|
|
|
|
$inputs = $field->get_entry_inputs();
|
|
if ( is_array( $inputs ) ) {
|
|
$field_value = array();
|
|
$field_keys = array_keys( $item );
|
|
foreach ( $field_keys as $input_id ) {
|
|
if ( is_numeric( $input_id ) && absint( $input_id ) == absint( $field->id ) ) {
|
|
$field_value[ $input_id ] = $item[ $input_id ];
|
|
}
|
|
}
|
|
} else {
|
|
$field_value = isset( $item[ $field->id ] ) ? $item[ $field->id ] : '';
|
|
}
|
|
|
|
if ( $field->isRequired && $field->is_value_empty( $field_value ) ) {
|
|
$field->failed_validation = true;
|
|
} else {
|
|
$field->validate( $field_value, $form );
|
|
}
|
|
|
|
/**
|
|
* Allows custom validation of the field value.
|
|
*
|
|
* @since Unknown
|
|
* @since 2.6.4 Added the $context param.
|
|
*
|
|
* @param array $result {
|
|
* An array containing the validation result properties.
|
|
*
|
|
* @type bool $is_valid The field validation result.
|
|
* @type array $message The field validation message.
|
|
* }
|
|
* @param mixed $value The field value currently being validated.
|
|
* @param array $form The form currently being validated.
|
|
* @param GF_Field $field The field currently being validated.
|
|
* @param string $context The context for the current submission. Possible values: form-submit, api-submit, api-validate.
|
|
*/
|
|
$result = gf_apply_filters( array( 'gform_field_validation', $form['id'], $field->id ), array(
|
|
'is_valid' => $field->failed_validation ? false : true,
|
|
'message' => $field->validation_message
|
|
), $field_value, $form, $field, $context );
|
|
$this->failed_validation = rgar( $result, 'is_valid' ) ? false : true;
|
|
|
|
// Reset the field validation and item index.
|
|
$field->failed_validation = false;
|
|
$field->set_context_property( 'itemIndex', null );
|
|
|
|
if ( $this->failed_validation ) {
|
|
// One field has failed validation so the entire repeater fails.
|
|
return;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the field value on submission.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param array $field_values The dynamic population parameter names with their corresponding values to be populated.
|
|
* @param bool|true $get_from_post_global_var Whether to get the value from the $_POST array as opposed to $field_values.
|
|
*
|
|
* @return array|string
|
|
*/
|
|
public function get_value_submission( $field_values, $get_from_post_global_var = true ) {
|
|
|
|
$submission_values = $this->get_value_submission_recursive( $field_values, $get_from_post_global_var );
|
|
|
|
$items = $this->hydrate( $submission_values );
|
|
return $items[ $this->id ];
|
|
}
|
|
|
|
/**
|
|
* Returns the submission values for the repeater.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param array $field_values
|
|
* @param bool $get_from_post_global_var
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_value_submission_recursive( $field_values, $get_from_post_global_var ) {
|
|
|
|
$items = array();
|
|
|
|
if ( isset( $this->fields ) && is_array( $this->fields ) ) {
|
|
|
|
foreach ( $this->fields as $sub_field ) {
|
|
/* @var GF_Field $sub_field */
|
|
if ( isset( $sub_field->fields ) && is_array( $sub_field->fields ) ) {
|
|
/* @var GF_Field_Repeater_Table $sub_field */
|
|
$field_items = $sub_field->get_value_submission_recursive( $field_values, $get_from_post_global_var );
|
|
} else {
|
|
$values = $sub_field->get_value_submission( $field_values, $get_from_post_global_var );
|
|
|
|
if ( is_array( $sub_field->get_entry_inputs() ) ) {
|
|
$prefix = '';
|
|
} else {
|
|
$prefix = $sub_field->id . '_';
|
|
}
|
|
|
|
if ( $sub_field instanceof GF_Field_List ) {
|
|
// List field expects an array. If we flatten the List Field values, we end up adding extra repeater items instead of populating the list.
|
|
if ( is_array( $values ) && is_array( $values[0] ) ) {
|
|
$field_items = array();
|
|
foreach ( $values as $key => $value ) {
|
|
$field_items[ $prefix . $key ] = $value;
|
|
}
|
|
} else {
|
|
$field_items = array( $prefix . '0' => $values );
|
|
}
|
|
} else {
|
|
$field_items = $this->flatten( is_array( $values ) ? $values : array( $values ), $prefix, $sub_field->is_value_submission_array() );
|
|
}
|
|
}
|
|
$items = array_merge( $items, $field_items );
|
|
}
|
|
} else {
|
|
$values = $this->get_value_submission( $field_values, $get_from_post_global_var );
|
|
$field_items = $this->flatten( $values, $this->id . '_', $this->is_value_submission_array() );
|
|
$items = array_merge( $items, $field_items );
|
|
}
|
|
|
|
return $items;
|
|
|
|
}
|
|
|
|
/**
|
|
* Utility to flatten array values recursively so they can be saved with the appropriate index.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param $array
|
|
* @param string $prefix
|
|
* @param bool $field_value_is_array
|
|
*
|
|
* @return array
|
|
*/
|
|
private function flatten( $array, $prefix = '', $field_value_is_array = false ) {
|
|
$result = array();
|
|
if ( ! is_array( $array ) ) {
|
|
return $result;
|
|
}
|
|
foreach ( $array as $key => $value ) {
|
|
if ( is_array( $value ) ) {
|
|
if ( $field_value_is_array && ! is_array( $value[0] ) ) {
|
|
$result[ $prefix . $key ] = $value;
|
|
} else {
|
|
$result = $result + $this->flatten( $value, $prefix . $key . '_' );
|
|
}
|
|
} else {
|
|
$result[ $prefix . $key ] = $value;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Returns the field inner markup.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param array $form The Form Object currently being processed.
|
|
* @param string|array $values The field values. From default/dynamic population, $_POST, or a resumed incomplete submission.
|
|
* @param null|array $entry Null or the Entry Object currently being edited.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_field_input( $form, $values = '', $entry = null ) {
|
|
|
|
if ( $this->is_form_editor() ) {
|
|
return sprintf( "<p>%s</p>", $this->label );
|
|
}
|
|
|
|
if ( empty( $values ) ) {
|
|
$values = array( '' );
|
|
}
|
|
|
|
$input_top = $this->get_input_top( $values );
|
|
|
|
$items = $this->get_input_items( $values, $entry );
|
|
|
|
$html = $input_top . $items . $this->get_input_bottom();
|
|
|
|
$max_items = intval( $this->maxItems );
|
|
|
|
return sprintf( "<div class='gfield_repeater_wrapper' data-max_items='{$max_items}'>%s</div>", $html );
|
|
}
|
|
|
|
/**
|
|
* Returns the markup for the top of the repeater container.
|
|
*
|
|
* This method must return the opening tag for the container and this tag must have the class 'gfield_repeater_container'
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param $values
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_input_top( $values ) {
|
|
$html = "<fieldset class='gfield_repeater gfield_repeater_container'>\n";
|
|
$label = esc_html( $this->label );
|
|
$html .= "<legend class='gfield_label gform-field-label'>{$label}</legend>";
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Returns the markup for the items.
|
|
*
|
|
* This method must return a single HTML element with the class 'gfield_repeater_items'. This elemment must contain
|
|
* all the items as direct children and each item must have the class 'gfield_repeater_item'.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param $values
|
|
* @param $entry
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_input_items( $values, $entry ) {
|
|
|
|
/* @var GF_Field[] $fields */
|
|
$fields = $this->fields;
|
|
|
|
$form = GFAPI::get_form( $this->formId );
|
|
|
|
$rows = '<div class="gfield_repeater_items">';
|
|
|
|
$i = 0;
|
|
foreach ( $values as $value ) {
|
|
$row = "<div class='gfield_repeater_item'>";
|
|
foreach ( $fields as $field ) {
|
|
|
|
$field_value = $this->get_field_value( $field, $value );
|
|
|
|
$field->set_context_property( 'itemIndex', $i );
|
|
|
|
$field_input = $this->get_sub_field_input( $field, $form, $field_value, $entry, $i );
|
|
|
|
$row .= "<div class='gfield_repeater_cell'>" . $field_input . '</div>';
|
|
|
|
$field->set_context_property( 'itemIndex', null );
|
|
}
|
|
$buttons = $this->get_buttons( $values );
|
|
$row .= "<div class='gfield_repeater_buttons'>{$buttons}</div>";
|
|
$row .= '</div>';
|
|
$rows .= $row;
|
|
$i++;
|
|
}
|
|
$rows .= '</div>';
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* Return the markup for the bottom of the repeater. Close the tags opened in the top.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_input_bottom() {
|
|
return '</fieldset>';
|
|
}
|
|
|
|
public function get_field_content( $value, $force_frontend_label, $form ) {
|
|
|
|
$is_form_editor = $this->is_form_editor();
|
|
$is_entry_detail = $this->is_entry_detail();
|
|
$is_admin = $is_form_editor || $is_entry_detail;
|
|
|
|
$admin_buttons = $this->get_admin_buttons();
|
|
|
|
$description = $this->get_description( $this->description, 'gfield_description' );
|
|
if ( $this->is_description_above( $form ) ) {
|
|
$clear = $is_admin ? "<div class='gf_clear'></div>" : '';
|
|
$field_content = sprintf( "%s%s{FIELD}$clear", $admin_buttons, $description );
|
|
} else {
|
|
$field_content = sprintf( "%s{FIELD}%s", $admin_buttons, $description );
|
|
}
|
|
|
|
return $field_content;
|
|
}
|
|
|
|
/**
|
|
* Returns the repeater buttons.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_buttons( $values ) {
|
|
$is_form_editor = $this->is_form_editor();
|
|
|
|
$delete_display = count( $values ) == 1 ? 'visibility:hidden;' : '';
|
|
|
|
$add_events = $is_form_editor ? '' : "onclick='gformAddRepeaterItem(this)' onkeypress='gformAddRepeaterItem(this)'";
|
|
$delete_events = $is_form_editor ? '' : sprintf( "onclick='if(confirm(\"%s\")){gformDeleteRepeaterItem(this)};' onkeypress='gformDeleteRepeaterItem(this)'", esc_js( __( 'Are you sure you want to remove this item?', 'gravityforms' ) ) );
|
|
|
|
$disabled_icon_class = ! empty( $this->maxItems ) && count( $values ) >= intval( $this->maxItems ) ? 'gfield_icon_disabled' : '';
|
|
|
|
$add_button_text = $this->addButtonText ? $this->addButtonText : '+';
|
|
$remove_button_text = $this->removeButtonText ? $this->removeButtonText : '-' ;
|
|
|
|
$add_button_class = $this->addButtonText ? 'add_repeater_item_text' : 'add_repeater_item_plus';
|
|
$remove_button_class = $this->removeButtonText ? 'remove_repeater_item_text' : 'remove_repeater_item_minus';
|
|
$html = "<button type='button' class='add_repeater_item gform-theme-button gform-theme-button--secondary gform-theme-button--size-sm {$disabled_icon_class} {$add_button_class}' {$add_events}>" . $add_button_text . "</button>" .
|
|
"<button type='button' class='remove_repeater_item gform-theme-button gform-theme-button--secondary gform-theme-button--size-sm {$remove_button_class}' {$delete_events} style='{$delete_display}'>" . $remove_button_text . "</button>";
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Gets merge tag values.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param array|string $value The value of the input.
|
|
* @param string $input_id The input ID to use.
|
|
* @param array $entry The Entry Object.
|
|
* @param array $form The Form Object
|
|
* @param string $modifier The modifier passed.
|
|
* @param array|string $raw_value The raw value of the input.
|
|
* @param bool $url_encode If the result should be URL encoded.
|
|
* @param bool $esc_html If the HTML should be escaped.
|
|
* @param string $format The format that the value should be.
|
|
* @param bool $nl2br If the nl2br function should be used.
|
|
*
|
|
* @return string The processed merge tag.
|
|
*/
|
|
public function get_value_merge_tag( $value, $input_id, $entry, $form, $modifier, $raw_value, $url_encode, $esc_html, $format, $nl2br ) {
|
|
|
|
$use_value = in_array( 'value', $this->get_modifiers() );
|
|
$use_text = ! $use_value;
|
|
|
|
if ( $format == 'html' ) {
|
|
$media = $esc_html ? 'screen' :'email';
|
|
$merge_tag = $this->get_value_entry_detail( $raw_value, $entry['currency'], $use_text, $format, $media );
|
|
} else {
|
|
$merge_tag = $this->get_value_export_recursive( $entry, $input_id, $use_text, false, 0, ' ' );
|
|
}
|
|
|
|
return $merge_tag;
|
|
}
|
|
|
|
/**
|
|
* Format the entry value safe for displaying on the entry list page.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param string $value The field value.
|
|
* @param array $entry The Entry Object currently being processed.
|
|
* @param string $field_id The field or input ID currently being processed.
|
|
* @param array $columns The properties for the columns being displayed on the entry list page.
|
|
* @param array $form The Form Object currently being processed.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_value_entry_list( $value, $entry, $field_id, $columns, $form ) {
|
|
|
|
/* translators: %d: the number of items in value of the repeater field. */
|
|
$display_value = is_array( $value ) ? sprintf( esc_html__( 'Number of items: %d' ), count( $value ) ) : '';
|
|
|
|
return $display_value;
|
|
}
|
|
|
|
/**
|
|
* Format the entry value safe for displaying on the entry detail page and for the {all_fields} merge tag.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param string|array $item_values The field value.
|
|
* @param string $currency The entry currency code.
|
|
* @param bool|false $use_text When processing choice based fields should the choice text be returned instead of the value.
|
|
* @param string $format The format requested for the location the merge is being used. Possible values: html, text or url.
|
|
* @param string $media The location where the value will be displayed. Possible values: screen or email.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_value_entry_detail( $item_values, $currency = '', $use_text = false, $format = 'html', $media = 'screen' ) {
|
|
|
|
if ( $format == 'text' ) {
|
|
return $this->get_value_export_recursive( array( $this->id => $item_values ), $this->id, $use_text, false, 0, ' ' );
|
|
}
|
|
|
|
$repeater_style = $media == 'email' ? "style='padding: 5px 0 0 15px;font-size: 14px'" : '';
|
|
$label_style = $media == 'email' ? "style='color: rgba(35, 40, 45, 1.000);font-weight:600; padding-top:10px;font-size: 14px'" : '';
|
|
$sub_field_label_style = $media == 'email' ? "style='color:rgb(155, 154, 154);padding-top:8px;font-size: 14px;'" : '';
|
|
|
|
/* @var GF_Field[] $fields */
|
|
$fields = $this->fields;
|
|
$html = "<div class='gfield_repeater' {$repeater_style}>";
|
|
$repeater_label = $this->nestingLevel === 0 ? '' : $this->label;
|
|
$html .= "<div class='gfield_label gform-field-label' {$label_style}>{$repeater_label}</div>";
|
|
$html .= '<div class="gfield_repeater_items">';
|
|
foreach ( $item_values as $item_value ) {
|
|
$html .= '<div class="gfield_repeater_item">';
|
|
foreach ( $fields as $sub_field ) {
|
|
if ( $sub_field->fields ) {
|
|
$sub_field_value = $item_value[ $sub_field->id ];
|
|
} else {
|
|
$sub_field_value = $this->get_field_value( $sub_field, $item_value );
|
|
}
|
|
$label = $sub_field->get_field_label( true, $item_values );
|
|
$label = empty( $sub_field->fields ) ? "<div class='gfield_repeater_label' {$sub_field_label_style}>{$label}</div>" : '';
|
|
$value = $sub_field->get_value_entry_detail( $sub_field_value, $currency, $use_text, 'html', $media );
|
|
$value = "<div class='gfield_repeater_value' style='color:rgba(117, 117, 117, 1);font-size: 14px'>{$value}</div>";
|
|
$html .= '<div class="gfield_repeater_cell">' . $label . $value . '</div>';
|
|
}
|
|
$html .= '</div>';
|
|
}
|
|
$html .= '</div>';
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Returns the value for a field inside a repeater.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param GF_Field $field
|
|
* @param array|string $value
|
|
*
|
|
* @return array|string
|
|
*/
|
|
public function get_field_value( $field, $value ) {
|
|
if ( $field->fields ) {
|
|
$field_value = isset( $value[ $field->id ] ) ? $value[ $field->id ] : '';
|
|
} else {
|
|
$inputs = $field->get_entry_inputs();
|
|
if ( is_array( $value ) ) {
|
|
if ( is_array( $inputs ) ) {
|
|
$field_value = array();
|
|
$field_keys = array_keys( $value );
|
|
natsort( $field_keys );
|
|
foreach ( $field_keys as $input_id ) {
|
|
if ( is_numeric( $input_id ) && absint( $input_id ) == absint( $field->id ) ) {
|
|
$val = $value[ $input_id ];
|
|
$field_value[ $input_id ] = $val;
|
|
}
|
|
}
|
|
} else {
|
|
$field_value = isset( $value[ $field->id ] ) ? $value[ $field->id ] : '';
|
|
}
|
|
} else {
|
|
$field_value = '';
|
|
}
|
|
}
|
|
|
|
return $field_value;
|
|
}
|
|
|
|
/**
|
|
* Returns the input markup for a field inside a repeater.
|
|
*
|
|
* Appends the item index to the name and id attributes and validates the value.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param GF_Field $field
|
|
* @param array $form
|
|
* @param array $field_value
|
|
* @param array $entry
|
|
* @param int $index
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function get_sub_field_input( $field, $form, $field_value, $entry, $index ) {
|
|
$field_content = $this->get_sub_field_content( $field, $field_value, $form, $entry );
|
|
|
|
// Adjust all the name attributes in the markup
|
|
preg_match_all( "/(name='input_[^\[|']*)((\[[0-9]*\])*)'/", $field_content, $matches, PREG_SET_ORDER );
|
|
|
|
$replaced = array();
|
|
foreach ( $matches as $match ) {
|
|
if ( ! in_array( $match[0], $replaced ) ) {
|
|
$input_name = str_replace( $match[1], $match[1] . "[{$index}]", $match[0] );
|
|
$field_content = str_replace( $match[0], $input_name, $field_content );
|
|
$replaced[] = $match[0];
|
|
}
|
|
}
|
|
|
|
// Adjust all the id attributes in the markup
|
|
preg_match_all( "/(id='((input|choice)_[0-9|_]*))[0-9|-]*'/", $field_content, $matches, PREG_SET_ORDER );
|
|
|
|
$replaced = array();
|
|
foreach ( $matches as $match ) {
|
|
if ( ! in_array( $match[0], $replaced ) ) {
|
|
$input_id = str_replace( $match[1], $match[1] . "-{$index}", $match[0] ) ;
|
|
$field_content = str_replace( $match[0], $input_id, $field_content );
|
|
$replaced[] = $match[0];
|
|
}
|
|
}
|
|
|
|
// Adjust all the for attributes in the markup
|
|
preg_match_all( "/(for='(input|choice)_[^\[']*)'/", $field_content, $matches, PREG_SET_ORDER );
|
|
|
|
$replaced = array();
|
|
foreach ( $matches as $match ) {
|
|
if ( ! in_array( $match[1], $replaced ) ) {
|
|
$input_id = str_replace( $match[1], $match[1] . "-{$index}", $match[0] );
|
|
$field_content = str_replace( $match[0], $input_id, $field_content );
|
|
$replaced[] = $match[1];
|
|
}
|
|
}
|
|
|
|
$target_page = rgpost( 'gform_target_page_number_' . $this->formId );
|
|
$source_page = rgpost( 'gform_source_page_number_' . $this->formId );
|
|
$validate = $source_page == $field->pageNumber && rgpost( 'is_submit_' . $this->formId ) && ( $target_page == 0 || $target_page > $source_page );
|
|
|
|
if ( $validate ) {
|
|
$field->failed_validation = false;
|
|
if ( $field->isRequired && $field->is_value_empty( $field_value ) ) {
|
|
$field->failed_validation = true;
|
|
$field->validation_message = empty( $field->errorMessage ) ? __( 'This field is required.', 'gravityforms' ) : $field->errorMessage;
|
|
}
|
|
|
|
if ( ! $field->failed_validation ) {
|
|
$field->validate( $field_value, $form );
|
|
}
|
|
|
|
$custom_validation_result = gf_apply_filters( array( 'gform_field_validation', $form['id'], $field->id ), array(
|
|
'is_valid' => $field->failed_validation ? false : true,
|
|
'message' => $field->validation_message
|
|
), $field_value, $form, $field );
|
|
$field->failed_validation = rgar( $custom_validation_result, 'is_valid' ) ? false : true;
|
|
}
|
|
|
|
$validation_message = ( $field->failed_validation && ! empty( $field->validation_message ) ) ? sprintf( "<div class='gfield_description validation_message'>%s</div>", $field->validation_message ) : '';
|
|
|
|
return $field_content . $validation_message;
|
|
}
|
|
|
|
/**
|
|
* Returns the markup for the sub field.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param GF_Field $field
|
|
* @param string|array $value
|
|
* @param array $form
|
|
* @param array $entry
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_sub_field_content( $field, $value, $form, $entry ) {
|
|
|
|
$validation_status = $field->failed_validation;
|
|
|
|
if ( empty( $field->fields ) ) {
|
|
// Validation will be handled later inside GF_Field_Repeater::get_sub_field_input so temporarily set failed_validation to false.
|
|
$field->failed_validation = false;
|
|
}
|
|
|
|
if ( ! class_exists( 'GFFormDisplay' ) ) {
|
|
require_once( GFCommon::get_base_path() .'/form_display.php' );
|
|
}
|
|
|
|
if ( $field->type === 'repeater' ) {
|
|
$field_content = GFFormDisplay::get_field_content( $field, $value, true, $form['id'], $form );
|
|
} else {
|
|
$field_content = GFFormDisplay::get_field( $field, $value, true, $form );
|
|
}
|
|
|
|
$field->failed_validation = $validation_status;
|
|
|
|
return $field_content;
|
|
}
|
|
|
|
/**
|
|
* Builds the repeater's array of items.
|
|
*
|
|
* @since 2.4
|
|
* @since 2.5 Added the $apply_filters parameter.
|
|
*
|
|
* @param $entry
|
|
* @param bool $apply_filters Whether to apply the filter_input_value filter to the entry.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function hydrate( $entry, $apply_filters = false ) {
|
|
$entry[ $this->id ] = $this->get_repeater_items( $entry, '', '', $apply_filters );
|
|
return $entry;
|
|
}
|
|
|
|
/**
|
|
* Recursively converts the repeater values from flattened values in the entry array into a multidimensional array
|
|
* of items.
|
|
*
|
|
* @since 2.4
|
|
* @since 2.5 Added the $apply_filters parameter.
|
|
*
|
|
* @param array $entry
|
|
* @param GF_Field_Repeater $repeater_field
|
|
* @param string $index
|
|
* @param bool $apply_filters Whether to apply the filter_input_value filter to the entry.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_repeater_items( &$entry, $repeater_field = null, $index = '', $apply_filters = false ) {
|
|
if ( ! $repeater_field ) {
|
|
$repeater_field = $this;
|
|
}
|
|
|
|
$items = array();
|
|
|
|
// Blank items are not stored but we need to display them if a value exists with a higher index.
|
|
$max_indexes = $this->get_max_indexes( $entry, $repeater_field, $index );
|
|
|
|
$repeater_fields = array();
|
|
|
|
$is_new_entry = empty( $_POST[ 'is_submit_' . $this->formId ] ) && ! array_key_exists( 'id', $entry );
|
|
|
|
foreach ( $repeater_field->fields as $field ) {
|
|
if ( is_array( $field->fields ) ) {
|
|
$repeater_fields[] = $field;
|
|
continue;
|
|
}
|
|
|
|
for ( $i = 0; $i <= $max_indexes[ $field->id ]; $i ++ ) {
|
|
$inputs = $field->get_entry_inputs();
|
|
|
|
if ( is_array( $inputs ) ) {
|
|
foreach ( $inputs as $input ) {
|
|
|
|
$input_id = $input['id'];
|
|
|
|
$key = $input_id . $index . ( $is_new_entry ? '' : '_' . $i );
|
|
|
|
$value = isset( $entry[ $key ] ) ? $entry[ $key ] : '';
|
|
|
|
// Don't add new item if max indexes is 0 and value is empty.
|
|
if ( $field->isRequired || $max_indexes[ $field->id ] > 0 || ( $max_indexes[ $field->id ] === 0 && $value !== '' ) ) {
|
|
if ( $apply_filters ) {
|
|
$items[ $i ][ $input_id ] = $field->filter_input_value( $value, $entry );
|
|
} else {
|
|
$items[ $i ][ $input_id ] = $value;
|
|
}
|
|
}
|
|
|
|
if ( isset( $entry[ $key ] ) ) {
|
|
unset( $entry[ $key ] );
|
|
}
|
|
}
|
|
} else {
|
|
|
|
$key = $field->id . $index . '_' . $i;
|
|
|
|
$value = isset( $entry[ $key ] ) ? $entry[ $key ] : '';
|
|
|
|
if ( $field->isRequired || $max_indexes[ $field->id ] > 0 || ( $max_indexes[ $field->id ] === 0 && $value !== '' ) ) {
|
|
if ( $apply_filters ) {
|
|
$items[ $i ][ $field->id ] = $field->filter_input_value( $value, $entry );
|
|
} else {
|
|
$items[ $i ][ $field->id ] = $value;
|
|
}
|
|
}
|
|
|
|
if ( isset( $entry[ $key ] ) ) {
|
|
unset( $entry[ $key ] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $repeater_fields ) ) {
|
|
|
|
$i = 0;
|
|
|
|
do {
|
|
$all_repeaters_have_values = true;
|
|
foreach ( $repeater_fields as $repeater ) {
|
|
$v = $this->get_repeater_items( $entry, $repeater, $index . '_' . $i );
|
|
|
|
$is_empty = $this->empty_deep( $v );
|
|
|
|
if ( ( $i == 0 || ! $is_empty ) || ( empty( $index ) && isset( $items[ $i ] ) && ! $this->empty_deep( $items[ $i ] ) ) ) {
|
|
$items[ $i ][ $repeater->id ] = $v;
|
|
}
|
|
|
|
if ( $is_empty ) {
|
|
$all_repeaters_have_values = false;
|
|
}
|
|
}
|
|
$i ++;
|
|
} while ( $all_repeaters_have_values );
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Parses all the flat entry array keys and returns the maximum index by field ID.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param array $entry The entry array
|
|
* @param GF_Field_Repeater $repeater_field The repeater field
|
|
* @param string $index The index prefix
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function get_max_indexes( $entry, $repeater_field, $index ) {
|
|
|
|
$field_ids = array_keys( $entry );
|
|
|
|
$max_indexes = array();
|
|
|
|
$matches = array();
|
|
|
|
|
|
foreach ( $repeater_field->fields as $field ) {
|
|
if ( ! isset( $matches[ $field->id ] ) ) {
|
|
$matches[ $field->id ] = array( 0 );
|
|
}
|
|
foreach ( $field_ids as $f_id ) {
|
|
if ( preg_match( "/{$field->id}[^_]*{$index}_([0-9]+)/", $f_id, $m ) ) {
|
|
$matches[ $field->id ][] = intval( $m[1] );
|
|
}
|
|
}
|
|
$max_indexes[ $field->id ] = max( $matches[ $field->id ] );
|
|
}
|
|
|
|
return $max_indexes;
|
|
}
|
|
|
|
/**
|
|
* Recursively checks whether a multi-dimensional array is empty.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param $val
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function empty_deep( $val ) {
|
|
|
|
$result = true;
|
|
|
|
if ( is_array( $val ) && count( $val ) > 0 ) {
|
|
foreach ( $val as $v ) {
|
|
$result = $result && $this->empty_deep( $v );
|
|
}
|
|
} else {
|
|
$result = empty( $val );
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns the sub-filters for the current field.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_filter_sub_filters() {
|
|
$filters = array();
|
|
$fields = $this->fields;
|
|
foreach ( $fields as $field ) {
|
|
/** @var GF_Field $field */
|
|
$filter_settings = array(
|
|
'key' => $field->id,
|
|
'text' => GFFormsModel::get_label( $field, false, true ),
|
|
);
|
|
|
|
if ( is_array( $field->fields ) ) {
|
|
$filter_settings = $field->get_filter_settings();
|
|
$filters[] = $filter_settings;
|
|
continue;
|
|
}
|
|
$sub_filters = $field->get_filter_sub_filters();
|
|
|
|
if ( ! empty( $sub_filters ) ) {
|
|
$filter_settings['group'] = true;
|
|
$filter_settings['filters'] = $sub_filters;
|
|
} else {
|
|
$filter_settings['preventMultiple'] = false;
|
|
$filter_settings['operators'] = $field->get_filter_operators();
|
|
|
|
$values = $field->get_filter_values();
|
|
if ( ! empty( $values ) ) {
|
|
$filter_settings['values'] = $values;
|
|
}
|
|
}
|
|
|
|
$values = $field->get_filter_values();
|
|
if ( ! empty( $values ) ) {
|
|
$filter_settings['values'] = $values;
|
|
}
|
|
|
|
$filters[] = $filter_settings;
|
|
}
|
|
|
|
return $filters;
|
|
}
|
|
|
|
/**
|
|
* Returns the filter settings for the current field.
|
|
*
|
|
* If overriding to add custom settings call the parent method first to get the default settings.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_filter_settings() {
|
|
|
|
$filter_settings = parent::get_filter_settings();
|
|
|
|
$filter_settings['isNestable'] = true;
|
|
|
|
return $filter_settings;
|
|
}
|
|
|
|
/**
|
|
* Format the entry value before it is used in entry exports and by framework add-ons using GFAddOn::get_field_value().
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param array $entry The entry currently being processed.
|
|
* @param string $input_id The field or input ID.
|
|
* @param bool|false $use_text When processing choice based fields should the choice text be returned instead of the value.
|
|
* @param bool|false $is_csv Is the value going to be used in the .csv entries export?
|
|
*
|
|
* @return string|array
|
|
*/
|
|
public function get_value_export( $entry, $input_id = '', $use_text = false, $is_csv = false ) {
|
|
$export = $this->get_value_export_recursive( $entry, $input_id, $use_text, $is_csv );
|
|
return $export;
|
|
}
|
|
|
|
/**
|
|
* Format the entry value before it is used in entry exports and by framework add-ons using GFAddOn::get_field_value().
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param array $entry The entry currently being processed.
|
|
* @param string $input_id The field or input ID.
|
|
* @param bool|false $use_text When processing choice based fields should the choice text be returned instead of the value.
|
|
* @param bool|false $is_csv Is the value going to be used in the .csv entries export?
|
|
*
|
|
* @return string|array
|
|
*/
|
|
public function get_value_export_recursive( $entry, $input_id = '', $use_text = false, $is_csv = false, $depth = 0, $padding = ' ' ) {
|
|
if ( empty( $input_id ) ) {
|
|
$input_id = $this->id;
|
|
}
|
|
|
|
$items = rgar( $entry, $input_id );
|
|
|
|
/* @var GF_Field[] $fields */
|
|
$fields = $this->fields;
|
|
|
|
$csv = array();
|
|
|
|
foreach ( $items as $item ) {
|
|
|
|
foreach ( $fields as $field ) {
|
|
|
|
$inputs = $field->get_entry_inputs();
|
|
|
|
if ( is_array( $inputs ) ) {
|
|
$field_value = array();
|
|
$field_keys = array_keys( $item );
|
|
foreach ( $field_keys as $input_id ) {
|
|
if ( is_numeric( $input_id ) && absint( $input_id ) == absint( $field->id ) ) {
|
|
$field_value[ $input_id ] = $item[ $input_id ];
|
|
}
|
|
}
|
|
} else {
|
|
$field_value = isset( $item[ $field->id ] ) ? $item[ $field->id ] : '';
|
|
$field_value = array( (string) $field->id => $field_value );
|
|
}
|
|
|
|
$label = str_repeat( $padding, $depth ) . GFFormsModel::get_label( $field );
|
|
|
|
if ( is_array( $field->fields ) ) {
|
|
$new_depth = $depth + 1;
|
|
$line = $label . "\n" . $field->get_value_export_recursive( $field_value, $field->id, $use_text, $is_csv, $new_depth, $padding );
|
|
if ( $depth == 0 ) {
|
|
$line .= "\n";
|
|
}
|
|
} else {
|
|
|
|
if ( 'list' === $field->get_input_type() && ! empty( $field_value[ $field->id ] ) ) {
|
|
|
|
$list_rows = maybe_unserialize( $field_value[ $field->id ] );
|
|
|
|
if ( is_array( $list_rows[0] ) ) {
|
|
$lines = array();
|
|
foreach ( $list_rows as $i => $list_row ) {
|
|
$row_label = $label . ' ' . ( $i + 1 );
|
|
|
|
// Prepare row value.
|
|
$row_value = implode( '|', $list_row );
|
|
if ( strpos( $row_value, '=' ) === 0 ) {
|
|
// Prevent Excel formulas
|
|
$row_value = "'" . $row_value;
|
|
}
|
|
|
|
$lines[] = $row_label . ': ' . $row_value;
|
|
}
|
|
$line = implode( "\n", $lines );
|
|
} else {
|
|
$value = implode( '|', $list_rows );
|
|
if ( strpos( $value, '=' ) === 0 ) {
|
|
// Prevent Excel formulas
|
|
$value = "'" . $value;
|
|
}
|
|
$line = $label . ': ' . $value;
|
|
}
|
|
|
|
} else {
|
|
$line = $label . ': ' . $field->get_value_export( $field_value, $field->id, $use_text, $is_csv );
|
|
}
|
|
|
|
}
|
|
|
|
$csv[] = $line;
|
|
}
|
|
}
|
|
|
|
return implode( "\n", $csv );
|
|
}
|
|
|
|
/**
|
|
* Store the modifiers so they can be accessed when preparing the {all_fields} and field merge tag output.
|
|
*
|
|
* @since 2.4
|
|
*
|
|
* @param array $modifiers An array of modifiers to be stored.
|
|
*/
|
|
public function set_modifiers( $modifiers ) {
|
|
parent::set_modifiers( $modifiers );
|
|
|
|
/* @var GF_Field $sub_field */
|
|
foreach ( $this->fields as $sub_field ) {
|
|
$sub_field->set_modifiers( $modifiers );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
GF_Fields::register( new GF_Field_Repeater() );
|