Project

General

Profile

Paste
Download (30.9 KB) Statistics
| Branch: | Revision:

root / drupal7 / sites / all / modules / webform / components / grid.inc @ 8d02775b

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
    '#element_validate' => array('webform_validate_grid'),
229
    '#translatable' => array('title', 'description', 'grid_options', 'grid_questions'),
230
  );
231

    
232
  // Enforce uniqueness.
233
  if ($component['extra']['unique']) {
234
    $element['#element_validate'][] = '_webform_edit_grid_unique_validate';
235
  }
236

    
237
  return $element;
238
}
239

    
240
/**
241
 * A Form API #process function for Webform grid fields.
242
 */
243
function webform_expand_grid($element) {
244
  $options = $element['#grid_options'];
245
  $questions = $element['#grid_questions'];
246
  $weights = array();
247

    
248
  // Process questions and options from nested components.
249
  foreach (element_children($element) as $key) {
250
    $question = $element[$key];
251
    // Both forms and grid displays have #webform_component.
252
    if (isset($question['#webform_component']) &&
253
        $question['#webform_component']['type'] == 'select' &&
254
        !$question['#webform_component']['extra']['aslist'] &&
255
        !$question['#webform_component']['extra']['other_option']) {
256
      $options = webform_grid_merge_options($options, $question['#options']);
257
      $weights[$key] = $question['#weight'];
258
    }
259
  }
260

    
261
  // Add the internal grid questions.
262
  $weight = -1000;
263
  $value = isset($element['#default_value']) ? $element['#default_value'] : array();
264
  foreach ($questions as $key => $question) {
265
    if ($question != '') {
266
      $question_value = isset($value[$key]) && $value[$key] !== '' ? $value[$key] : NULL;
267
      $element[$key] = array(
268
        '#grid_question' => TRUE,
269
        '#title' => $question,
270
        '#required' => $element['#required'],
271
        '#options' => $element['#grid_options'],
272
        '#type' => 'radios',
273
        '#default_value' => $question_value,
274
        '#value' => $question_value,
275
        '#process' => array('form_process_radios', 'webform_expand_select_ids'),
276

    
277
        // Webform handles validation manually.
278
        '#validated' => TRUE,
279
        '#webform_validated' => FALSE,
280
        '#translatable' => array('title'),
281
        '#weight' => $weight,
282
      );
283

    
284
      // Add HTML5 required attribute, if needed.
285
      if ($element['#required']) {
286
        $element[$key]['#attributes']['required'] = 'required';
287
      }
288

    
289
      $weights[$key] = $weight;
290
      $weight++;
291
    }
292
  }
293

    
294
  if (!empty($element['#optrand'])) {
295
    _webform_shuffle_options($options);
296
  }
297
  $element['#grid_options'] = $options;
298

    
299
  asort($weights);
300
  if (!empty($element['#qrand'])) {
301
    _webform_shuffle_options($weights);
302
  }
303
  if ($weights) {
304
    $weight = min($weights);
305
  }
306
  foreach ($weights as $key => $old_weight) {
307
    $element[$key]['#options'] = webform_grid_remove_options($options, $element[$key]['#options']);
308
    $element[$key]['#weight'] = $weight++;
309
    $element['#grid_questions'][$key] = $element[$key]['#title'];
310
  }
311

    
312
  return $element;
313
}
314

    
315
/**
316
 * Helper. Merge select component options in order.
317
 *
318
 * @param array $existing
319
 *   An array of existing values into which any values from $new that aren't in
320
 *   $existing are inserted.
321
 * @param array $new
322
 *   Values to be inserted into $existing.
323
 *
324
 * @return array
325
 *   The merged array.
326
 */
