Projet

Général

Profil

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

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

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
  }
201

    
202
  // Append the add form if not already printed.
203
  if ($add_form) {
204
    $rows[] = $add_form;
205
  }
206

    
207
  $variables['rows'] = $rows;
208
  $variables['header'] = $header;
209
  $variables['form'] = $form;
210
}
211

    
212
/**
213
 * Recursive function for nesting components into a table.
214
 *
215
 * @see preprocess_webform_components_form()
216
 */
217
function _webform_components_form_rows($node, $cid, $component, $level, &$form, &$rows, &$add_form) {
218
  // Create presentable values.
219
  $form_key = truncate_utf8($component['form_key'], 30, FALSE, TRUE);
220
  $value = truncate_utf8($component['value'], 30, TRUE, TRUE, 20);
221

    
222
  // Remove individual titles from the required and weight fields.
223
  unset($form['components'][$cid]['required']['#title']);
224
  unset($form['components'][$cid]['pid']['#title']);
225
  unset($form['components'][$cid]['weight']['#title']);
226

    
227
  // Add special classes for weight and parent fields.
228
  $form['components'][$cid]['cid']['#attributes']['class'] = array('webform-cid');
229
  $form['components'][$cid]['pid']['#attributes']['class'] = array('webform-pid');
230
  $form['components'][$cid]['weight']['#attributes']['class'] = array('webform-weight');
231

    
232
  // Build indentation for this row.
233
  $indents = '';
234
  for ($n = 1; $n <= $level; $n++) {
235
    $indents .= '<div class="indentation">&nbsp;</div>';
236
  }
237

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

    
268
  // Add the add form if this was the last edited component.
269
  if (isset($_GET['cid']) && $component['cid'] == $_GET['cid'] && $add_form) {
270
    $add_form['data'][0]['data'] = $indents . $add_form['data'][0]['data'];
271
    $rows[] = $add_form;
272
    $add_form = FALSE;
273
  }
274
}
275

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

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

    
310
  if (!empty($duplicates)) {
311
    $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.');
312
    $items = array();
313
    foreach ($duplicates as $form_key => $cids) {
314
      foreach ($cids as $cid) {
315
        $items[] = webform_filter_xss($form['#node']->webform['components'][$cid]['name']);
316
      }
317
    }
318

    
319
    form_error($form['components'], $error . theme('item_list', array('items' => $items)));
320
  }
321
}
322

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

    
333
/**
334
 * Submit handler for webform_components_form() to save component order.
335
 */
336
function webform_components_form_submit($form, &$form_state) {
337
  $node = node_load($form_state['values']['nid']);
338

    
339
  // Update all required and weight values.
340
  $changes = FALSE;
341
  foreach ($node->webform['components'] as $cid => $component) {
342
    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']) {
343
      $changes = TRUE;
344
      $node->webform['components'][$cid]['weight'] = $form_state['values']['components'][$cid]['weight'];
345
      $node->webform['components'][$cid]['required'] = $form_state['values']['components'][$cid]['required'];
346
      $node->webform['components'][$cid]['pid'] = $form_state['values']['components'][$cid]['pid'];
347
    }
348
  }
349

    
350
  if ($changes) {
351
    node_save($node);
352
  }
353

    
354
  drupal_set_message(t('The component positions and required values have been updated.'));
355
}
356

    
357
/**
358
 * Submit handler for webform_components_form() that adds a new component.
359
 */
360
function webform_components_form_add_submit($form, &$form_state) {
361
  $node = node_load($form_state['values']['nid']);
362

    
363
  $component = $form_state['values']['add'];
364

    
365
  // Set the values in the query string for the add component page.
366
  $query = array(
367
    'name' => $component['name'],
368
    'required' => $component['required'],
369
    'pid' => $component['pid'],
370
    'weight' => $component['weight'],
371
  );
372

    
373
  // Forward the "destination" query string value to the next form.
374
  if (isset($_GET['destination'])) {
375
    $query['destination'] = $_GET['destination'];
376
    unset($_GET['destination']);
377
    drupal_static_reset('drupal_get_destination');
378
  }
379
  $form_state['redirect'] = array('node/' . $node->nid . '/webform/components/new/' . $component['type'], array('query' => $query));
380
}
381

    
382
/**
383
 * Form to configure a webform component.
384
 */
