Projet

Général

Profil

Paste
Télécharger (44,9 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / webform / includes / webform.components.inc @ 024de6ea

1
<?php
2

    
3
/**
4
 * @file
5
 * Webform module component handling.
6
 */
7

    
8
/**
9
 * Provides interface and database handling for editing components of a webform.
10
 *
11
 * @author Nathan Haug <nate@lullabot.com>
12
 */
13

    
14
/**
15
 * Overview page of all components for this webform.
16
 */
17
function webform_components_page($node, $page_number = 1) {
18
  $output = drupal_get_form('webform_components_form', $node);
19

    
20
  return array(
21
    '#theme' => 'webform_components_page',
22
    '#node' => $node,
23
    '#form' => $output,
24
  );
25
}
26

    
27
/**
28
 * Theme the output of the main components page.
29
 *
30
 * This theming provides a way to toggle between the editing modes if Form
31
 * Builder module is available.
32
 */
33
function theme_webform_components_page($variables) {
34
  $node = $variables['node'];
35
  $form = $variables['form'];
36

    
37
  return drupal_render($form);
38
}
39

    
40
/**
41
 * The table-based listing of all components for this webform.
42
 */
43
function webform_components_form($form, $form_state, $node) {
44
  $form = array(
45
    '#tree' => TRUE,
46
    '#node' => $node,
47
    'components' => array(),
48
  );
49

    
50
  $form['nid'] = array(
51
    '#type' => 'value',
52
    '#value' => $node->nid,
53
  );
54

    
55
  $options = array();
56
  foreach ($node->webform['components'] as $cid => $component) {
57
    $options[$cid] = check_plain($component['name']);
58
    $form['components'][$cid]['cid'] = array(
59
      '#type' => 'hidden',
60
      '#default_value' => $component['cid'],
61
    );
62
    $form['components'][$cid]['pid'] = array(
63
      '#type' => 'hidden',
64
      '#default_value' => $component['pid'],
65
    );
66
    $form['components'][$cid]['weight'] = array(
67
      '#type' => 'textfield',
68
      '#size' => 4,
69
      '#title' => t('Weight'),
70
      '#default_value' => $component['weight'],
71
    );
72
    $form['components'][$cid]['required'] = array(
73
      '#type' => 'checkbox',
74
      '#title' => t('Required'),
75
      '#default_value' => $component['required'],
76
      '#access' => webform_component_feature($component['type'], 'required'),
77
    );
78
    if (!isset($max_weight) || $component['weight'] > $max_weight) {
79
      $max_weight = $component['weight'];
80
    }
81
  }
82

    
83
  $form['add']['name'] = array(
84
    '#type' => 'textfield',
85
    '#size' => 30,
86
    '#maxlength' => NULL,
87
  );
88

    
89
  $form['add']['type'] = array(
90
    '#type' => 'select',
91
    '#options' => webform_component_options(),
92
    '#weight' => 3,
93
    '#default_value' => (isset($_GET['cid']) && isset($node->webform['components'][$_GET['cid']])) ? $node->webform['components'][$_GET['cid']]['type'] : 'textfield',
94
  );
95
  $form['add']['required'] = array(
96
    '#type' => 'checkbox',
97
  );
98
  $form['add']['cid'] = array(
99
    '#type' => 'hidden',
100
    '#default_value' => '',
101
  );
102
  $form['add']['pid'] = array(
103
    '#type' => 'hidden',
104
    '#default_value' => (isset($_GET['cid']) && isset($node->webform['components'][$_GET['cid']])) ? $node->webform['components'][$_GET['cid']]['pid'] : 0,
105
  );
106
  $form['add']['weight'] = array(
107
    '#type' => 'textfield',
108
    '#size' => 4,
109
    '#delta' => count($node->webform['components']) > 10 ? count($node->webform['components']) : 10,
110
  );
111

    
112
  if (isset($_GET['cid']) && isset($node->webform['components'][$_GET['cid']])) {
113
    // Make the new component appear by default directly after the one that was
114
    // just added.
115
    $form['add']['weight']['#default_value'] = $node->webform['components'][$_GET['cid']]['weight'] + 1;
116
    foreach (array_keys($node->webform['components']) as $cid) {
117
      // Adjust all later components also, to make sure none of them have the
118
      // same weight as the new component.
119
      if ($form['components'][$cid]['weight']['#default_value'] >= $form['add']['weight']['#default_value']) {
120
        $form['components'][$cid]['weight']['#default_value']++;
121
      }
122
    }
123
  }
124
  else {
125
    // If no component was just added, the new component should appear by
126
    // default at the end of the list.
127
    $form['add']['weight']['#default_value'] = isset($max_weight) ? $max_weight + 1 : 0;
128
  }
129

    
130
  $form['add']['add'] = array(
131
    '#type' => 'submit',
132
    '#value' => t('Add'),
133
    '#weight' => 45,
134
    '#validate' => array('webform_components_form_add_validate', 'webform_components_form_validate'),
135
    '#submit' => array('webform_components_form_add_submit'),
136
  );
137

    
138
  $form['actions'] = array(
139
    '#type' => 'actions',
140
    '#weight' => 45,
141
  );
142
  $form['actions']['submit'] = array(
143
    '#type' => 'submit',
144
    '#value' => t('Save'),
145
    '#access' => count($node->webform['components']) > 0,
146
  );
147
  $form['warning'] = array(
148
    '#weight' => -1,
149
  );
150
  webform_input_vars_check($form, $form_state, 'components', 'warning');
151

    
152
  return $form;
153
}
154

    
155
/**
156
 * Preprocess variables for theming the webform components form.
157
 */
158
function template_preprocess_webform_components_form(&$variables) {
159
  $form = $variables['form'];
160

    
161
  $form['components']['#attached']['library'][] = array('webform', 'admin');
162

    
163
  // TODO: Attach these. See http://drupal.org/node/732022.
164
  drupal_add_tabledrag('webform-components', 'order', 'sibling', 'webform-weight');
165
  drupal_add_tabledrag('webform-components', 'match', 'parent', 'webform-pid', 'webform-pid', 'webform-cid');
166

    
167
  $node = $form['#node'];
168

    
169
  $header = array(t('Label'), t('Form key'), t('Type'), t('Value'), t('Required'), t('Weight'), array('data' => t('Operations'), 'colspan' => 3));
170
  $rows = array();
171

    
172
  // Add a row containing form elements for a new item.
173
  unset($form['add']['name']['#title'], $form['add_type']['#description']);
174
  $form['add']['name']['#attributes']['placeholder'] = t('New component name');
175
  $form['add']['cid']['#attributes']['class'][] = 'webform-cid';
176
  $form['add']['pid']['#attributes']['class'][] = 'webform-pid';
177
  $form['add']['weight']['#attributes']['class'][] = 'webform-weight';
178
  $row_data = array(
179
    array('data' => drupal_render($form['add']['name']), 'class' => array('webform-component-name'), 'colspan' => 2),
180
    array('data' => drupal_render($form['add']['type']), 'class' => array('webform-component-type')),
181
    array('data' => '', 'class' => array('webform-component-value')),
182
    array('data' => drupal_render($form['add']['required']), 'class' => array('webform-component-required', 'checkbox')),
183
    array('data' => drupal_render($form['add']['cid']) . drupal_render($form['add']['pid']) . drupal_render($form['add']['weight'])),
184
    array('colspan' => 3, 'data' => drupal_render($form['add']['add']), 'class' => array('webform-component-add')),
185
  );
186
  $add_form = array('data' => $row_data, 'class' => array('draggable', 'webform-add-form', 'tabledrag-leaf'));
187

    
188
  if (!empty($node->webform['components'])) {
189
    $component_tree = array();
190
    $page_count = 1;
191
    _webform_components_tree_build($node->webform['components'], $component_tree, 0, $page_count);
192
    $component_tree = _webform_components_tree_sort($component_tree);
193
    // Build the table rows recursively.
194
    foreach ($component_tree['children'] as $cid => $component) {
195
      _webform_components_form_rows($node, $cid, $component, 0, $form, $rows, $add_form);
196
    }
197
  }
198
  else {
199
    $rows[] = array(array('data' => t('No Components, add a component below.'), 'colspan' => 9));
200
    // When there are no components, tabledrag.js will look to the add component
201
    // row to locate the weight column. Because of the name/form_key colspan
202
    // it will mis-count the columns and locate the Required column instead of
203
    // the Weight column.
204
    unset($add_form['data'][0]['colspan']);
205
    array_splice($add_form['data'], 1, 0, '&nbsp;');
206
  }
207

    
208
  // Append the add form if not already printed.
209
  if ($add_form) {
210
    $rows[] = $add_form;
211
  }
212

    
213
  $variables['rows'] = $rows;
214
  $variables['header'] = $header;
215
  $variables['form'] = $form;
216
}
217

    
218
/**
219
 * Recursive function for nesting components into a table.
220
 *
221
 * @see preprocess_webform_components_form()
222
 */
223
function _webform_components_form_rows($node, $cid, $component, $level, &$form, &$rows, &$add_form) {
224
  // Create presentable values.
225
  $form_key = truncate_utf8($component['form_key'], 30, FALSE, TRUE);
226
  $value = truncate_utf8($component['value'], 30, TRUE, TRUE, 20);
227

    
228
  // Remove individual titles from the required and weight fields.
229
  unset($form['components'][$cid]['required']['#title']);
230
  unset($form['components'][$cid]['pid']['#title']);
231
  unset($form['components'][$cid]['weight']['#title']);
232

    
233
  // Add special classes for weight and parent fields.
234
  $form['components'][$cid]['cid']['#attributes']['class'] = array('webform-cid');
235
  $form['components'][$cid]['pid']['#attributes']['class'] = array('webform-pid');
236
  $form['components'][$cid]['weight']['#attributes']['class'] = array('webform-weight');
237

    
238
  // Build indentation for this row.
239
  $indents = '';
240
  for ($n = 1; $n <= $level; $n++) {
241
    $indents .= '<div class="indentation">&nbsp;</div>';
242
  }
243

    
244
  // Add each component to a table row.
245
  $row_data = array(
246
    array('data' => $indents . filter_xss($component['name']), 'class' => array('webform-component-name')),
247
    array('data' => check_plain($form_key), 'class' => array('webform-component-formkey')) +
248
          ($component['form_key'] == $form_key ? array() : array('title' => $component['form_key'])),
249
    array('data' => $form['add']['type']['#options'][$component['type']], 'class' => array('webform-component-type')),
250
    array('data' => ($value == '') ? '-' : check_plain($value), 'class' => array('webform-component-value')) +
251
          ($component['value'] == $value ? array() : array('title' => $component['value'])),
252
    array('data' => drupal_render($form['components'][$cid]['required']), 'class' => array('webform-component-required', 'checkbox')),
253
    array('data' => drupal_render($form['components'][$cid]['cid']) . drupal_render($form['components'][$cid]['pid']) . drupal_render($form['components'][$cid]['weight'])),
254
    array('data' => l(t('Edit'), 'node/' . $node->nid . '/webform/components/' . $cid, array('query' => drupal_get_destination())), 'class' => array('webform-component-edit')),
255
    array('data' => l(t('Clone'), 'node/' . $node->nid . '/webform/components/' . $cid . '/clone', array('query' => drupal_get_destination())), 'class' => array('webform-component-clone')),
256
    array('data' => l(t('Delete'), 'node/' . $node->nid . '/webform/components/' . $cid . '/delete', array('query' => drupal_get_destination())), 'class' => array('webform-component-delete')),
257
  );
258
  $row_class = array('draggable');
259
  if (!webform_component_feature($component['type'], 'group')) {
260
    $row_class[] = 'tabledrag-leaf';
261
  }
262
  if ($component['type'] == 'pagebreak') {
263
    $row_class[] = 'tabledrag-root';
264
    $row_class[] = 'webform-pagebreak';
265
    $row_data[0]['class'][] = 'webform-pagebreak';
266
  }
267
  $rows[] = array('data' => $row_data, 'class' => $row_class, 'data-cid' => $cid);
268
  if (isset($component['children']) && is_array($component['children'])) {
269
    foreach ($component['children'] as $cid => $component) {
270
      _webform_components_form_rows($node, $cid, $component, $level + 1, $form, $rows, $add_form);
271
    }
272
  }
273

    
274
  // Add the add form if this was the last edited component.
275
  if (isset($_GET['cid']) && $component['cid'] == $_GET['cid'] && $add_form) {
276
    $add_form['data'][0]['data'] = $indents . $add_form['data'][0]['data'];
277
    $rows[] = $add_form;
278
    $add_form = FALSE;
279
  }
280
}
281

    
282
/**
283
 * Theme the node components form. Use a table to organize the components.
284
 *
285
 * @return
286
 *   Formatted HTML form, ready for display.
287
 */
288
function theme_webform_components_form($variables) {
289
  $output = '';
290
  $output .= drupal_render_children($variables['form']['warning']);
291
  $output .= theme('table', array('header' => $variables['header'], 'rows' => $variables['rows'], 'attributes' => array('id' => 'webform-components')));
292
  $output .= drupal_render_children($variables['form']);
293
  return $output;
294
}
295

    
296
/**
297
 * Validate handler for webform_components_form().
298
 */
299
function webform_components_form_validate($form, &$form_state) {
300
  // Check that no two components end up with the same form key.
301
  $duplicates = array();
302
  $parents = array();
303
  if (isset($form_state['values']['components'])) {
304
    foreach ($form_state['values']['components'] as $cid => $component) {
305
      $form_key = $form['#node']->webform['components'][$cid]['form_key'];
306
      if (isset($parents[$component['pid']]) && ($existing = array_search($form_key, $parents[$component['pid']])) && $existing !== FALSE) {
307
        if (!isset($duplicates[$form_key])) {
308
          $duplicates[$form_key] = array($existing);
309
        }
310
        $duplicates[$form_key][] = $cid;
311
      }
312
      $parents[$component['pid']][$cid] = $form_key;
313
    }
314
  }
315

    
316
  if (!empty($duplicates)) {
317
    $error = t('The form order failed to save because the following elements have same form keys and are under the same parent. Edit each component and give them a unique form key, then try moving them again.');
318
    $items = array();
319
    foreach ($duplicates as $form_key => $cids) {
320
      foreach ($cids as $cid) {
321
        $items[] = webform_filter_xss($form['#node']->webform['components'][$cid]['name']);
322
      }
323
    }
324

    
325
    form_error($form['components'], $error . theme('item_list', array('items' => $items)));
326
  }
327
}
328

    
329
/**
330
 * Validate handler for webform_component_form() when adding a new component.
331
 */
332
function webform_components_form_add_validate($form, &$form_state) {
333
  // Check that the entered component name is valid.
334
  if (drupal_strlen(trim($form_state['values']['add']['name'])) <= 0) {
335
    form_error($form['add']['name'], t('When adding a new component, the name field is required.'));
336
  }
337
}
338

    
339
/**
340
 * Submit handler for webform_components_form() to save component order.
341
 */
342
function webform_components_form_submit($form, &$form_state) {
343
  $node = node_load($form_state['values']['nid']);
344

    
345
  // Update all required and weight values.
346
  $changes = FALSE;
347
  foreach ($node->webform['components'] as $cid => $component) {
348
    if ($component['pid'] != $form_state['values']['components'][$cid]['pid'] || $component['weight'] != $form_state['values']['components'][$cid]['weight'] || $component['required'] != $form_state['values']['components'][$cid]['required']) {
349
      $changes = TRUE;
350
      $node->webform['components'][$cid]['weight'] = $form_state['values']['components'][$cid]['weight'];
351
      $node->webform['components'][$cid]['required'] = $form_state['values']['components'][$cid]['required'];
352
      $node->webform['components'][$cid]['pid'] = $form_state['values']['components'][$cid]['pid'];
353
    }
354
  }
355

    
356
  if ($changes) {
357
    node_save($node);
358
  }
359

    
360
  drupal_set_message(t('The component positions and required values have been updated.'));
361
}
362

    
363
/**
364
 * Submit handler for webform_components_form() that adds a new component.
365
 */
366
function webform_components_form_add_submit($form, &$form_state) {
367
  $node = node_load($form_state['values']['nid']);
368

    
369
  $component = $form_state['values']['add'];
370

    
371
  // Set the values in the query string for the add component page.
372
  $query = array(
373
    'name' => $component['name'],
374
    'required' => $component['required'],
375
    'pid' => $component['pid'],
376
    'weight' => $component['weight'],
377
  );
378

    
379
  // Forward the "destination" query string value to the next form.
380
  if (isset($_GET['destination'])) {
381
    $query['destination'] = $_GET['destination'];
382
    unset($_GET['destination']);
383
    drupal_static_reset('drupal_get_destination');
384
  }
385
  $form_state['redirect'] = array('node/' . $node->nid . '/webform/components/new/' . $component['type'], array('query' => $query));
386
}
387

    
388
/**
389
 * Form to configure a webform component.
390
 */
391
function webform_component_edit_form($form, $form_state, $node, $component, $clone = FALSE) {
392
  drupal_set_title(t('Edit component: @name', array('@name' => $component['name'])), PASS_THROUGH);
393

    
394
  $form['#node'] = $node;
395
  $form['#tree'] = TRUE;
396

    
397
  // Print the correct field type specification.
398
  // We always need: name and description.
399
  $form['type'] = array(
400
    '#type' => 'value',
401
    '#value' => $component['type'],
402
  );
403
  $form['nid'] = array(
404
    '#type' => 'value',
405
    '#value' => $node->nid,
406
  );
407
  $form['cid'] = array(
408
    '#type' => 'value',
409
    '#value' => isset($component['cid']) ? $component['cid'] : NULL,
410
  );
411
  $form['clone'] = array(
412
    '#type' => 'value',
413
    '#value' => $clone,
414
  );
415

    
416
  if (webform_component_feature($component['type'], 'title')) {
417
    $form['name'] = array(
418
      '#type' => 'textfield',
419
      '#default_value' => $component['name'],
420
      '#title' => t('Label'),
421
      '#description' => t('This is used as a descriptive label when displaying this form element.'),
422
      '#required' => TRUE,
423
      '#weight' => -10,
424
      '#maxlength' => NULL,
425
    );
426
  }
427

    
428
  $form['form_key'] = array(
429
    '#type' => 'textfield',
430
    '#default_value' => empty($component['form_key']) ? _webform_safe_name($component['name']) : $component['form_key'],
431
    '#title' => t('Form Key'),
432
    '#description' => t('Enter a machine readable key for this form element. May contain only alphanumeric characters and underscores. This key will be used as the name attribute of the form element. This value has no effect on the way data is saved, but may be helpful if doing custom form processing.'),
433
    '#required' => TRUE,
434
    '#weight' => -9,
435
  );
436

    
437
  $form['extra'] = array();
438
  if (webform_component_feature($component['type'], 'description')) {
439
    $form['extra']['description'] = array(
440
      '#type' => 'textarea',
441
      '#default_value' => isset($component['extra']['description']) ? $component['extra']['description'] : '',
442
      '#title' => t('Description'),
443
      '#description' => t('A short description of the field used as help for the user when he/she uses the form.') . ' ' . theme('webform_token_help'),
444
      '#weight' => -1,
445
    );
446
  }
447

    
448
  // Display settings.
449
  $form['display'] = array(
450
    '#type' => 'fieldset',
451
    '#title' => t('Display'),
452
    '#collapsible' => TRUE,
453
    '#collapsed' => FALSE,
454
    '#weight' => 8,
455
  );
456
  if (webform_component_feature($component['type'], 'title_display')) {
457
    if (webform_component_feature($component['type'], 'title_inline')) {
458
      $form['display']['title_display'] = array(
459
        '#type' => 'select',
460
        '#title' => t('Label display'),
461
        '#default_value' => !empty($component['extra']['title_display']) ? $component['extra']['title_display'] : 'before',
462
        '#options' => array(
463
          'before' => t('Above'),
464
          'inline' => t('Inline'),
465
          'none' => t('None'),
466
        ),
467
        '#description' => t('Determines the placement of the component\'s label.'),
468
      );
469
    }
470
    else {
471
      $form['display']['title_display'] = array(
472
        '#type' => 'checkbox',
473
        '#title' => t('Hide label'),
474
        '#default_value' => strcmp($component['extra']['title_display'], 'none') === 0,
475
        '#return_value' => 'none',
476
        '#description' => t('Do not display the label of this component.'),
477
      );
478
    }
479
    $form['display']['title_display']['#weight'] = 8;
480
    $form['display']['title_display']['#parents'] = array('extra', 'title_display');
481
  }
482

    
483
  if (webform_component_feature($component['type'], 'description')) {
484
    $form['display']['description_above'] = array(
485
      '#type' => 'checkbox',
486
      '#default_value' => !empty($component['extra']['description_above']),
487
      '#title' => t('Description above field'),
488
      '#description' => t('Place the description above &mdash; rather than below &mdash; the field.'),
489
      '#weight' => 8.5,
490
      '#parents' => array('extra', 'description_above'),
491
    );
492
  }
493

    
494
  if (webform_component_feature($component['type'], 'private')) {
495
    // May user mark fields as Private?
496
    $form['display']['private'] = array(
497
      '#type' => 'checkbox',
498
      '#title' => t('Private'),
499
      '#default_value' => ($component['extra']['private'] == '1' ? TRUE : FALSE),
500
      '#description' => t('Private fields are shown only to users with results access.'),
501
      '#weight' => 45,
502
      '#parents' => array('extra', 'private'),
503
      '#disabled' => empty($node->nid) ? FALSE : !webform_results_access($node),
504
    );
505
  }
506

    
507
  if (webform_component_feature($component['type'], 'wrapper_classes')) {
508
    $form['display']['wrapper_classes'] = array(
509
      '#type' => 'textfield',
510
      '#title' => t('Wrapper CSS classes'),
511
      '#default_value' => isset($component['extra']['wrapper_classes']) ? $component['extra']['wrapper_classes'] : '',
512
      '#description' => t('Apply a class to the wrapper around both the field and its label. Separate multiple by spaces.'),
513
      '#weight' => 50,
514
      '#parents' => array('extra', 'wrapper_classes'),
515
    );
516
  }
517
  if (webform_component_feature($component['type'], 'css_classes')) {
518
    $form['display']['css_classes'] = array(
519
      '#type' => 'textfield',
520
      '#title' => t('CSS classes'),
521
      '#default_value' => isset($component['extra']['css_classes']) ? $component['extra']['css_classes'] : '',
522
      '#description' => t('Apply a class to the field. Separate multiple by spaces.'),
523
      '#weight' => 51,
524
      '#parents' => array('extra', 'css_classes'),
525
    );
526
  }
527

    
528
  // Validation settings.
529
  $form['validation'] = array(
530
    '#type' => 'fieldset',
531
    '#title' => t('Validation'),
532
    '#collapsible' => TRUE,
533
    '#collapsed' => FALSE,
534
    '#weight' => 5,
535
  );
536
  if (webform_component_feature($component['type'], 'required')) {
537
    $form['validation']['required'] = array(
538
      '#type' => 'checkbox',
539
      '#title' => t('Required'),
540
      '#default_value' => ($component['required'] == '1' ? TRUE : FALSE),
541
      '#description' => t('Check this option if the user must enter a value.'),
542
      '#weight' => -1,
543
      '#parents' => array('required'),
544
    );
545
  }
546

    
547
  // Position settings, only shown if JavaScript is disabled.
548
  $form['position'] = array(
549
    '#type' => 'fieldset',
550
    '#title' => t('Position'),
551
    '#collapsible' => TRUE,
552
    '#collapsed' => TRUE,
553
    '#tree' => FALSE,
554
    '#weight' => 20,
555
    '#attributes' => array('class' => array('webform-position')),
556
  );
557

    
558
  $options = array('0' => t('Root'));
559
  foreach ($node->webform['components'] as $existing_cid => $value) {
560
    if (webform_component_feature($value['type'], 'group') && (!isset($component['cid']) || $existing_cid != $component['cid'])) {
561
      $options[$existing_cid] = $value['name'];
562
    }
563
  }
564
  $form['position']['pid'] = array(
565
    '#type' => 'select',
566
    '#title' => t('Parent'),
567
    '#default_value' => $component['pid'],
568
    '#description' => t('Optional. You may organize your form by placing this component inside another fieldset.'),
569
    '#options' => $options,
570
    '#access' => count($options) > 1,
571
    '#weight' => 3,
572
  );
573
  $form['position']['weight'] = array(
574
    '#type' => 'textfield',
575
    '#size' => 4,
576
    '#title' => t('Weight'),
577
    '#default_value' => $component['weight'],
578
    '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
579
    '#weight' => 4,
580
  );
581

    
582
  // Add the fields specific to this component type:
583
  $additional_form_elements = (array) webform_component_invoke($component['type'], 'edit', $component);
584
  if (empty($additional_form_elements)) {
585
    drupal_set_message(t('The webform component of type @type does not have an edit function defined.', array('@type' => $component['type'])));
586
  }
587

    
588
  // Merge the additional fields with the current fields:
589
  if (isset($additional_form_elements['extra'])) {
590
    $form['extra'] = array_merge($form['extra'], $additional_form_elements['extra']);
591
    unset($additional_form_elements['extra']);
592
  }
593
  if (isset($additional_form_elements['position'])) {
594
    $form['position'] = array_merge($form['position'], $additional_form_elements['position']);
595
    unset($additional_form_elements['position']);
596
  }
597
  if (isset($additional_form_elements['display'])) {
598
    $form['display'] = array_merge($form['display'], $additional_form_elements['display']);
599
    unset($additional_form_elements['display']);
600
  }
601
  if (isset($additional_form_elements['validation'])) {
602
    $form['validation'] = array_merge($form['validation'], $additional_form_elements['validation']);
603
    unset($additional_form_elements['validation']);
604
  }
605
  elseif (count(element_children($form['validation'])) == 0) {
606
    unset($form['validation']);
607
  }
608
  $form = array_merge($form, $additional_form_elements);
609
  // Ensure that the webform admin library is attached, possibly in addition to
610
  // component-specific attachments.
611
  $form['#attached']['library'][] = array('webform', 'admin');
612

    
613
  // Add the submit button.
614
  $form['actions'] = array(
615
    '#type' => 'actions',
616
    '#weight' => 50,
617
  );
618
  $form['actions']['submit'] = array(
619
    '#type' => 'submit',
620
    '#value' => t('Save component'),
621
  );
622

    
623
  // Remove fieldsets without any child form controls.
624
  foreach (element_children($form) as $group_key) {
625
    $group = $form[$group_key];
626
    if (isset($group['#type']) && $group['#type'] === 'fieldset' && !element_children($group)) {
627
      unset($form[$group_key]);
628
    }
629
  }
630

    
631
  return $form;
632
}
633

    
634
/**
635
 * Field name validation for the webform unique key. Must be alphanumeric.
636
 */
637
function webform_component_edit_form_validate($form, &$form_state) {
638
  $node = $form['#node'];;
639

    
640
  if (!preg_match('/^[a-z0-9_]+$/i', $form_state['values']['form_key'])) {
641
    form_set_error('form_key', t('The form key %form_key is invalid. Please include only lowercase alphanumeric characters and underscores.', array('%form_key' => $form_state['values']['form_key'])));
642
  }
643

    
644
  foreach ($node->webform['components'] as $cid => $component) {
645
    if (($component['cid'] != $form_state['values']['cid'] || $form_state['values']['clone']) && ($component['pid'] == $form_state['values']['pid']) && (strcasecmp($component['form_key'], $form_state['values']['form_key']) == 0)) {
646
      form_set_error('form_key', t('The form key %form_key is already in use by the field labeled %existing_field. Please use a unique key.', array('%form_key' => $form_state['values']['form_key'], '%existing_field' => $component['name'])));
647
    }
648
  }
649
}
650

    
651
/**
652
 * Submit handler for webform_component_edit_form().
653
 */
654
function webform_component_edit_form_submit($form, &$form_state) {
655
  // Ensure a webform record exists.
656
  $node = $form['#node'];
657
  webform_ensure_record($node);
658

    
659
  // Remove extra values that match the default.
660
  if (isset($form_state['values']['extra'])) {
661
    $default = array('type' => $form_state['values']['type'], 'extra' => array());
662
    webform_component_defaults($default);
663
    foreach ($form_state['values']['extra'] as $key => $value) {
664
      if (isset($default['extra'][$key]) && $default['extra'][$key] === $value) {
665
        unset($form_state['values']['extra'][$key]);
666
      }
667
    }
668
  }
669

    
670
  // Remove empty attribute values.
671
  if (isset($form_state['values']['extra']['attributes'])) {
672
    foreach ($form_state['values']['extra']['attributes'] as $key => $value) {
673
      if ($value === '') {
674
        unset($form_state['values']['extra']['attributes'][$key]);
675
      }
676
    }
677
  }
678

    
679
  if ($form_state['values']['clone']) {
680
    webform_component_clone($node, $form_state['values']);
681
    drupal_set_message(t('Component %name cloned.', array('%name' => $form_state['values']['name'])));
682
  }
683
  elseif (!empty($form_state['values']['cid'])) {
684
    webform_component_update($form_state['values']);
685
    drupal_set_message(t('Component %name updated.', array('%name' => $form_state['values']['name'])));
686
  }
687
  else {
688
    $cid = webform_component_insert($form_state['values']);
689
    drupal_set_message(t('New component %name added.', array('%name' => $form_state['values']['name'])));
690
  }
691

    
692
  // Since Webform components have been updated but the node itself has not
693
  // been saved, it is necessary to explicitly clear the cache to make sure
694
  // the updated webform is visible to anonymous users. This resets the page
695
  // and block caches (only);
696
  cache_clear_all();
697

    
698
  // Refresh the entity cache, should it be cached in persistent storage.
699
  entity_get_controller('node')->resetCache(array($node->nid));
700

    
701
  $form_state['redirect'] = array('node/' . $node->nid . '/webform/components', isset($cid) ? array('query' => array('cid' => $cid)) : array());
702
}
703

    
704
/**
705
 * Form to confirm deletion of a component.
706
 */
707
function webform_component_delete_form($form, $form_state, $node, $component) {
708
  $cid = $component['cid'];
709

    
710
  $form = array();
711
  $form['node'] = array(
712
    '#type' => 'value',
713
    '#value' => $node,
714
  );
715
  $form['component'] = array(
716
    '#type' => 'value',
717
    '#value' => $component,
718
  );
719

    
720
  $component_type = $node->webform['components'][$cid]['type'];
721
  if (webform_component_feature($component_type, 'group')) {
722
    $question = t('Delete the %name fieldset?', array('%name' => $node->webform['components'][$cid]['name']));
723
    $description = t('This will immediately delete the %name @type component and all nested components within %name from the %webform webform. This cannot be undone.',
724
                     array('%name' => $node->webform['components'][$cid]['name'],
725
                           '@type' => webform_component_property($component_type, 'label'),
726
                           '%webform' => $node->title));
727
  }
728
  else {
729
    $question = t('Delete the %name component?', array('%name' => $node->webform['components'][$cid]['name']));
730
    $description = t('This will immediately delete the %name component from the %webform webform. This cannot be undone.', array('%name' => $node->webform['components'][$cid]['name'], '%webform' => $node->title));
731
  }
732

    
733
  return confirm_form($form, $question, 'node/' . $node->nid . '/webform/components', $description, t('Delete'));
734
}
735

    
736
/**
737
 * Submit handler for webform_component_delete_form().
738
 */
739
function webform_component_delete_form_submit($form, &$form_state) {
740
  // Delete the component.
741
  $node = $form_state['values']['node'];
742
  $component = $form_state['values']['component'];
743
  webform_component_delete($node, $component);
744
  drupal_set_message(t('Component %name deleted.', array('%name' => $component['name'])));
745

    
746
  // Check if this webform still contains any information.
747
  unset($node->webform['components'][$component['cid']]);
748
  webform_check_record($node);
749

    
750
  // Since Webform components have been updated but the node itself has not
751
  // been saved, it is necessary to explicitly clear the cache to make sure
752
  // the updated webform is visible to anonymous users. This resets the page
753
  // and block caches (only);
754
  cache_clear_all();
755

    
756
  // Refresh the entity cache, should it be cached in persistent storage.
757
  entity_get_controller('node')->resetCache(array($node->nid));
758

    
759
  $form_state['redirect'] = 'node/' . $node->nid . '/webform/components';
760
}
761

    
762
/**
763
 * Insert a new component into the database.
764
 *
765
 * @param $component
766
 *   A full component containing fields from the component form.
767
 */
768
function webform_component_insert(&$component) {
769
  // Allow modules to modify the component before saving.
770
  foreach (module_implements('webform_component_presave') as $module) {
771
    $function = $module . '_webform_component_presave';
772
    $function($component);
773
  }
774

    
775
  $component['value'] = isset($component['value']) ? $component['value'] : NULL;
776
  $component['required'] = isset($component['required']) ? $component['required'] : 0;
777
  $component['extra']['private'] = isset($component['extra']['private']) ? $component['extra']['private'] : 0;
778

    
779
  if (!isset($component['cid'])) {
780
    if (lock_acquire('webform_component_insert_' . $component['nid'], 5)) {
781
      $next_id_query = db_select('webform_component')->condition('nid', $component['nid']);
782
      $next_id_query->addExpression('MAX(cid) + 1', 'cid');
783
      $component['cid'] = $next_id_query->execute()->fetchField();
784
      if ($component['cid'] == NULL) {
785
        $component['cid'] = 1;
786
      }
787
      lock_release('webform_component_insert_' . $component['nid']);
788
    }
789
    else {
790
      watchdog('webform', 'A Webform component could not be saved because a timeout occurred while trying to acquire a lock for the node. Details: <pre>@component</pre>', array('@component' => print_r($component, TRUE)));
791
      return FALSE;
792
    }
793
  }
794

    
795
  $query = db_insert('webform_component')
796
    ->fields(array(
797
      'nid' => $component['nid'],
798
      'cid' => $component['cid'],
799
      'pid' => $component['pid'],
800
      'form_key' => $component['form_key'],
801
      'name' => $component['name'],
802
      'type' => $component['type'],
803
      'value' => (string) $component['value'],
804
      'extra' => serialize($component['extra']),
805
      'required' => $component['required'],
806
      'weight' => $component['weight'],
807
    ))
808
    ->execute();
809

    
810
  // Post-insert actions.
811
  module_invoke_all('webform_component_insert', $component);
812

    
813
  return $component['cid'];
814
}
815

    
816
/**
817
 * Update an existing component with new values.
818
 *
819
 * @param $component
820
 *   A full component containing a nid, cid, and all other fields from the
821
 *   component form. Additional properties are stored in the extra array.
822
 */
823
function webform_component_update($component) {
824
  // Allow modules to modify the component before saving.
825
  foreach (module_implements('webform_component_presave') as $module) {
826
    $function = $module . '_webform_component_presave';
827
    $function($component);
828
  }
829

    
830
  $component['value'] = isset($component['value']) ? $component['value'] : NULL;
831
  $component['required'] = isset($component['required']) ? $component['required'] : 0;
832
  $component['extra']['private'] = isset($component['extra']['private']) ? $component['extra']['private'] : 0;
833
  db_update('webform_component')
834
    ->fields(array(
835
      'pid' => $component['pid'],
836
      'form_key' => $component['form_key'],
837
      'name' => $component['name'],
838
      'type' => $component['type'],
839
      'value' => isset($component['value']) ? $component['value'] : '',
840
      'extra' => serialize($component['extra']),
841
      'required' => $component['required'],
842
      'weight' => $component['weight']
843
    ))
844
    ->condition('nid', $component['nid'])
845
    ->condition('cid', $component['cid'])
846
    ->execute();
847

    
848
  // Post-update actions.
849
  module_invoke_all('webform_component_update', $component);
850
}
851

    
852
function webform_component_delete($node, $component) {
853
  // Check if a delete function is available for this component. If so,
854
  // load all submissions and allow the component to delete each one.
855
  webform_component_include($component['type']);
856
  $delete_function = '_webform_delete_' . $component['type'];
857
  if (function_exists($delete_function)) {
858
    module_load_include('inc', 'webform', 'includes/webform.submissions');
859
    $submissions = webform_get_submissions($node->nid);
860
    foreach ($submissions as $submission) {
861
      if (isset($submission->data[$component['cid']])) {
862
        webform_component_invoke($component['type'], 'delete', $component, $submission->data[$component['cid']]);
863
      }
864
    }
865
  }
866

    
867
  // Remove database entries.
868
  db_delete('webform_component')
869
    ->condition('nid', $node->nid)
870
    ->condition('cid', $component['cid'])
871
    ->execute();
872
  db_delete('webform_submitted_data')
873
    ->condition('nid', $node->nid)
874
    ->condition('cid', $component['cid'])
875
    ->execute();
876

    
877
  // Delete any conditionals dependent on this component.
878
  module_load_include('inc', 'webform', 'includes/webform.conditionals');
879
  foreach ($node->webform['conditionals'] as $rgid => &$conditional) {
880
    $specs = array(
881
      array(
882
        'field' => 'rules',
883
        'table' => 'webform_conditional_rules',
884
        'component' => 'source',
885
        'component_type' => 'source_type',
886
        'index' => 'rid',
887
      ),
888
      array(
889
        'field' => 'actions',
890
        'table' => 'webform_conditional_actions',
891
        'component' => 'target',
892
        'component_type' => 'target_type',
893
        'index' => 'aid',
894
      ),
895
    );
896
    foreach ($specs as $spec) {
897
      $deleted = array();
898
      $field = $spec['field'];
899
      foreach ($conditional[$field] as $key => $thing) {
900
        if ($thing[$spec['component_type']] === 'component' && $thing[$spec['component']] == $component['cid']) {
901
          $deleted[$key] = $key;
902
          unset($conditional[$field][$key]);
903
        }
904
      }
905
      if ($spec['field'] == 'rules') {
906
        // Rules deleted because of the source component being deleted may have left
907
        // empty sub-conditionals. Delete them, and then the entire rule group if
908
        // there aren't any rules left.
909
        $deleted += webform_delete_empty_subconditionals($conditional);
910
      }
911
      // Delete the conditional if this component is the only source / target.
912
      if (empty($conditional[$field])) {
913
        webform_conditional_delete($node, $conditional);
914
        break; // Loop exit.
915
      }
916
      // Remove the deleted rules / actions from the database.
917
      foreach ($deleted as $key) {
918
        db_delete($spec['table'])
919
          ->condition('nid', $node->nid)
920
          ->condition('rgid', $rgid)
921
          ->condition($spec['index'], $key)
922
          ->execute();
923
      }
924
    }
925
  }
926

    
927
  // Delete all elements under this element.
928
  $result = db_select('webform_component', 'c')
929
    ->fields('c')
930
    ->condition('nid', $node->nid)
931
    ->condition('pid', $component['cid'])
932
    ->execute();
933
  foreach ($result as $row) {
934
    $child_component = $node->webform['components'][$row->cid];
935
    webform_component_delete($node, $child_component);
936
  }
937

    
938
  // Post-delete actions.
939
  module_invoke_all('webform_component_delete', $component);
940
}
941

    
942
/**
943
 * Recursively insert components into the database.
944
 *
945
 * @param $node
946
 *   The node object containing the current webform.
947
 * @param $component
948
 *   A full component containing fields from the component form.
949
 */
950
function webform_component_clone(&$node, &$component) {
951
  $original_cid = $component['cid'];
952
  $component['cid'] = NULL;
953
  $new_cid = webform_component_insert($component);
954
  $component['cid'] = $new_cid;
955
  if (webform_component_feature($component['type'], 'group')) {
956
    foreach ($node->webform['components'] as $cid => $child_component) {
957
      if ($child_component['pid'] == $original_cid) {
958
        $child_component['pid'] = $new_cid;
959
        webform_component_clone($node, $child_component);
960
      }
961
    }
962
  }
963
  return $new_cid;
964
}
965

    
966
/**
967
 * Check if a component has a particular feature.
968
 *
969
 * @see hook_webform_component_info()
970
 */
971
function webform_component_feature($type, $feature) {
972
  $components = webform_components();
973
  $defaults = array(
974
    'analysis' => TRUE,
975
    'csv' => TRUE,
976
    'default_value' => TRUE,
977
    'description' => TRUE,
978
    'email' => TRUE,
979
    'email_address' => FALSE,
980
    'email_name' => FALSE,
981
    'required' => TRUE,
982
    'title' => TRUE,
983
    'title_display' => TRUE,
984
    'title_inline' => TRUE,
985
    'conditional' => TRUE,
986
    'conditional_action_set' => FALSE,
987
    'spam_analysis' => FALSE,
988
    'group' => FALSE,
989
    'attachment' => FALSE,
990
    'private' => TRUE,
991
    'placeholder' => FALSE,
992
    'wrapper_classes' => TRUE,
993
    'css_classes' => TRUE,
994
    'views_range' => FALSE,
995
  );
996
  return isset($components[$type]['features'][$feature]) ? $components[$type]['features'][$feature] : !empty($defaults[$feature]);
997
}
998

    
999
/**
1000
 * Get a component property from the component definition.
1001
 *
1002
 * @see hook_webform_component_info()
1003
 */
1004
function webform_component_property($type, $property) {
1005
  $components = webform_components();
1006
  $defaults = array(
1007
    'conditional_type' => 'string',
1008
  );
1009
  return isset($components[$type][$property]) ? $components[$type][$property] : $defaults[$property];
1010
}
1011

    
1012
/**
1013
 * Create a list of components suitable for a select list.
1014
 *
1015
 * @param $node
1016
 *   The webform node.
1017
 * @param $component_filter
1018
 *   Either an array of components, or a string containing a feature name (csv,
1019
 *   email, required, conditional) on which this list of components will be
1020
 *   restricted.
1021
 * @param $prefix_group
1022
 *   TRUE to indent with a hyphen, or 'path" to Prepend enclosing group (e.g.
1023
 *   fieldset) name(s)
1024
 * @param $pagebreak_groups
1025
 *   Determine if pagebreaks should be converted to option groups in the
1026
 *   returned list of options.
1027
 */
1028
function webform_component_list($node, $component_filter = NULL, $prepend_group = TRUE, $pagebreak_groups = FALSE) {
1029
  $options = array();
1030
  $page_names = array();
1031
  $parent_names = array();
1032

    
1033
  $components = is_array($component_filter) ? $component_filter : $node->webform['components'];
1034
  $feature = is_string($component_filter) ? $component_filter : NULL;
1035

    
1036
  foreach ($components as $cid => $component) {
1037
    // If this component is a group (e.g. fieldset), then remember its name, including any parents.
1038
    if ($prepend_group && webform_component_feature($component['type'], 'group')) {
1039
      $parent_names[$cid] = ($component['pid'] ? $parent_names[$component['pid']] : '') .
1040
                            ($prepend_group === 'path' ? $component['name'] . ': ' : '-');
1041
    }
1042
    $page_num = $component['page_num'];
1043
    // If this component is a pagebreak, then generate an option group, ensuring a unique name.
1044
    if ($pagebreak_groups && $component['type'] == 'pagebreak') {
1045
      $page_name = $component['name'];
1046
      $copy = 1;
1047
      while (in_array($page_name, $page_names)) {
1048
        $page_name = $component['name'] . '_' . ++$copy;
1049
      }
1050
      $page_names[$page_num] = $page_name;
1051
    }
1052
    // If this component should be included in the options, add it with any prefix, in a page group, as needed.
1053
    if (!isset($feature) || webform_component_feature($component['type'], $feature) || $prepend_group === TRUE) {
1054
      $prefix = ($prepend_group && $component['pid']) ? $parent_names[$component['pid']] : '';
1055
      if ($pagebreak_groups && $page_num > 1) {
1056
        $options[$page_names[$page_num]][$cid] = $prefix . $component['name'];
1057
      }
1058
      else {
1059
        $options[$cid] = $prefix . $component['name'];
1060
      }
1061
    }
1062
  }
1063

    
1064
  return $options;
1065
}
1066

    
1067
/**
1068
 * A Form API process function to expand a component list into checkboxes.
1069
 */
1070
function webform_component_select($element) {
1071
  // Split the select list into checkboxes.
1072
  foreach ($element['#options'] as $key => $label) {
1073
    $label_length = strlen($label);
1074
    $label = preg_replace('/^(\-)+/', '', $label);
1075
    $indents = $label_length - strlen($label);
1076
    $element[$key] = array(
1077
      '#title' => $label,
1078
      '#type' => 'checkbox',
1079
      '#default_value' => array_search($key, $element['#value']) !== FALSE,
1080
      '#return_value' => $key,
1081
      '#parents' => array_merge($element['#parents'], array($key)),
1082
      '#indent' => $indents,
1083
    );
1084
  }
1085

    
1086
  $element['#theme_wrappers'] = array();
1087
  $element['#type'] = 'webform_component_select';
1088
  $element['#theme'] = 'webform_component_select';
1089
  $element['#attached'] = array(
1090
    'library' => array(
1091
      array('webform', 'admin'),
1092
      array('system', 'drupal.collapse'),
1093
    ),
1094
    'js' => array(
1095
      'misc/tableselect.js' => array(),
1096
    ),
1097
  );
1098

    
1099
  return $element;
1100
}
1101

    
1102
/**
1103
 * Theme the contents of a Webform component select element.
1104
 */
1105
function theme_webform_component_select($variables) {
1106
  $element = $variables['element'];
1107

    
1108
  $rows = array();
1109
  $header = array();
1110
  if (!isset($element['#all_checkbox']) || $element['#all_checkbox']) {
1111
    $header = array(array('class' => array('select-all'), 'data' => ' ' . t('Include all components')));
1112
  }
1113
  foreach (element_children($element) as $key) {
1114
    if ($key != 'suffix') {
1115
      $rows[] = array(
1116
        theme('indentation', array('size' => $element[$key]['#indent'])) . drupal_render($element[$key]),
1117
      );
1118
    }
1119
  }
1120

    
1121
  $element['#type'] = 'fieldset';
1122
  $element['#value'] = NULL;
1123
  $element['#attributes']['class'] = array('webform-component-select-table');
1124
  if (!isset($element['#collapsible']) || $element['#collapsible']) {
1125
    $element['#attributes']['class'][] = 'collapsible';
1126
  }
1127
  if (!isset($element['#collapsed']) || $element['#collapsed']) {
1128
    $element['#attributes']['class'][] = 'collapsed';
1129
  }
1130

    
1131
  if (empty($rows)) {
1132
    $element['#children'] = t('No available components.');
1133
  }
1134
  else {
1135
    $element['#children'] = '<div class="webform-component-select-wrapper">' . theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => FALSE)) . '</div>';
1136
  }