327
function webform_grid_merge_options(array $existing, array $new) {
328
  $insert = NULL;
329
  $queue = array();
330
  foreach ($new as $key => $value) {
331
    if (isset($existing[$key])) {
332
      // Insert the queue before the found item.
333
      $insert = array_search($key, array_keys($existing));
334
      if ($queue) {
335
        $existing = array_slice($existing, 0, $insert, TRUE) +
336
                    $queue +
337
                    array_slice($existing, $insert, NULL, TRUE);
338
        $insert += count($queue);
339
        $queue = array();
340
      }
341
      $insert++;
342
    }
343
    elseif (is_null($insert)) {
344
      // It is not yet clear yet where to put this item. Add it to the queue.
345
      $queue[$key] = $value;
346
    }
347
    else {
348
      // PHP array_splice does not preserved the keys of the inserted array,
349
      // but array_slice does (if the preserve keys parameter is TRUE).
350
      $existing = array_slice($existing, 0, $insert, TRUE) +
351
                  array($key => $value) +
352
                  array_slice($existing, $insert, NULL, TRUE);
353
      $insert++;
354
    }
355
  }
356
  // Append any left over queued items.
357
  $existing += $queue;
358
  return $existing;
359
}
360

    
361
/**
362
 * Helper. Replace missing options with empty values.
363
 *
364
 * @param array $header
365
 *   An array of options to be used at the grid table header.
366
 * @param array $row_options
367
 *   An array of options to be used for this row.
368
 *
369
 * @return array
370
 *   The $row_options with any missing options replaced with empty values.
371
 */
372
function webform_grid_remove_options(array $header, array $row_options) {
373
  foreach ($header as $key => $value) {
374
    if (!isset($row_options[$key])) {
375
      $header[$key] = '';
376
    }
377
  }
378
  return $header;
379
}
380

    
381
/**
382
 * Implements _webform_display_component().
383
 */
384
function _webform_display_grid($component, $value, $format = 'html', $submission = array()) {
385
  $node = node_load($component['nid']);
386
  $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE);
387
  $questions = _webform_select_replace_tokens($questions, $node);
388
  $options = _webform_select_options_from_text($component['extra']['options'], TRUE);
389
  $options = _webform_select_replace_tokens($options, $node);
390

    
391
  $element = array(
392
    '#title' => $component['name'],
393
    '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before',
394
    '#weight' => $component['weight'],
395
    '#format' => $format,
396
    '#grid_questions' => $questions,
397
    '#grid_options' => $options,
398
    '#default_value' => $value,
399
    '#sticky' => $component['extra']['sticky'],
400
    '#theme' => 'webform_display_grid',
401
    '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'),
402
    '#sorted' => TRUE,
403
    '#translatable' => array('#title', '#grid_questions', '#grid_options'),
404
  );
405

    
406
  foreach ($questions as $key => $question) {
407
    if ($question !== '') {
408
      $element[$key] = array(
409
        '#title' => $question,
410
        '#value' => isset($value[$key]) ? $value[$key] : NULL,
411
        '#translatable' => array('#title', '#value'),
412
      );
413
    }
414
  }
415

    
416
  return $element;
417
}
418

    
419
/**
420
 * Preprocess function for displaying a grid component.
421
 */
422
function template_preprocess_webform_display_grid(&$variables) {
423
  $element =& $variables['element'];
424
  // Expand the grid, suppressing randomization. This builds the grid
425
  // questions and options.
426
  $element['#qrand'] = FALSE;
427
  $element['#optrand'] = FALSE;
428
  $element['#required'] = FALSE;
429
  $element = webform_expand_grid($element);
430
}
431

    
432
/**
433
 * Format the text output for this component.
434
 */