385
function webform_component_edit_form($form, $form_state, $node, $component, $clone = FALSE) {
386
  drupal_set_title(t('Edit component: @name', array('@name' => $component['name'])), PASS_THROUGH);
387

    
388
  $form['#node'] = $node;
389
  $form['#tree'] = TRUE;
390

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

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

    
422
  $form['form_key'] = array(
423
    '#type' => 'textfield',
424
    '#default_value' => empty($component['form_key']) ? _webform_safe_name($component['name']) : $component['form_key'],
425
    '#title' => t('Field Key'),
426
    '#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.'),
427
    '#required' => TRUE,
428
    '#weight' => -9,
429
  );
430

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

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

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

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

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

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

    
541
  // Position settings, only shown if JavaScript is disabled.
542
  $form['position'] = array(
543
    '#type' => 'fieldset',
544
    '#title' => t('Position'),
545
    '#collapsible' => TRUE,
546
    '#collapsed' => TRUE,
547
    '#tree' => FALSE,
548
    '#weight' => 20,
549
    '#attributes' => array('class' => array('webform-position')),
550
  );
551

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

    
576
  // Add the fields specific to this component type:
577
  $additional_form_elements = (array) webform_component_invoke($component['type'], 'edit', $component);
578
  if (empty($additional_form_elements)) {
579
    drupal_set_message(t('The webform component of type @type does not have an edit function defined.', array('@type' => $component['type'])));
580
  }
581

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

    
607
  // Add the submit button.
608
  $form['actions'] = array(
609
    '#type' => 'actions',
610
    '#weight' => 50,
611
  );
612
  $form['actions']['submit'] = array(
613
    '#type' => 'submit',
614
    '#value' => t('Save component'),
615
  );
616

    
617
  // Remove fieldsets without any child form controls.
618
  foreach (element_children($form) as $group_key) {
619
    $group = $form[$group_key];
620
    if (isset($group['#type']) && $group['#type'] === 'fieldset' && !element_children($group)) {
621
      unset($form[$group_key]);
622
    }
623
  }
624

    
625
  return $form;
626
}
627

    
628
/**
629
 * Field name validation for the webform unique key. Must be alphanumeric.
630
 */
631
function webform_component_edit_form_validate($form, &$form_state) {
632
  $node = $form['#node'];;
633

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

    
638
  foreach ($node->webform['components'] as $cid => $component) {
639
    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)) {
640
      form_set_error('form_key', t('The field key %field_key is already in use by the field labeled %existing_field. Please use a unique key.', array('%field_key' => $form_state['values']['form_key'], '%existing_field' => $component['name'])));
641
    }
642
  }
643
}
644

    
645
/**
646
 * Submit handler for webform_component_edit_form().
647
 */
648
function webform_component_edit_form_submit($form, &$form_state) {
649
  // Ensure a webform record exists.
650
  $node = $form['#node'];
651
  webform_ensure_record($node);
652

    
653
  // Remove extra values that match the default.
654
  if (isset($form_state['values']['extra'])) {
655
    $default = array('type' => $form_state['values']['type'], 'extra' => array());
656
    webform_component_defaults($default);
657
    foreach ($form_state['values']['extra'] as $key => $value) {
658
      if (isset($default['extra'][$key]) && $default['extra'][$key] === $value) {
659
        unset($form_state['values']['extra'][$key]);
660
      }
661
    }
662
  }
663

    
664
  // Remove empty attribute values.
665
  if (isset($form_state['values']['extra']['attributes'])) {
666
    foreach ($form_state['values']['extra']['attributes'] as $key => $value) {
667
      if ($value === '') {
668
        unset($form_state['values']['extra']['attributes'][$key]);
669
      }
670
    }
671
  }
672

    
673
  if ($form_state['values']['clone']) {
674
    webform_component_clone($node, $form_state['values']);
675
    drupal_set_message(t('Component %name cloned.', array('%name' => $form_state['values']['name'])));
676
  }
677
  elseif (!empty($form_state['values']['cid'])) {
678
    webform_component_update($form_state['values']);
679
    drupal_set_message(t('Component %name updated.', array('%name' => $form_state['values']['name'])));
680
  }
681
  else {
682
    $cid = webform_component_insert($form_state['values']);
683
    drupal_set_message(t('New component %name added.', array('%name' => $form_state['values']['name'])));
684
  }
685

    
686
  // Since Webform components have been updated but the node itself has not
687
  // been saved, it is necessary to explicitly clear the cache to make sure
688
  // the updated webform is visible to anonymous users. This resets the page
689
  // and block caches (only);
690
  cache_clear_all();
691

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

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

    
698
/**
699
 * Form to confirm deletion of a component.
700
 */
701
function webform_component_delete_form($form, $form_state, $node, $component) {
702
  $cid = $component['cid'];
703

    
704
  $form = array();
705
  $form['node'] = array(
706
    '#type' => 'value',
707
    '#value' => $node,
708
  );
709
  $form['component'] = array(
710
    '#type' => 'value',
711
    '#value' => $component,
712
  );
713

    
714
  $component_type = $node->webform['components'][$cid]['type'];
715
  if (webform_component_feature($component_type, 'group')) {
716
    $question = t('Delete the %name fieldset?', array('%name' => $node->webform['components'][$cid]['name']));
717
    $description = t('This will immediately delete the %name @type component and all nested components within %name from the %webform webform. This cannot be undone.',
718
                     array('%name' => $node->webform['components'][$cid]['name'],
719
                           '@type' => webform_component_property($component_type, 'label'),
720
                           '%webform' => $node->title));
721
  }
722
  else {
723
    $question = t('Delete the %name component?', array('%name' => $node->webform['components'][$cid]['name']));
724
    $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));
725
  }
