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 .= '';
+
+ 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() {