435
function theme_webform_display_grid($variables) {
436
  $element = $variables['element'];
437
  $format = $element['#format'];
438

    
439
  if ($format == 'html') {
440
    $right_titles = _webform_grid_right_titles($element);
441
    $rows = array();
442

    
443
    // Set the header for the table.
444
    $header = _webform_grid_header($element, $right_titles);
445

    
446
    foreach (element_children($element) as $question_key) {
447
      $question_element = $element[$question_key];
448
      $row = array();
449
      $questions = explode('|', $question_element['#title'], 2);
450
      $values = $question_element['#value'];
451
      $values = is_array($values) ? $values : array($values);
452
      $row[] = array('data' => webform_filter_xss($questions[0]), 'class' => array('webform-grid-question'));
453
      if (isset($element['#grid_questions'][$question_key])) {
454
        foreach ($element['#grid_options'] as $option_value => $option_label) {
455
          if (in_array($option_value, $values)) {
456
            $row[] = array('data' => '<strong>X</strong>', 'class' => array('checkbox', 'webform-grid-option'));
457
          }
458
          else {
459
            $row[] = array('data' => '&nbsp;', 'class' => array('checkbox', 'webform-grid-option'));
460
          }
461
        }
462
      }
463
      else {
464
        $question_element['#title_display'] = 'none';
465
        $row[] = array(
466
          'data' => drupal_render($question_element),
467
          'colspan' => count($element['#grid_options']),
468
        );
469
      }
470
      if ($right_titles) {
471
        $row[] = array('data' => isset($questions[1]) ? webform_filter_xss($questions[1]) : '', 'class' => array('webform-grid-question'));
472
      }
473
      $rows[] = $row;
474
    }
475

    
476
    $option_count = count($header) - 1;
477
    $output = theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => $element['#sticky'], 'attributes' => array('class' => array('webform-grid', 'webform-grid-' . $option_count))));
478
  }
479
  else {
480
    $items = array();
481
    foreach (element_children($element) as $question_key) {
482
      $question_element = $element[$question_key];
483
      if (isset($element['#grid_questions'][$question_key])) {
484
        $values = $question_element['#value'];
485
        $values = is_array($values) ? $values : array($values);
486
        foreach ($values as $value_key => $value) {
487
          if (isset($element['#grid_options'][$value])) {
488
            $values[$value_key] = $element['#grid_options'][$value];
489
          }
490
          else {
491
            unset($values[$value_key]);
492
          }
493
        }
494
        $value = implode(', ', $values);
495
      }
496
      else {
497
        $element[$question_key]['#title'] = '';
498
        $value = drupal_render($element[$question_key]);
499
      }
500
      $items[] = ' - ' . _webform_grid_question_header($question_element['#title']) . ': ' . $value;
501
    }
502
    $output = implode("\n", $items);
503
  }
504

    
505
  return $output;
506
}
507

    
508
/**
509
 * Implements _webform_analysis_component().
510
 */
511
function _webform_analysis_grid($component, $sids = array(), $single = FALSE, $join = NULL) {
512
  // Generate the list of options and questions.
513
  $node = node_load($component['nid']);
514
  $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE);
515
  $questions = _webform_select_replace_tokens($questions, $node);
516
  $options = _webform_select_options_from_text($component['extra']['options'], TRUE);
517
  $options = _webform_select_replace_tokens($options, $node);
518

    
519
  // Generate a lookup table of results.
520
  $query = db_select('webform_submitted_data', 'wsd')
521
    ->fields('wsd', array('no', 'data'))
522
    ->condition('wsd.nid', $component['nid'])
523
    ->condition('wsd.cid', $component['cid'])
524
    ->condition('wsd.data', '', '<>')
525
    ->groupBy('wsd.no')
526
    ->groupBy('wsd.data');
527
  $query->addExpression('COUNT(wsd.sid)', 'datacount');
528

    
529
  if (count($sids)) {
530
    $query->condition('wsd.sid', $sids, 'IN');
531
  }
532

    
533
  if ($join) {
534
    $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid');
535
  }
536

    
537
  $result = $query->execute();
538
  $counts = array();
539
  foreach ($result as $data) {
540
    $counts[$data->no][$data->data] = $data->datacount;
541
  }
542

    
543
  // Create an entire table to be put into the returned row.
