-
-
Save spivurno/1da47636008cd90a8893 to your computer and use it in GitHub Desktop.
<?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 <[email protected]> | |
* @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 | |
) ); |
@ikantarellis, here is the example you needed:
http://www.screencast.com/t/yxYtQ4tl
Thanks for the code snippet. Your snippets have helped me a lot! This snippet works exactly as expected on the front-end. However there's a problem with storing the values when submitted.
I have a multi-page form and when the user press "next" the first value in the chain are stored as expected but the second value (and possibly subsequent values) in the chain is forgotten going back to it's unselected state.
Thanks in advance.
@tareqhi
Thanks for the video tutorial! It's really helpful!
Video isn't working... Having trouble implementing it and would really like to see this in action!
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
Where should this code be put or how is this called within the existing php files for Gravity Forms?
Is it possible to give an example of how to use this?
Thanks in advance
Iosif