webform.components.inc 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087
  1. <?php
  2. /**
  3. * @file
  4. * Webform module component handling.
  5. */
  6. /**
  7. * Provides interface and database handling for editing components of a webform.
  8. *
  9. * @author Nathan Haug <nate@lullabot.com>
  10. */
  11. /**
  12. * Overview page of all components for this webform.
  13. */
  14. function webform_components_page($node, $page_number = 1) {
  15. $output = drupal_get_form('webform_components_form', $node);
  16. return array(
  17. '#theme' => 'webform_components_page',
  18. '#node' => $node,
  19. '#form' => $output,
  20. );
  21. }
  22. /**
  23. * Theme the output of the main components page.
  24. *
  25. * This theming provides a way to toggle between the editing modes if Form
  26. * Builder module is available.
  27. */
  28. function theme_webform_components_page($variables) {
  29. $node = $variables['node'];
  30. $form = $variables['form'];
  31. return drupal_render($form);
  32. }
  33. /**
  34. * The table-based listing of all components for this webform.
  35. */
  36. function webform_components_form($form, $form_state, $node) {
  37. $form = array(
  38. '#tree' => TRUE,
  39. '#node' => $node,
  40. 'components' => array(),
  41. );
  42. $form['nid'] = array(
  43. '#type' => 'value',
  44. '#value' => $node->nid,
  45. );
  46. $options = array();
  47. foreach ($node->webform['components'] as $cid => $component) {
  48. $options[$cid] = check_plain($component['name']);
  49. $form['components'][$cid]['cid'] = array(
  50. '#type' => 'hidden',
  51. '#default_value' => $component['cid'],
  52. );
  53. $form['components'][$cid]['pid'] = array(
  54. '#type' => 'hidden',
  55. '#default_value' => $component['pid'],
  56. );
  57. $form['components'][$cid]['weight'] = array(
  58. '#type' => 'textfield',
  59. '#size' => 4,
  60. '#title' => t('Weight'),
  61. '#default_value' => $component['weight'],
  62. );
  63. $form['components'][$cid]['mandatory'] = array(
  64. '#type' => 'checkbox',
  65. '#title' => t('Mandatory'),
  66. '#default_value' => $component['mandatory'],
  67. '#access' => webform_component_feature($component['type'], 'required'),
  68. );
  69. if (!isset($max_weight) || $component['weight'] > $max_weight) {
  70. $max_weight = $component['weight'];
  71. }
  72. }
  73. $form['add']['name'] = array(
  74. '#type' => 'textfield',
  75. '#size' => 24,
  76. '#maxlength' => 255,
  77. );
  78. $form['add']['type'] = array(
  79. '#type' => 'select',
  80. '#options' => webform_component_options(),
  81. '#weight' => 3,
  82. '#default_value' => (isset($_GET['cid']) && isset($node->webform['components'][$_GET['cid']])) ? $node->webform['components'][$_GET['cid']]['type'] : 'textfield',
  83. );
  84. $form['add']['mandatory'] = array(
  85. '#type' => 'checkbox',
  86. );
  87. $form['add']['cid'] = array(
  88. '#type' => 'hidden',
  89. '#default_value' => '',
  90. );
  91. $form['add']['pid'] = array(
  92. '#type' => 'hidden',
  93. '#default_value' => (isset($_GET['cid']) && isset($node->webform['components'][$_GET['cid']])) ? $node->webform['components'][$_GET['cid']]['pid'] : 0,
  94. );
  95. $form['add']['weight'] = array(
  96. '#type' => 'textfield',
  97. '#size' => 4,
  98. '#delta' => count($node->webform['components']) > 10 ? count($node->webform['components']) : 10,
  99. );
  100. if (isset($_GET['cid']) && isset($node->webform['components'][$_GET['cid']])) {
  101. // Make the new component appear by default directly after the one that was
  102. // just added.
  103. $form['add']['weight']['#default_value'] = $node->webform['components'][$_GET['cid']]['weight'] + 1;
  104. foreach (array_keys($node->webform['components']) as $cid) {
  105. // Adjust all later components also, to make sure none of them have the
  106. // same weight as the new component.
  107. if ($form['components'][$cid]['weight']['#default_value'] >= $form['add']['weight']['#default_value']) {
  108. $form['components'][$cid]['weight']['#default_value']++;
  109. }
  110. }
  111. }
  112. else {
  113. // If no component was just added, the new component should appear by
  114. // default at the end of the list.
  115. $form['add']['weight']['#default_value'] = isset($max_weight) ? $max_weight + 1 : 0;
  116. }
  117. $form['add']['add'] = array(
  118. '#type' => 'submit',
  119. '#value' => t('Add'),
  120. '#weight' => 45,
  121. );
  122. $form['submit'] = array(
  123. '#type' => 'submit',
  124. '#value' => t('Save'),
  125. '#weight' => 45,
  126. '#access' => count($node->webform['components']) > 0,
  127. );
  128. return $form;
  129. }
  130. /**
  131. * Theme the node components form. Use a table to organize the components.
  132. *
  133. * @param $form
  134. * The form array.
  135. * @return
  136. * Formatted HTML form, ready for display.
  137. */
  138. function theme_webform_components_form($variables) {
  139. $form = $variables['form'];
  140. $form['components']['#attached']['library'][] = array('webform', 'admin');
  141. // TODO: Attach these. See http://drupal.org/node/732022.
  142. drupal_add_tabledrag('webform-components', 'order', 'sibling', 'webform-weight');
  143. drupal_add_tabledrag('webform-components', 'match', 'parent', 'webform-pid', 'webform-pid', 'webform-cid');
  144. $node = $form['#node'];
  145. $header = array(t('Label'), t('Type'), t('Value'), t('Mandatory'), t('Weight'), array('data' => t('Operations'), 'colspan' => 3));
  146. $rows = array();
  147. // Add a row containing form elements for a new item.
  148. unset($form['add']['name']['#title'], $form['add_type']['#description']);
  149. $form['add']['name']['#attributes']['rel'] = t('New component name');
  150. $form['add']['name']['#attributes']['class'] = array('webform-default-value');
  151. $form['add']['cid']['#attributes']['class'] = array('webform-cid');
  152. $form['add']['pid']['#attributes']['class'] = array('webform-pid');
  153. $form['add']['weight']['#attributes']['class'] = array('webform-weight');
  154. $row_data = array(
  155. drupal_render($form['add']['name']),
  156. drupal_render($form['add']['type']),
  157. '',
  158. drupal_render($form['add']['mandatory']),
  159. drupal_render($form['add']['cid']) . drupal_render($form['add']['pid']) . drupal_render($form['add']['weight']),
  160. array('colspan' => 3, 'data' => drupal_render($form['add']['add'])),
  161. );
  162. $add_form = array('data' => $row_data, 'class' => array('draggable', 'webform-add-form'));
  163. $form_rendered = FALSE;
  164. if (!empty($node->webform['components'])) {
  165. $component_tree = array();
  166. $page_count = 1;
  167. _webform_components_tree_build($node->webform['components'], $component_tree, 0, $page_count);
  168. $component_tree = _webform_components_tree_sort($component_tree);
  169. // Build the table rows.
  170. function _webform_components_form_rows($node, $cid, $component, $level, &$form, &$rows, &$add_form) {
  171. // Create presentable values.
  172. if (drupal_strlen($component['value']) > 30) {
  173. $component['value'] = drupal_substr($component['value'], 0, 30);
  174. $component['value'] .= '...';
  175. }
  176. $component['value'] = check_plain($component['value']);
  177. // Remove individual titles from the mandatory and weight fields.
  178. unset($form['components'][$cid]['mandatory']['#title']);
  179. unset($form['components'][$cid]['pid']['#title']);
  180. unset($form['components'][$cid]['weight']['#title']);
  181. // Add special classes for weight and parent fields.
  182. $form['components'][$cid]['cid']['#attributes']['class'] = array('webform-cid');
  183. $form['components'][$cid]['pid']['#attributes']['class'] = array('webform-pid');
  184. $form['components'][$cid]['weight']['#attributes']['class'] = array('webform-weight');
  185. // Build indentation for this row.
  186. $indents = '';
  187. for ($n = 1; $n <= $level; $n++) {
  188. $indents .= '<div class="indentation">&nbsp;</div>';
  189. }
  190. // Add each component to a table row.
  191. $row_data = array(
  192. $indents . filter_xss($component['name']),
  193. t($component['type']),
  194. ($component['value'] == '') ? '-' : $component['value'],
  195. drupal_render($form['components'][$cid]['mandatory']),
  196. drupal_render($form['components'][$cid]['cid']) . drupal_render($form['components'][$cid]['pid']) . drupal_render($form['components'][$cid]['weight']),
  197. l(t('Edit'), 'node/' . $node->nid . '/webform/components/' . $cid, array('query' => drupal_get_destination())),
  198. l(t('Clone'), 'node/' . $node->nid . '/webform/components/' . $cid . '/clone', array('query' => drupal_get_destination())),
  199. l(t('Delete'), 'node/' . $node->nid . '/webform/components/' . $cid . '/delete', array('query' => drupal_get_destination())),
  200. );
  201. $row_class = array('draggable');
  202. if (!webform_component_feature($component['type'], 'group')) {
  203. $row_class[] = 'tabledrag-leaf';
  204. }
  205. if ($component['type'] == 'pagebreak') {
  206. $row_class[] = 'tabledrag-root';
  207. $row_class[] = 'webform-pagebreak';
  208. $row_data[0] = array('class' => array('webform-pagebreak'), 'data' => $row_data[0]);
  209. }
  210. $rows[] = array('data' => $row_data, 'class' => $row_class);
  211. if (isset($component['children']) && is_array($component['children'])) {
  212. foreach ($component['children'] as $cid => $component) {
  213. _webform_components_form_rows($node, $cid, $component, $level + 1, $form, $rows, $add_form);
  214. }
  215. }
  216. // Add the add form if this was the last edited component.
  217. if (isset($_GET['cid']) && $component['cid'] == $_GET['cid'] && $add_form) {
  218. $add_form['data'][0] = $indents . $add_form['data'][0];
  219. $rows[] = $add_form;
  220. $add_form = FALSE;
  221. }
  222. }
  223. foreach ($component_tree['children'] as $cid => $component) {
  224. _webform_components_form_rows($node, $cid, $component, 0, $form, $rows, $add_form);
  225. }
  226. }
  227. else {
  228. $rows[] = array(array('data' => t('No Components, add a component below.'), 'colspan' => 9));
  229. }
  230. // Append the add form if not already printed.
  231. if ($add_form) {
  232. $rows[] = $add_form;
  233. }
  234. $output = '';
  235. $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'webform-components')));
  236. $output .= drupal_render_children($form);
  237. return $output;
  238. }
  239. function webform_components_form_validate($form, &$form_state) {
  240. // Check that the entered component name is valid.
  241. if ($form_state['values']['op'] == t('Add') && drupal_strlen(trim($form_state['values']['add']['name'])) <= 0) {
  242. form_error($form['add']['name'], t('When adding a new component, the name field is required.'));
  243. }
  244. // Check that no two components end up with the same form key.
  245. $duplicates = array();
  246. $parents = array();
  247. if (isset($form_state['values']['components'])) {
  248. foreach ($form_state['values']['components'] as $cid => $component) {
  249. $form_key = $form['#node']->webform['components'][$cid]['form_key'];
  250. if (isset($parents[$component['pid']]) && ($existing = array_search($form_key, $parents[$component['pid']])) && $existing !== FALSE) {
  251. if (!isset($duplicates[$component['form_key']])) {
  252. $duplicates[$component['form_key']] = array($existing);
  253. }
  254. $duplicates[$component['form_key']][] = $cid;
  255. }
  256. $parents[$component['pid']][$cid] = $form_key;
  257. }
  258. }
  259. if (!empty($duplicates)) {
  260. $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.');
  261. $items = array();
  262. foreach ($duplicates as $form_key => $cids) {
  263. foreach ($cids as $cid) {
  264. $items[] = $form['#node']->webform['components'][$cid]['name'];
  265. }
  266. }
  267. form_error($form['components'], $error . theme('item_list', $items));
  268. }
  269. }
  270. function webform_components_form_submit($form, &$form_state) {
  271. $node = node_load($form_state['values']['nid']);
  272. // Update all mandatory and weight values.
  273. foreach ($node->webform['components'] as $cid => $component) {
  274. if ($component['pid'] != $form_state['values']['components'][$cid]['pid'] || $component['weight'] != $form_state['values']['components'][$cid]['weight'] || $component['mandatory'] != $form_state['values']['components'][$cid]['mandatory']) {
  275. $component['weight'] = $form_state['values']['components'][$cid]['weight'];
  276. $component['mandatory'] = $form_state['values']['components'][$cid]['mandatory'];
  277. $component['pid'] = $form_state['values']['components'][$cid]['pid'];
  278. $component['nid'] = $node->nid;
  279. webform_component_update($component);
  280. }
  281. }
  282. if (isset($_POST['op']) && $_POST['op'] == t('Publish')) {
  283. $node->status = 1;
  284. node_save($node);
  285. drupal_set_message(t('Your webform has been published.'));
  286. return 'node/' . $node->nid;
  287. }
  288. elseif (isset($_POST['op']) && $_POST['op'] == t('Add')) {
  289. $component = $form_state['values']['add'];
  290. $form_state['redirect'] = array('node/' . $node->nid . '/webform/components/new/' . $component['type'], array('query' => array('name' => $component['name'], 'mandatory' => $component['mandatory'], 'pid' => $component['pid'], 'weight' => $component['weight'])));
  291. }
  292. else {
  293. drupal_set_message(t('The component positions and mandatory values have been updated.'));
  294. // Since Webform components have been updated but the node itself has not
  295. // been saved, it is necessary to explicitly clear the cache to make sure
  296. // the updated webform is visible to anonymous users.
  297. cache_clear_all();
  298. // Clear the entity cache if Entity Cache module is installed.
  299. if (module_exists('entitycache')) {
  300. cache_clear_all($node->nid, 'cache_entity_node');
  301. }
  302. }
  303. }
  304. function webform_component_edit_form($form, $form_state, $node, $component, $clone = FALSE) {
  305. drupal_set_title(t('Edit component: @name', array('@name' => $component['name'])), PASS_THROUGH);
  306. $form['#attached']['library'][] = array('webform', 'admin');
  307. $form['#tree'] = TRUE;
  308. // Print the correct field type specification.
  309. // We always need: name and description.
  310. $form['type'] = array(
  311. '#type' => 'value',
  312. '#value' => $component['type'],
  313. );
  314. $form['nid'] = array(
  315. '#type' => 'value',
  316. '#value' => $node->nid,
  317. );
  318. $form['cid'] = array(
  319. '#type' => 'value',
  320. '#value' => isset($component['cid']) ? $component['cid'] : NULL,
  321. );
  322. $form['clone'] = array(
  323. '#type' => 'value',
  324. '#value' => $clone,
  325. );
  326. if (webform_component_feature($component['type'], 'title')) {
  327. $form['name'] = array(
  328. '#type' => 'textfield',
  329. '#default_value' => $component['name'],
  330. '#title' => t('Label'),
  331. '#description' => t('This is used as a descriptive label when displaying this form element.'),
  332. '#required' => TRUE,
  333. '#weight' => -10,
  334. '#maxlength' => 255,
  335. );
  336. }
  337. $form['form_key'] = array(
  338. '#type' => 'textfield',
  339. '#default_value' => empty($component['form_key']) ? _webform_safe_name($component['name']) : $component['form_key'],
  340. '#title' => t('Field Key'),
  341. '#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.'),
  342. '#required' => TRUE,
  343. '#weight' => -9,
  344. );
  345. if (webform_component_feature($component['type'], 'description')) {
  346. $form['extra']['description'] = array(
  347. '#type' => 'textarea',
  348. '#default_value' => isset($component['extra']['description']) ? $component['extra']['description'] : '',
  349. '#title' => t('Description'),
  350. '#description' => t('A short description of the field used as help for the user when he/she uses the form.') . theme('webform_token_help'),
  351. '#weight' => -1,
  352. );
  353. }
  354. // Display settings.
  355. $form['display'] = array(
  356. '#type' => 'fieldset',
  357. '#title' => t('Display'),
  358. '#collapsible' => TRUE,
  359. '#collapsed' => FALSE,
  360. '#weight' => 8,
  361. );
  362. if (webform_component_feature($component['type'], 'title_display')) {
  363. if (webform_component_feature($component['type'], 'title_inline')) {
  364. $form['display']['title_display'] = array(
  365. '#type' => 'select',
  366. '#title' => t('Label display'),
  367. '#default_value' => !empty($component['extra']['title_display']) ? $component['extra']['title_display'] : 'before',
  368. '#options' => array(
  369. 'before' => t('Above'),
  370. 'inline' => t('Inline'),
  371. 'none' => t('None'),
  372. ),
  373. '#description' => t('Determines the placement of the component\'s label.'),
  374. );
  375. }
  376. else {
  377. $form['display']['title_display'] = array(
  378. '#type' => 'checkbox',
  379. '#title' => t('Hide label'),
  380. '#default_value' => strcmp($component['extra']['title_display'], 'none') === 0,
  381. '#return_value' => 'none',
  382. '#description' => t('Do not display the label of this component.'),
  383. );
  384. }
  385. $form['display']['title_display']['#weight'] = 8;
  386. $form['display']['title_display']['#parents'] = array('extra', 'title_display');
  387. if (webform_component_feature($component['type'], 'private')) {
  388. // May user mark fields as Private?
  389. $form['display']['private'] = array(
  390. '#type' => 'checkbox',
  391. '#title' => t('Private'),
  392. '#default_value' => ($component['extra']['private'] == '1' ? TRUE : FALSE),
  393. '#description' => t('Private fields are shown only to users with results access.'),
  394. '#weight' => 45,
  395. '#parents' => array('extra', 'private'),
  396. '#disabled' => !webform_results_access($node),
  397. );
  398. }
  399. }
  400. // Validation settings.
  401. $form['validation'] = array(
  402. '#type' => 'fieldset',
  403. '#title' => t('Validation'),
  404. '#collapsible' => TRUE,
  405. '#collapsed' => FALSE,
  406. '#weight' => 5,
  407. );
  408. if (webform_component_feature($component['type'], 'required')) {
  409. $form['validation']['mandatory'] = array(
  410. '#type' => 'checkbox',
  411. '#title' => t('Mandatory'),
  412. '#default_value' => ($component['mandatory'] == '1' ? TRUE : FALSE),
  413. '#description' => t('Check this option if the user must enter a value.'),
  414. '#weight' => -1,
  415. '#parents' => array('mandatory'),
  416. );
  417. }
  418. // Position settings, only shown if JavaScript is disabled.
  419. $form['position'] = array(
  420. '#type' => 'fieldset',
  421. '#title' => t('Position'),
  422. '#collapsible' => TRUE,
  423. '#collapsed' => TRUE,
  424. '#tree' => FALSE,
  425. '#weight' => 20,
  426. '#attributes' => array('class' => array('webform-position')),
  427. );
  428. if (variable_get('webform_enable_fieldset', TRUE) && is_array($node->webform['components'])) {
  429. $options = array('0' => t('Root'));
  430. foreach ($node->webform['components'] as $existing_cid => $value) {
  431. if (webform_component_feature($value['type'], 'group') && (!isset($component['cid']) || $existing_cid != $component['cid'])) {
  432. $options[$existing_cid] = $value['name'];
  433. }
  434. }
  435. $form['position']['pid'] = array(
  436. '#type' => 'select',
  437. '#title' => t('Parent Fieldset'),
  438. '#default_value' => $component['pid'],
  439. '#description' => t('Optional. You may organize your form by placing this component inside another fieldset.'),
  440. '#options' => $options,
  441. '#weight' => 3,
  442. );
  443. }
  444. $form['position']['weight'] = array(
  445. '#type' => 'textfield',
  446. '#size' => 4,
  447. '#title' => t('Weight'),
  448. '#default_value' => $component['weight'],
  449. '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
  450. '#weight' => 4,
  451. );
  452. // Add conditional fields.
  453. $conditional_components = array();
  454. $counter = 0;
  455. $last_pagebreak_slice = 0;
  456. foreach ($node->webform['components'] as $cid => $test_component) {
  457. // Only components before the pagebreak can be considered.
  458. if ($test_component['type'] == 'pagebreak') {
  459. $last_pagebreak_slice = $counter;
  460. }
  461. if (isset($component['cid']) && $cid == $component['cid']) {
  462. break;
  463. }
  464. if (webform_component_feature($test_component['type'], 'conditional')) {
  465. $conditional_components[$cid] = $test_component;
  466. }
  467. $counter++;
  468. }
  469. if ($component['type'] != 'pagebreak') {
  470. $fieldset_description = t('Create a rule to control whether or not to skip this page.');
  471. }
  472. else {
  473. $fieldset_description = t('Create a rule to control whether or not to show this form element.');
  474. }
  475. $conditional_components = array_slice($conditional_components, 0, $last_pagebreak_slice, TRUE);
  476. $form['conditional'] = array(
  477. '#weight' => 10,
  478. '#type' => 'fieldset',
  479. '#title' => t('Conditional rules'),
  480. '#collapsible' => TRUE,
  481. '#collapsed' => TRUE,
  482. '#description' => t('Create a rule to control whether or not to show this form element.'),
  483. '#tree' => FALSE,
  484. );
  485. $form['conditional']['extra'] = array(
  486. '#tree' => TRUE,
  487. );
  488. $form['conditional']['extra']['conditional_component'] = array(
  489. '#type' => 'select',
  490. '#title' => t('Component'),
  491. '#options' => webform_component_list($node, $conditional_components, FALSE, TRUE),
  492. '#description' => t('Select another component to decide whether to show or hide this component. You can only select components occurring before the most recent pagebreak.'),
  493. '#default_value' => $component['extra']['conditional_component'],
  494. );
  495. $form['conditional']['extra']['conditional_operator'] = array(
  496. '#type' => 'select',
  497. '#title' => t('Operator'),
  498. '#options' => array(
  499. '=' => t('Is one of'),
  500. '!=' => t('Is not one of')
  501. ),
  502. '#description' => t('Determines whether the list below is inclusive or exclusive.'),
  503. '#default_value' => $component['extra']['conditional_operator'],
  504. );
  505. $form['conditional']['extra']['conditional_values'] = array(
  506. '#type' => 'textarea',
  507. '#title' => t('Values'),
  508. '#description' => t('List values, one per line, that will trigger this action. If you leave this blank, this component will always display.'),
  509. '#default_value' => $component['extra']['conditional_values'],
  510. );
  511. if (empty($conditional_components)) {
  512. $form['conditional']['#access'] = FALSE;
  513. }
  514. // Add the fields specific to this component type:
  515. $additional_form_elements = (array) webform_component_invoke($component['type'], 'edit', $component);
  516. if (empty($additional_form_elements)) {
  517. drupal_set_message(t('The webform component of type @type does not have an edit function defined.', array('@type' => $component['type'])));
  518. }
  519. // Merge the additional fields with the current fields:
  520. if (isset($additional_form_elements['extra'])) {
  521. $form['extra'] = array_merge($form['extra'], $additional_form_elements['extra']);
  522. unset($additional_form_elements['extra']);
  523. }
  524. if (isset($additional_form_elements['position'])) {
  525. $form['position'] = array_merge($form['position'], $additional_form_elements['position']);
  526. unset($additional_form_elements['position']);
  527. }
  528. if (isset($additional_form_elements['display'])) {
  529. $form['display'] = array_merge($form['display'], $additional_form_elements['display']);
  530. unset($additional_form_elements['display']);
  531. }
  532. if (isset($additional_form_elements['validation'])) {
  533. $form['validation'] = array_merge($form['validation'], $additional_form_elements['validation']);
  534. unset($additional_form_elements['validation']);
  535. }
  536. elseif (count(element_children($form['validation'])) == 0) {
  537. unset($form['validation']);
  538. }
  539. $form = array_merge($form, $additional_form_elements);
  540. // Add the submit button.
  541. $form['submit'] = array(
  542. '#type' => 'submit',
  543. '#value' => t('Save component'),
  544. '#weight' => 50,
  545. );
  546. return $form;
  547. }
  548. /**
  549. * Field name validation for the webform unique key. Must be alphanumeric.
  550. */
  551. function webform_component_edit_form_validate($form, &$form_state) {
  552. $node = node_load($form_state['values']['nid']);
  553. if (!preg_match('/^[a-z0-9_]+$/i', $form_state['values']['form_key'])) {
  554. 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'])));
  555. }
  556. foreach ($node->webform['components'] as $cid => $component) {
  557. 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)) {
  558. 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'])));
  559. }
  560. }
  561. }
  562. function webform_component_edit_form_submit($form, &$form_state) {
  563. // Ensure a webform record exists.
  564. $node = node_load($form_state['values']['nid']);
  565. webform_ensure_record($node);
  566. // Remove empty extra values.
  567. if (isset($form_state['values']['extra'])) {
  568. foreach ($form_state['values']['extra'] as $key => $value) {
  569. if ($value === '') {
  570. unset($form_state['values']['extra'][$key]);
  571. }
  572. }
  573. }
  574. // Remove empty attribute values.
  575. if (isset($form_state['values']['extra']['attributes'])) {
  576. foreach ($form_state['values']['extra']['attributes'] as $key => $value) {
  577. if ($value === '') {
  578. unset($form_state['values']['extra']['attributes'][$key]);
  579. }
  580. }
  581. }
  582. if ($form_state['values']['clone']) {
  583. webform_component_clone($node, $form_state['values']);
  584. drupal_set_message(t('Component %name cloned.', array('%name' => $form_state['values']['name'])));
  585. }
  586. elseif (!empty($form_state['values']['cid'])) {
  587. webform_component_update($form_state['values']);
  588. drupal_set_message(t('Component %name updated.', array('%name' => $form_state['values']['name'])));
  589. }
  590. else {
  591. $cid = webform_component_insert($form_state['values']);
  592. drupal_set_message(t('New component %name added.', array('%name' => $form_state['values']['name'])));
  593. }
  594. // Since Webform components have been updated but the node itself has not
  595. // been saved, it is necessary to explicitly clear the cache to make sure
  596. // the updated webform is visible to anonymous users.
  597. cache_clear_all();
  598. // Clear the entity cache if Entity Cache module is installed.
  599. if (module_exists('entitycache')) {
  600. cache_clear_all($node->nid, 'cache_entity_node');
  601. }
  602. $form_state['redirect'] = array('node/' . $node->nid . '/webform/components', isset($cid) ? array('query' => array('cid' => $cid)) : array());
  603. }
  604. function webform_component_delete_form($form, $form_state, $node, $component) {
  605. $cid = $component['cid'];
  606. $form = array();
  607. $form['node'] = array(
  608. '#type' => 'value',
  609. '#value' => $node,
  610. );
  611. $form['component'] = array(
  612. '#type' => 'value',
  613. '#value' => $component,
  614. );
  615. if (webform_component_feature($node->webform['components'][$cid]['type'], 'group')) {
  616. $question = t('Delete the %name fieldset?', array('%name' => $node->webform['components'][$cid]['name']));
  617. $description = t('This will immediately delete the %name fieldset and all children elements within %name from the %webform webform. This cannot be undone.', array('%name' => $node->webform['components'][$cid]['name'], '%webform' => $node->title));
  618. }
  619. else {
  620. $question = t('Delete the %name component?', array('%name' => $node->webform['components'][$cid]['name']));
  621. $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));
  622. }
  623. return confirm_form($form, $question, 'node/' . $node->nid . '/webform/components', $description, t('Delete'));
  624. }
  625. function webform_component_delete_form_submit($form, &$form_state) {
  626. // Delete the component.
  627. $node = $form_state['values']['node'];
  628. $component = $form_state['values']['component'];
  629. webform_component_delete($node, $component);
  630. drupal_set_message(t('Component %name deleted.', array('%name' => $component['name'])));
  631. // Check if this webform still contains any information.
  632. unset($node->webform['components'][$component['cid']]);
  633. webform_check_record($node);
  634. // Since Webform components have been updated but the node itself has not
  635. // been saved, it is necessary to explicitly clear the cache to make sure
  636. // the updated webform is visible to anonymous users.
  637. cache_clear_all();
  638. // Clear the entity cache if Entity Cache module is installed.
  639. if (module_exists('entitycache')) {
  640. cache_clear_all($node->nid, 'cache_entity_node');
  641. }
  642. $form_state['redirect'] = 'node/' . $node->nid . '/webform/components';
  643. }
  644. /**
  645. * Insert a new component into the database.
  646. *
  647. * @param $component
  648. * A full component containing fields from the component form.
  649. */
  650. function webform_component_insert(&$component) {
  651. // Allow modules to modify the component before saving.
  652. foreach (module_implements('webform_component_presave') as $module) {
  653. $function = $module . '_webform_component_presave';
  654. $function($component);
  655. }
  656. $component['value'] = isset($component['value']) ? $component['value'] : NULL;
  657. $component['mandatory'] = isset($component['mandatory']) ? $component['mandatory'] : 0;
  658. $component['extra']['private'] = isset($component['extra']['private']) ? $component['extra']['private'] : 0;
  659. if (!isset($component['cid'])) {
  660. if (lock_acquire('webform_component_insert_' . $component['nid'], 5)) {
  661. $next_id_query = db_select('webform_component')->condition('nid', $component['nid']);
  662. $next_id_query->addExpression('MAX(cid) + 1', 'cid');
  663. $component['cid'] = $next_id_query->execute()->fetchField();
  664. if ($component['cid'] == NULL) {
  665. $component['cid'] = 1;
  666. }
  667. lock_release('webform_component_insert_' . $component['nid']);
  668. }
  669. else {
  670. 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)));
  671. return FALSE;
  672. }
  673. }
  674. $query = db_insert('webform_component')
  675. ->fields(array(
  676. 'nid' => $component['nid'],
  677. 'cid' => $component['cid'],
  678. 'pid' => $component['pid'],
  679. 'form_key' => $component['form_key'],
  680. 'name' => $component['name'],
  681. 'type' => $component['type'],
  682. 'value' => (string) $component['value'],
  683. 'extra' => serialize($component['extra']),
  684. 'mandatory' => $component['mandatory'],
  685. 'weight' => $component['weight'],
  686. ))
  687. ->execute();
  688. // Post-insert actions.
  689. module_invoke_all('webform_component_insert', $component);
  690. return $component['cid'];
  691. }
  692. /**
  693. * Update an existing component with new values.
  694. *
  695. * @param $component
  696. * A full component containing a nid, cid, and all other fields from the
  697. * component form. Additional properties are stored in the extra array.
  698. */
  699. function webform_component_update($component) {
  700. // Allow modules to modify the component before saving.
  701. foreach (module_implements('webform_component_presave') as $module) {
  702. $function = $module . '_webform_component_presave';
  703. $function($component);
  704. }
  705. $component['value'] = isset($component['value']) ? $component['value'] : NULL;
  706. $component['mandatory'] = isset($component['mandatory']) ? $component['mandatory'] : 0;
  707. $component['extra']['private'] = isset($component['extra']['private']) ? $component['extra']['private'] : 0;
  708. db_update('webform_component')
  709. ->fields(array(
  710. 'pid' => $component['pid'],
  711. 'form_key' => $component['form_key'],
  712. 'name' => $component['name'],
  713. 'type' => $component['type'],
  714. 'value' => isset($component['value']) ? $component['value'] : '',
  715. 'extra' => serialize($component['extra']),
  716. 'mandatory' => $component['mandatory'],
  717. 'weight' => $component['weight']
  718. ))
  719. ->condition('nid', $component['nid'])
  720. ->condition('cid', $component['cid'])
  721. ->execute();
  722. // Post-update actions.
  723. module_invoke_all('webform_component_update', $component);
  724. }
  725. function webform_component_delete($node, $component) {
  726. // Check if a delete function is available for this component. If so,
  727. // load all submissions and allow the component to delete each one.
  728. webform_component_include($component['type']);
  729. $delete_function = '_webform_delete_' . $component['type'];
  730. if (function_exists($delete_function)) {
  731. module_load_include('inc', 'webform', 'includes/webform.submissions');
  732. $submissions = webform_get_submissions($node->nid);
  733. foreach ($submissions as $submission) {
  734. if (isset($submission->data[$component['cid']])) {
  735. webform_component_invoke($component['type'], 'delete', $component, $submission->data[$component['cid']]['value']);
  736. }
  737. }
  738. }
  739. // Remove database entries.
  740. db_delete('webform_component')
  741. ->condition('nid', $node->nid)
  742. ->condition('cid', $component['cid'])
  743. ->execute();
  744. db_delete('webform_submitted_data')
  745. ->condition('nid', $node->nid)
  746. ->condition('cid', $component['cid'])
  747. ->execute();
  748. // Delete all elements under this element.
  749. $result = db_select('webform_component', 'c')
  750. ->fields('c')
  751. ->condition('nid', $node->nid)
  752. ->condition('pid', $component['cid'])
  753. ->execute();
  754. foreach ($result as $row) {
  755. $child_component = $node->webform['components'][$row->cid];
  756. webform_component_delete($node, $child_component);
  757. }
  758. // Post-delete actions.
  759. module_invoke_all('webform_component_delete', $component);
  760. }
  761. /**
  762. * Recursively insert components into the database.
  763. *
  764. * @param $node
  765. * The node object containing the current webform.
  766. * @param $component
  767. * A full component containing fields from the component form.
  768. */
  769. function webform_component_clone(&$node, &$component) {
  770. $original_cid = $component['cid'];
  771. $component['cid'] = NULL;
  772. $new_cid = webform_component_insert($component);
  773. $component['cid'] = $new_cid;
  774. if (webform_component_feature($component['type'], 'group')) {
  775. foreach ($node->webform['components'] as $cid => $child_component) {
  776. if ($child_component['pid'] == $original_cid) {
  777. $child_component['pid'] = $new_cid;
  778. webform_component_clone($node, $child_component);
  779. }
  780. }
  781. }
  782. return $new_cid;
  783. }
  784. /**
  785. * Check if a component has a particular feature.
  786. *
  787. * @see hook_webform_component_info()
  788. */
  789. function webform_component_feature($type, $feature) {
  790. $components = webform_components();
  791. $defaults = array(
  792. 'csv' => TRUE,
  793. 'default_value' => TRUE,
  794. 'description' => TRUE,
  795. 'email' => TRUE,
  796. 'email_address' => FALSE,
  797. 'email_name' => FALSE,
  798. 'required' => TRUE,
  799. 'title' => TRUE,
  800. 'title_display' => TRUE,
  801. 'title_inline' => TRUE,
  802. 'conditional' => TRUE,
  803. 'spam_analysis' => FALSE,
  804. 'group' => FALSE,
  805. 'attachment' => FALSE,
  806. 'private' => TRUE,
  807. );
  808. return isset($components[$type]['features'][$feature]) ? $components[$type]['features'][$feature] : !empty($defaults[$feature]);
  809. }
  810. /**
  811. * Create a list of components suitable for a select list.
  812. *
  813. * @param $node
  814. * The webform node.
  815. * @param $component_filter
  816. * Either an array of components, or a string containing a feature name (csv,
  817. * email, required, conditional) on which this list of components will be
  818. * restricted.
  819. * @param $indent
  820. * Indent components placed under fieldsets with hyphens.
  821. * @param $optgroups
  822. * Determine if pagebreaks should be converted to option groups in the
  823. * returned list of options.
  824. */
  825. function webform_component_list($node, $component_filter = NULL, $indent = TRUE, $optgroups = FALSE) {
  826. $options = array();
  827. $page_names = array();
  828. $components = is_array($component_filter) ? $component_filter : $node->webform['components'];
  829. $feature = is_string($component_filter) ? $component_filter : NULL;
  830. foreach ($components as $cid => $component) {
  831. if (!isset($feature) || webform_component_feature($component['type'], $feature) || ($indent && webform_component_feature($component['type'], 'group'))) {
  832. $prefix = '';
  833. $page_num = $component['page_num'];
  834. $page_index = 'p' . $page_num;
  835. if ($indent && ($parent_count = count(webform_component_parent_keys($node, $component)) - 1)) {
  836. $prefix = str_repeat('-', $parent_count);
  837. }
  838. if ($optgroups && $component['type'] == 'pagebreak') {
  839. $page_names[$page_index] = $component['name'];
  840. }
  841. elseif ($optgroups && $page_num > 1) {
  842. $options[$page_index][$cid] = $prefix . $component['name'];
  843. }
  844. else {
  845. $options[$cid] = $prefix . $component['name'];
  846. }
  847. }
  848. }
  849. // Convert page breaks into optgroups.
  850. if ($optgroups) {
  851. $grouped_options = $options;
  852. $options = array();
  853. foreach ($grouped_options as $key => $values) {
  854. if (is_array($values) && isset($page_names[$key])) {
  855. $options[$page_names[$key]] = $values;
  856. }
  857. else {
  858. $options[$key] = $values;
  859. }
  860. }
  861. }
  862. return $options;
  863. }
  864. /**
  865. * A Form API process function to expand a component list into checkboxes.
  866. */
  867. function webform_component_select($element) {
  868. // Split the select list into checkboxes.
  869. foreach ($element['#options'] as $key => $label) {
  870. $label_length = strlen($label);
  871. $label = preg_replace('/^(\-)+/', '', $label);
  872. $indents = $label_length - strlen($label);
  873. $element[$key] = array(
  874. '#title' => $label,
  875. '#type' => 'checkbox',
  876. '#default_value' => array_search($key, $element['#value']) !== FALSE,
  877. '#return_value' => $key,
  878. '#parents' => array_merge($element['#parents'], array($key)),
  879. '#indent' => $indents,
  880. );
  881. }
  882. $element['#theme_wrappers'] = array();
  883. $element['#type'] = 'webform_component_select';
  884. $element['#theme'] = 'webform_component_select';
  885. $element['#attached'] = array(
  886. 'library' => array(
  887. array('webform', 'admin'),
  888. ),
  889. 'js' => array(
  890. 'misc/tableselect.js' => array(),
  891. ),
  892. );
  893. return $element;
  894. }
  895. /**
  896. * Theme the contents of a Webform component select element.
  897. */
  898. function theme_webform_component_select($variables) {
  899. $element = $variables['element'];
  900. $rows = array();
  901. $header = array();
  902. if (!isset($element['#all_checkbox']) || $element['#all_checkbox']) {
  903. $header = array(array('class' => array('select-all'), 'data' => ' ' . t('Include all components')));
  904. }
  905. foreach (element_children($element) as $key) {
  906. $rows[] = array(
  907. theme('indentation', array('size' => $element[$key]['#indent'])) . drupal_render($element[$key]),
  908. );
  909. }
  910. $element['#type'] = 'fieldset';
  911. $element['#value'] = NULL;
  912. $element['#attributes']['class'] = array('webform-component-select-table');
  913. if (!isset($element['#collapsible']) || $element['#collapsible']) {
  914. $element['#attributes']['class'][] = 'collapsible';
  915. }
  916. if (!isset($element['#collapsed']) || $element['#collapsed']) {
  917. $element['#attributes']['class'][] = 'collapsed';
  918. }
  919. if (empty($rows)) {
  920. $element['#children'] = t('No available components.');
  921. }
  922. else {
  923. $element['#children'] = '<div class="webform-component-select-wrapper">' . theme('table', array('header' => $header, 'rows' => $rows)) . '</div>';
  924. }
  925. return theme('fieldset', array('element' => $element));
  926. }
  927. /**
  928. * Find a components parents within a node.
  929. */
  930. function webform_component_parent_keys($node, $component) {
  931. $parents = array($component['form_key']);
  932. $pid = $component['pid'];
  933. while ($pid) {
  934. $parents[] = $node->webform['components'][$pid]['form_key'];
  935. $pid = $node->webform['components'][$pid]['pid'];
  936. }
  937. return array_reverse($parents);
  938. }
  939. /**
  940. * Populate a component with the defaults for that type.
  941. */
  942. function webform_component_defaults(&$component) {
  943. if ($defaults = webform_component_invoke($component['type'], 'defaults')) {
  944. foreach ($defaults as $key => $default) {
  945. if (!isset($component[$key])) {
  946. $component[$key] = $default;
  947. }
  948. }
  949. foreach ($defaults['extra'] as $extra => $default) {
  950. if (!isset($component['extra'][$extra])) {
  951. $component['extra'][$extra] = $default;
  952. }
  953. }
  954. $component['extra'] += array(
  955. 'conditional_component' => '',
  956. 'conditional_operator' => '=',
  957. 'conditional_values' => '',
  958. );
  959. }
  960. }
  961. /**
  962. * Validate an element value is unique with no duplicates in the database.
  963. */
  964. function webform_validate_unique($element, $form_state) {
  965. if ($element['#value'] !== '') {
  966. $nid = $form_state['values']['details']['nid'];
  967. $sid = $form_state['values']['details']['sid'];
  968. $query = db_select('webform_submitted_data')
  969. ->fields('webform_submitted_data', array('sid'))
  970. ->condition('nid', $nid)
  971. ->condition('cid', $element['#webform_component']['cid'])
  972. ->condition('data', $element['#value'])
  973. ->range(0, 1); // More efficient than using countQuery() for data checks.
  974. if ($sid) {
  975. $query->condition('sid', $sid, '<>');
  976. }
  977. $count = $query->execute()->fetchField();
  978. if ($count) {
  979. 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'])));
  980. }
  981. }
  982. }