commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-new / webform / includes / webform.components.inc
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('Field 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 field key %field_key is invalid. Please include only lowercase alphanumeric characters and underscores.', array('%field_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 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'])));
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 }