elements.module 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. <?php
  2. /**
  3. * Implements hook_element_info().
  4. */
  5. function elements_element_info() {
  6. $types['emailfield'] = array(
  7. '#input' => TRUE,
  8. '#size' => 60,
  9. '#maxlength' => 128,
  10. '#autocomplete_path' => FALSE,
  11. '#process' => array('ajax_process_form', 'elements_process_pattern'),
  12. '#element_validate' => array('elements_validate_email'),
  13. '#theme' => 'emailfield',
  14. '#theme_wrappers' => array('form_element'),
  15. );
  16. $types['searchfield'] = array(
  17. '#input' => TRUE,
  18. '#size' => 60,
  19. '#maxlength' => 128,
  20. '#autocomplete_path' => FALSE,
  21. '#process' => array('ajax_process_form'),
  22. '#theme' => 'searchfield',
  23. '#theme_wrappers' => array('form_element'),
  24. );
  25. $types['telfield'] = array(
  26. '#input' => TRUE,
  27. '#size' => 20,
  28. '#maxlength' => 64,
  29. '#process' => array('ajax_process_form', 'elements_process_pattern'),
  30. '#theme' => 'telfield',
  31. '#theme_wrappers' => array('form_element'),
  32. );
  33. $types['urlfield'] = array(
  34. '#input' => TRUE,
  35. '#size' => 80,
  36. '#maxlength' => 128,
  37. '#autocomplete_path' => FALSE,
  38. '#process' => array('ajax_process_form', 'elements_process_pattern'),
  39. '#element_validate' => array('elements_validate_url'),
  40. '#theme' => 'urlfield',
  41. '#theme_wrappers' => array('form_element'),
  42. );
  43. $types['numberfield'] = array(
  44. '#input' => TRUE,
  45. '#step' => 1,
  46. '#process' => array('ajax_process_form'),
  47. '#element_validate' => array('elements_validate_number'),
  48. '#theme' => 'numberfield',
  49. '#theme_wrappers' => array('form_element'),
  50. );
  51. $types['rangefield'] = array(
  52. '#input' => TRUE,
  53. '#step' => 1,
  54. '#min' => 0,
  55. '#max' => 100,
  56. '#process' => array('ajax_process_form'),
  57. '#element_validate' => array('elements_validate_number'),
  58. '#theme' => 'rangefield',
  59. '#theme_wrappers' => array('form_element'),
  60. );
  61. // Backported table element from https://drupal.org/node/80855
  62. $types['table'] = array(
  63. '#header' => array(),
  64. '#rows' => array(),
  65. '#empty' => '',
  66. // Properties for tableselect support.
  67. '#input' => TRUE,
  68. '#tree' => TRUE,
  69. '#tableselect' => FALSE,
  70. '#multiple' => TRUE,
  71. '#js_select' => TRUE,
  72. '#value_callback' => 'elements_table_value',
  73. '#process' => array('elements_table_process'),
  74. '#element_validate' => array('elements_table_validate'),
  75. // Properties for tabledrag support.
  76. // The value is a list of arrays that are passed to drupal_add_tabledrag().
  77. // elements_pre_render_table() prepends the HTML ID of the table to each set
  78. // of arguments.
  79. // @see drupal_add_tabledrag()
  80. '#tabledrag' => array(),
  81. // Render properties.
  82. '#pre_render' => array('elements_pre_render_table'),
  83. '#theme' => 'table',
  84. );
  85. return $types;
  86. }
  87. /**
  88. * Implements hook_element_info_alter().
  89. */
  90. function elements_element_info_alter(&$types) {
  91. // Add placeholder and pattern support to core form elements.
  92. foreach (array_keys($types) as $type) {
  93. switch ($type) {
  94. case 'textfield':
  95. case 'textarea':
  96. case 'password':
  97. $types[$type]['#process'][] = 'elements_process_placeholder';
  98. $types[$type]['#process'][] = 'elements_process_pattern';
  99. break;
  100. }
  101. }
  102. }
  103. /**
  104. * Implements hook_theme().
  105. */
  106. function elements_theme() {
  107. return array(
  108. 'emailfield' => array(
  109. 'arguments' => array('element' => NULL),
  110. 'render element' => 'element',
  111. 'file' => 'elements.theme.inc',
  112. ),
  113. 'searchfield' => array(
  114. 'arguments' => array('element' => NULL),
  115. 'render element' => 'element',
  116. 'file' => 'elements.theme.inc',
  117. ),
  118. 'telfield' => array(
  119. 'arguments' => array('element' => NULL),
  120. 'render element' => 'element',
  121. 'file' => 'elements.theme.inc',
  122. ),
  123. 'urlfield' => array(
  124. 'arguments' => array('element' => NULL),
  125. 'render element' => 'element',
  126. 'file' => 'elements.theme.inc',
  127. ),
  128. 'numberfield' => array(
  129. 'arguments' => array('element' => NULL),
  130. 'render element' => 'element',
  131. 'file' => 'elements.theme.inc',
  132. ),
  133. 'rangefield' => array(
  134. 'arguments' => array('element' => NULL),
  135. 'render element' => 'element',
  136. 'file' => 'elements.theme.inc',
  137. ),
  138. );
  139. }
  140. /**
  141. * Return the autocompletion HTML for a form element.
  142. *
  143. * @param $element
  144. * The renderable element to process for autocompletion.
  145. *
  146. * @return
  147. * The rendered autocompletion element HTML, or an empty string if the field
  148. * has no autocompletion enabled.
  149. */
  150. function elements_add_autocomplete(&$element) {
  151. $extra = '';
  152. if (!empty($element['#autocomplete_path']) && drupal_valid_path($element['#autocomplete_path'])) {
  153. drupal_add_library('system', 'drupal.autocomplete');
  154. $element['#attributes']['class'][] = 'form-autocomplete';
  155. $attributes = array();
  156. $attributes['type'] = 'hidden';
  157. $attributes['id'] = $element['#attributes']['id'] . '-autocomplete';
  158. $attributes['value'] = url($element['#autocomplete_path'], array('absolute' => TRUE));
  159. $attributes['disabled'] = 'disabled';
  160. $attributes['class'][] = 'autocomplete';
  161. $extra = '<input' . drupal_attributes($attributes) . ' />';
  162. }
  163. return $extra;
  164. }
  165. /**
  166. * #process callback for #placeholder form element property.
  167. *
  168. * @param $element
  169. * An associative array containing the properties and children of the
  170. * generic input element.
  171. *
  172. * @return
  173. * The processed element.
  174. */
  175. function elements_process_placeholder($element) {
  176. if (isset($element['#placeholder']) && !isset($element['#attributes']['placeholder'])) {
  177. $element['#attributes']['placeholder'] = $element['#placeholder'];
  178. }
  179. return $element;
  180. }
  181. /**
  182. * #process callback for #pattern form element property.
  183. *
  184. * @param $element
  185. * An associative array containing the properties and children of the
  186. * generic input element.
  187. *
  188. * @return
  189. * The processed element.
  190. *
  191. * @see elements_validate_pattern()
  192. */
  193. function elements_process_pattern($element) {
  194. if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) {
  195. $element['#attributes']['pattern'] = $element['#pattern'];
  196. $element['#element_validate'][] = 'form_validate_pattern';
  197. }
  198. return $element;
  199. }
  200. /**
  201. * #element_validate callback for #pattern form element property.
  202. *
  203. * @param $element
  204. * An associative array containing the properties and children of the
  205. * generic form element.
  206. * @param $form_state
  207. * The $form_state array for the form this element belongs to.
  208. *
  209. * @see element_process_pattern()
  210. */
  211. function elements_validate_pattern($element, &$form_state) {
  212. if ($element['#value'] !== '') {
  213. // The pattern must match the entire string and should have the same
  214. // behavior as the RegExp object in ECMA 262.
  215. // - Use bracket-style delimiters to avoid introducing a special delimiter
  216. // character like '/' that would have to be escaped.
  217. // - Put in brackets so that the pattern can't interfere with what's
  218. // prepended and appended.
  219. $pattern = '{^(?:' . $element['#pattern'] . ')$}';
  220. if (!preg_match($pattern, $element['#value'])) {
  221. form_error($element, t('%name field is not in the right format.', array('%name' => $element['#title'])));
  222. }
  223. }
  224. }
  225. /**
  226. * Form element validation handler for #type 'email'.
  227. *
  228. * Note that #maxlength and #required is validated by _form_validate() already.
  229. */
  230. function elements_validate_email(&$element, &$form_state) {
  231. if ($element['#value'] && !valid_email_address($element['#value'])) {
  232. form_error($element, t('The e-mail address %mail is not valid.', array('%mail' => $element['#value'])));
  233. }
  234. }
  235. /**
  236. * Form element validation handler for #type 'url'.
  237. *
  238. * Note that #maxlength and #required is validated by _form_validate() already.
  239. */
  240. function elements_validate_url(&$element, &$form_state) {
  241. if ($element['#value'] && !valid_url($element['#value'], TRUE)) {
  242. form_error($element, t('The URL %url is not valid.', array('%url' => $element['#value'])));
  243. }
  244. }
  245. /**
  246. * Form element validation handler for #type 'number'.
  247. *
  248. * Note that #required is validated by _form_validate() already.
  249. */
  250. function elements_validate_number(&$element, &$form_state) {
  251. $value = $element['#value'];
  252. if ($value === '') {
  253. return;
  254. }
  255. $name = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
  256. // Ensure the input is numeric.
  257. if (!is_numeric($value)) {
  258. form_error($element, t('%name must be a number.', array('%name' => $name)));
  259. return;
  260. }
  261. // Ensure that the input is greater than the #min property, if set.
  262. if (isset($element['#min']) && $value < $element['#min']) {
  263. form_error($element, t('%name must be higher or equal to %min.', array('%name' => $name, '%min' => $element['#min'])));
  264. }
  265. // Ensure that the input is less than the #max property, if set.
  266. if (isset($element['#max']) && $value > $element['#max']) {
  267. form_error($element, t('%name must be below or equal to %max.', array('%name' => $name, '%max' => $element['#max'])));
  268. }
  269. if (isset($element['#step']) && strtolower($element['#step']) != 'any') {
  270. // Check that the input is an allowed multiple of #step (offset by #min if
  271. // #min is set).
  272. $offset = isset($element['#min']) ? $element['#min'] : 0.0;
  273. if (!elements_valid_number_step($value, $element['#step'], $offset)) {
  274. form_error($element, t('%name is not a valid number.', array('%name' => $name)));
  275. }
  276. }
  277. }
  278. /**
  279. * Verifies that a number is a multiple of a given step.
  280. *
  281. * The implementation assumes it is dealing with IEEE 754 double precision
  282. * floating point numbers that are used by PHP on most systems.
  283. *
  284. * This is based on the number/range verification methods of webkit.
  285. *
  286. * @param $value
  287. * The value that needs to be checked.
  288. * @param $step
  289. * The step scale factor. Must be positive.
  290. * @param $offset
  291. * (optional) An offset, to which the difference must be a multiple of the
  292. * given step.
  293. *
  294. * @return bool
  295. * TRUE if no step mismatch has occured, or FALSE otherwise.
  296. *
  297. * @see http://opensource.apple.com/source/WebCore/WebCore-1298/html/NumberInputType.cpp
  298. */
  299. function elements_valid_number_step($value, $step, $offset = 0.0) {
  300. $double_value = (double) abs($value - $offset);
  301. // The fractional part of a double has 53 bits. The greatest number that could
  302. // be represented with that is 2^53. If the given value is even bigger than
  303. // $step * 2^53, then dividing by $step will result in a very small remainder.
  304. // Since that remainder can't even be represented with a single precision
  305. // float the following computation of the remainder makes no sense and we can
  306. // safely ignore it instead.
  307. if ($double_value / pow(2.0, 53) > $step) {
  308. return TRUE;
  309. }
  310. // Now compute that remainder of a division by $step.
  311. $remainder = (double) abs($double_value - $step * round($double_value / $step));
  312. // $remainder is a double precision floating point number. Remainders that
  313. // can't be represented with single precision floats are acceptable. The
  314. // fractional part of a float has 24 bits. That means remainders smaller than
  315. // $step * 2^-24 are acceptable.
  316. $computed_acceptable_error = (double) ($step / pow(2.0, 24));
  317. return $computed_acceptable_error >= $remainder || $remainder >= ($step - $computed_acceptable_error);
  318. }
  319. /**
  320. * Determines the value of a table form element.
  321. *
  322. * @param array $element
  323. * The form element whose value is being populated.
  324. * @param array|false $input
  325. * The incoming input to populate the form element. If this is FALSE,
  326. * the element's default value should be returned.
  327. *
  328. * @return array
  329. * The data that will appear in the $form_state['values'] collection
  330. * for this element. Return nothing to use the default.
  331. */
  332. function elements_table_value(array $element, $input = FALSE) {
  333. // If #multiple is FALSE, the regular default value of radio buttons is used.
  334. if (!empty($element['#tableselect']) && !empty($element['#multiple'])) {
  335. // Contrary to #type 'checkboxes', the default value of checkboxes in a
  336. // table is built from the array keys (instead of array values) of the
  337. // #default_value property.
  338. // @todo D8: Remove this inconsistency.
  339. if ($input === FALSE) {
  340. $element += array('#default_value' => array());
  341. return drupal_map_assoc(array_keys(array_filter($element['#default_value'])));
  342. }
  343. else {
  344. return is_array($input) ? drupal_map_assoc($input) : array();
  345. }
  346. }
  347. }
  348. /**
  349. * Creates checkbox or radio elements to populate a tableselect table.
  350. *
  351. * @param $element
  352. * An associative array containing the properties and children of the
  353. * tableselect element.
  354. *
  355. * @return
  356. * The processed element.
  357. */
  358. function elements_table_process($element, &$form_state) {
  359. if ($element['#tableselect']) {
  360. if ($element['#multiple']) {
  361. $value = is_array($element['#value']) ? $element['#value'] : array();
  362. }
  363. // Advanced selection behaviour makes no sense for radios.
  364. else {
  365. $element['#js_select'] = FALSE;
  366. }
  367. // Add a "Select all" checkbox column to the header.
  368. // @todo D8: Rename into #select_all?
  369. if ($element['#js_select']) {
  370. $element['#attached']['js'][] = 'misc/tableselect.js';
  371. array_unshift($element['#header'], array('class' => array('select-all')));
  372. }
  373. // Add an empty header column for radio buttons or when a "Select all"
  374. // checkbox is not desired.
  375. else {
  376. array_unshift($element['#header'], '');
  377. }
  378. if (!isset($element['#default_value']) || $element['#default_value'] === 0) {
  379. $element['#default_value'] = array();
  380. }
  381. // Create a checkbox or radio for each row in a way that the value of the
  382. // tableselect element behaves as if it had been of #type checkboxes or
  383. // radios.
  384. foreach (element_children($element) as $key) {
  385. // Do not overwrite manually created children.
  386. if (!isset($element[$key]['select'])) {
  387. // Determine option label; either an assumed 'title' column, or the
  388. // first available column containing a #title or #markup.
  389. // @todo Consider to add an optional $element[$key]['#title_key']
  390. // defaulting to 'title'?
  391. $title = '';
  392. if (!empty($element[$key]['title']['#title'])) {
  393. $title = $element[$key]['title']['#title'];
  394. }
  395. else {
  396. foreach (element_children($element[$key]) as $column) {
  397. if (isset($element[$key][$column]['#title'])) {
  398. $title = $element[$key][$column]['#title'];
  399. break;
  400. }
  401. if (isset($element[$key][$column]['#markup'])) {
  402. $title = $element[$key][$column]['#markup'];
  403. break;
  404. }
  405. }
  406. }
  407. if ($title !== '') {
  408. $title = t('Update !title', array('!title' => $title));
  409. }
  410. // Prepend the select column to existing columns.
  411. $element[$key] = array('select' => array()) + $element[$key];
  412. $element[$key]['select'] += array(
  413. '#type' => $element['#multiple'] ? 'checkbox' : 'radio',
  414. '#title' => $title,
  415. '#title_display' => 'invisible',
  416. // @todo If rows happen to use numeric indexes instead of string keys,
  417. // this results in a first row with $key === 0, which is always FALSE.
  418. '#return_value' => $key,
  419. '#attributes' => $element['#attributes'],
  420. );
  421. $element_parents = array_merge($element['#parents'], array($key));
  422. if ($element['#multiple']) {
  423. $element[$key]['select']['#default_value'] = isset($value[$key]) ? $key : NULL;
  424. $element[$key]['select']['#parents'] = $element_parents;
  425. }
  426. else {
  427. $element[$key]['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL);
  428. $element[$key]['select']['#parents'] = $element['#parents'];
  429. $element[$key]['select']['#id'] = drupal_html_id('edit-' . implode('-', $element_parents));
  430. }
  431. }
  432. }
  433. }
  434. return $element;
  435. }
  436. /**
  437. * #element_validate callback for #type 'table'.
  438. *
  439. * @param array $element
  440. * An associative array containing the properties and children of the
  441. * table element.
  442. * @param array $form_state
  443. * The current state of the form.
  444. */
  445. function elements_table_validate($element, &$form_state) {
  446. // Skip this validation if the button to submit the form does not require
  447. // selected table row data.
  448. //if (empty($form_state['triggering_element']['#tableselect'])) {
  449. // return;
  450. //}
  451. if ($element['#multiple']) {
  452. if (!is_array($element['#value']) || !count(array_filter($element['#value']))) {
  453. form_error($element, t('No items selected.'));
  454. }
  455. }
  456. elseif (!isset($element['#value']) || $element['#value'] === '') {
  457. form_error($element, t('No item selected.'));
  458. }
  459. }
  460. /**
  461. * #pre_render callback to transform children of an element into #rows suitable for theme_table().
  462. *
  463. * This function converts sub-elements of an element of #type 'table' to be
  464. * suitable for theme_table():
  465. * - The first level of sub-elements are table rows. Only the #attributes
  466. * property is taken into account.
  467. * - The second level of sub-elements is converted into columns for the
  468. * corresponding first-level table row.
  469. *
  470. * Simple example usage:
  471. * @code
  472. * $form['table'] = array(
  473. * '#type' => 'table',
  474. * '#header' => array(t('Title'), array('data' => t('Operations'), 'colspan' => '1')),
  475. * // Optionally, to add tableDrag support:
  476. * '#tabledrag' => array(
  477. * array('order', 'sibling', 'thing-weight'),
  478. * ),
  479. * );
  480. * foreach ($things as $row => $thing) {
  481. * $form['table'][$row]['#weight'] = $thing['weight'];
  482. *
  483. * $form['table'][$row]['title'] = array(
  484. * '#type' => 'textfield',
  485. * '#default_value' => $thing['title'],
  486. * );
  487. *
  488. * // Optionally, to add tableDrag support:
  489. * $form['table'][$row]['#attributes']['class'][] = 'draggable';
  490. * $form['table'][$row]['weight'] = array(
  491. * '#type' => 'textfield',
  492. * '#title' => t('Weight for @title', array('@title' => $thing['title'])),
  493. * '#title_display' => 'invisible',
  494. * '#size' => 4,
  495. * '#default_value' => $thing['weight'],
  496. * '#attributes' => array('class' => array('thing-weight')),
  497. * );
  498. *
  499. * // The amount of link columns should be identical to the 'colspan'
  500. * // attribute in #header above.
  501. * $form['table'][$row]['edit'] = array(
  502. * '#type' => 'link',
  503. * '#title' => t('Edit'),
  504. * '#href' => 'thing/' . $row . '/edit',
  505. * );
  506. * }
  507. * @endcode
  508. *
  509. * @param array $element
  510. * A structured array containing two sub-levels of elements. Properties used:
  511. * - #tabledrag: The value is a list of arrays that are passed to
  512. * drupal_add_tabledrag(). The HTML ID of the table is prepended to each set
  513. * of arguments.
  514. *
  515. * @see elements_element_info()
  516. * @see theme_table()
  517. * @see drupal_process_attached()
  518. * @see drupal_add_tabledrag()
  519. */
  520. function elements_pre_render_table(array $element) {
  521. foreach (element_children($element) as $first) {
  522. $row = array('data' => array());
  523. // Apply attributes of first-level elements as table row attributes.
  524. if (isset($element[$first]['#attributes'])) {
  525. $row += $element[$first]['#attributes'];
  526. }
  527. // Turn second-level elements into table row columns.
  528. // @todo Do not render a cell for children of #type 'value'.
  529. // @see http://drupal.org/node/1248940
  530. foreach (element_children($element[$first]) as $second) {
  531. // Assign the element by reference, so any potential changes to the
  532. // original element are taken over.
  533. $column = array('data' => &$element[$first][$second]);
  534. // Apply wrapper attributes of second-level elements as table cell
  535. // attributes.
  536. //if (isset($element[$first][$second]['#wrapper_attributes'])) {
  537. // $column += $element[$first][$second]['#wrapper_attributes'];
  538. //}
  539. $row['data'][] = $column;
  540. }
  541. $element['#rows'][] = $row;
  542. }
  543. // Take over $element['#id'] as HTML ID attribute, if not already set.
  544. element_set_attributes($element, array('id'));
  545. // If the custom #tabledrag is set and there is a HTML ID, inject the table's
  546. // HTML ID as first callback argument and attach the behavior.
  547. if (!empty($element['#tabledrag']) && isset($element['#attributes']['id'])) {
  548. foreach ($element['#tabledrag'] as &$args) {
  549. array_unshift($args, $element['#attributes']['id']);
  550. }
  551. $element['#attached']['drupal_add_tabledrag'] = $element['#tabledrag'];
  552. }
  553. if (!empty($element['#tabledrag']) && !empty($element['#tableselect'])) {
  554. $element['#attached']['css'][] = drupal_get_path('module', 'elements') . '/elements.table.css';
  555. }
  556. return $element;
  557. }