diff --git a/components/textfield.inc b/components/textfield.inc index 11e11ab..2a04061 100644 --- a/components/textfield.inc +++ b/components/textfield.inc @@ -161,7 +161,7 @@ $element['#maxlength'] = $component['extra']['maxlength']; } - if (isset($value)) { + if (isset($value[0])) { $element['#default_value'] = $value[0]; } diff --git a/css/webform-admin.css b/css/webform-admin.css index 793e25f..396df34 100644 --- a/css/webform-admin.css +++ b/css/webform-admin.css @@ -117,3 +117,69 @@ #webform-components tr.webform-add-form { background-color: inherit; } + +/* Conditionals */ +.webform-conditional, +.webform-conditional-new { + display: block; + position: relative; + padding-left: 1em; + padding-right: 8em; + max-width: 500px; +} + +.webform-conditional-new { + text-align: right; + margin-left: 8em; +} + +.webform-conditional-if { + position: absolute; + left: -.5em; + margin-top: .2em; +} + +.webform-conditional-rule { + margin: .5em 0; +} + +.webform-conditional-condition { + display: inline; +} + +.webform-conditional-operations { + position: absolute; + right: 0; + margin-top: .2em; +} + +.webform-conditional-new input.form-submit, +.webform-conditional-operations input.form-submit { + margin: 0 2px; + padding: 2px 6px; +} + +#webform-conditionals-table input.progress-disabled, +.webform-conditional-operations input.progress-disabled { + float: none; +} + +#webform-conditionals-table .ahah-progress-throbber { + float: none; + display: inline; +} + +#webform-conditionals-table .ahah-progress-throbber .throbber { + float: none; + display: inline; + padding-right: 12px; +} + +.webform-conditional-andor { + display: inline; +} + +.webform-conditional-andor .form-item { + margin: 0; + padding: 0; +} diff --git a/includes/webform.components.inc b/includes/webform.components.inc index 2bc6134..ad1f3b1 100644 --- a/includes/webform.components.inc +++ b/includes/webform.components.inc @@ -829,7 +829,7 @@ $submissions = webform_get_submissions($node->nid); foreach ($submissions as $submission) { if (isset($submission->data[$component['cid']])) { - webform_component_invoke($component['type'], 'delete', $component, $submission->data[$component['cid']]['value']); + webform_component_invoke($component['type'], 'delete', $component, $submission->data[$component['cid']]); } } } @@ -911,6 +911,19 @@ } /** + * Get a component property from the component definition. + * + * @see hook_webform_component_info() + */ +function webform_component_property($type, $property) { + $components = webform_components(); + $defaults = array( + 'conditional_type' => 'string', + ); + return isset($components[$type][$property]) ? $components[$type][$property] : $defaults[$property]; +} + +/** * Create a list of components suitable for a select list. * * @param $node diff --git a/includes/webform.conditionals.inc b/includes/webform.conditionals.inc new file mode 100644 index 0000000..f13264c --- /dev/null +++ b/includes/webform.conditionals.inc @@ -0,0 +1,1043 @@ +webform['conditionals']; + } + // Empty out any conditionals that have no rules. + foreach ($conditionals as $rgid => $conditional) { + if (empty($conditional['rules'])) { + unset($conditionals[$rgid]); + } + } + + $form['#tree'] = TRUE; + $form['#node'] = $node; + + $form['#attached']['library'][] = array('webform', 'admin'); + $form['#attached']['css'][] = drupal_get_path('module', 'webform') . '/css/webform.css'; + + // Wrappers used for AJAX addition/removal. + $form['conditionals']['#theme'] = 'webform_conditional_groups'; + $form['conditionals']['#prefix'] = '
'; + $form['conditionals']['#suffix'] = '
'; + + $source_list = webform_component_list($node, 'conditional', TRUE, TRUE); + $target_list = webform_component_list($node, TRUE); + $weight = 0; + foreach ($conditionals as $rgid => $conditional_group) { + $weight++; + $form['conditionals'][$rgid] = array( + '#type' => 'webform_conditional', + '#default_value' => $conditional_group, + '#nid' => $node->nid, + '#sources' => $source_list, + '#actions' => array( + 'hide' => t('hide'), + 'show' => t('show'), + ), + '#targets' => $target_list, + '#weight' => $weight, + ); + $form['conditionals'][$rgid]['weight'] = array( + '#type' => 'hidden', + '#size' => 4, + '#default_value' => $conditional_group['weight'], + ); + } + + $form['conditionals']['new']['#weight'] = $weight + 1; + $form['conditionals']['new']['weight'] = array( + '#type' => 'hidden', + '#size' => 4, + '#default_value' => $weight + 1, + ); + $form['conditionals']['new']['new'] = array( + '#type' => 'submit', + '#value' => t('+'), + '#submit' => array('webform_conditionals_form_add'), + '#ajax' => array( + 'progress' => 'none', + 'effect' => 'fade', + 'callback' => 'webform_conditionals_ajax', + 'wrapper' => 'webform-conditionals-ajax', + ), + ); + + $form['actions'] = array('#type' => 'markup'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save conditions'), + '#submit' => array('webform_conditionals_form_submit'), + ); + + return $form; +} + +/** + * Submit handler for webform_conditionals_form(). Add an additional choice. + */ +function webform_conditionals_form_add($form, &$form_state) { + // Build a default new conditional. + $weight = 0; + foreach ($form_state['values']['conditionals'] as $key => $conditional) { + $weight = max($weight, $conditional['weight']); + } + + $conditional['weight'] = count($form_state['values']['conditionals']); + $conditional['rules'][0] = array( + 'source' => NULL, + 'operator' => NULL, + 'value' => NULL, + ); + $conditional['action'] = 'hide'; + $conditional['target'] = NULL; + $conditional['andor'] = 'and'; + + // Add the conditional to form state and rebuild the form. + $form_state['values']['conditionals'][] = $conditional; + $form_state['rebuild'] = TRUE; +} + +/** + * Submit handler for webform_conditionals_form(). + */ +function webform_conditionals_form_submit($form, &$form_state) { + $node = $form['#node']; + + // Remove the new conditional placeholder. + unset($form_state['values']['conditionals']['new']); + + $conditionals = array(); + + // Fill in missing properties for each value so that it can save properly. + // TODO: Remove hard-coded source and target type. + foreach ($form_state['values']['conditionals'] as $rgid => $conditional) { + $conditional['rgid'] = $rgid; + $conditional['target_type'] = 'component'; + foreach ($conditional['rules'] as $rid => $rule) { + $conditional['rules'][$rid]['source_type'] = 'component'; + } + $conditionals[$rgid] = $conditional; + } + + $node->webform['conditionals'] = $conditionals; + node_save($node); + drupal_set_message(t('Conditionals for %title saved.', array('%title' => $node->title))); +} + +/** + * AJAX callback to render out adding a new condition. + */ +function webform_conditionals_ajax($form, $form_state) { + $rgids = element_children($form['conditionals']); + $new_rgid = max($rgids); + $form['conditionals'][$new_rgid]['#ajax_added'] = TRUE; + + return drupal_render($form['conditionals']); +} + +/** + * Theme the $form['conditionals'] of webform_conditionals_form(). + */ +function theme_webform_conditional_groups($variables) { + $element = $variables['element']; + drupal_add_tabledrag('webform-conditionals-table', 'order', 'sibling', 'webform-conditional-weight', NULL, NULL, FALSE); + drupal_add_js('Drupal.theme.prototype.tableDragChangedMarker = function() { return ""; }', 'inline'); + drupal_add_js('Drupal.theme.prototype.tableDragChangedWarning = function() { return " "; }', 'inline'); + + $header = array(); + $rows = array(); + + $element_children = element_children($element, TRUE); + $element_count = count($element_children); + foreach ($element_children as $key) { + $row = array(); + $element[$key]['weight']['#attributes']['class'] = array('webform-conditional-weight'); + $row[] = array( + 'width' => 1, + 'data' => drupal_render($element[$key]['weight']), + ); + if ($key === 'new') { + $data = ''; + if ($element_count === 1) { + $data = t('There are conditional actions on this form.') . ' '; + } + $data = '
' . $data . t('Add a new condition: ') . drupal_render($element[$key]) . '
'; + } + else { + $data = drupal_render($element[$key]); + } + $row[] = array( + 'data' => $data, + ); + $classes = ($key === 'new') ? array() : array('draggable'); + if (!empty($element[$key]['#ajax_added'])) { + $classes[] = 'ajax-new-content'; + } + $rows[] = array( + 'class' => $classes, + 'data' => $row, + ); + } + + $output = ''; + $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'webform-conditionals-table'), 'sticky' => FALSE)); + $output .= drupal_render_children($element); + + return $output; +} + +/** + * Form API #process function to expand a webform conditional element. + */ +function _webform_conditional_expand($element) { + $node = node_load($element['#nid']); + _webform_conditional_expand_value_forms($node); + + $element['#tree'] = TRUE; + $element['#default_value'] += array( + 'andor' => 'and', + ); + + $wrapper_id = drupal_clean_css_identifier(implode('-', $element['#parents'])) . '-ajax'; + $element['#prefix'] = '
'; + $element['#suffix'] = '
'; + + foreach ($element['#default_value']['rules'] as $rid => $conditional) { + $element['rules'][$rid]['source'] = array( + '#type' => 'select', + '#title' => t('Source'), + '#options' => $element['#sources'], + '#default_value' => $element['#default_value']['rules'][$rid]['source'], + ); + $element['rules'][$rid]['operator'] = array( + '#type' => 'select', + '#title' => t('Operator'), + '#options' => webform_conditional_operators_list(), + '#default_value' => $element['#default_value']['rules'][$rid]['operator'], + ); + $element['rules'][$rid]['value'] = array( + '#type' => 'textfield', + '#title' => t('Value'), + '#size' => 20, + '#default_value' => $element['#default_value']['rules'][$rid]['value'], + ); + $element['rules'][$rid]['remove'] = array( + '#type' => 'submit', + '#value' => t('-'), + '#submit' => array('webform_conditional_element_remove'), + '#name' => implode('_', $element['#parents']) . '_rules_' . $rid . '_remove', + '#attributes' => array('class' => array('webform-conditional-rule-remove')), + '#ajax' => array( + 'progress' => 'none', + 'callback' => 'webform_conditional_element_ajax', + 'wrapper' => $wrapper_id, + ), + ); + $element['rules'][$rid]['add'] = array( + '#type' => 'submit', + '#value' => t('+'), + '#submit' => array('webform_conditional_element_add'), + '#name' => implode('_', $element['#parents']) . '_rules_' . $rid . '_add', + '#attributes' => array('class' => array('webform-conditional-rule-add')), + '#ajax' => array( + 'progress' => 'none', + 'callback' => 'webform_conditional_element_ajax', + 'wrapper' => $wrapper_id, + ), + ); + + // The and/or selector is shown for every rule, even though the whole + // conditional group shares a single and/or property. They are made to match + // via JavaScript (though they all share the same "name" attribute so only + // a single value is ever submitted via POST). + $element['rules'][$rid]['andor'] = array( + '#type' => 'select', + '#title' => t('And/or'), + '#options' => array( + 'and' => t('and'), + 'or' => t('or'), + ), + '#parents' => array_merge($element['#parents'], array('andor')), + '#default_value' => $element['#default_value']['andor'], + ); + } + + // Remove the last and/or. + unset($element['rules'][$rid]['andor']); + + $element['action'] = array( + '#type' => 'select', + '#title' => t('Action'), + '#options' => $element['#actions'], + '#default_value' => $element['#default_value']['action'], + ); + $element['target'] = array( + '#type' => 'select', + '#title' => t('Operator'), + '#options' => $element['#targets'], + '#default_value' => $element['#default_value']['target'], + ); + + return $element; +} + +/** + * Expand out all the value forms that could potentially be used. + * + * These forms are added to the page via JavaScript and swapped in only when + * needed. Because the user may change the source and operator at any time, + * all these forms need to be generated ahead of time and swapped in. This + * could have been done via AJAX, but having all forms available makes for a + * faster user experience. + * + * @param $node + * The Webform node for which these forms are being generated. + * @return + * An array settings suitable for adding to the page via JavaScript. This + * array contains the following keys: + * - operators: An array containing a map of data types, operators, and form + * keys. This array is structured as follows: + * @code + * - sources[$source_key] = array( + * 'data_type' => $data_type, + * ); + * $operators[$data_type][$operator] = array( + * 'form' => $form_key, + * ); + * @endcode + * - forms[$form_key]: A string representing an HTML form for an operator. + * - forms[$form_key][$source]: Or instead of a single form for all components, + * if each component requires its own form, key each component by its source + * value (currently always the component ID). + */ +function _webform_conditional_expand_value_forms($node, $add_to_page = TRUE) { + static $value_forms_added; + + $operators = webform_conditional_operators(); + $data = array(); + foreach ($operators as $data_type => $operator_info) { + foreach ($operator_info as $operator => $data_operator_info) { + $data['operators'][$data_type][$operator]['form'] = 'default'; + if (isset($data_operator_info['form callback'])) { + $form_callback = $data_operator_info['form callback']; + $data['operators'][$data_type][$operator]['form'] = $form_callback; + if ($form_callback !== FALSE && !isset($value_forms_added[$form_callback])) { + $data['forms'][$form_callback] = $form_callback($node); + } + } + } + } + + foreach ($node->webform['components'] as $cid => $component) { + if (webform_component_feature($component['type'], 'conditional')) { + $data['sources'][$cid]['data_type'] = webform_component_property($component['type'], 'conditional_type'); + } + } + + if (!isset($value_forms_added) && $add_to_page) { + $value_forms_added = TRUE; + drupal_add_js(array('webform' => array('conditionalValues' => $data)), 'setting'); + } + + return $data; +} + +/** + * Submit handler for webform_conditional elements to add a new rule. + */ +function webform_conditional_element_add($form, &$form_state) { + $button = $form_state['clicked_button']; + $parents = $button['#parents']; + $action = array_pop($parents); + $rid = array_pop($parents); + + // Recurse through the form values until we find the root Webform conditional. + $parent_values = &$form_state['values']; + foreach ($parents as $key) { + if (array_key_exists($key, $parent_values)) { + $parent_values = &$parent_values[$key]; + } + } + + // Split the list of rules in this conditional and inject into the right spot. + $rids = array_keys($parent_values); + $offset = array_search($rid, $rids); + $first = array_slice($parent_values, 0, $offset + 1); + $second = array_slice($parent_values, $offset + 1); + $new[0] = $parent_values[$rid]; + + $parent_values = array_merge($first, $new, $second); + + $form_state['rebuild'] = TRUE; +} + +/** + * Submit handler for webform_conditional elements to remove a rule. + */ +function webform_conditional_element_remove($form, &$form_state) { + $button = $form_state['clicked_button']; + $parents = $button['#parents']; + $action = array_pop($parents); + $current_rid = array_pop($parents); + + // Recurse through the form values until we find the root Webform conditional. + $parent_values = &$form_state['values']; + foreach ($parents as $key) { + if (array_key_exists($key, $parent_values)) { + $parent_values = &$parent_values[$key]; + } + } + + // Remove this rule from the list of conditionals. + unset($parent_values[$current_rid]); + + $form_state['rebuild'] = TRUE; +} + + +/** + * AJAX callback to render out adding a new condition. + */ +function webform_conditional_element_ajax($form, $form_state) { + $button = $form_state['clicked_button']; + $parents = $button['#parents']; + + // Trim down the parents to go back up to the level of this elements wrapper. + array_pop($parents); // The button name (add/remove). + array_pop($parents); // The rule ID. + array_pop($parents); // The "rules" grouping. + + $element = $form; + foreach ($parents as $key) { + $element = $element[$key]; + } + + return drupal_render($element); +} + +/** + * Theme the form for a conditional action. + */ +function theme_webform_conditional($variables) { + $element = $variables['element']; + + $output = ''; + $output .= '
'; + $output .= '' . t('If') . ''; + + foreach (element_children($element['rules']) as $rid) { + // Hide labels. + $element['rules'][$rid]['source']['#title_display'] = 'none'; + $element['rules'][$rid]['operator']['#title_display'] = 'none'; + $element['rules'][$rid]['value']['#title_display'] = 'none'; + $element['rules'][$rid]['andor']['#title_display'] = 'none'; + + $source = '
' . drupal_render($element['rules'][$rid]['source']) . '
'; + $operator = '
' . drupal_render($element['rules'][$rid]['operator']) . '
'; + $value = '
' . drupal_render($element['rules'][$rid]['value']) . '
'; + + $source_phrase = t('!source !operator !value', array( + '!source' => $source, + '!operator' => $operator, + '!value' => $value, + )); + + $output .= '
'; + $output .= '
'; + $output .= $source_phrase; + $output .= '
'; + + if (isset($element['rules'][$rid]['andor'])) { + $output .= '
'; + $output .= drupal_render($element['rules'][$rid]['andor']); + $output .= '
'; + } + + if (isset($element['rules'][$rid]['add']) || isset($element['rules'][$rid]['remove'])) { + $output .= ''; + $output .= drupal_render($element['rules'][$rid]['remove']); + $output .= drupal_render($element['rules'][$rid]['add']); + $output .= ''; + } + + $output .= '
'; + } + + // Hide labels. + $element['action']['#title_display'] = 'none'; + $element['target']['#title_display'] = 'none'; + + $action = '
' . drupal_render($element['action']) . '
'; + $target = '
' . drupal_render($element['target']) . '
'; + + $target_phrase = t('then !action !target', array( + '!action' => $action, + '!target' => $target, + )); + + $output .= '
'; + $output .= $target_phrase; + $output .= '
'; + + $output .= '
'; + + return $output; +} + +/** + * Return a list of all Webform conditional operators. + */ +function webform_conditional_operators() { + static $operators; + + if (!isset($operators)) { + $operators = module_invoke_all('webform_conditional_operator_info'); + drupal_alter('webform_conditional_operators', $operators); + } + + return $operators; +} + +/** + * Return a nested list of all available operators, suitable for a select list. + */ +function webform_conditional_operators_list() { + $options = array(); + $operators = webform_conditional_operators(); + + foreach ($operators as $data_type => $type_operators) { + $options[$data_type] = array(); + foreach ($type_operators as $operator => $operator_info) { + $options[$data_type][$operator] = $operator_info['label']; + } + } + + return $options; +} + +/** + * Internal implementation of hook_webform_conditional_operator_info(). + * + * Called from webform.module's webform_webform_conditional_operator_info(). + */ +function _webform_conditional_operator_info() { + // General operators: + $operators['string']['equal'] = array( + 'label' => t('is'), + 'comparison callback' => 'webform_conditional_operator_string_equal', + 'js comparison callback' => 'conditionalOperatorStringEqual', + // A form callback is not needed here, since we can use the default, + // non-JavaScript textfield for all text and numeric fields. + // 'form callback' => 'webform_conditional_operator_text', + ); + $operators['string']['not_equal'] = array( + 'label' => t('is not'), + 'comparison callback' => 'webform_conditional_operator_string_not_equal', + 'js comparison callback' => 'conditionalOperatorStringNotEqual', + ); + $operators['string']['contains'] = array( + 'label' => t('contains'), + 'comparison callback' => 'webform_conditional_operator_string_contains', + 'js comparison callback' => 'conditionalOperatorStringContains', + ); + $operators['string']['does_not_contain'] = array( + 'label' => t('does not contain'), + 'comparison callback' => 'webform_conditional_operator_string_does_not_contain', + 'js comparison callback' => 'conditionalOperatorStringDoesNotContain', + ); + $operators['string']['begins_with'] = array( + 'label' => t('begins with'), + 'comparison callback' => 'webform_conditional_operator_string_begins_with', + 'js comparison callback' => 'conditionalOperatorStringBeginsWith', + ); + $operators['string']['ends_with'] = array( + 'label' => t('ends with'), + 'comparison callback' => 'webform_conditional_operator_string_ends_with', + 'js comparison callback' => 'conditionalOperatorStringEndsWith', + ); + $operators['string']['empty'] = array( + 'label' => t('is blank'), + 'comparison callback' => 'webform_conditional_operator_string_empty', + 'js comparison callback' => 'conditionalOperatorStringEmpty', + 'form callback' => FALSE, // No value form at all. + ); + $operators['string']['not_empty'] = array( + 'label' => t('is not blank'), + 'comparison callback' => 'webform_conditional_operator_string_not_empty', + 'js comparison callback' => 'conditionalOperatorStringNotEmpty', + 'form callback' => FALSE, // No value form at all. + ); + + // Numeric operators. + $operators['numeric']['equal'] = array( + 'label' => t('is equal to'), + 'comparison callback' => 'webform_conditional_operator_numeric_equal', + 'js comparison callback' => 'conditionalOperatorNumericEqual', + ); + $operators['numeric']['not_equal'] = array( + 'label' => t('is not equal to'), + 'comparison callback' => 'webform_conditional_operator_numeric_not_equal', + 'js comparison callback' => 'conditionalOperatorNumericNotEqual', + ); + $operators['numeric']['greater_than'] = array( + 'label' => t('is greater than'), + 'comparison callback' => 'webform_conditional_operator_numeric_greater_than', + 'js comparison callback' => 'conditionalOperatorNumericGreaterThan', + ); + $operators['numeric']['less_than'] = array( + 'label' => t('is less than'), + 'comparison callback' => 'webform_conditional_operator_numeric_less_than', + 'js comparison callback' => 'conditionalOperatorNumericLessThan', + ); + $operators['numeric']['empty'] = array( + 'label' => t('is blank'), + 'comparison callback' => 'webform_conditional_operator_string_empty', + 'js comparison callback' => 'conditionalOperatorStringEmpty', + 'form callback' => FALSE, // No value form at all. + ); + $operators['numeric']['not_empty'] = array( + 'label' => t('is not blank'), + 'comparison callback' => 'webform_conditional_operator_string_not_empty', + 'js comparison callback' => 'conditionalOperatorStringNotEmpty', + 'form callback' => FALSE, // No value form at all. + ); + + // Select operators. + $operators['select']['equal'] = array( + 'label' => t('is'), + 'comparison callback' => 'webform_conditional_operator_string_equal', + 'js comparison callback' => 'conditionalOperatorStringEqual', + 'form callback' => 'webform_conditional_form_select', + ); + $operators['select']['not_equal'] = array( + 'label' => t('is not'), + 'comparison callback' => 'webform_conditional_operator_string_not_equal', + 'js comparison callback' => 'conditionalOperatorStringNotEqual', + 'form callback' => 'webform_conditional_form_select', + ); + + // Date operators: + $operators['date']['equal'] = array( + 'label' => t('is on'), + 'comparison callback' => 'webform_conditional_operator_datetime_equal', + 'comparison prepare js' => 'webform_conditional_prepare_date_js', + 'js comparison callback' => 'conditionalOperatorDateEqual', + 'form callback' => 'webform_conditional_form_date', + ); + $operators['date']['before'] = array( + 'label' => t('is before'), + 'comparison callback' => 'webform_conditional_operator_datetime_before', + 'comparison prepare js' => 'webform_conditional_prepare_date_js', + 'js comparison callback' => 'conditionalOperatorDateBefore', + 'form callback' => 'webform_conditional_form_date', + ); + $operators['date']['after'] = array( + 'label' => t('is after'), + 'comparison callback' => 'webform_conditional_operator_datetime_after', + 'comparison prepare js' => 'webform_conditional_prepare_date_js', + 'js comparison callback' => 'conditionalOperatorDateAfter', + 'form callback' => 'webform_conditional_form_date', + ); + + // Time operators: + $operators['time']['equal'] = array( + 'label' => t('is at'), + 'comparison callback' => 'webform_conditional_operator_datetime_equal', + 'comparison prepare js' => 'webform_conditional_prepare_time_js', + 'js comparison callback' => 'conditionalOperatorTimeEqual', + 'form callback' => 'webform_conditional_form_time', + ); + $operators['time']['before'] = array( + 'label' => t('is before'), + 'comparison callback' => 'webform_conditional_operator_datetime_before', + 'comparison prepare js' => 'webform_conditional_prepare_time_js', + 'js comparison callback' => 'conditionalOperatorTimeBefore', + 'form callback' => 'webform_conditional_form_time', + ); + $operators['time']['after'] = array( + 'label' => t('is after'), + 'comparison callback' => 'webform_conditional_operator_datetime_after', + 'comparison prepare js' => 'webform_conditional_prepare_time_js', + 'js comparison callback' => 'conditionalOperatorTimeAfter', + 'form callback' => 'webform_conditional_form_time', + ); + + return $operators; +} + +/** + * Form callback for select-type conditional fields. + * + * Unlike other built-in conditional value forms, the form callback for select + * types provides an array of forms, keyed by the $cid, which is the "source" + * for the condition. + */ +function webform_conditional_form_select($node) { + $forms = array(); + webform_component_include('select'); + foreach ($node->webform['components'] as $cid => $component) { + if (webform_component_property($component['type'], 'conditional_type') == 'select') { + // TODO: Use a pluggable mechanism for retrieving select list values. + $options = _webform_select_options($component); + $element = array( + '#type' => 'select', + '#multiple' => FALSE, + '#size' => NULL, + '#attributes' => array(), + '#id' => NULL, + '#name' => NULL, + '#options' => $options, + '#parents' => array(), + ); + $forms[$cid] = drupal_render($element); + } + } + return $forms; +} + +/** + * Form callback for date conditional fields. + */ +function webform_conditional_form_date($node) { + $element = array( + '#title' => NULL, + '#title_display' => 'none', + '#size' => 20, + //'#description' => t('MM/DD/YYYY or any date string such as "-1 day".'), + '#type' => 'textfield', + ); + return drupal_render($element); +} + +/** + * Form callback for time conditional fields. + */ +function webform_conditional_form_time($node) { + $element = array( + '#title' => NULL, + '#title_display' => 'none', + '#size' => 20, + //'#description' => t('HH:MM:SS or any time string such as "+2 hours".'), + '#type' => 'textfield', + ); + return drupal_render($element); +} + +/** + * Load a conditional setting from the database. + */ +function webform_conditional_load($rgid, $nid) { + $node = node_load($nid); + + $conditional = isset($node->webform['conditionals'][$rgid]) ? $node->webform['conditionals'][$rgid] : FALSE; + + return $conditional; +} + +/** + * Insert a conditional rule group into the database. + */ +function webform_conditional_insert($conditional) { + drupal_write_record('webform_conditional', $conditional); + foreach ($conditional['rules'] as $rid => $rule) { + $rule['nid'] = $conditional['nid']; + $rule['rgid'] = $conditional['rgid']; + $rule['rid'] = $rid; + drupal_write_record('webform_conditional_rules', $rule); + } +} + +/** + * Update a conditional setting in the database. + */ +function webform_conditional_update($node, $conditional) { + webform_conditional_delete($node, $conditional); + webform_conditional_insert($conditional); +} + +/** + * Delete a conditional rule group. + */ +function webform_conditional_delete($node, $conditional) { + db_delete('webform_conditional') + ->condition('nid', $node->nid) + ->condition('rgid', $conditional['rgid']) + ->execute(); + db_delete('webform_conditional_rules') + ->condition('nid', $node->nid) + ->condition('rgid', $conditional['rgid']) + ->execute(); +} + +/** + * Loop through all the conditional settings and add needed JavaScript settings. + * + * We do a bit of optimization for JavaScript before adding to the page as + * settings. We remove unnecessary data structures and provide a "source map" + * so that JavaScript can quickly determine if it needs to check rules when a + * field on the page has been modified. + */ +function webform_conditional_prepare_javascript($node, $submission_data) { + $settings = array( + 'ruleGroups' => array(), + 'sourceMap' => array(), + 'values' => array(), + ); + $operators = webform_conditional_operators(); + foreach ($node->webform['conditionals'] as $conditional) { + // Assemble the main conditional group settings. + if ($conditional['target_type'] == 'component') { + $target_component = $node->webform['components'][$conditional['target']]; + $target_parents = webform_component_parent_keys($node, $target_component); + $target_id = 'webform-component-' . str_replace('_', '-', implode('-', $target_parents)); + $settings['ruleGroups'][$conditional['rgid']]['target'] = $target_id; + $settings['ruleGroups'][$conditional['rgid']]['andor'] = $conditional['andor']; + $settings['ruleGroups'][$conditional['rgid']]['action'] = $conditional['action']; + } + // Add on the list of rules to the conditional group. + foreach ($conditional['rules'] as $rule) { + if ($rule['source_type'] == 'component') { + $source_component = $node->webform['components'][$rule['source']]; + $source_parents = webform_component_parent_keys($node, $source_component); + $source_id = 'webform-component-' . str_replace('_', '-', implode('-', $source_parents)); + + // If this source has a value set, add that as a setting. + if (isset($submission_data[$source_component['cid']])) { + $source_value = $submission_data[$source_component['cid']]; + $settings['values'][$source_id] = is_array($source_value) ? $source_value : array($source_value); + } + + $conditional_type = webform_component_property($source_component['type'], 'conditional_type'); + $operator_info = $operators[$conditional_type][$rule['operator']]; + $rule_settings = array(); + $rule_settings['source'] = $source_id; + $rule_settings['value'] = $rule['value']; + $rule_settings['callback'] = $operator_info['js comparison callback']; + if (isset($operator_info['comparison prepare js'])) { + $callback = $operator_info['comparison prepare js']; + $rule_settings['value'] = $callback($rule['value']); + } + $settings['ruleGroups'][$conditional['rgid']]['rules'][$rule['rid']] = $rule_settings; + $settings['sourceMap'][$source_id][$conditional['rgid']] = $conditional['rgid']; + } + } + } + + return $settings; +} + +/** + * Prepare a conditional value for adding as a JavaScript setting. + */ +function webform_conditional_prepare_date_js($rule_value) { + // Convert the time/date string to a UTC timestamp for comparison. Note that + // this means comparisons against immediate times (such as "now") may be + // slightly stale by the time the comparison executes. Timestamps are in + // milliseconds, as to match JavaScript's Date.toString() method. + $date = webform_strtodate('c', $rule_value, 'UTC'); + return (int) (webform_strtotime($date) . '000'); +} + +/** + * Prepare a conditional value for adding as a JavaScript setting. + */ +function webform_conditional_prepare_time_js($rule_value) { + $date = webform_conditional_prepare_date_js($rule_value); + $today = webform_strtodate('c', 'today', 'UTC'); + $today = (int) (webform_strtotime($today) . '000'); + return $date - $today; +} + +/** + * Conditional callback for string comparisons. + */ +function webform_conditional_operator_string_equal($input_values, $rule_value) { + foreach ($input_values as $value) { + // Checkbox values come in as 0 integers for unchecked boxes. + $value = ($value === 0) ? '' : $value; + if (strcasecmp($value, $rule_value) === 0) { + return TRUE; + } + } + return FALSE; +} + +/** + * Conditional callback for string comparisons. + */ +function webform_conditional_operator_string_not_equal($input_values, $rule_value) { + $found = FALSE; + foreach ($input_values as $value) { + // Checkbox values come in as 0 integers for unchecked boxes. + $value = ($value === 0) ? '' : $value; + if (strcasecmp($value, $rule_value) === 0) { + $found = TRUE; + } + } + return !$found; +} + +/** + * Conditional callback for string comparisons. + */ +function webform_conditional_operator_string_contains($input_values, $rule_value) { + foreach ($input_values as $value) { + if (stripos($value, $rule_value) !== FALSE) { + return TRUE; + } + } + return FALSE; +} + +/** + * Conditional callback for string comparisons. + */ +function webform_conditional_operator_string_does_not_contain($input_values, $rule_value) { + $found = FALSE; + foreach ($input_values as $value) { + if (stripos($value, $rule_value) !== FALSE) { + $found = TRUE; + } + } + return !$found; +} + +/** + * Conditional callback for string comparisons. + */ +function webform_conditional_operator_string_begins_with($input_values, $rule_value) { + foreach ($input_values as $value) { + if (stripos($value, $rule_value) === 0) { + return TRUE; + } + } + return FALSE; +} + +/** + * Conditional callback for string comparisons. + */ +function webform_conditional_operator_string_ends_with($input_values, $rule_value) { + foreach ($input_values as $value) { + if (strripos($value, $rule_value) === strlen($value) - strlen($rule_value)) { + return TRUE; + } + } + return FALSE; +} + +/** + * Conditional callback for checking for empty fields. + */ +function webform_conditional_operator_string_empty($input_values, $rule_value) { + foreach ($input_values as $value) { + if ($value !== '' || $value !== NULL) { + return FALSE; + } + } + return TRUE; +} + +/** + * Conditional callback for checking for empty fields. + */ +function webform_conditional_operator_string_not_empty($input_values, $rule_value) { + $empty = FALSE; + foreach ($input_values as $value) { + if ($value === '' || $value === NULL) { + $empty = TRUE; + } + } + return !$empty; +} + +/** + * Conditional callback for numeric comparisons. + */ +function webform_conditional_operator_numeric_equal($input_values, $rule_value) { + // See float comparison: http://php.net/manual/en/language.types.float.php + $epsilon = 0.000001; + // An empty string does not match any number. + return $input_values[0] === '' ? FALSE : (abs((float)$input_values[0] - (float)$rule_value) < $epsilon); +} + +/** + * Conditional callback for numeric comparisons. + */ +function webform_conditional_operator_numeric_not_equal($input_values, $rule_value) { + // See float comparison: http://php.net/manual/en/language.types.float.php + $epsilon = 0.000001; + // An empty string does not match any number. + return $input_values[0] === '' ? TRUE : (abs((float)$input_values[0] - (float)$rule_value) >= $epsilon); +} + +/** + * Conditional callback for numeric comparisons. + */ +function webform_conditional_operator_numeric_greater_than($input_values, $rule_value) { + return (float)$input_values[0] > (float)$rule_value; +} + +/** + * Conditional callback for numeric comparisons. + */ +function webform_conditional_operator_numeric_less_than($input_values, $rule_value) { + return (float)$input_values[0] < (float)$rule_value; +} + +/** + * Conditional callback for date and time comparisons. + */ +function webform_conditional_operator_datetime_equal($input_values, $rule_value) { + $input_values = webform_conditional_value_datetime($input_values); + return webform_strtotime($input_values[0]) === webform_strtotime($rule_value); +} + +/** + * Conditional callback for date and time comparisons. + */ +function webform_conditional_operator_datetime_after($input_values, $rule_value) { + $input_values = webform_conditional_value_datetime($input_values); + return webform_strtotime($input_values[0]) > webform_strtotime($rule_value); +} + +/** + * Conditional callback for date and time comparisons. + */ +function webform_conditional_operator_datetime_before($input_values, $rule_value) { + $input_values = webform_conditional_value_datetime($input_values); + return webform_strtotime($input_values[0]) < webform_strtotime($rule_value); +} + +/** + * Utility function to convert incoming time and dates into strings. + */ +function webform_conditional_value_datetime($input_values) { + // Convert times into a string. + $input_values = isset($input_values['hour']) ? array(webform_date_string(webform_time_convert($input_values, '24-hour'), 'time')) : $input_values; + // Convert dates into a string. + $input_values = isset($input_values['month']) ? array(webform_date_string($input_values, 'date')) : $input_values; + return $input_values; +} \ No newline at end of file diff --git a/includes/webform.report.inc b/includes/webform.report.inc index 8ceb920..0f6c7be 100644 --- a/includes/webform.report.inc +++ b/includes/webform.report.inc @@ -238,7 +238,7 @@ // Generate a cell for each component. foreach ($node->webform['components'] as $component) { - $data = isset($submission->data[$component['cid']]['value']) ? $submission->data[$component['cid']]['value'] : NULL; + $data = isset($submission->data[$component['cid']]) ? $submission->data[$component['cid']] : NULL; $submission_output = webform_component_invoke($component['type'], 'table', $component, $data); if ($submission_output !== NULL) { $component_headers[] = check_plain($component['name']); @@ -780,7 +780,7 @@ if (isset($node->webform['components'][$cid])) { $component = $node->webform['components'][$cid]; // Let each component add its data. - $raw_data = isset($submission->data[$cid]['value']) ? $submission->data[$cid]['value'] : NULL; + $raw_data = isset($submission->data[$cid]) ? $submission->data[$cid] : NULL; if (webform_component_feature($component['type'], 'csv')) { $data = webform_component_invoke($component['type'], 'csv_data', $component, $options, $raw_data); if (is_array($data)) { diff --git a/includes/webform.submissions.inc b/includes/webform.submissions.inc index eddf3b0..9cdfeb9 100644 --- a/includes/webform.submissions.inc +++ b/includes/webform.submissions.inc @@ -28,10 +28,10 @@ } if (is_array($values)) { - $data[$cid]['value'] = $values; + $data[$cid] = $values; } else { - $data[$cid]['value'][0] = $values; + $data[$cid][0] = $values; } } @@ -108,7 +108,7 @@ } foreach ($submission->data as $cid => $values) { - foreach ($values['value'] as $delta => $value) { + foreach ($values as $delta => $value) { $data = array( 'nid' => $node->webform['nid'], 'sid' => $submission->sid, @@ -140,7 +140,7 @@ // Iterate through all components and let each do cleanup if necessary. foreach ($node->webform['components'] as $cid => $component) { if (isset($submission->data[$cid])) { - webform_component_invoke($component['type'], 'delete', $component, $submission->data[$cid]['value']); + webform_component_invoke($component['type'], 'delete', $component, $submission->data[$cid]); } } @@ -274,9 +274,9 @@ if ($email['attachments']) { webform_component_include('file'); foreach ($node->webform['components'] as $component) { - if (webform_component_feature($component['type'], 'attachment') && !empty($submission->data[$component['cid']]['value'][0])) { + if (webform_component_feature($component['type'], 'attachment') && !empty($submission->data[$component['cid']][0])) { if (webform_component_implements($component['type'], 'attachments')) { - $files = webform_component_invoke($component['type'], 'attachments', $component, $submission->data[$component['cid']]['value']); + $files = webform_component_invoke($component['type'], 'attachments', $component, $submission->data[$component['cid']]); if ($files) { $attachments = array_merge($attachments, $files); } @@ -573,9 +573,10 @@ // Make sure at least one field is available if (isset($component_tree['children'])) { // Recursively add components to the form. + $input_values = $submission->data; foreach ($component_tree['children'] as $cid => $component) { - if (_webform_client_form_rule_check($node, $component, $component['page_num'], NULL, $submission)) { - _webform_client_form_add_component($node, $component, NULL, $renderable, $renderable, NULL, $submission, $format); + if (_webform_client_form_rule_check($node, $component, $component['page_num'], $input_values)) { + _webform_client_form_add_component($node, $component, NULL, $renderable, $renderable, $input_values, $format); } } } @@ -701,7 +702,7 @@ } // CID may be NULL if this submission does not actually contain any data. if ($row->cid) { - $submissions[$row->sid]->data[$row->cid]['value'][$row->no] = $row->data; + $submissions[$row->sid]->data[$row->cid][$row->no] = $row->data; } $previous = $row->sid; } diff --git a/js/webform-admin.js b/js/webform-admin.js index a8e7c9a..44bb80e 100644 --- a/js/webform-admin.js +++ b/js/webform-admin.js @@ -16,6 +16,8 @@ Drupal.webform.selectCheckboxesLink(context); // Enhance the normal tableselect.js file to support indentations. Drupal.webform.tableSelectIndentation(context); + // Enhancements for the conditionals administrative page. + Drupal.webform.conditionalAdmin(context); } Drupal.webform = Drupal.webform || {}; @@ -116,4 +118,135 @@ }); } +/** + * Attach behaviors for Webform conditional administration. + */ +Drupal.webform.conditionalAdmin = function(context) { + $('.webform-conditional:not(.webform-conditional-processed)').each(function() { + $(this).addClass('webform-conditional-processed'); + + // Rather than binding to click, we have to use mousedown to work with + // the AJAX handling, which disables the button and prevents "click" events. + // This handler needs a delay to let the form submit before we remove the + // table row. + $(this).find('.webform-conditional-rule-remove').mousedown(function() { + var button = this; + window.setTimeout(Drupal.webform.conditionalRemove.apply(button), 10); + }); + + $(this).find('.webform-conditional-source select').each(function() { + $(this).change(Drupal.webform.conditionalSourceChange).triggerHandler('change'); + }); + + $(this).find('.webform-conditional-operator select').each(function() { + $(this).change(Drupal.webform.conditionalOperatorChange).triggerHandler('change'); + }); + + $(this).find('.webform-conditional-andor select').each(function() { + $(this).change(Drupal.webform.conditionalAndOrChange).triggerHandler('change'); + }); + }); +} + +/** + * Event callback for the remove button next to an individual rule. + */ +Drupal.webform.conditionalRemove = function() { + // See if there are any remaining rules in this element. + var ruleCount = $(this).parents('.webform-conditional:first').find('.webform-conditional-rule-remove').length; + if (ruleCount <= 1) { + var $tableRow = $(this).parents('tr:first'); + var $table = $('#webform-conditionals-table'); + if ($tableRow.length && $table.length) { + $tableRow.remove(); + Drupal.webform.restripeTable($table[0]); + } + } +} + +/** + * Event callback to update the list of operators after a source change. + */ +Drupal.webform.conditionalSourceChange = function() { + var source = $(this).val(); + var dataType = Drupal.settings.webform.conditionalValues.sources[source]['data_type']; + var $operator = $(this).parents('.webform-conditional-rule:first').find('.webform-conditional-operator select'); + + // Store a the original list of all operators for all data types in the select + // list DOM element. + if (!$operator[0]['webformConditionalOriginal']) { + $operator[0]['webformConditionalOriginal'] = $operator[0].innerHTML; + } + + // Reference the original list to create a new list matching the data type. + var $originalList = $($operator[0]['webformConditionalOriginal']); + var $newList = $originalList.filter('optgroup[label=' + dataType + ']'); + $operator[0].innerHTML = $newList[0].innerHTML; + + // Fire the change event handler on the list to update the value field. + $operator.triggerHandler('change'); +} + +/** + * Event callback to update the list of operators after a source change. + */ +Drupal.webform.conditionalOperatorChange = function() { + var source = $(this).parents('.webform-conditional-rule:first').find('.webform-conditional-source select').val(); + var dataType = Drupal.settings.webform.conditionalValues.sources[source]['data_type']; + var operator = $(this).val(); + var $value = $(this).parents('.webform-conditional-rule:first').find('.webform-conditional-value'); + var value = $value.find('input, select, textarea').val(); + var name = $value.find('input, select, textarea').attr('name'); + + + // Given the dataType and operator, we can determine the form key. + var formKey = Drupal.settings.webform.conditionalValues.operators[dataType][operator]['form']; + + // Save the default field as printed on the original page. + if (!$value[0]['webformConditionalOriginal']) { + $value[0]['webformConditionalOriginal'] = $value[0].innerHTML; + } + + if (formKey === 'default') { + $value[0].innerHTML = $value[0]['webformConditionalOriginal']; + } + else if (formKey === false) { + $value[0].innerHTML = ' '; + } + else { + // If there is a per-source form for this operator (e.g. option lists), use + // the specialized value form. + if (typeof Drupal.settings.webform.conditionalValues.forms[formKey] == 'object') { + $value[0].innerHTML = Drupal.settings.webform.conditionalValues.forms[formKey][source]; + } + // Otherwise all the sources use a generic field (e.g. a text field). + else { + $value[0].innerHTML = Drupal.settings.webform.conditionalValues.forms[formKey]; + } + } + + $value.find('input, select, textarea').filter(':first').val(value).attr('name', name); +} + +/** + * Event callback to make sure all group and/or operators match. + */ +Drupal.webform.conditionalAndOrChange = function() { + $(this).parents('.webform-conditional:first').find('.webform-conditional-andor select').val(this.value); +} + +/** + * Given a table's DOM element, restripe the odd/even classes. + */ +Drupal.webform.restripeTable = function(table) { + // :even and :odd are reversed because jQuery counts from 0 and + // we count from 1, so we're out of sync. + // Match immediate children of the parent element to allow nesting. + $('> tbody > tr, > tr', table) + .filter(':odd').filter('.odd') + .removeClass('odd').addClass('even') + .end().end() + .filter(':even').filter('.even') + .removeClass('even').addClass('odd'); +}; })(jQuery); diff --git a/js/webform.js b/js/webform.js index 70ac37a..1655ca1 100644 --- a/js/webform.js +++ b/js/webform.js @@ -10,6 +10,11 @@ Drupal.behaviors.webform.attach = function(context) { // Calendar datepicker behavior. Drupal.webform.datepicker(context); + + // Conditional logic. + if (Drupal.settings.webform && Drupal.settings.webform.conditionals) { + Drupal.webform.conditional(context); + } }; Drupal.webform = Drupal.webform || {}; @@ -75,6 +80,312 @@ event.preventDefault(); }); }); -} +}; + +Drupal.webform.conditional = function(context) { + // Add the bindings to each webform on the page. + $.each(Drupal.settings.webform.conditionals, function(formId, settings) { + var $form = $('#' + formId + ':not(.webform-conditional-processed)'); + if ($form.length) { + $form.addClass('webform-conditional-processed'); + $form.bind('change', { 'settings': settings }, Drupal.webform.conditionalCheck); + + // Trigger all the elements that cause conditionals on this form. + $.each(Drupal.settings.webform.conditionals[formId]['sourceMap'], function(elementId) { + $('#' + elementId).find('input,select,textarea').filter(':first').trigger('change'); + }); + } + }); +}; + +/** + * Event handler to respond to field changes in a form. + * + * This event is bound to the entire form, not individual fields. + */ +Drupal.webform.conditionalCheck = function(e) { + var $triggerElement = $(e.target).parents('.webform-component:first'); + var triggerElementId = $triggerElement.attr('id'); + var settings = e.data.settings; + + if (settings.sourceMap[triggerElementId]) { + $.each(settings.sourceMap[triggerElementId], function(n, rgid) { + var ruleGroup = settings.ruleGroups[rgid]; + + // Perform the comparison callback and build the results for this group. + var conditionalResult = true; + var conditionalResults = []; + $.each(ruleGroup['rules'], function(m, rule) { + var elementId = rule['source']; + var element = document.getElementById(elementId); + var existingValue = settings.values[elementId] ? settings.values[elementId] : null; + conditionalResults.push(window['Drupal']['webform'][rule.callback](element, existingValue, rule['value'] )); + }); + + // Filter out false values. + var filteredResults = []; + for (var i = 0; i < conditionalResults.length; i++) { + if (conditionalResults[i]) { + filteredResults.push(conditionalResults[i]); + } + } + + // Calculate the and/or result. + if (ruleGroup['andor'] === 'or') { + conditionalResult = filteredResults.length > 0; + } + else { + conditionalResult = filteredResults.length === conditionalResults.length; + } + + // Flip the result of the action is to hide. + if (ruleGroup['action'] == 'hide') { + showComponent = !conditionalResult; + } + else { + showComponent = conditionalResult; + } + + if (showComponent) { + $('#' + ruleGroup['target']).show(); + } + else { + $('#' + ruleGroup['target']).find('input').val('').end().hide(); + } + + }); + } + +}; + +Drupal.webform.conditionalOperatorStringEqual = function(element, existingValue, ruleValue) { + var returnValue = false; + var currentValue = Drupal.webform.stringValue(element, existingValue); + $.each(currentValue, function(n, value) { + if (value.toLowerCase() === ruleValue.toLowerCase()) { + returnValue = true; + return false; // break. + } + }); + return returnValue; +}; + +Drupal.webform.conditionalOperatorStringNotEqual = function(element, existingValue, ruleValue) { + var found = false; + var currentValue = Drupal.webform.stringValue(element, existingValue); + $.each(currentValue, function(n, value) { + if (value.toLowerCase() === ruleValue.toLowerCase()) { + found = true; + } + }); + return !found; +}; + +Drupal.webform.conditionalOperatorStringContains = function(element, existingValue, ruleValue) { + var returnValue = false; + var currentValue = Drupal.webform.stringValue(element, existingValue); + $.each(currentValue, function(n, value) { + if (value.toLowerCase().indexOf(ruleValue.toLowerCase()) > -1) { + returnValue = true; + return false; // break. + } + }); + return returnValue; +}; + +Drupal.webform.conditionalOperatorStringDoesNotContain = function(element, existingValue, ruleValue) { + var found = false; + var currentValue = Drupal.webform.stringValue(element, existingValue); + $.each(currentValue, function(n, value) { + if (value.toLowerCase().indexOf(ruleValue.toLowerCase()) > -1) { + found = true; + } + }); + return !found; +}; + +Drupal.webform.conditionalOperatorStringBeginsWith = function(element, existingValue, ruleValue) { + var returnValue = false; + var currentValue = Drupal.webform.stringValue(element, existingValue); + $.each(currentValue, function(n, value) { + if (value.toLowerCase().indexOf(ruleValue.toLowerCase()) === 0) { + returnValue = true; + return false; // break. + } + }); + return returnValue; +}; + +Drupal.webform.conditionalOperatorStringEndsWith = function(element, existingValue, ruleValue) { + var returnValue = false; + var currentValue = Drupal.webform.stringValue(element, existingValue); + $.each(currentValue, function(n, value) { + if (value.toLowerCase().lastIndexOf(ruleValue.toLowerCase()) === value.length - ruleValue.length) { + returnValue = true; + return false; // break. + } + }); + return returnValue; +}; + +Drupal.webform.conditionalOperatorStringEmpty = function(element, existingValue, ruleValue) { + var currentValue = Drupal.webform.stringValue(element, existingValue); + var returnValue = true; + $.each(currentValue, function(n, value) { + if (value !== '') { + returnValue = false; + return false; // break. + } + }); + return returnValue; +}; + +Drupal.webform.conditionalOperatorStringNotEmpty = function(element, existingValue, ruleValue) { + var currentValue = Drupal.webform.stringValue(element, existingValue); + var empty = false; + $.each(currentValue, function(n, value) { + if (value === '') { + empty = true; + return false; // break. + } + }); + return !empty; +}; + +Drupal.webform.conditionalOperatorNumericEqual = function(element, existingValue, ruleValue) { + // See float comparison: http://php.net/manual/en/language.types.float.php + var currentValue = Drupal.webform.stringValue(element, existingValue); + var epsilon = 0.000001; + // An empty string does not match any number. + return currentValue[0] === '' ? false : (Math.abs(parseFloat(currentValue[0]) - parseFloat(ruleValue)) < epsilon); +}; + +Drupal.webform.conditionalOperatorNumericNotEqual = function(element, existingValue, ruleValue) { + // See float comparison: http://php.net/manual/en/language.types.float.php + var currentValue = Drupal.webform.stringValue(element, existingValue); + var epsilon = 0.000001; + // An empty string does not match any number. + return currentValue[0] === '' ? true : (Math.abs(parseFloat(currentValue[0]) - parseFloat(ruleValue)) >= epsilon); +}; + +Drupal.webform.conditionalOperatorNumericGreaterThan = function(element, existingValue, ruleValue) { + var currentValue = Drupal.webform.stringValue(element, existingValue); + return parseFloat(currentValue[0]) > parseFloat(ruleValue); +}; + +Drupal.webform.conditionalOperatorNumericLessThan = function(element, existingValue, ruleValue) { + var currentValue = Drupal.webform.stringValue(element, existingValue); + return parseFloat(currentValue[0]) < parseFloat(ruleValue); +}; + +Drupal.webform.conditionalOperatorDateEqual = function(element, existingValue, ruleValue) { + var currentValue = Drupal.webform.timeValue(element, existingValue); + return currentValue === ruleValue; +}; + +Drupal.webform.conditionalOperatorDateBefore = function(element, existingValue, ruleValue) { + var currentValue = Drupal.webform.dateValue(element, existingValue); + return (currentValue !== false) && currentValue < ruleValue; +}; + +Drupal.webform.conditionalOperatorDateAfter = function(element, existingValue, ruleValue) { + var currentValue = Drupal.webform.dateValue(element, existingValue); + return (currentValue !== false) && currentValue > ruleValue; +}; + +Drupal.webform.conditionalOperatorTimeEqual = function(element, existingValue, ruleValue) { + var currentValue = Drupal.webform.timeValue(element, existingValue); + return currentValue === ruleValue; +}; + +Drupal.webform.conditionalOperatorTimeBefore = function(element, existingValue, ruleValue) { + // Date and time operators intentionally exclusive for "before". + var currentValue = Drupal.webform.timeValue(element, existingValue); + return (currentValue !== false) && (currentValue < ruleValue); +}; + +Drupal.webform.conditionalOperatorTimeAfter = function(element, existingValue, ruleValue) { + // Date and time operators intentionally inclusive for "after". + var currentValue = Drupal.webform.timeValue(element, existingValue); + return (currentValue !== false) && (currentValue >= ruleValue); +}; + +/** + * Utility function to get a string value from a select/radios/text/etc. field. + */ +Drupal.webform.stringValue = function(element, existingValue) { + var value = []; + + if (element) { + // Simple textfields. + $(element).find('input:not([type=checkbox],[type=radio]),textarea').each(function() { + value.push(this.value); + }); + + // Checkboxes and radios. + if (!value.length) { + $(element).find('input:checked').each(function() { + value.push(this.value); + }); + } + // Select lists. + if (!value.length) { + var selectValue = $(element).find('select').val(); + if (selectValue) { + value.push(selectValue); + } + } + } + else if (existingValue) { + value = existingValue; + } + + return value; +}; + +/** + * Utility function to calculate a millisecond timestamp from a time field. + */ +Drupal.webform.dateValue = function(element, existingValue) { + if (element) { + var day = $(element).find('[name*=day]').val(); + var month = $(element).find('[name*=month]').val(); + var year = $(element).find('[name*=year]').val(); + // Months are 0 indexed in JavaScript. + if (month) { + month--; + } + return (year !== '' && month !== '' && day !== '') ? Date.UTC(year, month, day) : false; + } + else { + var existingValue = existingValue.length ? existingValue[0].split('-') : existingValue; + return existingValue.length ? Date.UTC(existingValue[0], existingValue[1], existingValue[2]) : false; + } +}; + +/** + * Utility function to calculate a millisecond timestamp from a time field. + */ +Drupal.webform.timeValue = function(element, existingValue) { + if (element) { + var hour = $(element).find('[name*=hour]').val(); + var minute = $(element).find('[name*=minute]').val(); + var ampm = $(element).find('[name*=ampm]:checked').val(); + + // Convert to integers if set. + hour = (hour === '') ? hour : parseInt(hour); + minute = (minute === '') ? minute : parseInt(minute); + + if (hour !== '') { + hour = (hour < 12 && ampm == 'pm') ? hour + 12 : hour; + hour = (hour === 12 && ampm == 'am') ? 0 : hour; + } + return (hour !== '' && minute !== '') ? Date.UTC(1970, 0, 1, hour, minute) : false; + } + else { + var existingValue = existingValue.length ? existingValue[0].split(':') : existingValue; + return existingValue.length ? Date.UTC(1970, 0, 1, existingValue[0], existingValue[1]) : false; + } +}; })(jQuery); diff --git a/tests/conditionals.test b/tests/conditionals.test new file mode 100644 index 0000000..42ee057 --- /dev/null +++ b/tests/conditionals.test @@ -0,0 +1,182 @@ + t('Webform conditionals'), + 'description' => t('Generates webforms to test conditional showing and hiding of fields.'), + 'group' => t('Webform'), + ); + } + + /** + * Implements setUp(). + */ + function setUp() { + parent::setUp(); + } + + /** + * Implements tearDown(). + */ + function tearDown() { + parent::tearDown(); + } + + /** + * Test that required fields with no default value can't be submitted as-is. + */ + function testWebformConditionals() { + $this->drupalLogin($this->webform_users['admin']); + $this->webformReset(); + + $test_components = $this->testWebformComponents(); + foreach ($test_components as $key => $component_info) { + if (isset($component_info['match conditional values'])) { + foreach ($component_info['match conditional values'] as $operator => $match_value) { + $this->webformTestConditionalComponent($component_info['component'], $component_info['sample values'], $operator, $match_value, TRUE); + } + } + if (isset($component_info['mismatch conditional values'])) { + foreach ($component_info['mismatch conditional values'] as $operator => $mismatch_value) { + $this->webformTestConditionalComponent($component_info['component'], $component_info['sample values'], $operator, $mismatch_value, FALSE); + } + } + } + + $this->drupalLogout(); + } + + /** + * Assembles a test node for checking if conditional properties are respected. + * + * @param $component + * The sample component that should be tested as the source of a conditional + * operator. + * @param $operator + * The operator that will be used to check the source component, such as + * "equal" or "starts_with". See _webform_conditional_operator_info(). + * @param $conditional_values + * The string match value that will be entered into the source component's + * conditional configuration. If passed in as an array, multiple rules + * will be added to the conditional. + * @param $should_match + * Boolean value indicating if the source values will cause the conditional + * operation to trigger or not. If TRUE, the submission should succeed. + * If FALSE, validation errors are expected to be triggered. The input + * $value will be compared against the "sample values" input provided by + * testWebformComponents(). + * @return + * None. This function executes its own assert statements to show results. + */ + private function webformTestConditionalComponent($component, $input_values, $operator, $conditional_values, $should_match) { + // Create the Webform test node and add a same-page conditional followed + // by a second-page conditional. Insert page breaks between all components. + $input_string = (is_array($input_values) ? print_r($input_values, 1) : $input_values); + $match_string = (is_array($conditional_values) ? print_r($conditional_values, 1) : $conditional_values); + $conditional_string = $should_match ? 'should' : 'should not'; + $settings = array( + 'title' => 'Test conditional webform: ' . $component['type'] . ' "' . $input_string .'"' . $conditional_string . ' be ' . $operator . ' "' . $match_string . '"', + 'type' => 'webform', + 'webform' => webform_node_defaults(), + ); + + $components = array(); + $components[] = $component; + + $test_components = $this->testWebformComponents(); + $textfield = $test_components['textfield']['component']; + + // Add a test textfield on the first page. + $textfield['weight'] = '199'; + $textfield['form_key'] = $this->randomName(); + $textfield['mandatory'] = '1'; + $components[] = $textfield; + + // Then add a page break and another textfield on the second page. + $components[] = array( + 'type' => 'pagebreak', + 'form_key' => 'pagebreak_' . $this->randomName(), + 'pid' => 0, + 'name' => 'Page break', + 'weight' => '200', + ); + $textfield['form_key'] = $this->randomName(); + $textfield['weight'] = '201'; + $components[] = $textfield; + + $settings['webform']['components'] = $components; + $node = $this->drupalCreateNode($settings); + $node = node_load($node->nid); // Needed to get a complete object. + + // We now have a new test node. First try checking same-page conditionals. + $rules = array(); + $conditional_values = is_array($conditional_values) ? $conditional_values : array($conditional_values); + foreach ($conditional_values as $conditional_value) { + $rules[] = array( + 'source_type' => 'component', + 'source' => 1, // The first component in the form is always the source. + 'operator' => $operator, + 'value' => $conditional_value, + ); + } + $conditional = array( + 'rgid' => 0, + 'andor' => 'and', + 'action' => 'hide', + 'target_type' => 'component', + 'target' => NULL, // Target set individually. + 'weight' => 0, + 'rules' => $rules, + ); + + $conditional['target'] = 2; // The same-page textfield test. + $node->webform['conditionals'] = array($conditional); + node_save($node); + + // Submit our test data. + $edit = $this->testWebformPost(array($component['form_key'] => $input_values)); + $this->drupalPost('node/' . $node->nid, $edit, 'Next Page >', array(), array(), 'webform-client-form-' . $node->nid); + + // Ensure we reached the second page for matched conditionals. + $message = t('Conditional same-page skipping of validation passed for "%form_key": %input_values @conditional_string be @operator %match_string', array('%form_key' => $component['form_key'], '%input_values' => $input_string, '@conditional_string' => $conditional_string, '@operator' => $operator, '%match_string' => $match_string)); + if ($should_match) { + $this->assertRaw('< Previous Page', $message, t('Webform')); + } + // Or that we did not reach the second page for mismatched conditionals. + else { + $this->assertNoRaw('< Previous Page', $message, t('Webform')); + } + + // Adjust the conditionals to make them separate-page conditionals. + $conditional['target'] = 3; // The separate-page textfield test. + $node->webform['conditionals'] = array($conditional); + $node->webform['components'][2]['mandatory'] = '0'; + node_save($node); + + // Re-submit the form again, this time checking for the field on the + // second page. + $this->drupalPost('node/' . $node->nid, $edit, 'Next Page >', array(), array(), 'webform-client-form-' . $node->nid); + $string_match = 'name="submitted[' . $textfield['form_key'] . ']"'; + + // Ensure that the field is properly hidden based on a match. + $message = t('Conditional separate-page skipping of validation passed for "%form_key": %input_values @conditional_string be @operator %match_string', array('%form_key' => $component['form_key'], '%input_values' => $input_string, '@conditional_string' => $conditional_string, '@operator' => $operator, '%match_string' => $match_string)); + if ($should_match) { + $this->assertNoRaw($string_match, $message, t('Webform')); + } + // Or that the field is still present on a mismatch. + else { + $this->assertRaw($string_match, $message, t('Webform')); + } + } +} diff --git a/tests/submission.test b/tests/submission.test index 12e83f0..8d4aaf6 100644 --- a/tests/submission.test +++ b/tests/submission.test @@ -131,7 +131,7 @@ $component_info = $this->testWebformComponents(); foreach ($node->webform['components'] as $cid => $component) { $stable_value = $value_type == 'sample' ? $component_info[$component['form_key']]['database values'] : $component_info[$component['form_key']]['database default values']; - $actual_value = $actual_submission->data[$cid]['value']; + $actual_value = $actual_submission->data[$cid]; $result = $this->assertEqual($stable_value, $actual_value, t('Component @form_key data integrity check', array('@form_key' => $component['form_key'])), t('Webform')); if (!$result || $result === 'fail') { $this->fail(t('Expected !expected', array('!expected' => print_r($stable_value, TRUE))) . "\n\n" . t('Recieved !recieved', array('!recieved' => print_r($actual_value, TRUE))), t('Webform')); diff --git a/tests/webform.test b/tests/webform.test index 65a4b78..bc2ca00 100644 --- a/tests/webform.test +++ b/tests/webform.test @@ -139,6 +139,17 @@ 'sample values' => array('day' => '30', 'month' => '9', 'year' => '1982'), 'database values' => array('1982-09-30'), 'database default values' => array('1978-11-19'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '1982/9/30', + 'before' => '1982/10/1', + 'after' => '1982/9/29', + ), + 'mismatch conditional values' => array( + 'equal' => '1981/9/30', + 'before' => '1982/9/30', + 'after' => '1982/9/30', + ), ), // Test grid components. @@ -197,6 +208,15 @@ 'sample values' => array('one' => TRUE, 'two' => FALSE, 'three' => TRUE), 'database values' => array('one', 'three'), 'database default values' => array('two'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => array('one', 'three'), // ANDed together match. + 'not_equal' => array('two'), + ), + 'mismatch conditional values' => array( + 'equal' => array('one', 'two'), + 'not_equal' => array('two', 'three'), + ), ), 'checkboxes_zero' => array( 'component' => array( @@ -215,6 +235,15 @@ 'sample values' => array('0' => TRUE), 'database values' => array('0'), 'database default values' => array('0'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '0', + 'not_equal' => '1', + ), + 'mismatch conditional values' => array( + 'equal' => '1', + 'not_equal' => '0', + ), ), 'radios' => array( 'component' => array( @@ -232,6 +261,15 @@ 'sample values' => 'one', 'database values' => array('one'), 'database default values' => array('two'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => 'one', + 'not_equal' => 'two', + ), + 'mismatch conditional values' => array( + 'equal' => 'two', + 'not_equal' => 'one', + ), ), 'radios_zero' => array( 'component' => array( @@ -249,6 +287,15 @@ 'sample values' => '0', 'database values' => array('0'), 'database default values' => array('0'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '0', + 'not_equal' => '1', + ), + 'mismatch conditional values' => array( + 'equal' => '1', + 'not_equal' => '0', + ), ), 'select' => array( 'component' => array( @@ -268,6 +315,15 @@ 'sample values' => 'two', 'database values' => array('two'), 'database default values' => array('one'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => 'two', + 'not_equal' => 'one', + ), + 'mismatch conditional values' => array( + 'equal' => 'one', + 'not_equal' => 'two', + ), ), 'select_zero' => array( 'component' => array( @@ -287,6 +343,15 @@ 'sample values' => '0', 'database values' => array('0'), 'database default values' => array('0'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '0', + 'not_equal' => '1', + ), + 'mismatch conditional values' => array( + 'equal' => '1', + 'not_equal' => '0', + ), ), 'select_no_default' => array( 'component' => array( @@ -306,6 +371,15 @@ 'sample values' => 'two', 'database values' => array('two'), 'database default values' => array(''), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => 'two', + 'not_equal' => '', + ), + 'mismatch conditional values' => array( + 'equal' => '', + 'not_equal' => 'two', + ), ), 'select_no_default_zero' => array( 'component' => array( @@ -325,6 +399,15 @@ 'sample values' => '0', 'database values' => array('0'), 'database default values' => array(''), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '0', + 'not_equal' => '', + ), + 'mismatch conditional values' => array( + 'equal' => '', + 'not_equal' => '0', + ), ), 'select_optgroup' => array( 'component' => array( @@ -403,6 +486,17 @@ 'sample values' => array('day' => '30', 'month' => '9', 'year' => '1982'), 'database values' => array('1982-09-30'), 'database default values' => array('1978-11-19'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '1982/9/30', + 'before' => '1982/10/1', + 'after' => '1982/9/29', + ), + 'mismatch conditional values' => array( + 'equal' => '1981/9/30', + 'before' => '1982/9/30', + 'after' => '1982/9/30', + ), ), // Test email components. @@ -423,6 +517,25 @@ 'sample values' => 'admin@localhost.localhost', 'database values' => array('admin@localhost.localhost'), 'database default values' => array($this->webform_users['admin']->mail), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => 'admin@localhost.localhost', + 'not_equal' => '', + 'contains' => 'admin', + 'does_not_contain' => 'foo', + 'begins_with' => 'admin', + 'ends_with' => 'localhost', + 'not_empty' => TRUE, + ), + 'mismatch conditional values' => array( + 'equal' => 'foo@localhost.localhost', + 'not_equal' => 'admin@localhost.localhost', + 'contains' => 'foo', + 'does_not_contain' => 'admin', + 'begins_with' => 'localhost', + 'ends_with' => 'admin', + 'empty' => TRUE, + ), ), // Test hidden components. @@ -439,6 +552,25 @@ 'sample values' => NULL, 'database values' => array('default hidden value'), 'database default values' => array('default hidden value'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => 'default hidden value', + 'not_equal' => '', + 'contains' => 'hidden', + 'does_not_contain' => 'foo', + 'begins_with' => 'default', + 'ends_with' => 'value', + 'not_empty' => TRUE, + ), + 'mismatch conditional values' => array( + 'equal' => '', + 'not_equal' => 'default hidden value', + 'contains' => 'foo', + 'does_not_contain' => 'hidden', + 'begins_with' => 'value', + 'ends_with' => 'default', + 'empty' => TRUE, + ), ), // Test textarea components. @@ -456,9 +588,42 @@ 'sample values' => 'sample textarea value', 'database values' => array('sample textarea value'), 'database default values' => array('sample textarea default value'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => 'sample textarea value', + 'not_equal' => '', + 'contains' => 'sample', + 'does_not_contain' => 'foo', + 'begins_with' => 'sample', + 'ends_with' => 'value', + 'not_empty' => TRUE, + ), + 'mismatch conditional values' => array( + 'equal' => '', + 'not_equal' => 'sample textarea value', + 'contains' => 'foo', + 'does_not_contain' => 'sample', + 'begins_with' => 'value', + 'ends_with' => 'sample', + 'empty' => TRUE, + ), ), // Test textfield components. + 'textfield' => array( + 'component' => array( + 'form_key' => 'textfield', + 'name' => 'Textfield', + 'type' => 'textfield', + 'value' => '', + 'mandatory' => '0', + 'pid' => '0', + 'weight' => '-14', + ), + 'sample values' => '', + 'database values' => array(''), + 'database default values' => array(''), + ), 'textfield_disabled' => array( 'component' => array( 'form_key' => 'textfield_disabled', @@ -513,9 +678,20 @@ 'pid' => '0', 'weight' => '16', ), - 'sample values' => array('hour' => '5', 'minute' => '0', 'ampm' => 'am'), - 'database values' => array('05:00:00'), + 'sample values' => array('hour' => '12', 'minute' => '0', 'ampm' => 'pm'), + 'database values' => array('12:00:00'), 'database default values' => array('22:30:00'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '12:00pm', + 'before' => '1:00pm', + 'after' => '11:00am', + ), + 'mismatch conditional values' => array( + 'equal' => '12:00am', + 'before' => '12:00pm', + 'after' => '12:00pm', + ), ), 'time_24h' => array( 'component' => array( @@ -534,6 +710,17 @@ 'sample values' => array('hour' => '5', 'minute' => '0'), 'database values' => array('05:00:00'), 'database default values' => array('22:30:00'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '5:00', + 'before' => '24:00', + 'after' => '00:00', + ), + 'mismatch conditional values' => array( + 'equal' => '5:01', + 'before' => '5:00', + 'after' => '5:00', + ), ), // Test number components. @@ -557,6 +744,21 @@ 'sample values' => '2', 'database values' => array('2'), 'database default values' => array('1'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '2', + 'not_equal' => '0', + 'less_than' => '3', + 'greater_than' => '1', + 'not_empty' => TRUE, + ), + 'mismatch conditional values' => array( + 'equal' => '0', + 'not_equal' => '2', + 'less_than' => '2', + 'greater_than' => '2', + 'empty' => TRUE, + ), 'error values' => array( '1.5' => t('%name field value of @value must be an integer.', array('%name' => 'Integer', '@value' => '1.5')), '101' => t('%name field value must be less than @max.', array('%name' => 'Integer', '@max' => '100')), @@ -607,6 +809,21 @@ 'sample values' => '2.00', 'database values' => array('2.00'), 'database default values' => array('1'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '2', + 'not_equal' => '0', + 'less_than' => '3.000', + 'greater_than' => '1.000', + 'not_empty' => TRUE, + ), + 'mismatch conditional values' => array( + 'equal' => '0', + 'not_equal' => '2', + 'less_than' => '2.0', + 'greater_than' => '2.00', + 'empty' => TRUE, + ), 'error values' => array( '-1' => t('%name field value must be greater than @min.', array('%name' => 'Decimal positive', '@min' => '0')), ), @@ -657,9 +874,22 @@ 'pid' => '0', 'weight' => '21', ), - 'sample values' => '11.5', - 'database values' => array('11.5'), + 'sample values' => '10', + 'database values' => array('10'), 'database default values' => array('1'), + // Conditionals match against the 'sample values'. + 'match conditional values' => array( + 'equal' => '10', + 'not_equal' => '2.5', + 'less_than' => '11.5', + 'greater_than' => '1', + ), + 'mismatch conditional values' => array( + 'equal' => '2.5', + 'not_equal' => '10', + 'less_than' => '10', + 'greater_than' => '11.5', + ), ), ); @@ -698,6 +928,7 @@ 'submit_notice' => '1', 'roles' => array('1', '2'), 'components' => array(), + 'conditionals' => array(), 'emails' => array(), ), ); @@ -717,17 +948,29 @@ /** * Generate a list of all values that would result in a valid submission. + * + * @param $input_values + * An array of input values keyed by the component form key. If none + * are specified, the defaults will be pulled from testWebformComponents(). */ - function testWebformPost() { + function testWebformPost($input_values = NULL) { $edit = array(); - foreach ($this->testWebformComponents() as $key => $component_info) { - if (is_array($component_info['sample values'])) { - foreach ($component_info['sample values'] as $subkey => $value) { + + if (empty($input_values)) { + $input_values = array(); + foreach ($this->testWebformComponents() as $key => $component_info) { + $input_values[$key] = $component_info['sample values']; + } + } + + foreach ($input_values as $key => $values) { + if (is_array($values)) { + foreach ($values as $subkey => $value) { $edit["submitted[$key][$subkey]"] = $value; } } - elseif ($component_info['sample values'] != NULL) { - $value = $component_info['sample values']; + elseif ($values != NULL) { + $value = $values; // Multiple selects have a funky extra empty bracket in the name. $extra = $key == 'select_multiple' ? '[]' : ''; $edit["submitted[$key]$extra"] = $value; diff --git a/webform.api.php b/webform.api.php index 7a21ba4..342a888 100644 --- a/webform.api.php +++ b/webform.api.php @@ -115,7 +115,7 @@ function hook_webform_submission_presave($node, &$submission) { // Update some component's value before it is saved. $component_id = 4; - $submission->data[$component_id]['value'][0] = 'foo'; + $submission->data[$component_id][0] = 'foo'; } /** diff --git a/webform.info b/webform.info index a47535b..12ae28a 100644 --- a/webform.info +++ b/webform.info @@ -20,6 +20,7 @@ files[] = views/webform.views.inc files[] = tests/components.test +files[] = tests/conditionals.test files[] = tests/permissions.test files[] = tests/submission.test files[] = tests/webform.test diff --git a/webform.install b/webform.install index 5672a42..1bb216c 100644 --- a/webform.install +++ b/webform.install @@ -182,6 +182,107 @@ 'primary key' => array('nid', 'cid'), ); + $schema['webform_conditional'] = array( + 'description' => 'Holds information about conditional logic.', + 'fields' => array( + 'nid' => array( + 'description' => 'The node identifier of a webform.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'rgid' => array( + 'description' => 'The rule group identifier for this group of rules.', + 'type' => 'int', + 'size' => 'small', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'andor' => array( + 'description' => 'Whether to AND or OR the actions in this group. All actions within the same crid should have the same andor value.', + 'type' => 'varchar', + 'length' => 128, + ), + 'action' => array( + 'description' => 'The action to be performed on the target. Typically "show" or "hide" for targets of type "component", and "send" for targets of type "email".', + 'type' => 'varchar', + 'length' => 128, + ), + 'target_type' => array( + 'description' => 'The type of target to be affected. Either "component" or "email". Indicates what type of ID the "target" column contains.', + 'type' => 'varchar', + 'length' => 128, + ), + 'target' => array( + 'description' => 'The ID of the target to be affected. Typically a component ID.', + 'type' => 'varchar', + 'length' => 128, + ), + 'weight' => array( + 'description' => 'Determines the position of this conditional compared to others.', + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('nid', 'rgid'), + ); + + $schema['webform_conditional_rules'] = array( + 'description' => 'Holds information about conditional logic.', + 'fields' => array( + 'nid' => array( + 'description' => 'The node identifier of a webform.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'rgid' => array( + 'description' => 'The rule group identifier for this group of rules.', + 'type' => 'int', + 'size' => 'small', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'rid' => array( + 'description' => 'The rule identifier for this conditional rule.', + 'type' => 'int', + 'size' => 'small', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'source_type' => array( + 'description' => 'The type of source on which the conditional is based. Currently always "component". Indicates what type of ID the "source" column contains.', + 'type' => 'varchar', + 'length' => 128, + ), + 'source' => array( + 'description' => 'The component ID being used in this condition.', + 'type' => 'int', + 'size' => 'small', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'operator' => array( + 'description' => 'Which operator (equal, contains, starts with, etc.) should be used for this comparison between the source and value?', + 'type' => 'varchar', + 'length' => 128, + ), + 'value' => array( + 'description' => 'The value to be compared with source.', + 'type' => 'text', + ), + ), + 'primary key' => array('nid', 'rgid', 'rid'), + ); + $schema['webform_emails'] = array( 'description' => 'Holds information regarding e-mails that should be sent upon submitting a webform', 'fields' => array( @@ -989,3 +1090,229 @@ $sandbox['#finished'] = $sandbox['progress'] / $sandbox['max']; } } + +/** + * Add the webform_conditional database table. + */ +function webform_update_7402() { + $schema['webform_conditional'] = array( + 'description' => 'Holds information about conditional logic.', + 'fields' => array( + 'nid' => array( + 'description' => 'The node identifier of a webform.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'rgid' => array( + 'description' => 'The rule group identifier for this group of rules.', + 'type' => 'int', + 'size' => 'small', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'andor' => array( + 'description' => 'Whether to AND or OR the actions in this group. All actions within the same crid should have the same andor value.', + 'type' => 'varchar', + 'length' => 128, + ), + 'action' => array( + 'description' => 'The action to be performed on the target. Typically "show" or "hide" for targets of type "component", and "send" for targets of type "email".', + 'type' => 'varchar', + 'length' => 128, + ), + 'target_type' => array( + 'description' => 'The type of target to be affected. Either "component" or "email". Indicates what type of ID the "target" column contains.', + 'type' => 'varchar', + 'length' => 128, + ), + 'target' => array( + 'description' => 'The ID of the target to be affected. Typically a component ID.', + 'type' => 'varchar', + 'length' => 128, + ), + 'weight' => array( + 'description' => 'Determines the position of this conditional compared to others.', + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('nid', 'rgid'), + ); + + $schema['webform_conditional_rules'] = array( + 'description' => 'Holds information about conditional logic.', + 'fields' => array( + 'nid' => array( + 'description' => 'The node identifier of a webform.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'rgid' => array( + 'description' => 'The rule group identifier for this group of rules.', + 'type' => 'int', + 'size' => 'small', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'rid' => array( + 'description' => 'The rule identifier for this conditional rule.', + 'type' => 'int', + 'size' => 'small', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'source_type' => array( + 'description' => 'The type of source on which the conditional is based. Currently always "component". Indicates what type of ID the "source" column contains.', + 'type' => 'varchar', + 'length' => 128, + ), + 'source' => array( + 'description' => 'The component ID being used in this condition.', + 'type' => 'int', + 'size' => 'small', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'operator' => array( + 'description' => 'Which operator (equal, contains, starts with, etc.) should be used for this comparison between the source and value?', + 'type' => 'varchar', + 'length' => 128, + ), + 'value' => array( + 'description' => 'The value to be compared with source.', + 'type' => 'text', + ), + ), + 'primary key' => array('nid', 'rgid', 'rid'), + ); + + db_create_table('webform_conditional', $schema['webform_conditional']); + db_create_table('webform_conditional_rules', $schema['webform_conditional_rules']); + // Rebuild schema so that webform_update_7403() can use drupal_write_record(). + drupal_get_schema(NULL, TRUE); +} + +/** + * Convert per-component conditionals to new more flexible conditional system. + */ +function webform_update_7403(&$sandbox) { + // Set up the initial batch process. + if (!isset($sandbox['progress'])) { + $sandbox['progress'] = 0; + $sandbox['last_nid_processed'] = -1; + $sandbox['converted_count'] = 0; + $sandbox['max'] = db_select('webform') + ->countQuery() + ->execute() + ->fetchField(); + } + + $limit = variable_get('webform_update_batch_size', 100); + $webforms = db_select('webform', 'w') + ->fields('w') + ->condition('nid', $sandbox['last_nid_processed'], '>') + ->orderBy('nid', 'ASC') + ->range(0, $limit) + ->execute() + ->fetchAllAssoc('nid', PDO::FETCH_ASSOC); + + foreach ($webforms as $nid => $webform) { + // Update tokens in component configurations. + $result = db_select('webform_component', 'wc', array('fetch' => PDO::FETCH_ASSOC)) + ->fields('wc') + ->condition('wc.nid', $nid) + ->execute(); + $rgid = 0; + foreach ($result as $component) { + // For each component, check if it has conditional properties that need + // to be removed and/or migrated. Because these properties may be in any + // order, copy the original extra array for comparison. + $component['extra'] = unserialize($component['extra']); + $original_extra = $component['extra']; + + // Remove conditional properties if present. + if (isset($component['extra']['conditional_component'])) { + unset($component['extra']['conditional_component']); + } + if (isset($component['extra']['conditional_operator'])) { + unset($component['extra']['conditional_operator']); + } + if (isset($component['extra']['conditional_values'])) { + unset($component['extra']['conditional_values']); + + // If the component has conditional values specified, that indicates + // that this component was conditionally shown. Convert it to a new + // conditional with multiple rules if needed. + if (strlen(trim($original_extra['conditional_values'])) && !empty($original_extra['conditional_operator']) && !empty($original_extra['conditional_component'])) { + $conditional_values = explode("\n", $original_extra['conditional_values']); + $rules = array(); + $rule = array( + 'nid' => $nid, + 'rgid' => $rgid, + 'rid' => NULL, + 'source_type' => 'component', + 'source' => $original_extra['conditional_component'], + 'operator' => 'equal', + 'value' => NULL, + ); + foreach ($conditional_values as $value) { + $value = trim($value); + if ($value) { + $new_rule = $rule; + $new_rule['rid'] = count($rules); + $new_rule['value'] = $value; + $rules[] = $new_rule; + } + } + if (count($rules)) { + $conditional = array( + 'nid' => $nid, + 'rgid' => $rgid, + 'andor' => 'or', + 'action' => ($original_extra['conditional_operator'] === '=') ? 'show' : 'hide', + 'target_type' => 'component', + 'target' => $component['cid'], + 'weight' => 0, + ); + drupal_write_record('webform_conditional', $conditional); + foreach ($rules as $rule) { + drupal_write_record('webform_conditional_rules', $rule); + } + $sandbox['converted_count']++; + $rgid++; + } + } + } + + // Update the component with the conditional properties removed. + if ($component['extra'] != $original_extra) { + $component['extra'] = serialize($component['extra']); + drupal_write_record('webform_component', $component, array('nid', 'cid')); + } + } + } + + // If less than limit was processed, the update process is finished. + if (count($webforms) < $limit || $sandbox['progress'] == $sandbox['max']) { + $finished = TRUE; + } + + // If there's no max value then there's nothing to update and we're finished. + if (empty($sandbox['max']) || isset($finished)) { + return t('@count webforms using conditionals updated to the new conditional system.', array('@count' => $sandbox['converted_count'])); + } + else { + // Indicate our current progress to the batch update system. + $sandbox['#finished'] = $sandbox['progress'] / $sandbox['max']; + } +} diff --git a/webform.module b/webform.module index 375d159..8307004 100644 --- a/webform.module +++ b/webform.module @@ -11,6 +11,13 @@ */ /** + * Constants used in conditional logic. + */ +define('WEBFORM_CONDITIONAL_EXCLUDE', 0); +define('WEBFORM_CONDITIONAL_INCLUDE', 1); +define('WEBFORM_CONDITIONAL_SAME_PAGE', 2); + +/** * Implements hook_help(). */ function webform_help($section = 'admin/help#webform', $arg = NULL) { @@ -64,6 +71,9 @@ case 'node/%/webform/components': $output .= '

' . t('This page displays all the components currently configured for this webform node. You may add any number of components to the form, even multiple of the same type. To add a new component, fill in a name and select a type from the fields at the bottom of the table. Submit the form to create the new component or update any changed form values.') . '

'; $output .= '

' . t('Click on any existing component\'s name to edit its settings.') . '

'; + break; + case 'node/%/webform/conditionals': + $output .= '

' . t('Conditionals may be used to hide or show certain components (or entire pages!) based on the value of other components.') . '

'; break; case 'node/%/submission/%/resend': $output .= '

' . t('This form may be used to resend e-mails configured for this webform. Check the e-mails that need to be sent and click Resend e-mails to send these e-mails again.') . '

'; @@ -132,6 +142,16 @@ 'weight' => 0, 'type' => MENU_DEFAULT_LOCAL_TASK, ); + $items['node/%webform_menu/webform/conditionals'] = array( + 'title' => 'Conditionals', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('webform_conditionals_form', 1), + 'access callback' => 'node_access', + 'access arguments' => array('update', 1), + 'file' => 'includes/webform.conditionals.inc', + 'weight' => 1, + 'type' => MENU_LOCAL_TASK, + ); $items['node/%webform_menu/webform/configure'] = array( 'title' => 'Form settings', 'page callback' => 'drupal_get_form', @@ -139,7 +159,7 @@ 'access callback' => 'node_access', 'access arguments' => array('update', 1), 'file' => 'includes/webform.pages.inc', - 'weight' => 2, + 'weight' => 5, 'type' => MENU_LOCAL_TASK, ); @@ -151,7 +171,7 @@ 'access callback' => 'node_access', 'access arguments' => array('update', 1), 'file' => 'includes/webform.emails.inc', - 'weight' => 1, + 'weight' => 4, 'type' => MENU_LOCAL_TASK, ); $items['node/%webform_menu/webform/emails/%webform_menu_email'] = array( @@ -595,6 +615,15 @@ 'render element' => 'element', 'file' => 'includes/webform.components.inc', ), + // webform.conditionals.inc. + 'webform_conditional_groups' => array( + 'render element' => 'element', + 'file' => 'includes/webform.conditionals.inc', + ), + 'webform_conditional' => array( + 'render element' => 'element', + 'file' => 'includes/webform.conditionals.inc', + ), // webform.pages.inc. 'webform_advanced_redirection_form' => array( 'render element' => 'form', @@ -726,6 +755,13 @@ '#step' => NULL, ); + $elements['webform_conditional'] = array( + '#input' => TRUE, + '#theme' => 'webform_conditional', + '#default_value' => NULL, + '#process' => array('webform_conditional_expand'), + ); + return $elements; } @@ -738,9 +774,9 @@ 'label' => t('Date'), 'description' => t('Presents month, day, and year fields.'), 'features' => array( - 'conditional' => FALSE, ), 'file' => 'components/date.inc', + 'conditional_type' => 'date', ), 'email' => array( 'label' => t('E-mail'), @@ -808,11 +844,13 @@ 'features' => array( ), 'file' => 'components/number.inc', + 'conditional_type' => 'numeric', ), 'pagebreak' => array( 'label' => t('Page break'), 'description' => t('Organize forms into multiple pages.'), 'features' => array( + 'conditional' => FALSE, 'csv' => FALSE, 'default_value' => FALSE, 'description' => FALSE, @@ -831,6 +869,7 @@ 'email_address' => TRUE, 'email_name' => TRUE, ), + 'conditional_type' => 'select', ), 'textarea' => array( 'label' => t('Textarea'), @@ -854,9 +893,9 @@ 'label' => t('Time'), 'description' => t('Presents the user with hour and minute fields. Optional am/pm fields.'), 'features' => array( - 'conditional' => FALSE, ), 'file' => 'components/time.inc', + 'conditional_type' => 'time', ), ); @@ -874,6 +913,14 @@ } return $component_info; +} + +/** + * Implements hook_webform_conditional_operator_info(). + */ +function webform_webform_conditional_operator_info() { + module_load_include('inc', 'webform', 'includes/webform.conditionals'); + return _webform_conditional_operator_info(); } /** @@ -947,8 +994,8 @@ foreach ($node->webform['components'] as $cid => $component) { if ($component['type'] == 'file') { $has_file_components = TRUE; - if (!empty($submission->data[$cid]['value'])) { - $new_fids = array_merge($new_fids, $submission->data[$cid]['value']); + if (!empty($submission->data[$cid])) { + $new_fids = array_merge($new_fids, $submission->data[$cid]); } } } @@ -960,8 +1007,8 @@ foreach ($node->webform['components'] as $cid => $component) { if ($component['type'] == 'file') { - if (!empty($old_submission->data[$cid]['value'])) { - $old_fids = array_merge($old_fids, $old_submission->data[$cid]['value']); + if (!empty($old_submission->data[$cid])) { + $old_fids = array_merge($old_fids, $old_submission->data[$cid]); } } } @@ -1122,6 +1169,15 @@ } } + // Insert conditionals. Also used with clone.module. + if (isset($node->webform['conditionals']) && !empty($node->webform['conditionals'])) { + foreach ($node->webform['conditionals'] as $rgid => $conditional) { + $conditional['nid'] = $node->nid; + $conditional['rgid'] = $rgid; + webform_conditional_insert($conditional); + } + } + // Insert emails. Also used with clone.module. if (isset($node->webform['emails']) && !empty($node->webform['emails'])) { foreach ($node->webform['emails'] as $eid => $email) { @@ -1188,6 +1244,24 @@ } } + // Compare the webform conditionals and don't do anything if it's not needed. + if ($original->webform['conditionals'] != $node->webform['conditionals']) { + module_load_include('inc', 'webform', 'includes/webform.conditionals'); + + // Conditionals don't have unique site-wide IDs or configuration, so our + // update here is a bit more aggressive than for components and e-mails. + foreach ($original->webform['conditionals'] as $rgid => $conditional) { + if (!isset($node->webform['conditionals'][$rgid]) || $original->webform['conditionals'][$rgid] != $node->webform['conditionals'][$rgid]) { + webform_conditional_delete($node, $conditional); + } + } + foreach ($node->webform['conditionals'] as $rgid => $conditional) { + $conditional['nid'] = $node->nid; + $conditional['rgid'] = $rgid; + webform_conditional_insert($conditional); + } + } + // Compare the webform e-mails and don't do anything if it's not needed. if ($original->webform['emails'] != $node->webform['emails']) { module_load_include('inc', 'webform', 'includes/webform.emails'); @@ -1239,6 +1313,8 @@ // Remove any trace of webform data from the database. db_delete('webform')->condition('nid', $node->nid)->execute(); db_delete('webform_component')->condition('nid', $node->nid)->execute(); + db_delete('webform_conditional')->condition('nid', $node->nid)->execute(); + db_delete('webform_conditional_rules')->condition('nid', $node->nid)->execute(); db_delete('webform_emails')->condition('nid', $node->nid)->execute(); db_delete('webform_roles')->condition('nid', $node->nid)->execute(); db_delete('webform_submissions')->condition('nid', $node->nid)->execute(); @@ -1269,6 +1345,7 @@ 'roles' => array('1', '2'), 'emails' => array(), 'components' => array(), + 'conditionals' => array(), ); drupal_alter('webform_node_defaults', $defaults); return $defaults; @@ -1365,6 +1442,25 @@ $page_count = 1; _webform_components_tree_build($nodes[$nid]->webform['components'], $component_tree, 0, $page_count); $nodes[$nid]->webform['components'] = _webform_components_tree_flatten($component_tree['children']); + } + + // Load all the conditional information, if any. + $nodes[$nid]->webform['conditionals'] = db_select('webform_conditional') + ->fields('webform_conditional') + ->condition('nid', $nid) + ->orderBy('rgid') + ->execute() + ->fetchAllAssoc('rgid', PDO::FETCH_ASSOC); + if ($nodes[$nid]->webform['conditionals']) { + $rules = db_select('webform_conditional_rules') + ->fields('webform_conditional_rules') + ->condition('nid', $nid) + ->orderBy('rgid') + ->orderBy('rid') + ->execute(); + foreach ($rules as $rule) { + $nodes[$nid]->webform['conditionals'][$rule->rgid]['rules'][$rule->rid] = (array) $rule; + } } } } @@ -1930,14 +2026,15 @@ // Recursively add components to the form. The unfiltered version of the // form (typically used in Form Builder), includes all components. + $input_values = isset($form_state['values']['submitted']) ? $form_state['values']['submitted'] : NULL; foreach ($component_tree['children'] as $cid => $component) { $component_value = isset($form_state['values']['submitted'][$cid]) ? $form_state['values']['submitted'][$cid] : NULL; - if ($filter == FALSE || _webform_client_form_rule_check($node, $component, $page_num, $form_state)) { + if ($filter == FALSE || _webform_client_form_rule_check($node, $component, $page_num, $input_values)) { if ($component['type'] == 'pagebreak') { $next_page_labels[$component['page_num'] - 1] = !empty($component['extra']['next_page_label']) ? $component['extra']['next_page_label'] : t('Next Page >'); $prev_page_labels[$component['page_num']] = !empty($component['extra']['prev_page_label']) ? $component['extra']['prev_page_label'] : t('< Previous Page'); } - _webform_client_form_add_component($node, $component, $component_value, $form['submitted'], $form, $form_state, $submission, 'form', $page_num, $filter); + _webform_client_form_add_component($node, $component, $component_value, $form['submitted'], $form, $input_values, 'form', $page_num, $filter); } } @@ -2038,11 +2135,53 @@ /** * Check if a component should be displayed on the current page. + * + * @param $node + * The full node object. + * @param $component + * The target component that is being checked if it should be shown. + * @param $page_num + * The page number of the component that is being checked. If the number "0" + * is passed in, the component visibility is checked regardless of its page + * number. + * @param $input_values + * An array of all the values in the form used for comparison. Values from + * $form_state['values']['submitted'] or $submission->data may be used. + * @return + * A Webform constant of one of the following: + * - WEBFORM_CONDITIONAL_EXCLUDE (0): The component should be hidden. + * - WEBFORM_CONDITIONAL_INCLUDE (1): The component should be shown. + * - WEBFORM_CONDITIONAL_SAME_PAGE (2): The component should be hidden, but needs + * to be rendered on the page because at least one source component is on + * the same page. The field will be hidden with JavaScript. */ -function _webform_client_form_rule_check($node, $component, $page_num, $form_state = NULL, $submission = NULL) { - $conditional_values = isset($component['extra']['conditional_values']) ? $component['extra']['conditional_values'] : NULL; - $conditional_component = isset($component['extra']['conditional_component']) && isset($node->webform['components'][$component['extra']['conditional_component']]) ? $node->webform['components'][$component['extra']['conditional_component']] : NULL; - $conditional_cid = $conditional_component['cid']; +function _webform_client_form_rule_check($node, $component, $page_num, $input_values) { + // Hold a static map of target CID to conditionals for lookup efficiency. + static $target_maps = array(); + $show_component = TRUE; + + if (!isset($target_maps[$node->nid])) { + $target_map = array(); + foreach ($node->webform['conditionals'] as $conditional) { + if ($conditional['target_type'] == 'component') { + $target_map[$conditional['target']][] = $conditional; + } + } + $target_maps[$node->nid] = $target_map; + } + else { + $target_map = $target_maps[$node->nid]; + } + + // Check for private components first, which is an easy check. + if ($component['extra']['private']) { + $show_component = webform_results_access($node); + } + + // Short cut the rest of this function if no conditionals are defined. + if (empty($target_map)) { + return $show_component ? WEBFORM_CONDITIONAL_INCLUDE : WEBFORM_CONDITIONAL_EXCLUDE; + } // Check the rules for this entire page. Note individual page breaks are // checked down below in the individual component rule checks. @@ -2050,7 +2189,7 @@ if ($component['page_num'] > 1 && $component['type'] != 'pagebreak') { foreach ($node->webform['components'] as $cid => $page_component) { if ($page_component['type'] == 'pagebreak' && $page_component['page_num'] == $page_num) { - $show_page = _webform_client_form_rule_check($node, $page_component, $page_num, $form_state, $submission); + $show_page = _webform_client_form_rule_check($node, $page_component, $page_num, $input_values); break; } } @@ -2060,44 +2199,70 @@ $show_parent = $show_page; if ($show_parent && $component['pid'] && isset($node->webform['components'][$component['pid']])) { $parent_component = $node->webform['components'][$component['pid']]; - $show_parent = _webform_client_form_rule_check($node, $parent_component, $page_num, $form_state, $submission); + $show_parent = _webform_client_form_rule_check($node, $parent_component, $page_num, $input_values); } // Check the individual component rules. $show_component = $show_parent; - if ($show_component && ($page_num == 0 || $component['page_num'] == $page_num) && $conditional_component && strlen(trim($conditional_values))) { - $input_values = array(); - if (isset($form_state)) { - $input_value = isset($form_state['values']['submitted'][$conditional_cid]) ? $form_state['values']['submitted'][$conditional_cid] : NULL; - $input_values = is_array($input_value) ? $input_value : array($input_value); - } - elseif (isset($submission)) { - $input_values = isset($submission->data[$conditional_cid]['value']) ? $submission->data[$conditional_cid]['value'] : array(); - } + if ($show_component && ($page_num == 0 || $component['page_num'] == $page_num) && isset($target_map[$component['cid']])) { + module_load_include('inc', 'webform', 'includes/webform.conditionals'); + $operators = webform_conditional_operators(); + $conditionals = $target_map[$component['cid']]; + foreach ($conditionals as $conditional) { + $conditional_result = TRUE; - $test_values = array_map('trim', explode("\n", $conditional_values)); - if (empty($input_values) && !empty($test_values)) { - $show_component = FALSE; - } - else { - foreach ($input_values as $input_value) { - if ($show_component = in_array($input_value, $test_values)) { - break; + // Execute each comparison callback. + $conditional_results = array(); + foreach ($conditional['rules'] as $rule) { + // TODO: Support other source types besides components? + if ($rule['source_type'] !== 'component') { + continue; } + $source_component = $node->webform['components'][$rule['source']]; + $source_cid = $source_component['cid']; + + // If a source component is on the same page as the current page, we + // are unable to tell if the target component is needed, so we return + // the same page constant, which evaluates to TRUE (a value of 2). + if ($source_component['page_num'] == $page_num) { + $show_component = WEBFORM_CONDITIONAL_SAME_PAGE; + break 2; + } + + $source_values = array(); + if (isset($input_values[$source_cid])) { + $source_values = is_array($input_values[$source_cid]) ? $input_values[$source_cid] : array($input_values[$source_cid]); + } + + // Determine the operator and callback. + $conditional_type = webform_component_property($source_component['type'], 'conditional_type'); + $operator_info = $operators[$conditional_type]; + + // Perform the comparison callback and build the results for this group. + $comparison_callback = $operator_info[$rule['operator']]['comparison callback']; + $conditional_results[] = $comparison_callback($source_values, $rule['value']); + } + + // Calculate the and/or result. + $filtered_results = array_filter($conditional_results); + if ($conditional['andor'] === 'or') { + $conditional_result = count($filtered_results) > 0; + } + else { + $conditional_result = count($filtered_results) === count($conditional_results); + } + + // Flip the result of the action is to hide. + if ($conditional['action'] == 'hide') { + $show_component = !$conditional_result; + } + else { + $show_component = $conditional_result; } } - - if ($component['extra']['conditional_operator'] == '!=') { - $show_component = !$show_component; - } } - // Private component? - if ($component['extra']['private']) { - $show_component = webform_results_access($node); - } - - return $show_component; + return $show_component ? WEBFORM_CONDITIONAL_INCLUDE : WEBFORM_CONDITIONAL_EXCLUDE; } /** @@ -2114,10 +2279,11 @@ * The fieldset to which this element will be added. * @param $form * The entire form array. - * @param $form_state - * The form state. - * @param $submission - * The Webform submission as retrieved from the database. + * @param $input_values + * All the values for this form, keyed by the component IDs. This may be + * pulled from $form_state['values']['submitted'] or $submission->data. + * These values are used to check if the component should be displayed + * conditionally. * @param $format * The format the form should be displayed as. May be one of the following: * - form: Show as an editable form. @@ -2130,13 +2296,13 @@ * @see webform_client_form() * @see webform_submission_render() */ -function _webform_client_form_add_component($node, $component, $component_value, &$parent_fieldset, &$form, $form_state, $submission, $format = 'form', $page_num = 0, $filter = TRUE) { +function _webform_client_form_add_component($node, $component, $component_value, &$parent_fieldset, &$form, $input_values, $format = 'form', $page_num = 0, $filter = TRUE) { $cid = $component['cid']; // Load with submission information if necessary. if ($format != 'form') { // This component is display only. - $data = empty($submission->data[$cid]['value']) ? NULL : $submission->data[$cid]['value']; + $data = empty($input_values[$cid]) ? NULL : $input_values[$cid]; if ($display_element = webform_component_invoke($component['type'], 'display', $component, $data, $format)) { // Ensure the component is added as a property. $display_element['#webform_component'] = $component; @@ -2163,7 +2329,7 @@ // version of the form (such as for Form Builder). elseif ($component['page_num'] == $page_num || $filter == FALSE) { // Add this user-defined field to the form (with all the values that are always available). - $data = isset($submission->data[$cid]['value']) ? $submission->data[$cid]['value'] : NULL; + $data = isset($input_values[$cid]) ? $input_values[$cid] : NULL; if ($element = webform_component_invoke($component['type'], 'render', $component, $data, $filter)) { // Ensure the component is added as a property. $element['#webform_component'] = $component; @@ -2200,16 +2366,19 @@ if (isset($component['children']) && is_array($component['children'])) { foreach ($component['children'] as $scid => $subcomponent) { - $subcomponent_value = isset($form_state['values']['submitted'][$scid]) ? $form_state['values']['submitted'][$scid] : NULL; - if (_webform_client_form_rule_check($node, $subcomponent, $page_num, $form_state, $submission)) { - _webform_client_form_add_component($node, $subcomponent, $subcomponent_value, $parent_fieldset[$component['form_key']], $form, $form_state, $submission, $format, $page_num, $filter); + $subcomponent_value = isset($input_values[$scid]) ? $input_values[$scid] : NULL; + if (_webform_client_form_rule_check($node, $subcomponent, $page_num, $input_values)) { + _webform_client_form_add_component($node, $subcomponent, $subcomponent_value, $parent_fieldset[$component['form_key']], $form, $input_values, $format, $page_num, $filter); } } } } +/** + * Form API #validate handler for the webform_client_form() form. + */ function webform_client_form_validate($form, &$form_state) { - $node = node_load($form_state['values']['details']['nid']); + $node = $form['#node']; $finished = $form_state['values']['details']['finished']; // Check that the submissions have not exceeded the total submission limit. @@ -2240,9 +2409,22 @@ } } + // Assemble an array of all past and new input values that will determine if + // certain elements need validation at all. + if (!empty($node->webform['conditionals'])) { + $input_values = isset($form_state['storage']['submitted']) ? $form_state['storage']['submitted'] : array(); + $new_values = _webform_client_form_submit_flatten($form['#node'], $form_state['values']['submitted']); + foreach ($new_values as $cid => $values) { + $input_values[$cid] = $values; + } + } + else { + $input_values = NULL; + } + // Run all #element_validate and #required checks. These are skipped initially // by setting #validated = TRUE on all components when they are added. - _webform_client_form_validate($form, $form_state); + _webform_client_form_validate($form, $form_state, 'webform_client_form', $input_values); } /** @@ -2251,16 +2433,22 @@ * This function imitates _form_validate in Drupal's form.inc, only it sets * a different property to ensure that validation has occurred. */ -function _webform_client_form_validate($elements, &$form_state, $first_run = TRUE) { - static $form; - if ($first_run) { - $form = $elements; +function _webform_client_form_validate(&$elements, &$form_state, $form_id = NULL, $input_values = NULL) { + // Webform-specific enhancement, only validate the field if it was used in + // this submission. This both skips validation on the field and sets the value + // of the field to NULL, preventing any dangerous input. + if (isset($input_values) && isset($elements['#webform_component'])) { + $needs_validation = _webform_client_form_rule_check($form_state['complete form']['#node'], $elements['#webform_component'], 0, $input_values); + if (!$needs_validation) { + form_set_value($elements, NULL, $form_state); + return; + } } // Recurse through all children. foreach (element_children($elements) as $key) { if (isset($elements[$key]) && $elements[$key]) { - _webform_client_form_validate($elements[$key], $form_state, FALSE); + _webform_client_form_validate($elements[$key], $form_state, NULL, $input_values); } } // Validate the current input. @@ -2303,12 +2491,16 @@ } } + // Call user-defined form level validators. + if (isset($form_id)) { + form_execute_handlers('validate', $elements, $form_state); + } // Call any element-specific validators. These must act on the element // #value data. - if (isset($elements['#element_validate'])) { + elseif (isset($elements['#element_validate'])) { foreach ($elements['#element_validate'] as $function) { if (function_exists($function)) { - $function($elements, $form_state, $form); + $function($elements, $form_state, $form_state['complete form']); } } } @@ -2329,12 +2521,11 @@ $form_state['storage']['page_num'] = $form_state['webform']['page_num']; } + // Flatten trees within the submission. + $form_state['values']['submitted'] = _webform_client_form_submit_flatten($node, $form_state['values']['submitted']); + // Perform post processing by components. _webform_client_form_submit_process($node, $form_state['values']['submitted']); - - // Flatten trees within the submission. - $form_state['values']['submitted_tree'] = $form_state['values']['submitted']; - $form_state['values']['submitted'] = _webform_client_form_submit_flatten($node, $form_state['values']['submitted']); // Assume the form is completed unless the page logic says otherwise. $form_state['webform_completed'] = TRUE; @@ -2382,7 +2573,7 @@ if (isset($previous_pagebreak)) { $page_num = $previous_pagebreak['page_num'] + $direction - 1; // If we've found an component on this page, advance to that page. - if ($component['page_num'] == $page_num && _webform_client_form_rule_check($node, $component, $page_num, $form_state)) { + if ($component['page_num'] == $page_num && _webform_client_form_rule_check($node, $component, $page_num, $form_state['values']['submitted'])) { $form_state['storage']['page_num'] = $page_num; break; } @@ -2576,20 +2767,13 @@ * @param $parent * Internal use. The current parent CID whose children are being processed. */ -function _webform_client_form_submit_process($node, &$form_values, $types = NULL, $parent = 0) { - if (is_array($form_values)) { - foreach ($form_values as $form_key => $value) { - $cid = webform_get_cid($node, $form_key, $parent); - if (is_array($value) && isset($node->webform['components'][$cid]['type']) && webform_component_feature($node->webform['components'][$cid]['type'], 'group')) { - _webform_client_form_submit_process($node, $form_values[$form_key], $types, $cid); - } - - if (isset($node->webform['components'][$cid])) { - // Call the component process submission function. - $component = $node->webform['components'][$cid]; - if ((!isset($types) || in_array($component['type'], $types)) && webform_component_implements($component['type'], 'submit')) { - $form_values[$component['form_key']] = webform_component_invoke($component['type'], 'submit', $component, $form_values[$component['form_key']]); - } +function _webform_client_form_submit_process($node, &$form_values) { + foreach ($form_values as $cid => $value) { + if (isset($node->webform['components'][$cid])) { + // Call the component process submission function. + $component = $node->webform['components'][$cid]; + if ((!isset($types) || in_array($component['type'], $types)) && webform_component_implements($component['type'], 'submit')) { + $form_values[$cid] = webform_component_invoke($component['type'], 'submit', $component, $form_values[$cid]); } } } @@ -2637,6 +2821,14 @@ elseif (isset($vars['form']['submission']['#value'])) { $vars['nid'] = $vars['form']['submission']['#value']->nid; } + + if (!empty($vars['form']['#node']->webform['conditionals'])) { + module_load_include('inc', 'webform', 'includes/webform.conditionals'); + $submission_data = isset($vars['form']['#parameters'][1]['storage']['submitted']) ? $vars['form']['#parameters'][1]['storage']['submitted'] : array(); + $settings = webform_conditional_prepare_javascript($vars['form']['#node'], $submission_data); + drupal_add_js(array('webform' => array('conditionals' => array('webform-client-form-' . $vars['nid'] => $settings))), 'setting'); + } + } /** @@ -3107,8 +3299,8 @@ $name = webform_variable_get('webform_default_from_name'); } elseif (is_numeric($name) && isset($node->webform['components'][$name])) { - if (isset($submission->data[$name]['value'])) { - $name = $submission->data[$name]['value']; + if (isset($submission->data[$name])) { + $name = $submission->data[$name]; } else { $name = t('Value of !component', array('!component' => $node->webform['components'][$name]['name'])); @@ -3119,8 +3311,8 @@ $address = webform_variable_get('webform_default_from_address'); } elseif (is_numeric($address) && isset($node->webform['components'][$address])) { - if (isset($submission->data[$address]['value'])) { - $values = $submission->data[$address]['value'];; + if (isset($submission->data[$address])) { + $values = $submission->data[$address]; $address = array(); foreach ($values as $value) { $address = array_merge($address, explode(',', $value)); @@ -3168,9 +3360,9 @@ } elseif (is_numeric($subject) && isset($node->webform['components'][$subject])) { $component = $node->webform['components'][$subject]; - if (isset($submission->data[$subject]['value'])) { + if (isset($submission->data[$subject])) { $display_function = '_webform_display_' . $component['type']; - $value = $submission->data[$subject]['value']; + $value = $submission->data[$subject]; // Convert the value to a clean text representation if possible. if (function_exists($display_function)) { @@ -3354,6 +3546,14 @@ } /** + * Form API #process function to expand a webform conditional element. + */ +function webform_conditional_expand($element) { + module_load_include('inc', 'webform', 'includes/webform.conditionals'); + return _webform_conditional_expand($element); +} + +/** * Disable the Drupal page cache. */ function webform_disable_page_cache() {