544
  $rows = array();
545
  $header = array('');
546

    
547
  // Add options as a header row.
548
  foreach ($options as $option) {
549
    $header[] = webform_filter_xss($option);
550
  }
551

    
552
  // Add questions as each row.
553
  foreach ($questions as $qkey => $question) {
554
    $row = array(webform_filter_xss($question));
555
    foreach ($options as $okey => $option) {
556
      $row[] = !empty($counts[$qkey][$okey]) ? $counts[$qkey][$okey] : 0;
557
    }
558
    $rows[] = $row;
559
  }
560

    
561
  // Return return the table unless there are no internal questions in the grid.
562
  if ($rows) {
563
    return array(
564
      'table_header' => $header,
565
      'table_rows' => $rows,
566
    );
567
  }
568
}
569

    
570
/**
571
 * Implements _webform_table_component().
572
 */
573
function _webform_table_grid($component, $value) {
574
  $node = node_load($component['nid']);
575
  $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE);
576
  $questions = _webform_select_replace_tokens($questions, $node);
577
  $options = _webform_select_options_from_text($component['extra']['options'], TRUE);
578
  $options = _webform_select_replace_tokens($options, $node);
579

    
580
  $output = '';
581
  // Set the value as a single string.
582
  foreach ($questions as $key => $label) {
583
    if (isset($value[$key]) && isset($options[$value[$key]])) {
584
      $output .= webform_filter_xss(_webform_grid_question_header($label)) . ': ' . webform_filter_xss($options[$value[$key]]) . '<br />';
585
    }
586
  }
587

    
588
  // Return output if the grid contains internal questions.
589
  if (count($questions)) {
590
    return $output;
591
  }
592
}
593

    
594
/**
595
 * Implements _webform_csv_headers_component().
596
 */
597
function _webform_csv_headers_grid($component, $export_options) {
598
  $node = node_load($component['nid']);
599
  $items = _webform_select_options_from_text($component['extra']['questions'], TRUE);
600
  $items = _webform_select_replace_tokens($items, $node);
601

    
602
  $header = array();
603
  $header[0] = array('');
604
  $header[1] = array($export_options['header_keys'] ? $component['form_key'] : $component['name']);
605
  $count = 0;
606
  foreach ($items as $key => $item) {
607
    // Empty column per sub-field in main header.
608
    if ($count != 0) {
609
      $header[0][] = '';
610
      $header[1][] = '';
611
    }
612
    // The value for this option.
613
    $header[2][] = $export_options['header_keys'] ? $key : _webform_grid_question_header($item);
614
    $count++;
615
  }
616

    
617
  return $header;
618
}
619

    
620
/**
621
 * Implements _webform_csv_data_component().
622
 */
623
function _webform_csv_data_grid($component, $export_options, $value) {
624
  $node = node_load($component['nid']);
625
  $questions = _webform_select_options_from_text($component['extra']['questions'], TRUE);
626
  $questions = _webform_select_replace_tokens($questions, $node);
627
  $options = _webform_select_options_from_text($component['extra']['options'], TRUE);
628
  $options = _webform_select_replace_tokens($options, $node);
629

    
630
  $return = array();
631
  foreach ($questions as $key => $question) {
632
    if (isset($value[$key]) && isset($options[$value[$key]])) {
633
      $return[] = $export_options['select_keys'] ? $value[$key] : $options[$value[$key]];
634
    }
635
    else {
636
      $return[] = '';
637
    }
638
  }
639
  return $return;
640
}
641

    
642
/**
643
 * A Form API element validate function to check that all choices are unique.
644
 */