726

    
727
  return confirm_form($form, $question, 'node/' . $node->nid . '/webform/components', $description, t('Delete'));
728
}
729

    
730
/**
731
 * Submit handler for webform_component_delete_form().
732
 */
733
function webform_component_delete_form_submit($form, &$form_state) {
734
  // Delete the component.
735
  $node = $form_state['values']['node'];
736
  $component = $form_state['values']['component'];
737
  webform_component_delete($node, $component);
738
  drupal_set_message(t('Component %name deleted.', array('%name' => $component['name'])));
739

    
740
  // Check if this webform still contains any information.
741
  unset($node->webform['components'][$component['cid']]);
742
  webform_check_record($node);
743

    
744
  // Since Webform components have been updated but the node itself has not
745
  // been saved, it is necessary to explicitly clear the cache to make sure
746
  // the updated webform is visible to anonymous users. This resets the page
747
  // and block caches (only);
748
  cache_clear_all();
749

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

    
753
  $form_state['redirect'] = 'node/' . $node->nid . '/webform/components';
754
}
755

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

    
769
  $component['value'] = isset($component['value']) ? $component['value'] : NULL;
770
  $component['required'] = isset($component['required']) ? $component['required'] : 0;
771
  $component['extra']['private'] = isset($component['extra']['private']) ? $component['extra']['private'] : 0;
772

    
773
  if (!isset($component['cid'])) {
774
    if (lock_acquire('webform_component_insert_' . $component['nid'], 5)) {
775
      $next_id_query = db_select('webform_component')->condition('nid', $component['nid']);
776
      $next_id_query->addExpression('MAX(cid) + 1', 'cid');
777
      $component['cid'] = $next_id_query->execute()->fetchField();
778
      if ($component['cid'] == NULL) {
779
        $component['cid'] = 1;
780
      }
781
      lock_release('webform_component_insert_' . $component['nid']);
782
    }
783
    else {
784
      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)));
785
      return FALSE;
786
    }
787
  }
788

    
789
  $query = db_insert('webform_component')
790
    ->fields(array(
791
      'nid' => $component['nid'],
792
      'cid' => $component['cid'],
793
      'pid' => $component['pid'],
794
      'form_key' => $component['form_key'],
795
      'name' => $component['name'],
796
      'type' => $component['type'],
797
      'value' => (string) $component['value'],
798
      'extra' => serialize($component['extra']),
799
      'required' => $component['required'],
800
      'weight' => $component['weight'],
801
    ))
802
    ->execute();
803

    
804
  // Post-insert actions.
805
  module_invoke_all('webform_component_insert', $component);
806

    
807
  return $component['cid'];
808
}
809

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

    
824
  $component['value'] = isset($component['value']) ? $component['value'] : NULL;
825
  $component['required'] = isset($component['required']) ? $component['required'] : 0;
826
  $component['extra']['private'] = isset($component['extra']['private']) ? $component['extra']['private'] : 0;
827
  db_update('webform_component')
