Skip to content

Instantly share code, notes, and snippets.

@spivurno
Last active October 11, 2020 19:55
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save spivurno/1da47636008cd90a8893 to your computer and use it in GitHub Desktop.
Save spivurno/1da47636008cd90a8893 to your computer and use it in GitHub Desktop.
Gravity Wiz // Gravity Forms // Chained Selects for List Fields
<?php
/**
* Gravity Wiz // Gravity Forms // Chained Selects for List Fields
*
* Convert List Field inputs into selects and allow them to be chained.
*
* @version 1.13
* @author David Smith <david@gravitywiz.com>
* @license GPL-2.0+
* @link http://gravitywiz.com/...
* @copyright 2015 Gravity Wiz
*/
class GW_List_Field_Chained_Selects {
private $_choices = null;
private $_enable_product = null;
private $_product_id = null;
private $_input_counter = 0;
protected static $is_script_output = false;
public function __construct( $args = array() ) {
// set our default arguments, parse against the provided arguments, and store for use throughout the class
$this->_args = wp_parse_args( $args, array(
'form_id' => false,
'field_id' => false
) );
// do version check in the init to make sure if GF is going to be loaded, it is already loaded
add_action( 'init', array( $this, 'init' ) );
}
function init() {
// make sure we're running the required minimum version of Gravity Forms
if( ! property_exists( 'GFCommon', 'version' ) || ! version_compare( GFCommon::$version, '1.8', '>=' ) ) {
return;
}
// rendering
add_filter( 'gform_form_post_get_meta', array( $this, 'add_product_field' ) );
add_filter( 'gform_pre_render', array( $this, 'load_form_script' ) );
add_filter( 'gform_register_init_scripts', array( $this, 'add_init_script' ) );
add_filter( 'gform_column_input', array( $this, 'modify_list_field_input_type' ), 10, 6 );
add_filter( 'gform_list_field_parameter_delimiter', array( $this, 'set_custom_list_field_delimiter' ), 10, 2 );
// submission
add_filter( 'gform_product_info', array( $this, 'add_list_field_products' ), 20, 3 );
// magic
add_action( 'wp_ajax_gwlfcs_get_next_chained_select_choices', array( $this, 'ajax_get_next_chained_select_choices' ) );
add_action( 'wp_ajax_nopriv_gwlfcs_get_next_chained_select_choices', array( $this, 'ajax_get_next_chained_select_choices' ) );
}
function load_form_script( $form ) {
if( $this->is_applicable_form( $form ) && ! has_action( 'wp_footer', array( __class__, 'output_script' ) ) ) {
add_action( 'wp_footer', array( __class__, 'output_script' ) );
add_action( 'gform_footer', array( __class__, 'output_script' ) );
}
return $form;
}
static function output_script() {
?>
<script type="text/javascript">
( function( $ ) {
window.GWListFieldChainedSelects = function( args ) {
var self = this;
// copy all args to current object: (list expected props)
for( prop in args ) {
if( args.hasOwnProperty( prop ) ) {
self[prop] = args[prop];
}
}
var $form = $( '#gform_' + self.formId ),
$field = $( '#field_' + self.formId + '_' + self.fieldId ),
$product = $( '#ginput_base_price_' + self.formId + '_' + self.productFieldId );
self.init = function() {
$form.on( 'submit', function() {
$field.find( 'select' ).prop( 'disabled', false );
} );
$field.on( 'change', self.getSelectSelectors().join( ',' ), function() {
var $select = $( this ),
inputId = self.getInputId( $select );
self.populateNextChoices( inputId, $select.val(), $select );
self.updatePricing();
//$( document ).trigger( 'gform_post_conditional_logic' );
} );
gform.addFilter( 'gform_list_item_pre_add', function( $clone ) {
var selectors = self.getSelectSelectors();
selectors.shift();
$clone.find( selectors.join( ',' ) ).prop( 'disabled', true );
return $clone;
} );
$field.find( 'img.delete_list_item' ).data( 'onclick', $field.find( 'img.delete_list_item' ).attr( 'onclick' ) ).attr( 'onclick', '' );
$field.on( 'click', 'img.delete_list_item', function() {
$( this ).parents( '.gfield_list_group' ).detach();
self.updatePricing();
eval( $field.find( 'img.delete_list_item' ).data( 'onclick' ) );
} );
self.initSelects();
self.updatePricing();
$( document ).bind( 'gform_post_conditional_logic', function() {
self.updatePricing();
} );
};
self.getInputId = function( $select ) {
var $parent = $select.parents( '.gfield_list_cell' ),
$row = $parent.parents( '.gfield_list_group' ),
inputId = $row.find( '.gfield_list_cell' ).index( $parent ) + 1;
return inputId;
};
self.populateNextChoices = function( inputId, selectedValue, $select ) {
var nextInputId = self.getNextInputId( inputId ),
$nextSelect = self.$selects( $select ).filter( '.gfield_list_' + self.fieldId + '_cell' + nextInputId + ' select' );
// if there is no $nextSelect, we're at the end of our chain
if( $nextSelect.length <= 0 ) {
self.resetSelects( $select, true );
return;
} else {
self.resetSelects( $select );
}
if( ! selectedValue ) {
return;
}
var loadingText = 'Loading',
$loadingOption = $( '<option value="">' + loadingText + '...</option>' ),
dotCount = 2,
loadingInterval = setInterval( function() {
$loadingOption.text( loadingText + ( new Array( dotCount ).join( '.' ) ) );
dotCount = dotCount > 3 ? 0 : dotCount + 1;
}, 250 );
$loadingOption.prependTo( $nextSelect ).prop( 'selected', true );
$nextSelect.css( { minWidth: $nextSelect.width() } );
$loadingOption.text( loadingText + '.' );
$.post( self.ajaxUrl, {
action: 'gwlfcs_get_next_chained_select_choices',
input_id: inputId,
next_input_id: self.getNextInputId( inputId ),
form_id: self.formId,
field_id: self.fieldId,
value: self.getChainedSelectsValue( $select )
}, function( response ) {
clearInterval( loadingInterval );
$loadingOption.remove();
if( ! response ) {
return;
}
var choices = $.parseJSON( response ),
optionsMarkup = '';
$nextSelect.find( 'option:not(:first)' ).remove();
if( choices.length <= 0 ) {
self.resetSelects( $select, true );
} else {
$.each( choices, function( i, choice ) {
optionsMarkup += '<option value="' + choice.value + '">' + choice.text + '</option>';
} );
$nextSelect.show().append( optionsMarkup );
// the placeholder will be selected by default, rather than removing it and re-adding, just force the noOptions option to be selected
if( choices[0].noOptions ) {
var $noOption = $nextSelect.find( 'option:last-child' ).clone(),
$nextSelects = $nextSelect.parents( 'span' ).nextAll().find( 'select' );
$nextSelects.append( $noOption );
$nextSelects.add( $nextSelect )
.addClass( 'gf_no_options' )
.find( 'option:last-child' )
.prop( 'selected', true );
} else {
$nextSelect
.removeClass( 'gf_no_options' )
.prop( 'disabled', false );
}
}
} );
};
self.getChainedSelectsValue = function( $select ) {
var value = {};
self.$selects( $select ).each( function() {
var inputId = self.getInputId( $( this ) );
value[ inputId ] = $( this ).val();
} );
return value;
};
self.getNextInputId = function( currentInputId ) {
var nextInputIndex = self.getInputIndex( currentInputId ) + 1;
return self.columns[ nextInputIndex ];
};
self.getInputIndex = function( inputId ) {
var index = [];
$.each( self.columns, function( key, value ) {
index[ value ] = key;
} );
return index[ inputId ];
};
self.initSelects = function( $selects ) {
if( typeof $selects == 'undefined' ) {
$selects = self.$selects();
}
$selects.filter( function() {
return $( this ).hasClass( 'gf_no_options' ) || $( this ).find( 'option' ).length <= 1 || $( this ).find( 'option' ).length == $( this ).find( 'option[value=""]' ).length;
} ).prop( 'disabled', true );
};
self.resetSelects = function( $currentSelect ) {
var currentInputId = self.getInputId( $currentSelect ),
currentInputIndex = self.getInputIndex( currentInputId ),
$nextSelects = self.$selects( $currentSelect ).filter( ':gt(' + currentInputIndex + ')' );
$nextSelects
.prop( 'disabled', true )
.find( 'option:not(:first)' )
.remove()
.val( '' )
.change();
};
self.getSelectSelectors = function() {
var selectors = [];
for( var i = 0; i < self.columns.length; i++ ) {
selectors.push( '.gfield_list_' + self.fieldId + '_cell' + self.columns[i] + ' select' );
}
return selectors;
};
self.$selects = function( $currentSelect ) {
var $parent = $field;
// if current select is provided, find selects of the current row only
if( typeof $currentSelect != 'undefined' ) {
$parent = $currentSelect.parents( '.gfield_list_group' );
}
return $parent.find( self.getSelectSelectors().join( ',' ) );
};
self.updatePricing = function() {
var total = 0;
if( $field.css( 'display' ) != 'none' ) {
var $inputs = $field.find( 'input[name="input_' + self.fieldId + '[]"], select[name="input_' + self.fieldId + '[]"]' );
$inputs.each( function( i, input ) {
var value = $( input ).val(),
bits = value.split( '|' ),
price = bits[1] ? parseFloat( bits[1] ) : 0;
total += price;
} );
}
if( $product.val() != total ) {
$product.val( total ).change();
gformCalculateTotalPrice( self.formId );
}
};
self.init();
};
} )( jQuery );
</script>
<?php
self::$is_script_output = true;
}
function add_init_script( $form ) {
if( ! $this->is_applicable_form( $form ) ) {
return;
}
$args = array(
'formId' => $this->_args['form_id'],
'fieldId' => $this->_args['field_id'],
'columns' => $this->_args['columns'],
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'productFieldId' => $this->_product_id,
);
$script = 'new GWListFieldChainedSelects( ' . json_encode( $args ) . ' );';
$slug = implode( '_', array( 'gw_list_field_chained_selects', $this->_args['form_id'], $this->_args['field_id'] ) );
GFFormDisplay::add_init_script( $this->_args['form_id'], $slug, GFFormDisplay::ON_PAGE_RENDER, $script );
}
function modify_list_field_input_type( $input, $field, $column, $value, $form_id, $input_id /* aka $column_index */ ) {
if( ! $this->is_applicable_field( $field ) ) {
return $input;
}
$this->_input_counter++;
$row = ceil( $this->_input_counter / count( $field->choices ) );
$full_chain_value = $this->get_chain_value_by_row( $field, $row );
$input_id = $this->get_column_index( $column, $field );
if( $this->is_applicable_input( $input_id, $field ) ) {
$choices = $this->get_input_choices( $full_chain_value, $input_id );
$no_options = empty( $choices );
if( $no_options ) {
array_unshift( $choices, array(
'text' => __( 'No options' ),
'value' => '',
'isSelected' => true,
'noOptions' => true,
) );
}
array_unshift( $choices, array(
'text' => sprintf( __( 'Select a %s' ), $column ),
'value' => '',
'isSelected' => ! $no_options,
) );
$input = array(
'type' => 'select',
'choices' => $choices
);
}
return $input;
}
function get_chain_value_by_row( $field, $row ) {
$full_list_value = GFFormsModel::get_field_value( $field );
if( empty( $full_list_value ) ) {
$return = array_map(
function( $value ) {
return '';
},
array_flip( $this->_args['columns'] )
);
} else {
$return = array_values( $full_list_value[ $row - 1 ] );
}
return $return;
}
function is_applicable_form( $form ) {
$form_id = isset( $form['id'] ) ? $form['id'] : $form;
return $form_id == $this->_args['form_id'];
}
function is_applicable_field( $field ) {
return $field->id == $this->_args['field_id'] && $this->is_applicable_form( $field->formId );
}
function is_applicable_input( $index, $field ) {
return $this->is_applicable_field( $field ) && in_array( $index, $this->_args['columns'] );
}
function get_input_choices( $chain_value, $input_id = false, $depth = false, $choices = null, $full_chain_value = null ) {
$full_chain_value = $full_chain_value !== null ? $full_chain_value : $chain_value;
$value = array_shift( $chain_value );
$index = $input_id ? $this->get_input_index( $input_id ) : 0;
$depth = $depth ? $depth : 0;
$choices = $choices !== null ? $choices: $this->get_choices();
$input_choices = array();
if ( $depth == $index ) {
$input_choices = $choices;
} else {
foreach ( $choices as $choice ) {
if ( $choice['value'] == $value ) {
$input_choices = $this->get_input_choices( $chain_value, $input_id, $depth + 1, isset( $choice['choices'] ) ? $choice['choices'] : array(), $full_chain_value );
break;
}
}
}
if ( empty( $input_choices ) ) {
if( $this->get_previous_input_value( $input_id, $full_chain_value ) ) {
$input_choices = array(
array(
'text' => __( 'No options' ),
'value' => '',
'isSelected' => true,
'noOptions' => true,
)
);
}
}
return $input_choices;
}
function get_choices() {
if( $this->_choices != null ) {
return $this->_choices;
}
$choices = $this->_args['choices'];
if( ! is_array( $choices ) ) {
$form = GFAPI::get_form( $this->_args['form_id'] );
$field = GFFormsModel::get_field( $form, $choices );
if( is_callable( array( $field, 'get_input_type' ) ) && $field->get_input_type() == 'html' ) {
$choices = $this->convert_string_to_choices( $field->content );
} else {
$choices = $field['choices']; // $field->choices; @todo
}
}
$this->_choices = $choices;
return $this->_choices;
}
function convert_string_to_choices( &$string, $depth = 0 ) {
if( is_array( $string ) ) {
$lines = &$string;
} else {
$lines = explode( "\n", $string );
}
$choices = array();
while( count( $lines ) > 0 ) {
$line = reset( $lines );
$dash_count = $this->get_dash_count( $line );
if( $dash_count > $depth ) {
$choices[ count( $choices ) - 1 ]['choices'] = $this->convert_string_to_choices( $lines, $dash_count );
} else if( $dash_count < $depth ) {
break;
} else {
// remove current line
array_shift( $lines );
$cleaned = trim( $line, ' -' );
list( $text, $value, $price ) = array_pad( explode( '|', $cleaned ), 3, false );
if( ! $value ) {
$value = $text;
}
if( $price ) {
$value .= '|' . $price;
$this->_enable_product = true; // used to flag the addition of a hidden product field for displaying list field total on the frontend
}
$choices[] = array(
'text' => $text,
'value' => $value,
'price' => $price,
);
}
}
return $choices;
}
function get_input_index( $input_id ) {
$index = array_flip( $this->_args['columns'] );
return $index[ $input_id ];
}
function get_previous_input_value( $current_input_id, $full_chain_value ) {
$current_input_index = $this->get_input_index( $current_input_id );
$prev_input_index = $current_input_index - 1;
$prev_input_id = $this->_args['columns'][ $prev_input_index ];
return $full_chain_value[ $prev_input_id ];
}
function modify_submitted_data( $form ) {
if( ! $this->is_applicable_form( $form ) ) {
return;
}
}
function add_product_field( $form ) {
if( GFCommon::is_form_editor() || ! $this->is_applicable_form( $form ) ) {
return $form;
}
// avoid infinite recursion issue
remove_filter( 'gform_form_post_get_meta', array( $this, 'add_product_field' ) );
$is_product_mode_enabled = $this->is_product_mode_enabled();
add_filter( 'gform_form_post_get_meta', array( $this, 'add_product_field' ) );
if( ! $is_product_mode_enabled || ! $this->is_applicable_form( $form ) ) {
return $form;
}
$ids = wp_list_pluck( $form['fields'], 'id' );
$this->_product_id = max( $ids ) + 1;
$label = __( 'Hidden Product Field for List Field Products' );
$product_field = new GF_Field_HiddenProduct( array(
'id' => $this->_product_id,
'type' => 'product',
'inputType' => 'hiddenproduct',
'label' => $label,
'basePrice' => 0,
'conditionalLogic' => 0, // @todo copy from list field
'inputs' => array(
array(
'id' => $this->_product_id . '.1',
'label' => $label,
'name' => ''
),
array(
'id' => $this->_product_id . '.2',
'label' => sprintf( '%s ( %s )', $label, __( 'Price' ) ),
'name' => ''
),
array(
'id' => $this->_product_id . '.3',
'label' => sprintf( '%s ( %s )', $label, __( 'Quantity' ) ),
'name' => ''
)
),
) );
$form['fields'][] = $product_field;
return $form;
}
function add_list_field_products( $products, $form, $entry ) {
if( ! $this->is_applicable_form( $form ) || ! $this->is_product_mode_enabled() ) {
return $products;
}
/**
* Add each List field row as a product
*/
foreach( $form['fields'] as $field ) {
if( ! $this->is_applicable_field( $field ) ) {
continue;
}
$value = $this->get_stashed_list_field_value( $entry['id'], $field->id, rgar( $entry, $field->id ) );
if( ! $value ) {
continue;
}
$groups = maybe_unserialize( $value );
foreach( $groups as $group_index => $group ) {
$group_total = 0;
$group_product = array();
foreach( $group as $value ) {
list( $text, $price ) = array_pad( explode( '|', $value ), 2, false );
if( $price ) {
$group_total += $price;
}
}
if( $group_total > 0 ) {
$group_product = array(
'name' => implode( ' / ', $this->remove_prices( array_filter( $group ) ) ),
'price' => $group_total,
'quantity' => 1
);
}
$group_id = sprintf( '%d.%d', $field->id, $group_index );
$products['products'] = array( $group_id => $group_product ) + $products['products'];
}
}
/**
* Remove Placeholder Product
*/
unset( $products['products'][ $this->_product_id ] );
return $products;
}
function get_stashed_list_field_value( $entry_id, $field_id, $default_value = array() ) {
global $_gform_lead_meta;
if( $entry_id == null ) {
return $default_value;
}
$key = 'gwlfcs_stashed_list_field_value_' . $field_id;
$value = gform_get_meta( $entry_id, $key );
if( $value === false ) {
gform_add_meta( $entry_id, $key, $default_value );
if( isset( $_gform_lead_meta[ $entry_id . '_' . $key ] ) ) {
unset( $_gform_lead_meta[ $entry_id . '_' . $key ] );
}
GFAPI::update_entry_field( $entry_id, $field_id, null ); // delete list field value from the entry
$value = $default_value;
}
return $value;
}
function get_column_index( $column, $field ) {
$column_index = 1;
if ( is_array( $field->choices ) ) {
foreach ( $field->choices as $choice ) {
if ( $choice['text'] == $column ) {
break;
}
$column_index ++;
}
}
return $column_index;
}
public function remove_prices( $group ) {
foreach( $group as &$value ) {
list( $text, $price ) = array_pad( explode( '|', $value ), 2, false );
$value = $text;
}
return $group;
}
public function is_product_mode_enabled() {
if( $this->_enable_product == null ) {
// get_choices() will set the _enable_product flag
$this->get_choices();
}
return $this->_enable_product;
}
public function get_dash_count( $string ) {
$chars = str_split( $string );
$count = 0;
foreach( $chars as $char ) {
if( $char == '-' ) {
$count++;
} else {
break;
}
}
return $count;
}
public function set_custom_list_field_delimiter( $delimiter, $field ) {
if( $this->is_applicable_field( $field ) ) {
$delimiter = '||';
}
return $delimiter;
}
public function ajax_get_next_chained_select_choices() {
$form_id = rgpost( 'form_id' );
$field_id = rgpost( 'field_id' );
$form = GFAPI::get_form( $form_id );
$field = GFFormsModel::get_field( $form, $field_id );
if( ! $this->is_applicable_field( $field ) ) {
return;
}
$next_input_id = rgpost( 'next_input_id' );
$value = rgpost( 'value' );
$choices = $next_input_id ? $this->get_input_choices( $value, $next_input_id ) : array();
die( json_encode( $choices ) );
}
}
# Configuration
new GW_List_Field_Chained_Selects( array(
'form_id' => 1148,
'field_id' => 2,
'columns' => array( 2, 3, 4 ),
'choices' => 1, // takes a field ID or array of choices
) );
@michaeledi
Copy link

michaeledi commented Apr 25, 2018

@tareqhi
Thanks for the video tutorial! It's really helpful!

@yellowstonebum
Copy link

Video isn't working... Having trouble implementing it and would really like to see this in action!

@sidouglas
Copy link

Can't PR a gist, but please update L:62
add_action( 'gform_preview_footer', array( __class__, 'output_script' ) );
there is no longer a hook named: gform_footer

@src1988
Copy link

src1988 commented Dec 3, 2018

Where should this code be put or how is this called within the existing php files for Gravity Forms?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment