commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-old / webform / includes / webform.conditionals.inc
1 <?php
2
3 /**
4 * @file
5 * Form elements and menu callbacks to provide conditional handling in Webform.
6 */
7
8 /**
9 * Form builder; Provide the form for adding conditionals to a webform node.
10 */
11 function webform_conditionals_form($form, &$form_state, $node) {
12 form_load_include($form_state, 'inc', 'webform', $name = 'includes/webform.components');
13 form_load_include($form_state, 'inc', 'webform', $name = 'includes/webform.conditionals');
14
15 // Add JavaScript settings to the page needed for conditional elements.
16 _webform_conditional_expand_value_forms($node);
17
18 if (isset($form_state['values']['conditionals'])) {
19 // Remove the "new" conditional that always comes in.
20 unset($form_state['values']['conditionals']['new']);
21
22 $conditionals = $form_state['values']['conditionals'];
23 }
24 else {
25 $conditionals = $node->webform['conditionals'];
26 }
27 // Empty out any conditionals that have no rules or actions.
28 foreach ($conditionals as $rgid => &$conditional) {
29 webform_delete_empty_subconditionals($conditional);
30 if (empty($conditional['rules']) || empty($conditional['actions'])) {
31 unset($conditionals[$rgid]);
32 }
33 }
34 unset($conditional); // Drop PHP reference.
35
36 // Check the current topological sort order for the conditionals and report any errors,
37 // but only for actual form submissions and not for ajax-related form builds, such as
38 // adding or removing a condtion or conditional group.
39 if (empty($form_state['triggering_element']['#ajax'])) {
40 $node->webform['conditionals'] = $conditionals;
41 webform_get_conditional_sorter($node)->reportErrors($conditionals);
42 }
43
44 $form['#tree'] = TRUE;
45 $form['#node'] = $node;
46
47 $form['#attached']['library'][] = array('webform', 'admin');
48 $form['#attached']['css'][] = drupal_get_path('module', 'webform') . '/css/webform.css';
49
50 // Wrappers used for AJAX addition/removal.
51 $form['conditionals']['#theme'] = 'webform_conditional_groups';
52 $form['conditionals']['#prefix'] = '<div id="webform-conditionals-ajax">';
53 $form['conditionals']['#suffix'] = '</div>';
54
55 // Keep track of the max conditional count to use as the range for weights.
56 $form_state['conditional_count'] = isset($form_state['conditional_count']) ? $form_state['conditional_count'] : 1;
57 $form_state['conditional_count'] = count($conditionals) > $form_state['conditional_count'] ? count($conditionals) : $form_state['conditional_count'];
58
59 $source_list = webform_component_list($node, 'conditional', 'path', TRUE);
60 $target_list = webform_component_list($node, TRUE, 'path', TRUE);
61 $components = $node->webform['components'];
62 $delta = $form_state['conditional_count'];
63 $weight = -$delta - 1;
64 $index = 0;
65 foreach ($conditionals as $rgid => $conditional_group) {
66 $weight = $conditional_group['weight'];
67 $form['conditionals'][$rgid] = array(
68 '#theme' => 'webform_conditional_group_row',
69 '#even_odd' => ++$index % 2 ? 'odd' : 'even',
70 '#weight' => $weight,
71 'rgid' => array(
72 '#type' => 'value',
73 '#value' => $rgid,
74 ),
75 'conditional' => array(
76 '#type' => 'webform_conditional',
77 '#default_value' => $conditional_group,
78 '#nid' => $node->nid,
79 '#sources' => $source_list,
80 '#actions' => array(
81 'show' => t('shown'),
82 'require' => t('required'),
83 'set' => t('set to'),
84 ),
85 '#targets' => $target_list,
86 '#parents' => array('conditionals', $rgid),
87 ),
88 );
89 foreach ($conditional_group['actions'] as $action) {
90 $cid = $action['target'];
91 if ($action['action'] == 'require' && !$components[$cid]['required']) {
92 drupal_set_message(t('Component %title must be configured as Required for Webform to conditionally change its required status. <a href="!url">Configure %title.</a>',
93 array('%title' => $components[$cid]['name'],
94 '!url' => url("node/{$node->nid}/webform/components/$cid", array('query' => array('destination' => "node/{$node->nid}/webform/conditionals"))))
95 ), 'error');
96 }
97 }
98 $form['conditionals'][$rgid]['weight'] = array(
99 '#type' => 'weight',
100 '#title' => t('Weight for rule group !rgid', array('!rgid' => $rgid)),
101 '#title_display' => 'invisible',
102 '#default_value' => $weight,
103 '#delta' => $delta,
104 );
105 }
106
107 $form['conditionals']['new']['#weight'] = $weight + 1;
108 $form['conditionals']['new']['weight'] = array(
109 '#type' => 'weight',
110 '#title' => t('Weight for new rule group'),
111 '#title_display' => 'invisible',
112 '#default_value' => $weight + 1,
113 '#delta' => $delta,
114 );
115 $form['conditionals']['new']['new'] = array(
116 '#type' => 'submit',
117 '#value' => t('+'),
118 '#submit' => array('webform_conditionals_form_add'),
119 '#ajax' => array(
120 'progress' => 'none',
121 'effect' => 'fade',
122 'callback' => 'webform_conditionals_ajax',
123 ),
124 );
125 // Create dummy remove button for form alignment only.
126 $form['conditionals']['new']['remove'] = array(
127 '#type' => 'submit',
128 '#value' => t('-'),
129 '#disabled' => TRUE,
130 );
131
132 $form['actions'] = array(
133 '#type' => 'actions',
134 '#tree' => FALSE,
135 );
136 $form['actions']['submit'] = array(
137 '#type' => 'submit',
138 '#value' => t('Save conditions'),
139 '#validate' => array('webform_conditionals_form_validate'),
140 '#submit' => array('webform_conditionals_form_submit'),
141 );
142
143 // Estimate if the form is too long for PHP max_input_vars and detect whether a previous submission was truncated.
144 // The estimate will be accurate because the form elements for this page are well known. Ajax use of this
145 // page will not generate user-visible errors, so a preflight may be the only indication to the user that
146 // the page is too long.
147 webform_input_vars_check($form, $form_state, 'conditionals', '');
148 return $form;
149 }
150
151 /**
152 * Submit handler for webform_conditionals_form(). Add an additional choice.
153 */
154 function webform_conditionals_form_add($form, &$form_state) {
155 // Build a default new conditional.
156 unset($form_state['values']['conditionals']['new']);
157 $weight = count($form_state['values']['conditionals']) > 10 ? -count($form_state['values']['conditionals']) : -10;
158 foreach ($form_state['values']['conditionals'] as $key => $conditional) {
159 $weight = max($weight, $conditional['weight']);
160 }
161
162 // Add the conditional to form state and rebuild the form.
163 $form_state['values']['conditionals'][] = array(
164 'rules' => array(
165 array(
166 'source_type' => 'component',
167 'source' => NULL,
168 'operator' => NULL,
169 'value' => NULL,
170 ),
171 ),
172 'andor' => 'and',
173 'actions' => array(
174 array(
175 'target_type' => 'component',
176 'target' => NULL,
177 'invert' => NULL,
178 'action' => NULL,
179 'argument' => NULL,
180 ),
181 ),
182 'weight' => $weight + 1,
183 );
184 $form_state['rebuild'] = TRUE;
185 }
186
187 /**
188 * Validate handler for webform_conditionals_form().
189 *
190 * Prohibit the source and target of a conditional rule from being the same.
191 */
192 function webform_conditionals_form_validate($form, &$form_state) {
193 // Skip validation unless this is saving the form.
194 $button_key = end($form_state['triggering_element']['#array_parents']);
195 if ($button_key !== 'submit') {
196 return;
197 }
198
199 $node = $form['#node'];
200 $components = $node->webform['components'];
201 $component_options = webform_component_options();
202 foreach ($form_state['complete form']['conditionals'] as $conditional_key => $element) {
203 if (substr($conditional_key, 0, 1) !== '#' && $conditional_key !== 'new') {
204 $conditional = $element['conditional'];
205 $targets = array();
206 foreach ($conditional['actions'] as $action_key => $action) {
207 if (is_numeric($action_key)) {
208 $operation = $action['action']['#value'];
209 $target_id = $action['target']['#value'];
210 if (isset($targets[$target_id][$operation])) {
211 form_set_error('conditionals][' . $conditional_key . '][actions][' . $action_key . '][target',
212 t('A operation %op cannot be made for a component more than once. (%target).',
213 array('%op' => $action['action']['#options'][$operation],
214 '%target' => $components[$action['target']['#value']]['name'])));
215 }
216 $component_type = $node->webform['components'][$action['target']['#value']]['type'];
217 if (!webform_conditional_action_able($component_type, $action['action']['#value'])) {
218 form_set_error('conditionals][' . $conditional_key . '][actions][' . $action_key . '][action',
219 t('A component of type %type can\'t be %action. (%target)',
220 array(
221 '%action' => $action['action']['#options'][$action['action']['#value']],
222 '%type' => $component_options[$component_type],
223 '%target' => $components[$action['target']['#value']]['name'])));
224 }
225 $targets[$target_id][$operation] = $target_id;
226 }
227 }
228 foreach ($conditional['rules'] as $rule_key => $rule) {
229 // Validate component rules, but not conditional_start/end rules.
230 if (is_numeric($rule_key) && $rule['source_type']['#value'] == 'component' && isset($targets[$rule['source']['#value']])) {
231 form_set_error('conditionals][' . $conditional_key . '][rules][' . $rule_key . '][source',
232 t('The subject of the conditional cannot be the same as the component that is changed (%target).',
233 array('%target' => $components[$rule['source']['#value']]['name'])));
234 }
235 }
236 }
237 }
238
239 // Form validation will not rebuild the form, so we need to ensure
240 // necessary JavaScript will still exist.
241 _webform_conditional_expand_value_forms($node);
242 }
243
244 /**
245 * Submit handler for webform_conditionals_form().
246 */
247 function webform_conditionals_form_submit($form, &$form_state) {
248 $node = $form['#node'];
249
250 // Remove the new conditional placeholder.
251 unset($form_state['values']['conditionals']['new']);
252
253 $node->webform['conditionals'] = $form_state['values']['conditionals'];
254 node_save($node);
255 drupal_set_message(t('Conditionals for %title saved.', array('%title' => $node->title)));
256 }
257
258 /**
259 * AJAX callback to render out adding a new condition.
260 */
261 function webform_conditionals_ajax($form, $form_state) {
262 $rgids = element_children($form['conditionals']);
263 $new_rgid = max($rgids);
264 $form['conditionals'][$new_rgid]['#ajax_added'] = TRUE;
265
266 $commands = array('#type' => 'ajax');
267 $commands['#commands'][] = ajax_command_before('.webform-conditional-new-row', drupal_render($form['conditionals'][$new_rgid]));
268 $commands['#commands'][] = ajax_command_restripe('#webform-conditionals-table');
269 return $commands;
270 }
271
272 /**
273 * Theme the $form['conditionals'] of webform_conditionals_form().
274 */
275 function theme_webform_conditional_groups($variables) {
276 $element = $variables['element'];
277 drupal_add_tabledrag('webform-conditionals-table', 'order', 'sibling', 'webform-conditional-weight');
278 drupal_add_js('Drupal.theme.prototype.tableDragChangedMarker = function() { return ""; }', 'inline');
279 drupal_add_js('Drupal.theme.prototype.tableDragChangedWarning = function() { return "<span>&nbsp;</span>"; }', 'inline');
280
281 $output = '<table id="webform-conditionals-table"><tbody>';
282 $element_children = element_children($element, TRUE);
283 $element_count = count($element_children);
284 foreach ($element_children as $index => $key) {
285 if ($key === 'new') {
286 $even_odd = ($index + 1) % 2 ? 'odd' : 'even';
287 $element[$key]['weight']['#attributes']['class'] = array('webform-conditional-weight');
288 $data = '<div class="webform-conditional-new">';
289 if ($element_count === 1) {
290 $data .= t('There are no conditional actions on this form.') . ' ';
291 }
292 $data .= t('Add a new condition:') . ' ' . drupal_render($element[$key]['new']) . drupal_render($element[$key]['remove']);
293 $data .= '</div>';
294 $output .= '<tr class="webform-conditional-new-row ' . $even_odd . '">';
295 $output .= '<td>' . $data . '</td>';
296 $output .= '<td>' . drupal_render($element[$key]['weight']) . '</td>';
297 $output .= '</tr>';
298 }
299 else {
300 $output .= drupal_render($element[$key]);
301 }
302 }
303 $output .= '</tbody></table>';
304 $output .= drupal_render_children($element);
305
306 return $output;
307 }
308
309 /**
310 * Theme an individual conditional row of webform_conditionals_form().
311 */
312 function theme_webform_conditional_group_row($variables) {
313 $element = $variables['element'];
314
315 $element['weight']['#attributes']['class'] = array('webform-conditional-weight');
316 $weight = drupal_render($element['weight']);
317 $classes = array('draggable');
318 if (!empty($element['#even_odd'])) {
319 $classes[] = $element['#even_odd'];
320 }
321 if (!empty($element['#ajax_added'])) {
322 $classes[] = 'ajax-new-content';
323 }
324
325 $output = '';
326 $output .= '<tr class="' . implode(' ', $classes) . '">';
327 $output .= '<td>' . drupal_render_children($element) . '</td>';
328 $output .= '<td>' . $weight . '</td>';
329 $output .= '</tr>';
330
331 return $output;
332 }
333
334 /**
335 * Form API #process function to expand a webform conditional element.
336 */
337 function _webform_conditional_expand($element) {
338 $element['#tree'] = TRUE;
339 $element['#default_value'] += array(
340 'andor' => 'and',
341 );
342
343 $wrapper_id = drupal_clean_css_identifier(implode('-', $element['#parents'])) . '-ajax';
344 $element['#prefix'] = '<div id="' . $wrapper_id . '">';
345 $element['#suffix'] = '</div>';
346 $element['#wrapper_id'] = $wrapper_id;
347
348 // Note: When rules or actions are added, the new rules are inserted into
349 // $form_state['values']. So that FAPI can merge data from the post,
350 // $form_state['input'] must be adjusted to. To make this easier, hidden
351 // fields are added to the conditional_start and _end rules to ensure that
352 // each rule is represented in the POST.
353
354 $level = 0;
355 $andor_stack[0] = array(
356 'value' => $element['#default_value']['andor'],
357 'parents' => array_merge($element['#parents'], array('andor')),
358 'rid' => 0,
359 'first' => TRUE,
360 );
361
362 $last_rid = -1;
363 foreach ($element['#default_value']['rules'] as $rid => $conditional) {
364 switch ($conditional['source_type']) {
365 case 'conditional_start':
366 $element['rules'][$rid] = array(
367 '#level' => $level,
368 'source_type' => array(
369 '#type' => 'hidden',
370 '#value' => 'conditional_start',
371 ),
372 // The andor operator is located in the first child, which is
373 // guaranteed to exist. Therefore, don't add a 'value' element here.
374 'add_subconditional' => _webform_conditional_add_expand($element, $rid, TRUE),
375 'add' => _webform_conditional_add_expand($element, $rid, FALSE),
376 'remove' => _webform_conditional_remove_expand($element, $rid),
377 );
378 $andor_stack[++$level] = array(
379 'value' => $conditional['operator'],
380 'parents' => array_merge($element['#parents'], array('rules', $rid, 'operator')),
381 'rid' => $rid,
382 'first' => TRUE,
383 );
384 break;
385 case 'conditional_end':
386 --$level;
387 $element['rules'][$rid] = array(
388 '#level' => $level,
389 'source_type' => array(
390 '#type' => 'hidden',
391 '#value' => 'conditional_end',
392 ),
393 'add_subconditional' => _webform_conditional_add_expand($element, $rid, TRUE),
394 'add' => _webform_conditional_add_expand($element, $rid, FALSE),
395 'remove' => _webform_conditional_remove_expand($element, $rid),
396 'andor' => _webform_conditional_andor_expand($andor_stack[$level]),
397 );
398 // Remove the last nested and/or.
399 unset($element['rules'][$last_rid]['andor']);
400 break;
401 case 'component':
402 $element['rules'][$rid] = _webform_conditional_rule_expand($element, $rid, $conditional, $level, $andor_stack[$level]);
403 break;
404 default:
405 drupal_set_message(t('Unexpected conditional rule source type found (rule id @rid). Contact the administrator.', array('@rid' => $rid)), 'error');
406 // break;
407 }
408 $last_rid = $rid;
409 }
410
411 // Remove the last and/or.
412 unset($element['rules'][$rid]['andor']);
413
414 foreach ($element['#default_value']['actions'] as $aid => $action) {
415 $element['actions'][$aid] = _webform_conditional_action_expand($element, $aid, $action);
416 }
417
418 return $element;
419 }
420
421
422 /**
423 * Helper. Generate the and/or select or static text.
424 */
425 function _webform_conditional_andor_expand(&$andor) {
426 if ($andor['first']) {
427 $andor['first'] = FALSE;
428 return array(
429 '#type' => 'select',
430 '#title' => t('And/or'),
431 '#options' => array(
432 'and' => t('and'),
433 'or' => t('or'),
434 ),
435 '#parents' => $andor['parents'],
436 '#default_value' => $andor['value'],
437 '#attributes' => array('data-rid' => $andor['rid']),
438 );
439 }
440 else {
441 return array(
442 '#type' => 'container',
443 '#attributes' => array('class' => array('webform-andor'), 'data-rid' => $andor['rid']),
444 'andor_text' => array(
445 '#markup' => $andor['value'] == 'or' ? t('or') : t('and'),
446 ),
447 );
448 }
449 }
450
451 /**
452 * Helper. Generate the add_subconditional (+) or add + button.
453 */
454 function _webform_conditional_add_expand($element, $rid, $subconditional) {
455 return array(
456 '#type' => 'submit',
457 '#value' => $subconditional ? t('(+)') : t('+'),
458 '#submit' => array('webform_conditional_element_add'),
459 '#subconditional' => $subconditional,
460 '#name' => implode('_', $element['#parents']) . '_rules_' . $rid .
461 ($subconditional ? '_add_subconditional' : '_add'),
462 '#attributes' => array('class' => array('webform-conditional-rule-add')),
463 '#ajax' => array(
464 'progress' => 'none',
465 'callback' => 'webform_conditional_element_ajax',
466 'wrapper' => $element['#wrapper_id'],
467 'event' => 'click',
468 ),
469 );
470 }
471
472 /**
473 * Helper. Generate the add_subconditional (+), add + or remove - button.
474 */
475 function _webform_conditional_remove_expand($element, $rid) {
476 return array(
477 '#type' => 'submit',
478 '#value' => t('-'),
479 '#submit' => array('webform_conditional_element_remove'),
480 '#name' => implode('_', $element['#parents']) . '_rules_' . $rid . '_remove',
481 '#attributes' => array('class' => array('webform-conditional-rule-remove')),
482 '#ajax' => array(
483 'progress' => 'none',
484 'callback' => 'webform_conditional_element_ajax',
485 'wrapper' => $element['#wrapper_id'],
486 'event' => 'click',
487 ),
488 );
489 }
490
491 /**
492 * Helper. Generate form elements for one rule.
493 */
494 function _webform_conditional_rule_expand($element, $rid, $conditional, $level, &$andor) {
495 return array(
496 '#level' => $level,
497 'source_type' => array(
498 '#type' => 'value',
499 '#value' => $conditional['source_type'],
500 ),
501 'source' => array(
502 '#type' => 'select',
503 '#title' => t('Source'),
504 '#options' => $element['#sources'],
505 '#default_value' => $conditional['source'],
506 ),
507 'operator' => array(
508 '#type' => 'select',
509 '#title' => t('Operator'),
510 '#options' => webform_conditional_operators_list(),
511 '#default_value' => $conditional['operator'],
512 ),
513 'value' => array(
514 '#type' => 'textfield',
515 '#title' => t('Value'),
516 '#size' => 20,
517 '#default_value' => $conditional['value'],
518 ),
519 'add_subconditional' => _webform_conditional_add_expand($element, $rid, TRUE),
520 'add' => _webform_conditional_add_expand($element, $rid, FALSE),
521 'remove' => _webform_conditional_remove_expand($element, $rid),
522 'andor' => _webform_conditional_andor_expand($andor),
523 );
524 }
525
526 /**
527 * Helper. Generate form elements for one action.
528 */
529 function _webform_conditional_action_expand($element, $aid, $action) {
530 return array(
531 'target_type' => array(
532 '#type' => 'value',
533 '#value' => $action['target_type'],
534 ),
535 'target' => array(
536 '#type' => 'select',
537 '#title' => t('Target'),
538 '#options' => $element['#targets'],
539 '#default_value' => $action['target'],
540 ),
541 'invert' => array(
542 '#type' => 'select',
543 '#title' => t('Is/Isn\'t'),
544 '#options' => array(
545 '0' => t('is'),
546 '1' => t('isn\'t'),
547 ),
548 '#default_value' => $action['invert'],
549 ),
550 'action' => array(
551 '#type' => 'select',
552 '#title' => t('Action'),
553 '#options' => $element['#actions'],
554 '#default_value' => $action['action'],
555 ),
556 'argument' => array(
557 '#type' => 'textfield',
558 '#title' => t('Argument'),
559 '#size' => 20,
560 '#maxlength' => NULL,
561 '#default_value' => $action['argument'],
562 ),
563 'add' => array(
564 '#type' => 'submit',
565 '#value' => t('+'),
566 '#submit' => array('webform_conditional_element_add'),
567 '#name' => implode('_', $element['#parents']) . '_actions_' . $aid . '_add',
568 '#attributes' => array('class' => array('webform-conditional-action-add')),
569 '#ajax' => array(
570 'progress' => 'none',
571 'callback' => 'webform_conditional_element_ajax',
572 'wrapper' => $element['#wrapper_id'],
573 'event' => 'click',
574 ),
575 ),
576 'remove' => array(
577 '#type' => 'submit',
578 '#value' => t('-'),
579 '#submit' => array('webform_conditional_element_remove'),
580 '#name' => implode('_', $element['#parents']) . '_actions_' . $aid . '_remove',
581 '#attributes' => array('class' => array('webform-conditional-action-remove')),
582 '#ajax' => array(
583 'progress' => 'none',
584 'callback' => 'webform_conditional_element_ajax',
585 'wrapper' => $element['#wrapper_id'],
586 'event' => 'click',
587 ),
588 ),
589 );
590 }
591
592 /**
593 * Expand out all the value forms that could potentially be used.
594 *
595 * These forms are added to the page via JavaScript and swapped in only when
596 * needed. Because the user may change the source and operator at any time,
597 * all these forms need to be generated ahead of time and swapped in. This
598 * could have been done via AJAX, but having all forms available makes for a
599 * faster user experience.
600 *
601 * Added to the JavaScript settings is conditionalValues which contains
602 * an array settings suitable for adding to the page via JavaScript. This
603 * array contains the following keys:
604 * - operators: An array containing a map of data types, operators, and form
605 * keys. This array is structured as follows:
606 * @code
607 * - sources[$source_key] = array(
608 * 'data_type' => $data_type,
609 * );
610 * $operators[$data_type][$operator] = array(
611 * 'form' => $form_key,
612 * );
613 * @endcode
614 * - forms[$form_key]: A string representing an HTML form for an operator.
615 * - forms[$form_key][$source]: Or instead of a single form for all components,
616 * if each component requires its own form, key each component by its source
617 * value (currently always the component ID).
618 *
619 * @param $node
620 * The Webform node for which these forms are being generated.
621 */
622 function _webform_conditional_expand_value_forms($node) {
623 $operators = webform_conditional_operators();
624 $data = array();
625 foreach ($operators as $data_type => $operator_info) {
626 foreach ($operator_info as $operator => $data_operator_info) {
627 $data['operators'][$data_type][$operator]['form'] = 'default';
628 if (isset($data_operator_info['form callback'])) {
629 $form_callback = $data_operator_info['form callback'];
630 $data['operators'][$data_type][$operator]['form'] = $form_callback;
631 if ($form_callback !== FALSE && !isset($data['forms'][$form_callback])) {
632 $data['forms'][$form_callback] = $form_callback($node);
633 }
634 }
635 }
636 }
637
638 foreach ($node->webform['components'] as $cid => $component) {
639 if (webform_component_feature($component['type'], 'conditional')) {
640 $data['sources'][$cid]['data_type'] = webform_component_property($component['type'], 'conditional_type');
641 }
642 }
643
644 drupal_add_js(array('webform' => array('conditionalValues' => $data)), 'setting');
645 }
646
647 /**
648 * Helper. Find the matching end of a given subconditional.
649 *
650 * @param array $rules
651 * Array of conditional rules to be searched.
652 * @param integer $origin_rid
653 * The starting rule id for the search
654 * @param integer $target_delta_level
655 * The level that is sought. 0 for current left. -1 for parent.
656 * @return integer
657 * The rid of the found rule, or -1 if none. Note that NULL is not used as a
658 * semaphore for "not found" because it casts to 0, which is a valid rule id.
659 */
660 function _webform_conditional_find_end($rules, $origin_rid, $target_delta_level = 0) {
661 $rids = array_keys($rules);
662 $offset = array_search($origin_rid, $rids);
663 $delta_level = 0;
664 foreach (array_slice($rules, $offset, NULL, TRUE) as $rid => $conditional) {
665 switch ($conditional['source_type']) {
666 case 'conditional_start':
667 $delta_level++;
668 break;
669 case 'conditional_end':
670 $delta_level--;
671 break;
672 }
673 if ($delta_level == $target_delta_level) {
674 return $rid;
675 }
676 }
677 // Mis-matched conditional_start / _end. Return -1.
678 return -1;
679 }
680
681 /**
682 * Helper. Find the matching start or end of a given subconditional.
683 *
684 * @see _webform_conditional_find_end().
685 */
686 function _webform_conditional_find_start($rules, $origin_rid, $target_delta_level = 0) {
687 $rids = array_keys($rules);
688 $offset = array_search($origin_rid, $rids);
689 $delta_level = 0;
690 foreach (array_reverse(array_slice($rules, 0, $offset + 1, TRUE), TRUE) as $rid => $conditional) {
691 switch ($conditional['source_type']) {
692 case 'conditional_end':
693 $delta_level++;
694 break;
695 case 'conditional_start':
696 $delta_level--;
697 break;
698 }
699 if ($delta_level == $target_delta_level) {
700 return $rid;
701 }
702 }
703 // Mis-matched conditional_start / _end. Return -1.
704 return -1;
705 }
706
707 /**
708 * Submit handler for webform_conditional elements to add a new rule or action.
709 */
710 function webform_conditional_element_add($form, &$form_state) {
711 $button = $form_state['clicked_button'];
712 $parents = $button['#parents'];
713 $action = array_pop($parents);
714 $rid = array_pop($parents);
715
716 // Recurse through the form values until we find the Webform conditional rules
717 // or actions. Save the conditional prior to descending to rules/actions.
718 $parent_values = &$form_state['values'];
719 $input_values = &$form_state['input'];
720 foreach ($parents as $key) {
721 if (array_key_exists($key, $parent_values)) {
722 $conditional = $parent_values;
723 $parent_values = &$parent_values[$key];
724 }
725 if (array_key_exists($key, $input_values)) {
726 $input_values = &$input_values[$key];
727 }
728 }
729
730 // Split the list of rules/actions in this conditional and inject into the
731 // right spot.
732 $rids = array_keys($parent_values);
733 $offset = array_search($rid, $rids);
734 $default_rule = array(
735 'source' => NULL,
736 'source_type' => 'component',
737 'operator' => NULL,
738 'value' => NULL,
739 );
740 if (empty($button['#subconditional'])) {
741 $new[0] = $parent_values[$rid]['source_type'] == 'component' ? $parent_values[$rid] : $default_rule;
742 }
743 else {
744 // The default andor operator is opposite of current subconditional's
745 // operatior.
746 $parent_rid = _webform_conditional_find_start($parent_values, $rid, -1);
747 $current_op = $parent_rid < 0 ? $conditional['andor'] : $parent_values[$parent_rid]['operator'];
748 $current_op = $current_op == 'and' ? 'or' : 'and';
749 $new = array(
750 array('source_type' => 'conditional_start', 'operator' => $current_op) + $default_rule,
751 $default_rule,
752 $default_rule,
753 array('source_type' => 'conditional_end') + $default_rule,
754 );
755 }
756
757 // Update both $form_state['values'] and ['input] so that FAPI can merge
758 // input values from the POST into the new form.
759 $parent_values = array_merge(array_slice($parent_values, 0, $offset + 1), $new, array_slice($parent_values, $offset + 1));
760 $input_values = array_merge(array_slice($input_values, 0, $offset + 1), $new, array_slice($input_values, $offset + 1));
761 $form_state['rebuild'] = TRUE;
762 }
763
764 /**
765 * Submit handler for webform_conditional elements to remove a rule or action.
766 */
767 function webform_conditional_element_remove($form, &$form_state) {
768 $button = $form_state['clicked_button'];
769 $parents = $button['#parents'];
770 $action = array_pop($parents);
771 $rid = array_pop($parents);
772
773 // Recurse through the form values until we find the root Webform conditional.
774 $parent_values = &$form_state['values'];
775 foreach ($parents as $key) {
776 if (array_key_exists($key, $parent_values)) {
777 $parent_values = &$parent_values[$key];
778 }
779 }
780 switch ($parent_values[$rid]['source_type']) {
781 case 'conditional_start':
782 unset($parent_values[_webform_conditional_find_end($parent_values, $rid)]);
783 break;
784 case 'conditional_end':
785 unset($parent_values[_webform_conditional_find_start($parent_values, $rid)]);
786 break;
787 }
788 // Remove this rule or action from the list of conditionals.
789 unset($parent_values[$rid]);
790
791 $form_state['rebuild'] = TRUE;
792 }
793
794 /**
795 * Helper. Delete any subconditionals which contain no rules.
796 *
797 * @param &array $conditional
798 * Conditional array containing the rules.
799 * @return array
800 * Array of deleted subconditionals. Empty array if none were deleted.
801 */
802 function webform_delete_empty_subconditionals(&$conditional) {
803 $deleted = array();
804 do {
805 $empty_deleted = FALSE;
806 $open_rid = NULL;
807 foreach ($conditional['rules'] as $rid => $rule) {
808 switch ($rule['source_type']) {
809 case 'conditional_start':
810 $open_rid = $rid;
811 break;
812 case 'conditional_end':
813 if ($open_rid) {
814 // A conditional_start rule was immediately followed by a
815 // conditional_end rule. Delete them both. Repeat the check in case
816 // the parent is now empty.
817 $deleted[$open_rid] = $open_rid;
818 $deleted[$rid] = $rid;
819 unset($conditional['rules'][$open_rid], $conditional['rules'][$rid]);
820 $open_rid = NULL;
821 $empty_deleted = TRUE;
822 }
823 break;
824 default:
825 $open_rid = NULL;
826 //break;
827 }
828 }
829 } while ($empty_deleted);
830 return $deleted;
831 }
832
833 /**
834 * AJAX callback to render out adding a new condition.
835 */
836 function webform_conditional_element_ajax($form, $form_state) {
837 $button = $form_state['clicked_button'];
838 $parents = $button['#parents'];
839
840 // Trim down the parents to go back up to the level of this elements wrapper.
841 array_pop($parents); // The button name (add/remove).
842 array_pop($parents); // The rule ID.
843 array_pop($parents); // The "rules" grouping.
844
845 $element = $form;
846 foreach ($parents as $key) {
847 if (!isset($element[$key])) {
848 // The entire conditional has been removed
849 return '';
850 }
851 $element = $element[$key];
852 }
853
854 return drupal_render($element['conditional']);
855 }
856
857 /**
858 * Theme the form for a conditional action.
859 */
860 function theme_webform_conditional($variables) {
861 $element = $variables['element'];
862
863 $output = '';
864 $output .= '<div class="webform-conditional">';
865 $output .= '<span class="webform-conditional-if">' . t('If') . '</span>';
866
867 foreach (element_children($element['rules']) as $rid) {
868 $rule = &$element['rules'][$rid];
869 switch ($rule['source_type']['#value']) {
870 case 'conditional_start':
871 $source_phrase = '<div class="webform-subconditional">' . t('(') . '</div>';
872 break;
873 case 'conditional_end':
874 $source_phrase = '<div class="webform-subconditional">' . t(')') . '</div>';
875 break;
876 default:
877 // Hide labels.
878 $rule['source']['#title_display'] = 'none';
879 $rule['operator']['#title_display'] = 'none';
880 $rule['value']['#title_display'] = 'none';
881
882 $source = '<div class="webform-conditional-source">' . drupal_render($rule['source']) . '</div>';
883 $operator = '<div class="webform-conditional-operator">' . drupal_render($rule['operator']) . '</div>';
884 $value = '<div class="webform-conditional-value">' . drupal_render($rule['value']) . '</div>';
885
886 $source_phrase = t('!source !operator !value', array(
887 '!source' => $source,
888 '!operator' => $operator,
889 '!value' => $value,
890 ));
891 // break
892 }
893
894 $output .= '<div class="webform-conditional-rule">';
895 // Can't use theme('indentation') here because it causes the draghandle to
896 // be located after the last indentation div.
897 $output .= str_repeat('<div class="webform-indentation">&nbsp;</div>', $rule['#level']);
898 $output .= drupal_render($rule['source_type']);
899 $output .= '<div class="webform-container-inline webform-conditional-condition">';
900 $output .= $source_phrase;
901 $output .= '</div>';
902
903 if (isset($rule['andor'])) {
904 $rule['andor']['#title_display'] = 'none';
905 $output .= '<div class="webform-conditional-andor webform-container-inline">';
906 $output .= drupal_render($rule['andor']);
907 $output .= '</div>';
908 }
909
910 if (isset($rule['add']) || isset($rule['remove'])) {
911 $output .= '<span class="webform-conditional-operations webform-container-inline">';
912 $output .= drupal_render($rule['add_subconditional']);
913 $output .= drupal_render($rule['add']);
914 $output .= drupal_render($rule['remove']);
915 $output .= '</span>';
916 }
917
918 $output .= '</div>';
919 }
920
921 // Hide labels.
922 foreach (element_children($element['actions']) as $aid) {
923 // Hide labels.
924 $element['actions'][$aid]['target']['#title_display'] = 'none';
925 $element['actions'][$aid]['invert']['#title_display'] = 'none';
926 $element['actions'][$aid]['action']['#title_display'] = 'none';
927 $element['actions'][$aid]['argument']['#title_display'] = 'none';
928
929 $target = '<div class="webform-conditional-target">' . drupal_render($element['actions'][$aid]['target']) . '</div>';
930 $invert = '<div class="webform-conditional-invert">' . drupal_render($element['actions'][$aid]['invert']) . '</div>';
931 $action = '<div class="webform-conditional-action">' . drupal_render($element['actions'][$aid]['action']) . '</div>';
932 $argument = '<div class="webform-conditional-argument">' . drupal_render($element['actions'][$aid]['argument']) . '</div>';
933
934 $target_phrase = t('then !target !invert !action !argument', array(
935 '!target' => $target,
936 '!invert' => $invert,
937 '!action' => $action,
938 '!argument' => $argument,
939 ));
940
941 $output .= '<div class="webform-conditional-action">';
942 $output .= '<div class="webform-container-inline webform-conditional-condition">';
943 $output .= $target_phrase;
944 $output .= '</div>';
945
946 if (isset($element['actions'][$aid]['add']) || isset($element['actions'][$aid]['remove'])) {
947 $output .= '<span class="webform-conditional-operations webform-container-inline">';
948 $output .= drupal_render($element['actions'][$aid]['add']);
949 $output .= drupal_render($element['actions'][$aid]['remove']);
950 $output .= '</span>';
951 }
952
953 $output .= '</div>';
954 }
955
956 $output .= '</div>';
957
958 return $output;
959 }
960
961 /**
962 * Return a list of all Webform conditional operators.
963 */
964 function webform_conditional_operators() {
965 static $operators;
966
967 if (!isset($operators)) {
968 $operators = module_invoke_all('webform_conditional_operator_info');
969 drupal_alter('webform_conditional_operators', $operators);
970 }
971
972 return $operators;
973 }
974
975 /**
976 * Return a nested list of all available operators, suitable for a select list.
977 */
978 function webform_conditional_operators_list() {
979 $options = array();
980 $operators = webform_conditional_operators();
981
982 foreach ($operators as $data_type => $type_operators) {
983 $options[$data_type] = array();
984 foreach ($type_operators as $operator => $operator_info) {
985 $options[$data_type][$operator] = $operator_info['label'];
986 }
987 }
988
989 return $options;
990 }
991
992 /**
993 * Internal implementation of hook_webform_conditional_operator_info().
994 *
995 * Called from webform.module's webform_webform_conditional_operator_info().
996 */
997 function _webform_conditional_operator_info() {
998 // General operators:
999 $operators['string']['equal'] = array(
1000 'label' => t('is'),
1001 'comparison callback' => 'webform_conditional_operator_string_equal',
1002 'js comparison callback' => 'conditionalOperatorStringEqual',
1003 // A form callback is not needed here, since we can use the default,
1004 // non-JavaScript textfield for all text and numeric fields.
1005 // 'form callback' => 'webform_conditional_operator_text',
1006 );
1007 $operators['string']['not_equal'] = array(
1008 'label' => t('is not'),
1009 'comparison callback' => 'webform_conditional_operator_string_not_equal',
1010 'js comparison callback' => 'conditionalOperatorStringNotEqual',
1011 );
1012 $operators['string']['contains'] = array(
1013 'label' => t('contains'),
1014 'comparison callback' => 'webform_conditional_operator_string_contains',
1015 'js comparison callback' => 'conditionalOperatorStringContains',
1016 );
1017 $operators['string']['does_not_contain'] = array(
1018 'label' => t('does not contain'),
1019 'comparison callback' => 'webform_conditional_operator_string_does_not_contain',
1020 'js comparison callback' => 'conditionalOperatorStringDoesNotContain',
1021 );
1022 $operators['string']['begins_with'] = array(
1023 'label' => t('begins with'),
1024 'comparison callback' => 'webform_conditional_operator_string_begins_with',
1025 'js comparison callback' => 'conditionalOperatorStringBeginsWith',
1026 );
1027 $operators['string']['ends_with'] = array(
1028 'label' => t('ends with'),
1029 'comparison callback' => 'webform_conditional_operator_string_ends_with',
1030 'js comparison callback' => 'conditionalOperatorStringEndsWith',
1031 );
1032 $operators['string']['empty'] = array(
1033 'label' => t('is blank'),
1034 'comparison callback' => 'webform_conditional_operator_string_empty',
1035 'js comparison callback' => 'conditionalOperatorStringEmpty',
1036 'form callback' => FALSE, // No value form at all.
1037 );
1038 $operators['string']['not_empty'] = array(
1039 'label' => t('is not blank'),
1040 'comparison callback' => 'webform_conditional_operator_string_not_empty',
1041 'js comparison callback' => 'conditionalOperatorStringNotEmpty',
1042 'form callback' => FALSE, // No value form at all.
1043 );
1044
1045 // Numeric operators.
1046 $operators['numeric']['equal'] = array(
1047 'label' => t('is equal to'),
1048 'comparison callback' => 'webform_conditional_operator_numeric_equal',
1049 'js comparison callback' => 'conditionalOperatorNumericEqual',
1050 );
1051 $operators['numeric']['not_equal'] = array(
1052 'label' => t('is not equal to'),
1053 'comparison callback' => 'webform_conditional_operator_numeric_not_equal',
1054 'js comparison callback' => 'conditionalOperatorNumericNotEqual',
1055 );
1056 $operators['numeric']['less_than'] = array(
1057 'label' => t('is less than'),
1058 'comparison callback' => 'webform_conditional_operator_numeric_less_than',
1059 'js comparison callback' => 'conditionalOperatorNumericLessThan',
1060 );
1061 $operators['numeric']['less_than_equal'] = array(
1062 'label' => t('is less than or equal'),
1063 'comparison callback' => 'webform_conditional_operator_numeric_less_than_equal',
1064 'js comparison callback' => 'conditionalOperatorNumericLessThanEqual',
1065 );
1066 $operators['numeric']['greater_than'] = array(
1067 'label' => t('is greater than'),
1068 'comparison callback' => 'webform_conditional_operator_numeric_greater_than',
1069 'js comparison callback' => 'conditionalOperatorNumericGreaterThan',
1070 );
1071 $operators['numeric']['greater_than_equal'] = array(
1072 'label' => t('is greater than or equal'),
1073 'comparison callback' => 'webform_conditional_operator_numeric_greater_than_equal',
1074 'js comparison callback' => 'conditionalOperatorNumericGreaterThanEqual',
1075 );
1076 $operators['numeric']['empty'] = array(
1077 'label' => t('is blank'),
1078 'comparison callback' => 'webform_conditional_operator_string_empty',
1079 'js comparison callback' => 'conditionalOperatorStringEmpty',
1080 'form callback' => FALSE, // No value form at all.
1081 );
1082 $operators['numeric']['not_empty'] = array(
1083 'label' => t('is not blank'),
1084 'comparison callback' => 'webform_conditional_operator_string_not_empty',
1085 'js comparison callback' => 'conditionalOperatorStringNotEmpty',
1086 'form callback' => FALSE, // No value form at all.
1087 );
1088
1089 // Select operators.
1090 $operators['select']['equal'] = array(
1091 'label' => t('is'),
1092 'comparison callback' => 'webform_conditional_operator_string_equal',
1093 'js comparison callback' => 'conditionalOperatorStringEqual',
1094 'form callback' => 'webform_conditional_form_select',
1095 );
1096 $operators['select']['not_equal'] = array(
1097 'label' => t('is not'),
1098 'comparison callback' => 'webform_conditional_operator_string_not_equal',
1099 'js comparison callback' => 'conditionalOperatorStringNotEqual',
1100 'form callback' => 'webform_conditional_form_select',
1101 );
1102 $operators['select']['less_than'] = array(
1103 'label' => t('is before'),
1104 'comparison callback' => 'webform_conditional_operator_select_less_than',
1105 'js comparison callback' => 'conditionalOperatorSelectLessThan',
1106 'form callback' => 'webform_conditional_form_select',
1107 );
1108 $operators['select']['less_than_equal'] = array(
1109 'label' => t('is or is before'),
1110 'comparison callback' => 'webform_conditional_operator_select_less_than_equal',
1111 'js comparison callback' => 'conditionalOperatorSelectLessThanEqual',
1112 'form callback' => 'webform_conditional_form_select',
1113 );
1114 $operators['select']['greater_than'] = array(
1115 'label' => t('is after'),
1116 'comparison callback' => 'webform_conditional_operator_select_greater_than',
1117 'js comparison callback' => 'conditionalOperatorSelectGreaterThan',
1118 'form callback' => 'webform_conditional_form_select',
1119 );
1120 $operators['select']['greater_than_equal'] = array(
1121 'label' => t('is or is after'),
1122 'comparison callback' => 'webform_conditional_operator_select_greater_than_equal',
1123 'js comparison callback' => 'conditionalOperatorSelectGreaterThanEqual',
1124 'form callback' => 'webform_conditional_form_select',
1125 );
1126 $operators['select']['empty'] = array(
1127 'label' => t('is empty'),
1128 'comparison callback' => 'webform_conditional_operator_string_empty',
1129 'js comparison callback' => 'conditionalOperatorStringEmpty',
1130 'form callback' => FALSE, // No value form at all.
1131 );
1132 $operators['select']['not_empty'] = array(
1133 'label' => t('is not empty'),
1134 'comparison callback' => 'webform_conditional_operator_string_not_empty',
1135 'js comparison callback' => 'conditionalOperatorStringNotEmpty',
1136 'form callback' => FALSE, // No value form at all.
1137 );
1138
1139 // Date operators:
1140 $operators['date']['equal'] = array(
1141 'label' => t('is on'),
1142 'comparison callback' => 'webform_conditional_operator_datetime_equal',
1143 'comparison prepare js' => 'webform_conditional_prepare_date_js',
1144 'js comparison callback' => 'conditionalOperatorDateEqual',
1145 'form callback' => 'webform_conditional_form_date',
1146 );
1147 $operators['date']['not_equal'] = array(
1148 'label' => t('is not on'),
1149 'comparison callback' => 'webform_conditional_operator_datetime_not_equal',
1150 'comparison prepare js' => 'webform_conditional_prepare_date_js',
1151 'js comparison callback' => 'conditionalOperatorDateNotEqual',
1152 'form callback' => 'webform_conditional_form_date',
1153 );
1154 $operators['date']['before'] = array(
1155 'label' => t('is before'),
1156 'comparison callback' => 'webform_conditional_operator_datetime_before',
1157 'comparison prepare js' => 'webform_conditional_prepare_date_js',
1158 'js comparison callback' => 'conditionalOperatorDateBefore',
1159 'form callback' => 'webform_conditional_form_date',
1160 );
1161 $operators['date']['before_equal'] = array(
1162 'label' => t('is on or before'),
1163 'comparison callback' => 'webform_conditional_operator_datetime_before_equal',
1164 'comparison prepare js' => 'webform_conditional_prepare_date_js',
1165 'js comparison callback' => 'conditionalOperatorDateBeforeEqual',
1166 'form callback' => 'webform_conditional_form_date',
1167 );
1168 $operators['date']['after'] = array(
1169 'label' => t('is after'),
1170 'comparison callback' => 'webform_conditional_operator_datetime_after',
1171 'comparison prepare js' => 'webform_conditional_prepare_date_js',
1172 'js comparison callback' => 'conditionalOperatorDateAfter',
1173 'form callback' => 'webform_conditional_form_date',
1174 );
1175 $operators['date']['after_equal'] = array(
1176 'label' => t('is on or after'),
1177 'comparison callback' => 'webform_conditional_operator_datetime_after_equal',
1178 'comparison prepare js' => 'webform_conditional_prepare_date_js',
1179 'js comparison callback' => 'conditionalOperatorDateAfterEqual',
1180 'form callback' => 'webform_conditional_form_date',
1181 );
1182
1183 // Time operators:
1184 $operators['time']['equal'] = array(
1185 'label' => t('is at'),
1186 'comparison callback' => 'webform_conditional_operator_datetime_equal',
1187 'comparison prepare js' => 'webform_conditional_prepare_time_js',
1188 'js comparison callback' => 'conditionalOperatorTimeEqual',
1189 'form callback' => 'webform_conditional_form_time',
1190 );
1191 $operators['time']['not_equal'] = array(
1192 'label' => t('is not at'),
1193 'comparison callback' => 'webform_conditional_operator_datetime_not_equal',
1194 'comparison prepare js' => 'webform_conditional_prepare_time_js',
1195 'js comparison callback' => 'conditionalOperatorTimeNotEqual',
1196 'form callback' => 'webform_conditional_form_time',
1197 );
1198 $operators['time']['before'] = array(
1199 'label' => t('is before'),
1200 'comparison callback' => 'webform_conditional_operator_datetime_before',
1201 'comparison prepare js' => 'webform_conditional_prepare_time_js',
1202 'js comparison callback' => 'conditionalOperatorTimeBefore',
1203 'form callback' => 'webform_conditional_form_time',
1204 );
1205 $operators['time']['before_equal'] = array(
1206 'label' => t('is at or before'),
1207 'comparison callback' => 'webform_conditional_operator_datetime_before_equal',
1208 'comparison prepare js' => 'webform_conditional_prepare_time_js',
1209 'js comparison callback' => 'conditionalOperatorTimeBeforeEqual',
1210 'form callback' => 'webform_conditional_form_time',
1211 );
1212 $operators['time']['after'] = array(
1213 'label' => t('is after'),
1214 'comparison callback' => 'webform_conditional_operator_datetime_after',
1215 'comparison prepare js' => 'webform_conditional_prepare_time_js',
1216 'js comparison callback' => 'conditionalOperatorTimeAfter',
1217 'form callback' => 'webform_conditional_form_time',
1218 );
1219 $operators['time']['after_equal'] = array(
1220 'label' => t('is at or after'),
1221 'comparison callback' => 'webform_conditional_operator_datetime_after_equal',
1222 'comparison prepare js' => 'webform_conditional_prepare_time_js',
1223 'js comparison callback' => 'conditionalOperatorTimeAfterEqual',
1224 'form callback' => 'webform_conditional_form_time',
1225 );
1226
1227 return $operators;
1228 }
1229
1230 /**
1231 * Form callback for select-type conditional fields.
1232 *
1233 * Unlike other built-in conditional value forms, the form callback for select
1234 * types provides an array of forms, keyed by the $cid, which is the "source"
1235 * for the condition.
1236 */
1237 function webform_conditional_form_select($node) {
1238 static $count = 0;
1239 $forms = array();
1240 webform_component_include('select');
1241 foreach ($node->webform['components'] as $cid => $component) {
1242 if (webform_component_property($component['type'], 'conditional_type') == 'select') {
1243 // TODO: Use a pluggable mechanism for retrieving select list values.
1244 $options = _webform_select_options($component);
1245 $element = array(
1246 '#type' => 'select',
1247 '#multiple' => FALSE,
1248 '#size' => NULL,
1249 '#attributes' => array(),
1250 '#id' => NULL,
1251 '#name' => 'webform-conditional-select-' . $cid . '-' . $count,
1252 '#options' => $options,
1253 '#parents' => array(),
1254 );
1255 $forms[$cid] = drupal_render($element);
1256 }
1257 }
1258 $count++;
1259 return $forms;
1260 }
1261
1262 /**
1263 * Form callback for date conditional fields.
1264 */
1265 function webform_conditional_form_date($node) {
1266 static $count = 0;
1267 $element = array(
1268 '#title' => NULL,
1269 '#title_display' => 'none',
1270 '#size' => 24,
1271 '#attributes' => array('placeholder' => t('@format or valid date', array('@format' => webform_date_format('short')))),
1272 '#type' => 'textfield',
1273 '#name' => 'webform-conditional-date-' . $count++,
1274 );
1275 return drupal_render($element);
1276 }
1277
1278 /**
1279 * Form callback for time conditional fields.
1280 */
1281 function webform_conditional_form_time($node) {
1282 static $count = 0;
1283 $element = array(
1284 '#title' => NULL,
1285 '#title_display' => 'none',
1286 '#size' => 24,
1287 '#attributes' => array('placeholder' => t('HH:MMam or valid time')),
1288 '#type' => 'textfield',
1289 '#name' => 'webform-conditional-time-' . $count++,
1290 );
1291 return drupal_render($element);
1292 }
1293
1294 /**
1295 * Load a conditional setting from the database.
1296 */
1297 function webform_conditional_load($rgid, $nid) {
1298 $node = node_load($nid);
1299
1300 $conditional = isset($node->webform['conditionals'][$rgid]) ? $node->webform['conditionals'][$rgid] : FALSE;
1301
1302 return $conditional;
1303 }
1304
1305 /**
1306 * Insert a conditional rule group into the database.
1307 */
1308 function webform_conditional_insert($conditional) {
1309 drupal_write_record('webform_conditional', $conditional);
1310 foreach ($conditional['rules'] as $rid => $rule) {
1311 $rule['nid'] = $conditional['nid'];
1312 $rule['rgid'] = $conditional['rgid'];
1313 $rule['rid'] = $rid;
1314 drupal_write_record('webform_conditional_rules', $rule);
1315 }
1316 foreach ($conditional['actions'] as $aid => $action) {
1317 $action['nid'] = $conditional['nid'];
1318 $action['rgid'] = $conditional['rgid'];
1319 $action['aid'] = $aid;
1320 drupal_write_record('webform_conditional_actions', $action);
1321 }
1322 }
1323
1324 /**
1325 * Update a conditional setting in the database.
1326 */
1327 function webform_conditional_update($node, $conditional) {
1328 webform_conditional_delete($node, $conditional);
1329 webform_conditional_insert($conditional);
1330 }
1331
1332 /**
1333 * Delete a conditional rule group.
1334 */
1335 function webform_conditional_delete($node, $conditional) {
1336 db_delete('webform_conditional')
1337 ->condition('nid', $node->nid)
1338 ->condition('rgid', $conditional['rgid'])
1339 ->execute();
1340 db_delete('webform_conditional_rules')
1341 ->condition('nid', $node->nid)
1342 ->condition('rgid', $conditional['rgid'])
1343 ->execute();
1344 db_delete('webform_conditional_actions')
1345 ->condition('nid', $node->nid)
1346 ->condition('rgid', $conditional['rgid'])
1347 ->execute();
1348 }
1349
1350 /**
1351 * Loop through all the conditional settings and add needed JavaScript settings.
1352 *
1353 * We do a bit of optimization for JavaScript before adding to the page as
1354 * settings. We remove unnecessary data structures and provide a "source map"
1355 * so that JavaScript can quickly determine if it needs to check rules when a
1356 * field on the page has been modified.
1357 *
1358 * @param object $node
1359 * The loaded node object, containing the webform.
1360 * @param array $submission_data
1361 * The cid-indexed array of existing submission values to be included for
1362 * sources outside of the current page.
1363 * @param integer $page_num
1364 * The number of the page for which javascript settings should be generated.
1365 * @return array
1366 * Array of settings to be send to the browser as javascript settings.
1367 */
1368 function webform_conditional_prepare_javascript($node, $submission_data, $page_num) {
1369 $settings = array(
1370 'ruleGroups' => array(),
1371 'sourceMap' => array(),
1372 'values' => array(),
1373 );
1374 $operators = webform_conditional_operators();
1375 $conditionals = $node->webform['conditionals'];
1376 $components = $node->webform['components'];
1377 $topological_order = webform_get_conditional_sorter($node)->getOrder();
1378 foreach ($topological_order[$page_num] as $conditional_spec) {
1379 $conditional = $conditionals[$conditional_spec['rgid']];
1380 $rgid_key = 'rgid_' . $conditional['rgid'];
1381 // Assemble the main conditional group settings.
1382
1383 $settings['ruleGroups'][$rgid_key] = array(
1384 'andor' => $conditional['andor'],
1385 );
1386 foreach ($conditional['actions'] as $action) {
1387 if ($action['target_type'] == 'component') {
1388 $target_component = $components[$action['target']];
1389 $target_parents = webform_component_parent_keys($node, $target_component);
1390 $aid_key = 'aid_' . $action['aid'];
1391 $action_settings = array(
1392 'target' => 'webform-component--' . str_replace('_', '-', implode('--', $target_parents)),
1393 'invert' => (int)$action['invert'],
1394 'action' => $action['action'],
1395 'argument' => $components[$action['target']]['type'] == 'markup' ? filter_xss_admin($action['argument']) : $action['argument'],
1396 );
1397 $settings['ruleGroups'][$rgid_key]['actions'][$aid_key] = $action_settings;
1398 }
1399 }
1400 // Add on the list of rules to the conditional group.
1401 foreach ($conditional['rules'] as $rule) {
1402 $rid_key = 'rid_' . $rule['rid'];
1403 switch ($rule['source_type']) {
1404 case 'component':
1405 $source_component = $components[$rule['source']];
1406 $source_parents = webform_component_parent_keys($node, $source_component);
1407 $source_id = 'webform-component--' . str_replace('_', '-', implode('--', $source_parents));
1408
1409 // If this source has a value set, add that as a setting.
1410 // NULL or array(NULL) should be sent as an empty array to simplify the jQuery.
1411 if (isset($submission_data[$source_component['cid']])) {
1412 $source_value = $submission_data[$source_component['cid']];
1413 $source_value = is_array($source_value) ? $source_value : array($source_value);
1414 $settings['values'][$source_id] = $source_value === array(NULL) ? array() : $source_value;
1415 }
1416
1417 $conditional_type = webform_component_property($source_component['type'], 'conditional_type');
1418 $operator_info = $operators[$conditional_type][$rule['operator']];
1419 $rule_settings = array(
1420 'source_type' => $rule['source_type'],
1421 'source' => $source_id,
1422 'value' => $rule['value'],
1423 'callback' => $operator_info['js comparison callback'],
1424 );
1425 if (isset($operator_info['comparison prepare js'])) {
1426 $callback = $operator_info['comparison prepare js'];
1427 $rule_settings['value'] = $callback($rule['value']);
1428 }
1429 $settings['ruleGroups'][$rgid_key]['rules'][$rid_key] = $rule_settings;
1430 $settings['sourceMap'][$source_id][$rgid_key] = $rgid_key;
1431 break;
1432 case 'conditional_start':
1433 $settings['ruleGroups'][$rgid_key]['rules'][$rid_key] = array(
1434 'source_type' => $rule['source_type'],
1435 'andor' => $rule['operator'],
1436 );
1437 break;
1438 case 'conditional_end':
1439 $settings['ruleGroups'][$rgid_key]['rules'][$rid_key] = array(
1440 'source_type' => $rule['source_type'],
1441 );
1442 break;
1443 }
1444 }
1445 }
1446
1447 return $settings;
1448 }
1449
1450 /**
1451 * Determine whether a component type is capable of a given conditional action.
1452 */
1453 function webform_conditional_action_able($component_type, $action) {
1454 switch ($action) {
1455 case 'show':
1456 return TRUE;
1457 // break;
1458 case 'require':
1459 return webform_component_feature($component_type, 'required');
1460 // break;
1461 default:
1462 return webform_component_feature($component_type, "conditional_action_$action");
1463 // break;
1464 }
1465 }
1466
1467 /**
1468 * Prepare a conditional value for adding as a JavaScript setting.
1469 */
1470 function webform_conditional_prepare_date_js($rule_value) {
1471 // Convert the time/date string to a UTC timestamp for comparison. Note that
1472 // this means comparisons against immediate times (such as "now") may be
1473 // slightly stale by the time the comparison executes. Timestamps are in
1474 // milliseconds, as to match JavaScript's Date.toString() method.
1475 $date = webform_strtodate('c', $rule_value, 'UTC');
1476 return webform_strtotime($date);
1477 }
1478
1479 /**
1480 * Prepare a conditional value for adding as a JavaScript setting.
1481 */
1482 function webform_conditional_prepare_time_js($rule_value) {
1483 $date = webform_conditional_prepare_date_js($rule_value);
1484 $today = webform_strtodate('c', 'today', 'UTC');
1485 $today = webform_strtotime($today);
1486 return $date - $today;
1487 }
1488
1489 /**
1490 * Conditional callback for string comparisons.
1491 */
1492 function webform_conditional_operator_string_equal($input_values, $rule_value) {
1493 foreach ($input_values as $value) {
1494 // Checkbox values come in as 0 integers for unchecked boxes.
1495 $value = ($value === 0) ? '' : $value;
1496 if (strcasecmp($value, $rule_value) === 0) {
1497 return TRUE;
1498 }
1499 }
1500 return FALSE;
1501 }
1502
1503 /**
1504 * Conditional callback for string comparisons.
1505 */
1506 function webform_conditional_operator_string_not_equal($input_values, $rule_value) {
1507 return !webform_conditional_operator_string_equal($input_values, $rule_value);
1508 }
1509
1510 /**
1511 * Conditional callback for string comparisons.
1512 */
1513 function webform_conditional_operator_string_contains($input_values, $rule_value) {
1514 foreach ($input_values as $value) {
1515 if (stripos($value, $rule_value) !== FALSE) {
1516 return TRUE;
1517 }
1518 }
1519 return FALSE;
1520 }
1521
1522 /**
1523 * Conditional callback for string comparisons.
1524 */
1525 function webform_conditional_operator_string_does_not_contain($input_values, $rule_value) {
1526 return !webform_conditional_operator_string_contains($input_values, $rule_value);
1527 }
1528
1529 /**
1530 * Conditional callback for string comparisons.
1531 */
1532 function webform_conditional_operator_string_begins_with($input_values, $rule_value) {
1533 foreach ($input_values as $value) {
1534 if (stripos($value, $rule_value) === 0) {
1535 return TRUE;
1536 }
1537 }
1538 return FALSE;
1539 }
1540
1541 /**
1542 * Conditional callback for string comparisons.
1543 */
1544 function webform_conditional_operator_string_ends_with($input_values, $rule_value) {
1545 foreach ($input_values as $value) {
1546 if (strripos($value, $rule_value) === strlen($value) - strlen($rule_value)) {
1547 return TRUE;
1548 }
1549 }
1550 return FALSE;
1551 }
1552
1553 /**
1554 * Conditional callback for checking for empty fields.
1555 */
1556 function webform_conditional_operator_string_empty($input_values, $rule_value) {
1557 $empty = TRUE;
1558 foreach ($input_values as $value) {
1559 if ($value !== '' && $value !== NULL && $value !== 0) {
1560 $empty = FALSE;
1561 break;
1562 }
1563 }
1564 return $empty;
1565 }
1566
1567 /**
1568 * Conditional callback for checking for empty fields.
1569 */
1570 function webform_conditional_operator_string_not_empty($input_values, $rule_value) {
1571 return !webform_conditional_operator_string_empty($input_values, $rule_value);
1572 }
1573
1574 /**
1575 * Conditional callback for select comparisons.
1576 */
1577 function webform_conditional_operator_select_less_than($input_values, $rule_value, $component) {
1578 return empty($input_values) ? FALSE : webform_compare_select($input_values[0], $rule_value, _webform_select_options($component, TRUE)) < 0;
1579 }
1580
1581 /**
1582 * Conditional callback for select comparisons.
1583 */
1584 function webform_conditional_operator_select_less_than_equal($input_values, $rule_value, $component) {
1585 $comparison = empty($input_values) ? NULL : webform_compare_select($input_values[0], $rule_value, _webform_select_options($component, TRUE));
1586 return $comparison < 0 || $comparison === 0;
1587 }
1588
1589 /**
1590 * Conditional callback for select comparisons.
1591 */
1592 function webform_conditional_operator_select_greater_than($input_values, $rule_value, $component) {
1593 return empty($input_values) ? FALSE : webform_compare_select($input_values[0], $rule_value, _webform_select_options($component, TRUE)) > 0;
1594 }
1595
1596 /**
1597 * Conditional callback for select comparisons.
1598 */
1599 function webform_conditional_operator_select_greater_than_equal($input_values, $rule_value, $component) {
1600 $comparison = empty($input_values) ? NULL : webform_compare_select($input_values[0], $rule_value, _webform_select_options($component, TRUE));
1601 return $comparison > 0 || $comparison === 0;
1602 }
1603
1604 /**
1605 * Conditional callback for numeric comparisons.
1606 */
1607 function webform_conditional_operator_numeric_equal($input_values, $rule_value) {
1608 return empty($input_values) ? FALSE : webform_compare_floats($input_values[0], $rule_value) === 0;
1609 }
1610
1611 /**
1612 * Conditional callback for numeric comparisons.
1613 */
1614 function webform_conditional_operator_numeric_not_equal($input_values, $rule_value) {
1615 return !webform_conditional_operator_numeric_equal($input_values, $rule_value);
1616 }
1617
1618 /**
1619 * Conditional callback for numeric comparisons.
1620 */
1621 function webform_conditional_operator_numeric_less_than($input_values, $rule_value) {
1622 return empty($input_values) ? FALSE : webform_compare_floats($input_values[0], $rule_value) < 0;
1623 }
1624
1625 /**
1626 * Conditional callback for numeric comparisons.
1627 */
1628 function webform_conditional_operator_numeric_less_than_equal($input_values, $rule_value) {
1629 $comparison = empty($input_values) ? NULL : webform_compare_floats($input_values[0], $rule_value);
1630 return $comparison < 0 || $comparison === 0;
1631 }
1632
1633 /**
1634 * Conditional callback for numeric comparisons.
1635 */
1636 function webform_conditional_operator_numeric_greater_than($input_values, $rule_value) {
1637 return empty($input_values) ? FALSE : webform_compare_floats($input_values[0], $rule_value) > 0;
1638 }
1639
1640 /**
1641 * Conditional callback for numeric comparisons.
1642 */
1643 function webform_conditional_operator_numeric_greater_than_equal($input_values, $rule_value) {
1644 $comparison = empty($input_values) ? NULL : webform_compare_floats($input_values[0], $rule_value);
1645 return $comparison > 0 || $comparison === 0;
1646 }
1647
1648 /**
1649 * Conditional callback for date and time comparisons.
1650 */
1651 function webform_conditional_operator_datetime_equal($input_values, $rule_value) {
1652 $input_values = webform_conditional_value_datetime($input_values);
1653 return empty($input_values) ? FALSE : webform_strtotime($input_values[0]) === webform_strtotime($rule_value);
1654 }
1655
1656 /**
1657 * Conditional callback for date and time comparisons.
1658 */
1659 function webform_conditional_operator_datetime_not_equal($input_values, $rule_value) {
1660 return !webform_conditional_operator_datetime_equal($input_values, $rule_value);
1661 }
1662
1663 /**
1664 * Conditional callback for date and time comparisons.
1665 */
1666 function webform_conditional_operator_datetime_after($input_values, $rule_value) {
1667 $input_values = webform_conditional_value_datetime($input_values);
1668 return empty($input_values) ? FALSE : webform_strtotime($input_values[0]) > webform_strtotime($rule_value);
1669 }
1670
1671 /**
1672 * Conditional callback for date and time comparisons.
1673 */
1674 function webform_conditional_operator_datetime_after_equal($input_values, $rule_value) {
1675 return webform_conditional_operator_datetime_after($input_values, $rule_value) ||
1676 webform_conditional_operator_datetime_equal($input_values, $rule_value);
1677 }
1678
1679 /**
1680 * Conditional callback for date and time comparisons.
1681 */
1682 function webform_conditional_operator_datetime_before($input_values, $rule_value) {
1683 $input_values = webform_conditional_value_datetime($input_values);
1684 return empty($input_values) ? FALSE : webform_strtotime($input_values[0]) < webform_strtotime($rule_value);
1685 }
1686
1687 /**
1688 * Conditional callback for date and time comparisons.
1689 */
1690 function webform_conditional_operator_datetime_before_equal($input_values, $rule_value) {
1691 return webform_conditional_operator_datetime_before($input_values, $rule_value) ||
1692 webform_conditional_operator_datetime_equal($input_values, $rule_value);
1693 }
1694
1695 /**
1696 * Utility function to convert incoming time and dates into strings.
1697 */
1698 function webform_conditional_value_datetime($input_values) {
1699 // Convert times into a string.
1700 $input_values = isset($input_values['hour']) ? array(webform_date_string(webform_time_convert($input_values, '24-hour'), 'time')) : $input_values;
1701 // Convert dates into a string.
1702 $input_values = isset($input_values['month']) ? array(webform_date_string($input_values, 'date')) : $input_values;
1703 return $input_values;
1704 }
1705
1706 /**
1707 * Utility function to compare values of a select component.
1708 * @param string $a
1709 * First select option key to compare
1710 * @param string $b
1711 * Second select option key to compare
1712 * @param array $options
1713 * Associative array where the $a and $b are within the keys
1714 * @return integer based upon position of $a and $b in $options
1715 * -N if $a above (<) $b
1716 * 0 if $a = $b
1717 * +N if $a is below (>) $b
1718 */
1719 function webform_compare_select($a, $b, $options) {
1720 // Select keys that are integer-like strings are numberic indices in PHP.
1721 // Convert the array keys to an array of strings.
1722 $options_array = array_map(function($i) {return (string) $i; }, array_keys($options));
1723 $a_position = array_search($a, $options_array, TRUE);
1724 $b_position = array_search($b, $options_array, TRUE);
1725 return ($a_position === FALSE || $a_position === FALSE) ? NULL : $a_position - $b_position;
1726
1727 }