828
    ->fields(array(
829
      'pid' => $component['pid'],
830
      'form_key' => $component['form_key'],
831
      'name' => $component['name'],
832
      'type' => $component['type'],
833
      'value' => isset($component['value']) ? $component['value'] : '',
834
      'extra' => serialize($component['extra']),
835
      'required' => $component['required'],
836
      'weight' => $component['weight']
837
    ))
838
    ->condition('nid', $component['nid'])
839
    ->condition('cid', $component['cid'])
840
    ->execute();
841

    
842
  // Post-update actions.
843
  module_invoke_all('webform_component_update', $component);
844
}
845

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

    
861
  // Remove database entries.
862
  db_delete('webform_component')
863
    ->condition('nid', $node->nid)
864
    ->condition('cid', $component['cid'])
865
    ->execute();
866
  db_delete('webform_submitted_data')
867
    ->condition('nid', $node->nid)
868
    ->condition('cid', $component['cid'])
869
    ->execute();
870

    
871
  // Delete any conditionals dependent on this component.
872
  module_load_include('inc', 'webform', 'includes/webform.conditionals');
873
  foreach ($node->webform['conditionals'] as $rgid => $conditional) {
874
    $delete_conditional = FALSE;
875
    $specs = array(
876
      array(
877
        'field' => 'rules',
878
        'table' => 'webform_conditional_rules',
879
        'component' => 'source',
880
        'component_type' => 'source_type',
881
        'index' => 'rid',
882
      ),
883
      array(
884
        'field' => 'actions',
885
        'table' => 'webform_conditional_actions',
886
        'component' => 'target',
887
        'component_type' => 'target_type',
888
        'index' => 'aid',
889
      ),
890
    );
891
    foreach ($specs as $spec) {
892
      $count = count($conditional[$spec['field']]);
893
      foreach ($conditional[$spec['field']] as $key => $thing) {
894
        if ($thing[$spec['component_type']] === 'component' && $thing[$spec['component']] == $component['cid']) {
895
          if ($count == 1) {
896
            // Delete the conditional if this component is the only source or target.
897
            webform_conditional_delete($node, $conditional);
898
            break 2;
899
          }
900
          db_delete($spec['table'])
901
            ->condition('nid', $node->nid)
902
            ->condition('rgid', $rgid)
903
            ->condition($spec['index'], $key)
904
            ->execute();
905
          $count--;
906
        }
907
      }
908
    }
909
  }
910

    
911
  // Delete all elements under this element.
912
  $result = db_select('webform_component', 'c')
913
    ->fields('c')
914
    ->condition('nid', $node->nid)
915
    ->condition('pid', $component['cid'])
916
    ->execute();
917
  foreach ($result as $row) {
918
    $child_component = $node->webform['components'][$row->cid];
919
    webform_component_delete($node, $child_component);
920
  }
921

    
922
  // Post-delete actions.
923
  module_invoke_all('webform_component_delete', $component);
924
}
925

    
926
/**
927
 * Recursively insert components into the database.
928
 *
929
 * @param $node
930
 *   The node object containing the current webform.
931
 * @param $component
932
 *   A full component containing fields from the component form.
933
 */
934
function webform_component_clone(&$node, &$component) {
935
  $original_cid = $component['cid'];
936
  $component['cid'] = NULL;
937
  $new_cid = webform_component_insert($component);
938
  $component['cid'] = $new_cid;
939
  if (webform_component_feature($component['type'], 'group')) {
940
    foreach ($node->webform['components'] as $cid => $child_component) {
941
      if ($child_component['pid'] == $original_cid) {
942
        $child_component['pid'] = $new_cid;
943
        webform_component_clone($node, $child_component);
944
      }
945
    }
946
  }
947
  return $new_cid;
948
}
949

    
950
/**
951
 * Check if a component has a particular feature.
952
 *
953
 * @see hook_webform_component_info()
954
 */