1137

    
1138
  if (isset($element['suffix'])) {
1139
    $element['#children'] .= '<div class="webform-component-select-suffix">' . drupal_render($element['suffix']) . '</div>';
1140
  }
1141

    
1142

    
1143
  return theme('fieldset', array('element' => $element));
1144
}
1145

    
1146
/**
1147
 * Find a components parents within a node.
1148
 */
1149
function webform_component_parent_keys($node, $component) {
1150
  $parents = array($component['form_key']);
1151
  $pid = $component['pid'];
1152
  while ($pid) {
1153
    $parents[] = $node->webform['components'][$pid]['form_key'];
1154
    $pid = $node->webform['components'][$pid]['pid'];
1155
  }
1156
  return array_reverse($parents);
1157
}
1158

    
1159
/**
1160
 * Populate a component with the defaults for that type.
1161
 */
1162
function webform_component_defaults(&$component) {
1163
  $defaults = webform_component_invoke($component['type'], 'defaults');
1164
  drupal_alter('webform_component_defaults', $defaults, $component['type']);
1165
  if (!empty($defaults)) {
1166
    foreach ($defaults as $key => $default) {
1167
      if (!isset($component[$key])) {
1168
        $component[$key] = $default;
1169
      }
1170
    }
1171
    foreach ($defaults['extra'] as $extra => $default) {
1172
      if (!isset($component['extra'][$extra])) {
1173
        $component['extra'][$extra] = $default;
1174
      }
1175
    }
1176
  }
1177
}
1178

    
1179
/**
1180
 * Validate an element value is unique with no duplicates in the database.
1181
 */
1182
function webform_validate_unique($element, $form_state) {
1183
  if ($element['#value'] !== '') {
1184
    $nid = $form_state['values']['details']['nid'];
1185
    $sid = $form_state['values']['details']['sid'];
1186
    $query = db_select('webform_submitted_data')
1187
      ->fields('webform_submitted_data', array('sid'))
1188
      ->condition('nid', $nid)
1189
      ->condition('cid', $element['#webform_component']['cid'])
1190
      ->condition('data', $element['#value'])
1191
      ->range(0, 1); // More efficient than using countQuery() for data checks.
1192
    if ($sid) {
1193
      $query->condition('sid', $sid, '<>');
1194
    }
1195
    $count = $query->execute()->fetchField();
1196
    if ($count) {
1197
      form_error($element, t('The value %value has already been submitted once for the %title field. You may have already submitted this form, or you need to use a different value.', array('%value' => $element['#value'], '%title' => $element['#title'])));
1198
    }
1199
  }
1200
}