commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-old / webform / components / grid.inc
1 <?php
2
3 /**
4 * @file
5 * Webform module grid component.
6 */
7
8 // Grid depends on functions provided by select.
9 webform_component_include('select');
10
11 /**
12 * Implements _webform_defaults_component().
13 */
14 function _webform_defaults_grid() {
15 return array(
16 'name' => '',
17 'form_key' => NULL,
18 'required' => 0,
19 'pid' => 0,
20 'weight' => 0,
21 'value' => '',
22 'extra' => array(
23 'options' => '',
24 'questions' => '',
25 'optrand' => 0,
26 'qrand' => 0,
27 'unique' => 0,
28 'title_display' => 0,
29 'custom_option_keys' => 0,
30 'custom_question_keys' => 0,
31 'sticky' => TRUE,
32 'description' => '',
33 'description_above' => FALSE,
34 'private' => FALSE,
35 'analysis' => TRUE,
36 ),
37 );
38 }
39
40 /**
41 * Implements _webform_theme_component().
42 */
43 function _webform_theme_grid() {
44 return array(
45 'webform_grid' => array(
46 'render element' => 'element',
47 'file' => 'components/grid.inc',
48 ),
49 'webform_display_grid' => array(
50 'render element' => 'element',
51 'file' => 'components/grid.inc',
52 ),
53 );
54 }
55
56 /**
57 * Implements _webform_edit_component().
58 */
59 function _webform_edit_grid($component) {
60 $form = array();
61
62 $form['help'] = array(
63 '#type' => 'fieldset',
64 '#collapsible' => TRUE,
65 '#collapsed' => !empty($component['cid']),
66 '#title' => t('About options and questions&hellip;'),
67 '#description' => t('Options and questions may be configured here, in additional nested Select Options components, or even both.'),
68 '#weight' => -4,
69 'pros_and_cons' => array(
70 '#theme' => 'table',
71 '#header' => array('', t('Options and questions configured <strong>here</strong>'), t('Configured in additional <strong>nested</strong> components'), t('Both')),
72 '#rows' => array(
73 array(t('Questions'), t('Enter the questions below.'), t('Configure and save this grid, then add additional Select Options components nested (indented) below this grid.'), t('Additional questions from nested components will be displayed below any questions configured here.')),
74 array(t('Options'), t('Enter options below.'), t('May be different for each question. Initially the same as defined below.'), t('Options from additional nested components will be merged with any options configured here.')),
75 array(t('Checkboxes'), t('No. Radio buttons only.'), t('Yes. Some or all questions may be multiple choice with check boxes.'), ''),
76 array(t('Default'), t('Yes. Must be same for all questions.'), t('Yes. May all be the same or different.'), ''),
77 array(t('Pre-built option lists'), t('No.'), t('Yes.'), ''),
78 array(t('Required'), t('Yes. Must be same for all questions.'), t('Yes. May all be the same or different.'), ''),
79 array(t('Question conditionals'), t('No.'), t('Yes. Individual questions may be used in conditional rules and/or actions.'), t('The whole grid may be conditionally shown or required.')),
80 array(t('Other types of nested components'), t('No.'), t('Yes. Other component types may also be included in the grid. They will be displayed where the options would normally be.'), ''),
81 ),
82 ),
83 );
84
85 if (module_exists('options_element')) {
86 $form['options'] = array(
87 '#type' => 'fieldset',
88 '#title' => t('Options'),
89 '#collapsible' => TRUE,
90 '#attributes' => array('class' => array('webform-options-element')),
91 '#element_validate' => array('_webform_edit_validate_options'),
92 '#weight' => -3,
93 );
94 $form['options']['options'] = array(
95 '#type' => 'options',
96 '#options' => _webform_select_options_from_text($component['extra']['options'], TRUE),
97 '#default_value' => $component['value'],
98 '#default_value_allowed' => TRUE,
99 '#optgroups' => FALSE,
100 '#key_type' => 'mixed',
101 '#key_type_toggle' => t('Customize option keys (Advanced)'),
102 '#key_type_toggled' => $component['extra']['custom_option_keys'],
103 '#default_value_pattern' => '^%.+\[.+\]$',
104 '#description' => t('<strong>Options to select across the top</strong>, such as "Poor" through "Excellent". Indicate the default to the left of the desired item. Use of only alphanumeric characters and underscores is recommended in keys.') . ' ' . theme('webform_token_help'),
105 );
106
107 $form['questions'] = array(
108 '#type' => 'fieldset',
109 '#title' => t('Questions'),
110 '#collapsible' => TRUE,
111 '#attributes' => array('class' => array('webform-options-element')),
112 '#element_validate' => array('_webform_edit_validate_options'),
113 '#weight' => -2,
114 );
115 $form['questions']['options'] = array(
116 '#type' => 'options',
117 '#options' => _webform_select_options_from_text($component['extra']['questions'], TRUE),
118 '#optgroups' => FALSE,
119 '#default_value' => FALSE,
120 '#default_value_allowed' => FALSE,
121 '#key_type' => 'mixed',
122 '#key_type_toggle' => t('Customize question keys (Advanced)'),
123 '#key_type_toggled' => $component['extra']['custom_question_keys'],
124 '#description' => t('<strong>Questions list down the side of the grid.</strong> For a heading column on the right, append "|" and the right-side heading. Use of only alphanumeric characters and underscores is recommended in keys.') . ' ' . theme('webform_token_help'),
125 );
126 }
127 else {
128 $form['extra']['options'] = array(
129 '#type' => 'textarea',
130 '#title' => t('Options'),
131 '#default_value' => $component['extra']['options'],
132 '#description' => t('Options to select across the top, such as "Poor" through "Excellent" or "Stronly Disagree" through "Strongly Agree".') .
133 '<p>' . t('One key-value option per line. <strong>Key-value pairs MUST be specified as "safe_key|Some readable option"</strong>. Use of only alphanumeric characters and underscores is recommended in keys.') . '</p>' . theme('webform_token_help'),
134 '#cols' => 60,
135 '#rows' => 5,
136 '#weight' => -3,
137 '#wysiwyg' => FALSE,
138 '#element_validate' => array('_webform_edit_validate_select'),
139 );
140 $form['extra']['questions'] = array(
141 '#type' => 'textarea',
142 '#title' => t('Questions'),
143 '#default_value' => $component['extra']['questions'],
144 '#description' => t('Questions list down the side of the grid. One question per line. <strong>Key-value pairs MUST be specified as "safe_key|Some readable question"</strong>. For a heading column on the right, append "|" and the right-side heading. Use of only alphanumeric characters and underscores is recommended in keys.') . ' ' . theme('webform_token_help') . ' ' .
145 '<p>' . t('<strong>Or for more control</strong> over the appearance and configuration, create additional additional Select Options or other type components nested under this grid. They will operate as separate components, but be displayed within this grid.') . '</p>',
146 '#cols' => 60,
147 '#rows' => 5,
148 '#weight' => -2,
149 '#wysiwyg' => FALSE,
150 '#element_validate' => array('_webform_edit_validate_select'),
151 );
152 $form['value'] = array(
153 '#type' => 'textfield',
154 '#title' => t('Default value'),
155 '#default_value' => $component['value'],
156 '#description' => t('The default option of the grid identified by its key.') . ' ' . theme('webform_token_help'),
157 '#size' => 60,
158 '#maxlength' => 1024,
159 '#weight' => 1,
160 );
161 }
162
163 $form['display']['optrand'] = array(
164 '#type' => 'checkbox',
165 '#title' => t('Randomize Options'),
166 '#default_value' => $component['extra']['optrand'],
167 '#description' => t('Randomizes the order of options on the top when they are displayed in the form.'),
168 '#parents' => array('extra', 'optrand')
169 );
170 $form['display']['qrand'] = array(
171 '#type' => 'checkbox',
172 '#title' => t('Randomize Questions'),
173 '#default_value' => $component['extra']['qrand'],
174 '#description' => t('Randomize the order of the questions on the side when they are displayed in the form.'),
175 '#parents' => array('extra', 'qrand')
176 );
177 $form['display']['sticky'] = array(
178 '#type' => 'checkbox',
179 '#title' => t('Sticky table header'),
180 '#default_value' => $component['extra']['sticky'],
181 '#description' => t('Use a sticky (non-scrolling) table header.'),
182 '#parents' => array('extra', 'sticky')
183 );
184
185 $form['validation']['unique'] = array(
186 '#type' => 'checkbox',
187 '#title' => t('Unique'),
188 '#return_value' => 1,
189 '#description' => t('Check that all entered values for this field are unique. The same value is not allowed to be used twice.'),
190 '#weight' => 1,
191 '#default_value' => $component['extra']['unique'],
192 '#parents' => array('extra', 'unique'),
193 );
194
195 return $form;
196 }
197
198 /**
199 * Implements _webform_render_component().
200 */
201 function _webform_render_grid($component, $value = NULL, $filter = TRUE, $submission = NULL) {
202 $node = isset($component['nid']) ? node_load($component['nid']) : NULL;
203
204 $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE);
205 $options = _webform_select_options_from_text($component['extra']['options'], TRUE);
206 if ($filter) {
207 $questions = _webform_select_replace_tokens($questions, $node);
208 $options = _webform_select_replace_tokens($options, $node);
209 }
210
211 $element = array(
212 '#type' => 'webform_grid',
213 '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'],
214 '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before',
215 '#required' => $component['required'],
216 '#weight' => $component['weight'],
217 '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'],
218 '#grid_questions' => $questions,
219 '#grid_options' => $options,
220 '#default_value' => isset($value) || !strlen($component['value']) ? $value : array_fill_keys(array_keys($questions), $component['value']),
221 '#grid_default' => $component['value'],
222 '#optrand' => $component['extra']['optrand'],
223 '#qrand' => $component['extra']['qrand'],
224 '#sticky' => $component['extra']['sticky'],
225 '#theme' => 'webform_grid',
226 '#theme_wrappers' => array('webform_element'),
227 '#process' => array('webform_expand_grid'),
228 '#translatable' => array('title', 'description', 'grid_options', 'grid_questions'),
229 );
230
231 // Enforce uniqueness.
232 if ($component['extra']['unique']) {
233 $element['#element_validate'][] = '_webform_edit_grid_unique_validate';
234 }
235
236 return $element;
237 }
238
239 /**
240 * A Form API #process function for Webform grid fields.
241 */
242 function webform_expand_grid($element) {
243 $options = $element['#grid_options'];
244 $questions = $element['#grid_questions'];
245 $weights = array();
246
247 // Process questions and options from nested components.
248 foreach (element_children($element) as $key) {
249 $question = $element[$key];
250 // Both forms and grid displays have #webform_component.
251 if (isset($question['#webform_component']) &&
252 $question['#webform_component']['type'] == 'select' &&
253 !$question['#webform_component']['extra']['aslist'] &&
254 !$question['#webform_component']['extra']['other_option']) {
255 $options = webform_grid_merge_options($options, $question['#options']);
256 $weights[$key] = $question['#weight'];
257 }
258 }
259
260 // Add the internal grid questions.
261 $weight = -1000;
262 $value = isset($element['#default_value']) ? $element['#default_value'] : array();
263 foreach ($questions as $key => $question) {
264 if ($question != '') {
265 $question_value = isset($value[$key]) && $value[$key] !== '' ? $value[$key] : NULL;
266 $element[$key] = array(
267 '#grid_question' => TRUE,
268 '#title' => $question,
269 '#required' => $element['#required'],
270 '#options' => $element['#grid_options'],
271 '#type' => 'radios',
272 '#default_value' => $question_value,
273 '#value' => $question_value,
274 '#process' => array('form_process_radios', 'webform_expand_select_ids'),
275
276 // Webform handles validation manually.
277 '#validated' => TRUE,
278 '#webform_validated' => FALSE,
279 '#translatable' => array('title'),
280 '#weight' => $weight,
281 );
282
283 // Add HTML5 required attribute, if needed.
284 if ($element['#required']) {
285 $element[$key]['#attributes']['required'] = 'required';
286 }
287
288 $weights[$key] = $weight;
289 $weight++;
290 }
291 }
292
293 if (!empty($element['#optrand'])) {
294 _webform_shuffle_options($options);
295 }
296 $element['#grid_options'] = $options;
297
298 asort($weights);
299 if (!empty($element['#qrand'])) {
300 _webform_shuffle_options($weights);
301 }
302 $weight = min($weights);
303 foreach ($weights as $key => $old_weight) {
304 $element[$key]['#options'] = webform_grid_remove_options($options, $element[$key]['#options']);
305 $element[$key]['#weight'] = $weight++;
306 $element['#grid_questions'][$key] = $element[$key]['#title'];
307 }
308
309 return $element;
310 }
311
312 /**
313 * Helper. Merge select component options in order.
314 *
315 * @param array $existing
316 * An array of existing values into which any values from $new that aren't in
317 * $existing are inserted.
318 * @param array $new
319 * Values to be inserted into $existing.
320 * @return array
321 * The merged array.
322 */
323 function webform_grid_merge_options($existing, $new) {
324 $insert = NULL;
325 $queue = array();
326 foreach ($new as $key => $value) {
327 if (isset($existing[$key])) {
328 // Insert the queue before the found item.
329 $insert = array_search($key, array_keys($existing));
330 if ($queue) {
331 $existing = array_slice($existing, 0, $insert, TRUE) +
332 $queue +
333 array_slice($existing, $insert, NULL, TRUE);
334 $insert += count($queue);
335 $queue = array();
336 }
337 $insert++;
338 }
339 elseif (is_null($insert)) {
340 // It is not yet clear yet where to put this item. Add it to the queue.
341 $queue[$key] = $value;
342 }
343 else {
344 // PHP array_splice does not preserved the keys of the inserted array,
345 // but array_slice does (if the preserve keys parameter is TRUE).
346 $existing = array_slice($existing, 0, $insert, TRUE) +
347 array($key => $value) +
348 array_slice($existing, $insert, NULL, TRUE);
349 $insert++;
350 }
351 }
352 // Append any left over queued items.
353 $existing += $queue;
354 return $existing;
355 }
356
357 /**
358 * Helper. Replace missing options with empty values.
359 *
360 * @param array $header
361 * An array of options to be used at the grid table header.
362 * @param array $row_options
363 * An array of options to be used for this row.
364 * @return array
365 * The $row_options with any missing options replaced with empty values.
366 **/
367 function webform_grid_remove_options($header, $row_options) {
368 foreach ($header as $key => $value) {
369 if (!isset($row_options[$key])) {
370 $header[$key] = '';
371 }
372 }
373 return $header;
374 }
375
376 /**
377 * Implements _webform_display_component().
378 */
379 function _webform_display_grid($component, $value, $format = 'html', $submission = array()) {
380 $node = node_load($component['nid']);
381 $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE);
382 $questions = _webform_select_replace_tokens($questions, $node);
383 $options = _webform_select_options_from_text($component['extra']['options'], TRUE);
384 $options = _webform_select_replace_tokens($options, $node);
385
386 $element = array(
387 '#title' => $component['name'],
388 '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before',
389 '#weight' => $component['weight'],
390 '#format' => $format,
391 '#grid_questions' => $questions,
392 '#grid_options' => $options,
393 '#default_value' => $value,
394 '#sticky' => $component['extra']['sticky'],
395 '#theme' => 'webform_display_grid',
396 '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'),
397 '#sorted' => TRUE,
398 '#translatable' => array('#title', '#grid_questions', '#grid_options'),
399 );
400
401 foreach ($questions as $key => $question) {
402 if ($question !== '') {
403 $element[$key] = array(
404 '#title' => $question,
405 '#value' => isset($value[$key]) ? $value[$key] : NULL,
406 '#translatable' => array('#title', '#value'),
407 );
408 }
409 }
410
411 return $element;
412 }
413
414 /**
415 * Preprocess function for displaying a grid component.
416 */
417 function template_preprocess_webform_display_grid(&$variables) {
418 $element =& $variables['element'];
419 // Expand the grid, suppressing randomization. This builds the grid
420 // questions and options.
421 $element['#qrand'] = FALSE;
422 $element['#optrand'] = FALSE;
423 $element['#required'] = FALSE;
424 $element = webform_expand_grid($element);
425 }
426
427 /**
428 * Format the text output for this component.
429 */
430 function theme_webform_display_grid($variables) {
431 $element = $variables['element'];
432
433 $component = $element['#webform_component'];
434 $format = $element['#format'];
435
436 if ($format == 'html') {
437 $right_titles = _webform_grid_right_titles($element);
438 $rows = array();
439
440 // Set the header for the table.
441 $header = _webform_grid_header($element, $right_titles);
442
443 foreach (element_children($element) as $question_key) {
444 $question_element = $element[$question_key];
445 $row = array();
446 $questions = explode('|', $question_element['#title'], 2);
447 $values = $question_element['#value'];
448 $values = is_array($values) ? $values : array($values);
449 $row[] = array('data' => webform_filter_xss($questions[0]), 'class' => array('webform-grid-question'));
450 if (isset($element['#grid_questions'][$question_key])) {
451 foreach ($element['#grid_options'] as $option_value => $option_label) {
452 if (in_array($option_value, $values)) {
453 $row[] = array('data' => '<strong>X</strong>', 'class' => array('checkbox', 'webform-grid-option'));
454 }
455 else {
456 $row[] = array('data' => '&nbsp;', 'class' => array('checkbox', 'webform-grid-option'));
457 }
458 }
459 }
460 else {
461 $question_element['#title_display'] = 'none';
462 $row[] = array(
463 'data' => drupal_render($question_element),
464 'colspan' => count($element['#grid_options']),
465 );
466 }
467 if ($right_titles) {
468 $row[] = array('data' => isset($questions[1]) ? webform_filter_xss($questions[1]) : '', 'class' => array('webform-grid-question'));
469 }
470 $rows[] = $row;
471 }
472
473 $option_count = count($header) - 1;
474 $output = theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => $element['#sticky'], 'attributes' => array('class' => array('webform-grid', 'webform-grid-' . $option_count))));
475 }
476 else {
477 $items = array();
478 foreach (element_children($element) as $question_key) {
479 $question_element = $element[$question_key];
480 if (isset($element['#grid_questions'][$question_key])) {
481 $values = $question_element['#value'];
482 $values = is_array($values) ? $values : array($values);
483 foreach ($values as $value_key => $value) {
484 if (isset($element['#grid_options'][$value])) {
485 $values[$value_key] = $element['#grid_options'][$value];
486 }
487 else {
488 unset($values[$value_key]);
489 }
490 }
491 $value = implode(', ', $values);
492 }
493 else {
494 $element[$question_key]['#title'] = '';
495 $value = drupal_render($element[$question_key]);
496 }
497 $items[] = ' - ' . _webform_grid_question_header($question_element['#title']) . ': ' . $value;
498 }
499 $output = implode("\n", $items);
500 }
501
502 return $output;
503 }
504
505 /**
506 * Implements _webform_analysis_component().
507 */
508 function _webform_analysis_grid($component, $sids = array(), $single = FALSE, $join = NULL) {
509 // Generate the list of options and questions.
510 $node = node_load($component['nid']);
511 $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE);
512 $questions = _webform_select_replace_tokens($questions, $node);
513 $options = _webform_select_options_from_text($component['extra']['options'], TRUE);
514 $options = _webform_select_replace_tokens($options, $node);
515
516 // Generate a lookup table of results.
517 $query = db_select('webform_submitted_data', 'wsd')
518 ->fields('wsd', array('no', 'data'))
519 ->condition('wsd.nid', $component['nid'])
520 ->condition('wsd.cid', $component['cid'])
521 ->condition('wsd.data', '', '<>')
522 ->groupBy('wsd.no')
523 ->groupBy('wsd.data');
524 $query->addExpression('COUNT(wsd.sid)', 'datacount');
525
526 if (count($sids)) {
527 $query->condition('wsd.sid', $sids, 'IN');
528 }
529
530 if ($join) {
531 $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid');
532 }
533
534 $result = $query->execute();
535 $counts = array();
536 foreach ($result as $data) {
537 $counts[$data->no][$data->data] = $data->datacount;
538 }
539
540 // Create an entire table to be put into the returned row.
541 $rows = array();
542 $header = array('');
543
544 // Add options as a header row.
545 foreach ($options as $option) {
546 $header[] = webform_filter_xss($option);
547 }
548
549 // Add questions as each row.
550 foreach ($questions as $qkey => $question) {
551 $row = array(webform_filter_xss($question));
552 foreach ($options as $okey => $option) {
553 $row[] = !empty($counts[$qkey][$okey]) ? $counts[$qkey][$okey] : 0;
554 }
555 $rows[] = $row;
556 }
557
558 // Return return the table unless there are no internal questions in the grid.
559 if ($rows) {
560 return array(
561 'table_header' => $header,
562 'table_rows' => $rows,
563 );
564 }
565 }
566
567 /**
568 * Implements _webform_table_component().
569 */
570 function _webform_table_grid($component, $value) {
571 $node = node_load($component['nid']);
572 $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE);
573 $questions = _webform_select_replace_tokens($questions, $node);
574 $options = _webform_select_options_from_text($component['extra']['options'], TRUE);
575 $options = _webform_select_replace_tokens($options, $node);
576
577 $output = '';
578 // Set the value as a single string.
579 foreach ($questions as $key => $label) {
580 if (isset($value[$key]) && isset($options[$value[$key]])) {
581 $output .= webform_filter_xss(_webform_grid_question_header($label)) . ': ' . webform_filter_xss($options[$value[$key]]) . '<br />';
582 }
583 }
584
585 // Return output if the grid contains internal questions.
586 if (count($questions)) {
587 return $output;
588 }
589 }
590
591 /**
592 * Implements _webform_csv_headers_component().
593 */
594 function _webform_csv_headers_grid($component, $export_options) {
595 $node = node_load($component['nid']);
596 $items = _webform_select_options_from_text($component['extra']['questions'], TRUE);
597 $items = _webform_select_replace_tokens($items, $node);
598
599 $header = array();
600 $header[0] = array('');
601 $header[1] = array($export_options['header_keys'] ? $component['form_key'] : $component['name']);
602 $count = 0;
603 foreach ($items as $key => $item) {
604 // Empty column per sub-field in main header.
605 if ($count != 0) {
606 $header[0][] = '';
607 $header[1][] = '';
608 }
609 // The value for this option.
610 $header[2][] = $export_options['header_keys'] ? $key : _webform_grid_question_header($item);
611 $count++;
612 }
613
614 return $header;
615 }
616
617 /**
618 * Implements _webform_csv_data_component().
619 */
620 function _webform_csv_data_grid($component, $export_options, $value) {
621 $node = node_load($component['nid']);
622 $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE);
623 $questions = _webform_select_replace_tokens($questions, $node);
624 $options = _webform_select_options_from_text($component['extra']['options'], TRUE);
625 $options = _webform_select_replace_tokens($options, $node);
626
627 $return = array();
628 foreach ($questions as $key => $question) {
629 if (isset($value[$key]) && isset($options[$value[$key]])) {
630 $return[] = $export_options['select_keys'] ? $value[$key] : $options[$value[$key]];
631 }
632 else {
633 $return[] = '';
634 }
635 }
636 return $return;
637 }
638
639 /**
640 * A Form API element validate function to check that all choices are unique.
641 */
642 function _webform_edit_grid_unique_validate($element) {
643 // Grids may contain nested multiple value select components.
644 // Create a flat array of values.
645 $values = array();
646 array_walk_recursive($element['#value'], function($a) use (&$values) { $values[] = $a; });
647
648 $nr_unique = count(array_unique($values));
649 $nr_values = count($values);
650 $nr_possible = count($element['#grid_options']);
651 if (strlen($element['#grid_default']) && isset($element['#grid_options'][$element['#grid_default']])) {
652 // A default is defined and is one of the options. Don't count default values
653 // toward uniqueness.
654 $nr_defaults = count(array_keys($element['#value'], $element['#grid_default']));
655 if ($nr_defaults) {
656 $nr_values -= $nr_defaults;
657 $nr_unique--;
658 }
659 }
660 if ($nr_unique < $nr_values && $nr_unique < $nr_possible) {
661 // Fewer unique values than values means that at least one value is duplicated.
662 // Fewer unique values than possible values means that there is a possible choice
663 // that wasn't used.
664 form_error($element, t('!title is not allowed to have the same answer for more than one question.', array('!title' => $element['#title'])));
665 }
666 }
667
668 function theme_webform_grid($variables) {
669 $element = $variables['element'];
670 $right_titles = _webform_grid_right_titles($element);
671
672 $rows = array();
673
674 // Set the header for the table.
675 $header = _webform_grid_header($element, $right_titles);
676
677 foreach (element_children($element) as $key) {
678 $question_element = $element[$key];
679 $title_element =& $question_element;
680 if ($question_element['#type'] == 'select_or_other') {
681 $title_element =& $question_element['select'];
682 }
683 $question_titles = explode('|', $title_element['#title'], 2);
684
685 // Create a row with the question title.
686 $required = !empty($question_element['#required']) ? theme('form_required_marker', array('element' => $question_element)) : '';
687 $row = array(array('data' => t('!title !required', array('!title' => webform_filter_xss($question_titles[0]), '!required' => $required)), 'class' => array('webform-grid-question')));
688
689 // Render each radio button in the row.
690 if ($question_element['#type'] == 'radios' || $question_element['#type'] == 'checkboxes') {
691 $radios = form_process_radios($question_element);
692 foreach (element_children($radios) as $key) {
693 $radio_title = $radios[$key]['#title'];
694 if (!strlen($radio_title)) {
695 $row[] = '&nbsp;';
696 }
697 else {
698 $radios[$key]['#title'] = $question_element['#title'] . ' - ' . $radio_title;
699 $radios[$key]['#title_display'] = 'invisible';
700 $row[] = array('data' => drupal_render($radios[$key]), 'class' => array('checkbox', 'webform-grid-option'), 'data-label' => array($radio_title));
701 }
702 }
703 }
704 else {
705 $title_element['#title_display'] = 'none';
706 $row[] = array(
707 'data' => drupal_render($question_element),
708 'colspan' => count($element['#grid_options']),
709 );
710 }
711 if ($right_titles) {
712 $row[] = array('data' => isset($question_titles[1]) ? webform_filter_xss($question_titles[1]) : '', 'class' => array('webform-grid-question'));
713 }
714
715 // Convert the parents array into a string, excluding the "submitted" wrapper.
716 $nested_level = $question_element['#parents'][0] == 'submitted' ? 1 : 0;
717 $parents = str_replace('_', '-', implode('--', array_slice($question_element['#parents'], $nested_level)));
718
719 $rows[] = array(
720 'data' => $row,
721 'class' => empty($question_element['#grid_question'])
722 ? array('webform-component', 'webform-component-' . str_replace('_', '-', $question_element['#type']), 'webform-component--' . $parents)
723 : array(),
724 );
725 }
726
727 $option_count = count($header) - 1;
728 return theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => $element['#sticky'], 'attributes' => array('class' => array('webform-grid', 'webform-grid-' . $option_count))));
729 }
730
731 /**
732 * Generate a table header suitable for form or html display.
733 */
734 function _webform_grid_header($element, $right_titles) {
735 $titles = explode('|', $element['#title'], 2);
736 $header = array(array('data' => _webform_grid_header_title($element, $titles[0]), 'class' => array('webform-grid-question')));
737 foreach ($element['#grid_options'] as $option) {
738 $header[] = array('data' => webform_filter_xss($option), 'class' => array('checkbox', 'webform-grid-option'));
739 }
740 if ($right_titles) {
741 $header[] = array('data' => _webform_grid_header_title($element, isset($titles[1]) ? $titles[1] : ''), 'class' => array('webform-grid-question'));
742 }
743 return $header;
744 }
745
746 /**
747 * Create internal component title for table header, if any.
748 */
749 function _webform_grid_header_title($element, $title) {
750 $header_title = '';
751 if ($element['#title_display'] == 'internal') {
752 $variables = array('element' => $element);
753 $variables['element']['#title_display'] = 'before';
754 $variables['element']['#title'] = $title;
755 $header_title = theme('form_element_label', $variables);
756 }
757 return $header_title;
758 }
759
760 /**
761 * Determine if a right-side title column has been specified.
762 */
763 function _webform_grid_right_titles($element) {
764 if ($element['#title_display'] == 'internal' && substr_count($element['#title'], '|')) {
765 return TRUE;
766 }
767 foreach ($element['#grid_questions'] as $question_key => $question) {
768 if (substr_count($question, '|')) {
769 return TRUE;
770 }
771 }
772 return FALSE;
773 }
774
775 /**
776 * Create a question header for left, right or left/right question headers.
777 */
778 function _webform_grid_question_header($text) {
779 return implode('/', array_filter(explode('|', $text)));
780 }