645
function _webform_edit_grid_unique_validate($element) {
646
  // Grids may contain nested multiple value select components.
647
  // Create a flat array of values.
648
  $values = array();
649
  $element['#value'] = (array) $element['#value'];
650
  array_walk_recursive($element['#value'], function ($a) use (&$values) {
651
    $values[] = $a;
652
  });
653

    
654
  $nr_unique = count(array_unique($values));
655
  $nr_values = count($values);
656
  $nr_possible = count($element['#grid_options']);
657
  if (strlen($element['#grid_default']) && isset($element['#grid_options'][$element['#grid_default']])) {
658
    // A default is defined and is one of the options. Don't count default values
659
    // toward uniqueness.
660
    $nr_defaults = count(array_keys($element['#value'], $element['#grid_default']));
661
    if ($nr_defaults) {
662
      $nr_values -= $nr_defaults;
663
      $nr_unique--;
664
    }
665
  }
666
  if ($nr_unique < $nr_values && $nr_unique < $nr_possible) {
667
    // Fewer unique values than values means that at least one value is duplicated.
668
    // Fewer unique values than possible values means that there is a possible choice
669
    // that wasn't used.
670
    form_error($element, t('!title is not allowed to have the same answer for more than one question.', array('!title' => $element['#title'])));
671
  }
672
}
673

    
674
/**
675
 * Theme function to render a grid component.
676
 */
677
function theme_webform_grid($variables) {
678
  $element = $variables['element'];
679
  $right_titles = _webform_grid_right_titles($element);
680

    
681
  $rows = array();
682

    
683
  // Set the header for the table.
684
  $header = _webform_grid_header($element, $right_titles);
685

    
686
  foreach (element_children($element) as $key) {
687
    $question_element = $element[$key];
688
    $title_element =& $question_element;
689
    if ($question_element['#type'] == 'select_or_other') {
690
      $title_element =& $question_element['select'];
691
    }
692
    $question_titles = explode('|', $title_element['#title'], 2);
693

    
694
    // Create a row with the question title.
695
    $required = !empty($question_element['#required']) ? theme('form_required_marker', array('element' => $question_element)) : '';
696
    $row = array(array('data' => t('!title !required', array('!title' => webform_filter_xss($question_titles[0]), '!required' => $required)), 'class' => array('webform-grid-question')));
697

    
698
    // Render each radio button in the row.
699
    if ($question_element['#type'] == 'radios' || $question_element['#type'] == 'checkboxes') {
700
      $radios = form_process_radios($question_element);
701
      foreach (element_children($radios) as $key) {
702
        $radio_title = $radios[$key]['#title'];
703
        if (!strlen($radio_title)) {
704
          $row[] = '&nbsp;';
705
        }
706
        else {
707
          $radios[$key]['#title'] = $question_element['#title'] . ' - ' . $radio_title;
708
          $radios[$key]['#title_display'] = 'invisible';
709
          $row[] = array('data' => drupal_render($radios[$key]), 'class' => array('checkbox', 'webform-grid-option'), 'data-label' => array($radio_title));
710
        }
711
      }
712
    }
713
    else {
714
      $title_element['#title_display'] = 'none';
715
      $row[] = array(
716
        'data' => drupal_render($question_element),
717
        'colspan' => count($element['#grid_options']),
718
      );
719
    }
720
    if ($right_titles) {
721
      $row[] = array('data' => isset($question_titles[1]) ? webform_filter_xss($question_titles[1]) : '', 'class' => array('webform-grid-question'));
722
    }
723

    
724
    // Convert the parents array into a string, excluding the "submitted" wrapper.
725
    $nested_level = $question_element['#parents'][0] == 'submitted' ? 1 : 0;
726
    $parents = str_replace('_', '-', implode('--', array_slice($question_element['#parents'], $nested_level)));
727

    
728
    $rows[] = array(
729
      'data' => $row,
730
      'class' => empty($question_element['#grid_question'])
731
      ? array(
732
        'webform-component',
733
        'webform-component-' . str_replace('_', '-', $question_element['#type']),
734
        'webform-component--' . $parents,
735
      )
736
      : array(),
737
    );
738
  }
739

    
740
  $option_count = count($header) - 1;
741
  return theme('table', array(
742
    'header' => $header,
743
    'rows' => $rows,
744
    'sticky' => $element['#sticky'],
745
    'attributes' => array(
746
      'class' => array(
747
        'webform-grid',
748
        'webform-grid-' . $option_count,
749
      ),
750
    ),
751
  ));
752
}
753

    
754
/**
755
 * Generate a table header suitable for form or html display.
756
 *
757
 * @param array $element
758
 *   The element array.
759
 * @param bool $right_titles
760
 *   If TRUE, display a right-side title column.
761
 *
762
 * @return array
763
 *   An array of headers.
764
 */