955
function webform_component_feature($type, $feature) {
956
  $components = webform_components();
957
  $defaults = array(
958
    'analysis' => TRUE,
959
    'csv' => TRUE,
960
    'default_value' => TRUE,
961
    'description' => TRUE,
962
    'email' => TRUE,
963
    'email_address' => FALSE,
964
    'email_name' => FALSE,
965
    'required' => TRUE,
966
    'title' => TRUE,
967
    'title_display' => TRUE,
968
    'title_inline' => TRUE,
969
    'conditional' => TRUE,
970
    'conditional_action_set' => FALSE,
971
    'spam_analysis' => FALSE,
972
    'group' => FALSE,
973
    'attachment' => FALSE,
974
    'private' => TRUE,
975
    'placeholder' => FALSE,
976
    'wrapper_classes' => TRUE,
977
    'css_classes' => TRUE,
978
    'views_range' => FALSE,
979
  );
980
  return isset($components[$type]['features'][$feature]) ? $components[$type]['features'][$feature] : !empty($defaults[$feature]);
981
}
982

    
983
/**
984
 * Get a component property from the component definition.
985
 *
986
 * @see hook_webform_component_info()
987
 */
988
function webform_component_property($type, $property) {
989
  $components = webform_components();
990
  $defaults = array(
991
    'conditional_type' => 'string',
992
  );
993
  return isset($components[$type][$property]) ? $components[$type][$property] : $defaults[$property];
994
}
995

    
996
/**
997
 * Create a list of components suitable for a select list.
998
 *
999
 * @param $node
1000
 *   The webform node.
1001
 * @param $component_filter
1002
 *   Either an array of components, or a string containing a feature name (csv,
1003
 *   email, required, conditional) on which this list of components will be
1004
 *   restricted.
1005
 * @param $prefix_group
1006
 *   TRUE to indent with a hyphen, or 'path" to Prepend enclosing group (e.g.
1007
 *   fieldset) name(s)
1008
 * @param $pagebreak_groups
1009
 *   Determine if pagebreaks should be converted to option groups in the
1010
 *   returned list of options.
1011
 */
1012
function webform_component_list($node, $component_filter = NULL, $prepend_group = TRUE, $pagebreak_groups = FALSE) {
1013
  $options = array();
1014
  $page_names = array();
1015
  $parent_names = array();
1016

    
1017
  $components = is_array($component_filter) ? $component_filter : $node->webform['components'];
1018
  $feature = is_string($component_filter) ? $component_filter : NULL;
1019

    
1020
  foreach ($components as $cid => $component) {
1021
    // If this component is a group (e.g. fieldset), then remember its name, including any parents.
1022
    if ($prepend_group && webform_component_feature($component['type'], 'group')) {
1023
      $parent_names[$cid] = ($component['pid'] ? $parent_names[$component['pid']] : '') .
1024
                            ($prepend_group === 'path' ? $component['name'] . ': ' : '-');
1025
    }
1026
    $page_num = $component['page_num'];
1027
    // If this component is a pagebreak, then generate an option group, ensuring a unique name.
1028
    if ($pagebreak_groups && $component['type'] == 'pagebreak') {
1029
      $page_name = $component['name'];
1030
      $copy = 1;
1031
      while (in_array($page_name, $page_names)) {
1032
        $page_name = $component['name'] . '_' . ++$copy;
1033
      }
1034
      $page_names[$page_num] = $page_name;
1035
    }
1036
    // If this component should be included in the options, add it with any prefix, in a page group, as needed.
1037
    if (!isset($feature) || webform_component_feature($component['type'], $feature) || $prepend_group === TRUE) {
1038
      $prefix = ($prepend_group && $component['pid']) ? $parent_names[$component['pid']] : '';
1039
      if ($pagebreak_groups && $page_num > 1) {
1040
        $options[$page_names[$page_num]][$cid] = $prefix . $component['name'];
1041
      }
1042
      else {
1043
        $options[$cid] = $prefix . $component['name'];
1044
      }
1045
    }
1046
  }
1047

    
1048
  return $options;
1049
}
1050

    
1051
/**
1052
 * A Form API process function to expand a component list into checkboxes.
1053
 */
1054
function webform_component_select($element) {
1055
  // Split the select list into checkboxes.
1056
  foreach ($element['#options'] as $key => $label) {
1057
    $label_length = strlen($label);
1058
    $label = preg_replace('/^(\-)+/', '', $label);
1059
    $indents = $label_length - strlen($label);
1060
    $element[$key] = array(
1061
      '#title' => $label,
1062
      '#type' => 'checkbox',
1063
      '#default_value' => array_search($key, $element['#value']) !== FALSE,
1064
      '#return_value' => $key,
1065
      '#parents' => array_merge($element['#parents'], array($key)),
1066
      '#indent' => $indents,
1067
    );
1068
  }
1069

    
1070
  $element['#theme_wrappers'] = array();
1071
  $element['#type'] = 'webform_component_select';
1072
  $element['#theme'] = 'webform_component_select';
1073
  $element['#attached'] = array(
1074
    'library' => array(
1075
      array('webform', 'admin'),
1076
      array('system', 'drupal.collapse'),
1077
    ),
1078
    'js' => array(
1079
      'misc/tableselect.js' => array(),
1080
    ),
1081
  );
1082

    
1083
  return $element;
1084
}
1085

    
1086
/**
1087
 * Theme the contents of a Webform component select element.
1088
 */
