/* _______ __ _______ __ _______ .______ __ __ __ _______ _______. | ____|| | | ____|| | | \ | _ \ | | | | | | | ____| / | | |__ | | | |__ | | | .--. | | |_) | | | | | | | | |__ | (----` | __| | | | __| | | | | | | | / | | | | | | | __| \ \ | | | | | |____ | `----.| '--' | | |\ \----.| `--' | | `----.| |____.----) | |__| |__| |_______||_______||_______/ | _| `._____| \______/ |_______||_______|_______/ Episode IV: A new hope **************** this plugin has been manually updated to be able to used with bee editor. The modified line is below: $('.bee-page-container').length ? $first_input.parents('.bee-form-row').hide() : $first_input.parents(this.control_group_wrapper).hide(); **************** */ SITE.Field_rules_lib = (function($) { function Field_rules(){ this.field_rules = null; //object containing all the field rules for the client this.default_options = null; //object containing the full list of formatted and sorted options for each field this.usable_fields = null; //object containing all the fields from the db that are associated to field rules this.$form = null; //the form that the field rules are being applied to this.form_fields = {}; //reference to elements on the form this.applied_rules = {}; //the state for all currently applied field rules } //list of date field types that need to be converted to js dates for comparisons Field_rules.prototype.date_field_types = [ '27', //DOB_FIELD_TYPE_ID '4' //US_DATE_FIELD_TYPE_ID ]; Field_rules.prototype.control_group_wrapper = '.form-group'; Field_rules.prototype.control_wrapper = '.form-group div'; Field_rules.prototype.hidden_input_class = 'field-rule-hidden-input'; //needed for checkbox and radio fields /** * Retrieve the all the field rule related data via ajax and register all the fields on the form * @param $form_element - jquery object containing the form to preform field rules on */ Field_rules.prototype.init = function($form_element){ var supportsCors = ($.support.cors || !$.ajaxTransport || !window.XDomainRequest) ? true : false, supportsXDomain = (!$.support.cors || $.ajaxTransport || window.XDomainRequest) ? true : false, isHTTPS = window.location.protocol === 'https:', field_rules_endpoint, that = this; this.$form = $form_element; //use WebCampaigns Web servers to proxy the requests field_rules_endpoint = SITE.data.field_rules_url; $.get(field_rules_endpoint, function(data) { if (data.status === 'success') { that.field_rules = data.data.rules; that.default_options = data.data.default_options; that.usable_fields = data.data.fields; that.form_fields = that.get_form_fields(that.$form); that.attach_event_listeners(); that.process_field_rules(that.field_rules); } }, 'json' ); }; /** * Grab all the fields on the form * returns object in the form of: * { * field_id1: [html input1, html input2], * field_id2: [html input1, html input2] * } * @param {Object} $form -jquery form object containing the form * @returns {Object} object containing all the fields on the form */ Field_rules.prototype.get_form_fields = function ($form){ //find all the inputs with a name attribute on the form var $inputs_on_form = $form.find(':input[name]'), form_fields = {}, control_wrapper = this.control_wrapper, hidden_input_class = this.hidden_input_class; //index all the inputs by the form name $inputs_on_form.each(function(){ // we need to strip out [] if they trail a field name - such as 608[] var $field = $(this), field_name = $field.attr('name').replace('[]', ''), $field_container, hidden_input_html; if(!form_fields.hasOwnProperty(field_name)){ form_fields[field_name] = []; //check if this is a field type that needs to have the hidden input injected if( $field.hasClass('iqs-form-checkbox') || $field.hasClass('iqs-form-radio') || $field.hasClass('iqs-form-multi-select') ) { $field_container = $field.parents(control_wrapper); hidden_input_html = ''; $field_container.prepend(hidden_input_html); } } form_fields[field_name].push(this); }); return form_fields; }; /** * Attach event listeners to all fields that have field rules and are a part of the form */ Field_rules.prototype.attach_event_listeners = function(){ for (var field_id in this.field_rules) { if (this.is_field_on_form(field_id)){ this.attach_change_event_listener($(this.form_fields[field_id])); } } }; /** * Attach the change event to the specified field(s) * @param {Object} $field - jquery object containing the field that needs the change event attached to it */ Field_rules.prototype.attach_change_event_listener = function($field){ var that = this; $field.on('change.field_rules', function(e){ that.process_field_rules(that.field_rules); }); }; /** * Loop through all the field rules and apply the rules whose conditions have been satisfied * and removing existing rules that should no longer be applied * @param {Object} field_rules - object containing all the keyed by field_id */ Field_rules.prototype.process_field_rules = function(field_rules){ var that = this; for (var field_id in field_rules) { //only process rules if the field is on the form if(!that.is_field_on_form(field_id)){ continue; } field_rules[field_id].forEach(function(rule){ //only process rule if the dependant field is on the form if(!that.is_field_on_form(rule.dependentUsableFieldID)){ return; } //if the rule is not already applied and it should be applied then apply the rule if(!that.is_rule_already_applied(rule) && that.should_rule_be_applied(rule)){ if(!that.applied_rules.hasOwnProperty(rule.dependentUsableFieldID)){ that.applied_rules[rule.dependentUsableFieldID] = {} } that.applied_rules[rule.dependentUsableFieldID][rule.id] = rule; that.add_to_applied_rules(rule); } //if the rule already applied and it should be removed else if (that.is_rule_already_applied(rule) && !that.should_rule_be_applied(rule)){ that.remove_rule(rule); } }); } this.apply_all_rules(); }; /** * Iterate through the list of applied rules make the resulting changes to the DOM */ Field_rules.prototype.apply_all_rules = function(){ for(var field_id in this.applied_rules) { for (var rule_id in this.applied_rules[field_id]) { this.apply_rule(this.applied_rules[field_id][rule_id]); } } }; /** * Find the intersection of 2 arrays. The arrays should only contain primitives/ * @param {Array} array1 * @param {Array} array2 * @returns {Array} the array containing only elements that were a part of both arrays */ Field_rules.prototype.intersect_arrays = function (array1, array2){ return array1.filter(function(val){ return array2.indexOf(val) !== -1; }); }; /** * Returns the current value(s) for a field * @param {Number|String} field_id * @returns {Array} - array containing all the values of a field */ Field_rules.prototype.get_field_value = function (field_id){ var $field, field_value = [], dob_date = []; if(this.form_fields.hasOwnProperty(field_id)){ $field = $(this.form_fields[field_id]); if ($field.hasClass('dob-field')) { $.each($field, function() { if ($(this).val() !== '') { dob_date.push($(this).val()); } }); //if a full date was selected if (dob_date.length === 3) { field_value = [dob_date[0] + '/' + dob_date[1] + '/' + dob_date[2]]; } // post YYYY-mm-dd values as mm/dd/YYYY } else if ($field.hasClass('iqs-form-date')) { var date_string = $field.val(), date = date_string.split('-'); //if a full date was selected if (date.length === 3) { field_value = [date[1] + '/' + date[2] + '/' + date[0]]; } } else { var temp_values = $(this.form_fields[field_id]).serializeArray(); if (temp_values.length !== 0) { temp_values.forEach(function (val) { field_value.push(val.value); }); } } } return field_value; }; /** * check if a field is a date type field * @param {Number|String} field_id * @returns {boolean} */ Field_rules.prototype.is_date_field = function(field_id){ return this.date_field_types.indexOf(this.usable_fields[field_id].fieldTypeID) !== -1; }; /** * check if a field rule is already being applied * @param {Object} rule - object containing the field rule from the db * @returns {boolean} */ Field_rules.prototype.is_rule_already_applied = function (rule){ return ( this.applied_rules.hasOwnProperty(rule.dependentUsableFieldID) && this.applied_rules[rule.dependentUsableFieldID].hasOwnProperty(rule.id) ); }; /** * check if a field is on the form * @param {Number|String} field_id * @returns {boolean} */ Field_rules.prototype.is_field_on_form = function (field_id){ return this.form_fields.hasOwnProperty(field_id); }; /** * check to see if a field currently has other field rules applied * useful in determining if a hidden field should be shown due to field rules being removed * @param {Number|String} field_id * @param {String} action_type * @returns {boolean} */ Field_rules.prototype.has_other_applied_rules = function(field_id, action_type){ var has_other_applied_rules = false; if(this.applied_rules.hasOwnProperty(field_id)){ for (var rule_id in this.applied_rules[field_id]){ if (this.applied_rules[field_id][rule_id].action === action_type){ has_other_applied_rules = true; break; } } } return has_other_applied_rules; }; /** * Return any 1 of the currently applied field rules for the specified field * @param {Number|String} field_id * @returns {Object} - object containing the first field rule found */ Field_rules.prototype.get_any_applied_rule = function (field_id){ var other_applied_rule; for (var rule_id in this.applied_rules[field_id]){ //return the first applied rule we find other_applied_rule = this.applied_rules[field_id][rule_id]; break; } return other_applied_rule; }; /** * determine if a field rule's conditions are satisfied and should therefore be applied * @param {Object} rule - object containing all the field rule data * @returns {boolean} */ Field_rules.prototype.should_rule_be_applied = function (rule){ var field_values = this.get_field_value(rule.usableFieldID), that = this, apply_rule = false; //check if the field is set to one of these values if(rule.comparator === 'equal') { apply_rule = this.intersect_arrays(field_values, rule.value).length !== 0; } else if(rule.comparator === 'not_equal'){ apply_rule = this.intersect_arrays(field_values, rule.value).length === 0; } else if(rule.comparator === 'greater_than'){ apply_rule = field_values.some(function(value){ //if DOB or a Date field convert the date string to a JS Dates for comparison if(that.is_date_field(rule.usableFieldID)){ value = new Date(value); rule.value[0] = new Date(rule.value[0]); } else { value = parseFloat(value); rule.value[0] = parseFloat(rule.value[0]); } if(value > rule.value[0]){ return true; } }); } else if(rule.comparator === 'less_than'){ //check if field value is less than rule value apply_rule = field_values.some(function(value){ //if DOB or a Date field convert the date string to a JS Dates for comparison if(that.is_date_field(rule.usableFieldID)){ value = new Date(value); rule.value[0] = new Date(rule.value[0]); } else { value = parseFloat(value); rule.value[0] = parseFloat(rule.value[0]); } if(value < rule.value[0]){ return true; } }); } else if(rule.comparator === 'blank'){ //this rule type doesn't have any possible values - only need to check if the field is blank if(field_values.length !== 0){ //the field does have values so check if all of them are blank apply_rule = field_values.every(function(value){ return (value.trim() === ''); }); } else{ //no values - so the field is blank apply_rule = true; } } else if(rule.comparator === 'not_blank'){ //this rule type doesn't have any possible values - only need to check if the field is not blank if(field_values.length !== 0){ //the field does have values so check if at least one of them isn't blank apply_rule = field_values.some(function(value){ if(value.trim() !== ''){ return true; } }); } else{ //no values - so the field is blank apply_rule = false; } } return apply_rule; }; /** * Add a field rule to the list of currently applied rules * @param rule - object - the field rule to add */ Field_rules.prototype.add_to_applied_rules = function(rule){ //add the rule to the list of applied rules if (!this.applied_rules.hasOwnProperty(rule.dependentUsableFieldID)){ this.applied_rules[rule.dependentUsableFieldID] = {}; } this.applied_rules[rule.dependentUsableFieldID][rule.id] = rule; }; /** * Make the DOM changes necessary for the rule to be applied * @param rule */ Field_rules.prototype.apply_rule = function(rule){ //do the dom manipulations as a result of the field rule being applied if (rule.action === 'hide') { this.hide_field(rule.dependentUsableFieldID); } else if (rule.action === 'show') { this.show_field(rule.dependentUsableFieldID); } else if (rule.action === 'limit_options') { this.limit_options(rule.dependentUsableFieldID, rule.options, false); } }; /** * Remove the rule and make any necessary updates to the DOM (re-show a previously hidden field) * @param {Object} rule - object containing all the field rule data */ Field_rules.prototype.remove_rule = function(rule){ var other_applied_rule, use_default_options = false; if ( this.applied_rules.hasOwnProperty(rule.dependentUsableFieldID) && this.applied_rules[rule.dependentUsableFieldID].hasOwnProperty(rule.id) ){ delete this.applied_rules[rule.dependentUsableFieldID][rule.id]; } if (rule.action === 'hide'){ //if there are no other applied rules stating to hide this field, then we need to show it if(! this.has_other_applied_rules(rule.dependentUsableFieldID, 'hide') ) { this.show_field(rule.dependentUsableFieldID); } } else if (rule.action === 'limit_options'){ if(this.has_other_applied_rules(rule.dependentUsableFieldID, 'limit_options') ) { //ensure the field is visible this.show_field(rule.dependentUsableFieldID); //get any of the other applied rules for this field other_applied_rule = this.get_any_applied_rule(rule.dependentUsableFieldID); this.limit_options(rule.dependentUsableFieldID, other_applied_rule.options, use_default_options); } else { //field should be reverted back to the default set of options use_default_options = true; this.limit_options(rule.dependentUsableFieldID, [], use_default_options); } } }; /** * Hide a field and it's parent elements. * This done when a field rule's action is to hide a field, or if there are no options after limit_options action * @param {Number|String} field_id */ Field_rules.prototype.hide_field = function (field_id){ var $field_inputs = $(this.form_fields[field_id]), $first_input = $field_inputs.first(), isBeeForm = $('.bee-page-container').length ? true : false; // some of these fields have additional labels/inputs to hide when the form is a bee form if (isBeeForm) { $first_input.parents('.bee-form-row').hide() if ($first_input.hasClass('iqs-form-phone-number')) { $('#' + $first_input.attr('id') + '-country-code').parents('.bee-form-row').hide(); $('#' + $first_input.attr('id') + '-text-opt-in').parents('.bee-form-row').hide(); } else if ($first_input.hasClass('bee_high_school_ceeb')) { $('#beeHighSchoolButton').parents('.bee-form-row').hide(); } else if ($first_input.hasClass('bee_college_ceeb')) { $('#beeCollegeButton').parents('.bee-form-row').hide(); } } else { $first_input.parents(this.control_group_wrapper).hide(); } // temporarily hide any required class but be prepared to add it back if we have to reshow this field if ($first_input.hasClass('required')) { $field_inputs.removeClass('required').addClass('required-hidden'); } }; /** * Show a field and it's parent elements. * This done when fields rules that were hiding a field are no longer applied * @param {Number|String} field_id */ Field_rules.prototype.show_field = function(field_id){ var $field_inputs = $(this.form_fields[field_id]), $first_input = $field_inputs.first(), isBeeForm = $('.bee-page-container').length ? true : false; // some of these fields have additional labels/inputs to hide when the form is a bee form if (isBeeForm) { $first_input.parents('.bee-form-row').show() if ($first_input.hasClass('iqs-form-phone-number')) { $('#' + $first_input.attr('id') + '-country-code').parents('.bee-form-row').show(); $('#' + $first_input.attr('id') + '-text-opt-in').parents('.bee-form-row').show(); } else if ($first_input.hasClass('bee_high_school_ceeb')) { $('#beeHighSchoolButton').parents('.bee-form-row').show(); } else if ($first_input.hasClass('bee_college_ceeb')) { $('#beeCollegeButton').parents('.bee-form-row').show(); } } else { $first_input.parents(this.control_group_wrapper).show(); } // reshow any required fields that were previously hidden if ($first_input.hasClass('required-hidden')) { $field_inputs.removeClass('required-hidden').addClass('required'); } }; /** * Limit the set of options available for select, multi-select, checkbox, and radio fields * @param {Number|String} field_id * @param {Array} options - the list of options to be available for a field * @param {boolean} use_default_options - if true the default list of options for the field should be used */ Field_rules.prototype.limit_options = function (field_id, options, use_default_options){ var $field = $(this.form_fields[field_id]); if(!use_default_options) { // intersect this rule's list of options with the list of rules from other applied rules options = this.get_field_option_list(field_id, options); options = this.format_options(this.default_options[field_id], options) } else { options = this.default_options[field_id] } if ($field.is('select')) { this.update_select_options(field_id, options); } else if ($field.hasClass('iqs-form-checkbox') || $field.hasClass('iqs-form-radio')) { this.update_checkbox_and_radio_options(field_id, options); } }; /** * Return the list of options for a field after considering all other applied rules that limit options for the field * @param {Number|String} field_id * @param {Array} options - list of options to start filtering * @returns {Array} - array containing the filtered list of options based on the applied field rules */ Field_rules.prototype.get_field_option_list = function(field_id, options){ if (this.applied_rules.hasOwnProperty(field_id)){ for (var rule_id in this.applied_rules[field_id]) { if (this.applied_rules[field_id].hasOwnProperty(rule_id)) { options = this.intersect_arrays( this.applied_rules[field_id][rule_id].options, options ); } } } return options; }; /** * Update the DOM to display the specified list of options for a select field * @param {Number|String} field_id * @param {Array} options - list of options to apply to the select field */ Field_rules.prototype.update_select_options = function (field_id, options){ var $field = $(this.form_fields[field_id]).first(), old_value = $field.val(), //save the current value and will try to reselect it after new options are set old_blank_value_text = $field.find('option[value=""]').text(), option_html = '', is_multi_select = $field.hasClass('iqs-form-multi-select'), trigger_change_event = true; //should we trigger a field change event if(options.length === 0){ this.hide_field(field_id); if (is_multi_select && old_value) { //check if there is at least one non blank value selected in the multi-select trigger_change_event = old_value.some(function(option){ return option !== ''; }); } else { trigger_change_event = (old_value !== ''); } //update the options for the select field option_html += ''; $field.html(option_html); //try to reselect the original value $field.val(''); } else { if (!is_multi_select) { trigger_change_event = (old_value !== ''); option_html += ''; } //loop over the passed in formatted options and build the options html options.forEach(function (option) { for (var option_id in option) { if (option.hasOwnProperty(option_id)) { if (option[option_id] instanceof Array) { option_html += ''; option[option_id].forEach(function (nested_option) { for (var nested_option_id in nested_option) { if (nested_option.hasOwnProperty(nested_option_id)) { option_html += ''; if (old_value == nested_option_id) { trigger_change_event = false; } } } }); option_html += ''; } else { option_html += ''; if (old_value == option_id) { trigger_change_event = false; } } } } }); //update the options for the select field $field.html(option_html); //try to reselect the original value $field.val(old_value); this.show_field(field_id); } //if the selected value has changed trigger a change event if (trigger_change_event) { $field.trigger('change.field_rules'); } }; /** * Update the DOM to display the specified list of options for a checkbox group or radio group field * @param {Number|String} field_id * @param {Array} options - list of options to apply to the select field */ Field_rules.prototype.update_checkbox_and_radio_options = function(field_id, options){ var $field_items = $(this.form_fields[field_id]), that = this; if(options.length === 0){ //hide the field this.hide_field(field_id); //uncheck all the selected items $field_items.each(function (index, input){ var $input = $(input); if($input.is(':checked')) { $input.attr('checked', false); $input.trigger('change.field_rules'); } }); } else { $field_items.each(function() { var input_value = $(this).val(); if(that.is_valid_checkbox_radio_option(options, input_value)){ that.show_checkbox_radio_label(field_id, input_value) } else { that.hide_checkbox_radio_label(field_id, input_value) } }); this.show_field(field_id); } }; /** * Return true if a specified value is the array of options * * @param options * @param value * @return {boolean} */ Field_rules.prototype.is_valid_checkbox_radio_option = function(options, value){ return options.some(function(option){ if(option.hasOwnProperty(value)){ return true; } }); }; /** * Uncheck a specific option of a checkbox/radio field and hide it * @param field_id * @param value */ Field_rules.prototype.hide_checkbox_radio_label = function(field_id, value){ var $field_inputs = $(this.form_fields[field_id]), $input = $field_inputs.filter('[value="' + value + '"]'); if($input.is(':checked')) { $input.attr('checked', false); $input.trigger('change.field_rules'); } $input.parent('label').hide(); }; /** * Show a specific option of a checkbox/radio field * @param field_id * @param value */ Field_rules.prototype.show_checkbox_radio_label = function(field_id, value){ var $field_inputs = $(this.form_fields[field_id]), $input = $field_inputs.filter('[value="' + value + '"]'); $input.parent('label').show(); }; /** * return the formatted and sorted list of options based on the array of filtered options * @param {Array} default_formatted_options - array of default options for the the field * @param {Array} filtered_options - array containing the filtered list of options * @returns {Array} - the filtered list of options after being sorted and formatted */ Field_rules.prototype.format_options = function (default_formatted_options, filtered_options){ var formatted_options = []; //loop through the default options and populate the formatted_options which will sort them as well default_formatted_options.forEach(function(option, index){ //check if this option is in the filtered list of options for(var option_id in option){ //check if this is a nested option if(option[option_id] instanceof Array){ var temp = []; temp[option_id] = []; option[option_id].forEach(function(nested_option, nested_index){ if(filtered_options.indexOf(nested_option) !== -1){ //the option was in the list, so add it to the temp array temp[option_id].push(nested_option); } }); if(temp[option_id].length !== 0){ formatted_options.push(temp); } } else { if (filtered_options.indexOf(option_id) !== -1) { //the option was in the list, so add it to the formatted list formatted_options.push(option); } } } }); return formatted_options; }; return Field_rules; }(jQuery));