765
function _webform_grid_header(array $element, $right_titles) {
766
  $titles = explode('|', $element['#title'], 2);
767
  $title_left = $titles[0];
768
  $header = array(
769
    array(
770
      'data' => _webform_grid_header_title($element, $title_left),
771
      'class' => array('webform-grid-question'),
772
      'scope' => 'col',
773
    ),
774
  );
775
  foreach ($element['#grid_options'] as $option) {
776
    $header[] = array(
777
      'data' => webform_filter_xss($option),
778
      'class' => array('checkbox', 'webform-grid-option'),
779
      'scope' => 'col',
780
    );
781
  }
782
  if ($right_titles) {
783
    $title_right = isset($titles[1]) ? $titles[1] : $title_left;
784
    $header[] = array(
785
      'data' => _webform_grid_header_title($element, $title_right),
786
      'class' => array('webform-grid-question'),
787
      'scope' => 'col',
788
    );
789
  }
790
  return $header;
791
}
792

    
793
/**
794
 * Create internal component title for table header, if any.
795
 */
796
function _webform_grid_header_title($element, $title) {
797
  $header_title = '';
798
  if ($element['#title_display'] == 'internal') {
799
    $header_title = $title;
800
  }
801
  return $header_title;
802
}
803

    
804
/**
805
 * Determine if a right-side title column has been specified.
806
 */
807
function _webform_grid_right_titles($element) {
808
  if ($element['#title_display'] == 'internal' && substr_count($element['#title'], '|')) {
809
    return TRUE;
810
  }
811
  foreach ($element['#grid_questions'] as $question_key => $question) {
812
    if (substr_count($question, '|')) {
813
      return TRUE;
814
    }
815
  }
816
  return FALSE;
817
}
818

    
819
/**
820
 * Create a question header for left, right or left/right question headers.
821
 */
822
function _webform_grid_question_header($text) {
823
  return implode('/', array_filter(explode('|', $text)));
824
}
825

    
826
/**
827
 * Element validation for Webform grid fields.
828
 *
829
 * Requires a component implementation because the default required validation
830
 * passes when at least one value is supplied, rather than every value. This
831
 * makes the server validation match the browser validation.
832
 */
833
function webform_validate_grid($element, $form_state) {
834
  if ($element['#required']) {
835
    $values = $form_state['input'];
836
    foreach ($element['#parents'] as $key) {
837
      $values = isset($values[$key]) ? $values[$key] : $values;
838
    }
839
    // Remove any values that aren't grid question (i.e. nested components).
840
    $grid_questions = $element['#grid_questions'];
841
    $values = array_intersect_key($values, $grid_questions);
842
    // Remove any unanswered grid questions.
843
    $answers = array_filter($values, function ($item) {
844
      return !is_null($item);
845
    });
846
    // Give required errors for any questions that aren't answered.
847
    foreach (array_diff_key($grid_questions, $answers) as $question_key => $question) {
848
      // If the question is still required (e.g not modified by an after_build
849
      // function), give the required error.
850
      if (!empty($element[$question_key]['#required'])) {
851
        form_error($element[$question_key], t('!question field within !name is required.', array('!question' => $question, '!name' => $element['#title'])));
852
      }
853
    }
854
  }
855
}