1089
function theme_webform_component_select($variables) {
1090
  $element = $variables['element'];
1091

    
1092
  $rows = array();
1093
  $header = array();
1094
  if (!isset($element['#all_checkbox']) || $element['#all_checkbox']) {
1095
    $header = array(array('class' => array('select-all'), 'data' => ' ' . t('Include all components')));
1096
  }
1097
  foreach (element_children($element) as $key) {
1098
    if ($key != 'suffix') {
1099
      $rows[] = array(
1100
        theme('indentation', array('size' => $element[$key]['#indent'])) . drupal_render($element[$key]),
1101
      );
1102
    }
1103
  }
1104

    
1105
  $element['#type'] = 'fieldset';
1106
  $element['#value'] = NULL;
1107
  $element['#attributes']['class'] = array('webform-component-select-table');
1108
  if (!isset($element['#collapsible']) || $element['#collapsible']) {
1109
    $element['#attributes']['class'][] = 'collapsible';
1110
  }
1111
  if (!isset($element['#collapsed']) || $element['#collapsed']) {
1112
    $element['#attributes']['class'][] = 'collapsed';
1113
  }
1114

    
1115
  if (empty($rows)) {
1116
    $element['#children'] = t('No available components.');
1117
  }
1118
  else {
1119
    $element['#children'] = '<div class="webform-component-select-wrapper">' . theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => FALSE)) . '</div>';
1120
  }
1121

    
1122
  if (isset($element['suffix'])) {
1123
    $element['#children'] .= '<div class="webform-component-select-suffix">' . drupal_render($element['suffix']) . '</div>';
1124
  }
1125

    
1126

    
1127
  return theme('fieldset', array('element' => $element));
1128
}
1129

    
1130
/**
1131
 * Find a components parents within a node.
1132
 */
1133
function webform_component_parent_keys($node, $component) {
1134
  $parents = array($component['form_key']);
1135
  $pid = $component['pid'];
1136
  while ($pid) {
1137
    $parents[] = $node->webform['components'][$pid]['form_key'];
1138
    $pid = $node->webform['components'][$pid]['pid'];
1139
  }
1140
  return array_reverse($parents);
1141
}
1142

    
1143
/**
1144
 * Populate a component with the defaults for that type.
1145
 */
1146
function webform_component_defaults(&$component) {
1147
  $defaults = webform_component_invoke($component['type'], 'defaults');
1148
  drupal_alter('webform_component_defaults', $defaults, $component['type']);
1149
  if (!empty($defaults)) {
1150
    foreach ($defaults as $key => $default) {
1151
      if (!isset($component[$key])) {
1152
        $component[$key] = $default;
1153
      }
1154
    }
1155
    foreach ($defaults['extra'] as $extra => $default) {
1156
      if (!isset($component['extra'][$extra])) {
1157
        $component['extra'][$extra] = $default;
1158
      }
1159
    }
1160
  }
1161
}
1162

    
1163
/**
1164
 * Validate an element value is unique with no duplicates in the database.
1165
 */
1166
function webform_validate_unique($element, $form_state) {
1167
  if ($element['#value'] !== '') {
1168
    $nid = $form_state['values']['details']['nid'];
1169
    $sid = $form_state['values']['details']['sid'];
1170
    $query = db_select('webform_submitted_data')
1171
      ->fields('webform_submitted_data', array('sid'))
1172
      ->condition('nid', $nid)
1173
      ->condition('cid', $element['#webform_component']['cid'])
1174
      ->condition('data', $element['#value'])
1175
      ->range(0, 1); // More efficient than using countQuery() for data checks.
1176
    if ($sid) {
1177
      $query->condition('sid', $sid, '<>');
1178
    }
1179
    $count = $query->execute()->fetchField();
1180
    if ($count) {
1181
      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'])));
1182
    }
1183
  }
1184
}