datetime.module

Field hooks to implement a simple datetime field.

File

drupal/core/modules/datetime/datetime.module
View source
  1. <?php
  2. /**
  3. * @file
  4. * Field hooks to implement a simple datetime field.
  5. */
  6. use Drupal\Component\Utility\NestedArray;
  7. use Drupal\Core\Datetime\DrupalDateTime;
  8. use Drupal\Core\Template\Attribute;
  9. use Drupal\datetime\DateHelper;
  10. use Drupal\node\NodeInterface;
  11. /**
  12. * Defines the timezone that dates should be stored in.
  13. */
  14. const DATETIME_STORAGE_TIMEZONE = 'UTC';
  15. /**
  16. * Defines the format that date and time should be stored in.
  17. */
  18. const DATETIME_DATETIME_STORAGE_FORMAT = 'Y-m-d\TH:i:s';
  19. /**
  20. * Defines the format that dates should be stored in.
  21. */
  22. const DATETIME_DATE_STORAGE_FORMAT = 'Y-m-d';
  23. /**
  24. * Implements hook_element_info().
  25. */
  26. function datetime_element_info() {
  27. $format_type = datetime_default_format_type();
  28. $types['datetime'] = array(
  29. '#input' => TRUE,
  30. '#element_validate' => array('datetime_datetime_validate'),
  31. '#process' => array('datetime_datetime_form_process'),
  32. '#theme' => 'datetime_form',
  33. '#theme_wrappers' => array('datetime_wrapper'),
  34. '#date_date_format' => config('system.date')->get('formats.html_date.pattern.' . $format_type),
  35. '#date_date_element' => 'date',
  36. '#date_date_callbacks' => array(),
  37. '#date_time_format' => config('system.date')->get('formats.html_time.pattern.' . $format_type),
  38. '#date_time_element' => 'time',
  39. '#date_time_callbacks' => array(),
  40. '#date_year_range' => '1900:2050',
  41. '#date_increment' => 1,
  42. '#date_timezone' => '',
  43. );
  44. $types['datelist'] = array(
  45. '#input' => TRUE,
  46. '#element_validate' => array('datetime_datelist_validate'),
  47. '#process' => array('datetime_datelist_form_process'),
  48. '#theme' => 'datelist_form',
  49. '#theme_wrappers' => array('datetime_wrapper'),
  50. '#date_part_order' => array('year', 'month', 'day', 'hour', 'minute'),
  51. '#date_year_range' => '1900:2050',
  52. '#date_increment' => 1,
  53. '#date_date_callbacks' => array(),
  54. '#date_timezone' => '',
  55. );
  56. return $types;
  57. }
  58. /**
  59. * Implements hook_theme().
  60. */
  61. function datetime_theme() {
  62. return array(
  63. 'datetime_form' => array(
  64. 'render element' => 'element',
  65. ),
  66. 'datelist_form' => array(
  67. 'render element' => 'element',
  68. ),
  69. 'datetime_wrapper' => array(
  70. 'render element' => 'element',
  71. ),
  72. );
  73. }
  74. /**
  75. * Implements hook_field_is_empty().
  76. */
  77. function datetime_field_is_empty($item, $field) {
  78. if (empty($item['value'])) {
  79. return TRUE;
  80. }
  81. return FALSE;
  82. }
  83. /**
  84. * Implements hook_field_info().
  85. */
  86. function datetime_field_info() {
  87. return array(
  88. 'datetime' => array(
  89. 'label' => 'Date',
  90. 'description' => t('Create and store date values.'),
  91. 'settings' => array(
  92. 'datetime_type' => 'datetime',
  93. ),
  94. 'instance_settings' => array(
  95. 'default_value' => 'now',
  96. ),
  97. 'default_widget' => 'datetime_default',
  98. 'default_formatter' => 'datetime_default',
  99. 'default_token_formatter' => 'datetime_plain',
  100. 'field item class' => '\Drupal\datetime\Type\DateTimeItem',
  101. ),
  102. );
  103. }
  104. /**
  105. * Implements hook_field_settings_form().
  106. */
  107. function datetime_field_settings_form($field, $instance, $has_data) {
  108. $settings = $field['settings'];
  109. $form['datetime_type'] = array(
  110. '#type' => 'select',
  111. '#title' => t('Date type'),
  112. '#description' => t('Choose the type of date to create.'),
  113. '#default_value' => $settings['datetime_type'],
  114. '#options' => array(
  115. 'datetime' => t('Date and time'),
  116. 'date' => t('Date only'),
  117. ),
  118. );
  119. return $form;
  120. }
  121. /**
  122. * Implements hook_field_instance_settings_form().
  123. */
  124. function datetime_field_instance_settings_form($field, $instance) {
  125. $settings = $instance['settings'];
  126. $form['default_value'] = array(
  127. '#type' => 'select',
  128. '#title' => t('Default date'),
  129. '#description' => t('Set a default value for this date.'),
  130. '#default_value' => $settings['default_value'],
  131. '#options' => array('blank' => t('No default value'), 'now' => t('The current date')),
  132. '#weight' => 1,
  133. );
  134. return $form;
  135. }
  136. /**
  137. * Validation callback for the datetime widget element.
  138. *
  139. * The date has already been validated by the datetime form type validator and
  140. * transformed to an date object. We just need to convert the date back to a the
  141. * storage timezone and format.
  142. *
  143. * @param array $element
  144. * The form element whose value is being validated.
  145. * @param array $form_state
  146. * The current state of the form.
  147. */
  148. function datetime_datetime_widget_validate(&$element, &$form_state) {
  149. if (!form_get_errors()) {
  150. $input_exists = FALSE;
  151. $input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
  152. if ($input_exists) {
  153. // The date should have been returned to a date object at this point by
  154. // datetime_validate(), which runs before this.
  155. if (!empty($input['value'])) {
  156. $date = $input['value'];
  157. if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
  158. // If this is a date-only field, set it to the default time so the
  159. // timezone conversion can be reversed.
  160. if ($element['value']['#date_time_element'] == 'none') {
  161. datetime_date_default_time($date);
  162. }
  163. // Adjust the date for storage.
  164. $date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
  165. $value = $date->format($element['value']['#date_storage_format']);
  166. form_set_value($element['value'], $value, $form_state);
  167. }
  168. }
  169. }
  170. }
  171. }
  172. /**
  173. * Validation callback for the datelist widget element.
  174. *
  175. * The date has already been validated by the datetime form type validator and
  176. * transformed to an date object. We just need to convert the date back to a the
  177. * storage timezone and format.
  178. *
  179. * @param array $element
  180. * The form element whose value is being validated.
  181. * @param array $form_state
  182. * The current state of the form.
  183. */
  184. function datetime_datelist_widget_validate(&$element, &$form_state) {
  185. if (!form_get_errors()) {
  186. $input_exists = FALSE;
  187. $input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
  188. if ($input_exists) {
  189. // The date should have been returned to a date object at this point by
  190. // datetime_validate(), which runs before this.
  191. if (!empty($input['value'])) {
  192. $date = $input['value'];
  193. if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
  194. // If this is a date-only field, set it to the default time so the
  195. // timezone conversion can be reversed.
  196. if (!in_array('hour', $element['value']['#date_part_order'])) {
  197. datetime_date_default_time($date);
  198. }
  199. // Adjust the date for storage.
  200. $date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
  201. $value = $date->format($element['value']['#date_storage_format']);
  202. form_set_value($element['value'], $value, $form_state);
  203. }
  204. }
  205. }
  206. }
  207. }
  208. /**
  209. * Implements hook_field_load().
  210. *
  211. * The function generates a Date object for each field early so that it is
  212. * cached in the field cache. This avoids the need to generate the object later.
  213. * The date will be retrieved in UTC, the local timezone adjustment must be made
  214. * in real time, based on the preferences of the site and user.
  215. */
  216. function datetime_field_load($entity_type, $entities, $field, $instances, $langcode, &$items) {
  217. foreach ($entities as $id => $entity) {
  218. foreach ($items[$id] as $delta => $item) {
  219. $items[$id][$delta]['date'] = NULL;
  220. $value = isset($item['value']) ? $item['value'] : NULL;
  221. if (!empty($value)) {
  222. $storage_format = $field['settings']['datetime_type'] == 'date' ? DATETIME_DATE_STORAGE_FORMAT: DATETIME_DATETIME_STORAGE_FORMAT;
  223. $date = new DrupalDateTime($value, DATETIME_STORAGE_TIMEZONE, $storage_format);
  224. if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
  225. $items[$id][$delta]['date'] = $date;
  226. }
  227. }
  228. }
  229. }
  230. }
  231. /**
  232. * Sets a default value for an empty date field.
  233. *
  234. * Callback for $instance['default_value_function'], as implemented by
  235. * Drupal\datetime\Plugin\field\widget\DateTimeDatepicker.
  236. *
  237. * @param $entity_type
  238. *
  239. * @param $entity
  240. *
  241. * @param array $field
  242. *
  243. * @param array $instance
  244. *
  245. * @param $langcode
  246. *
  247. *
  248. * @return array
  249. *
  250. */
  251. function datetime_default_value($entity, $field, $instance, $langcode) {
  252. $value = '';
  253. $date = '';
  254. if ($instance['settings']['default_value'] == 'now') {
  255. // A default value should be in the format and timezone used for date
  256. // storage.
  257. $date = new DrupalDateTime('now', DATETIME_STORAGE_TIMEZONE);
  258. $storage_format = $field['settings']['datetime_type'] == 'date' ? DATETIME_DATE_STORAGE_FORMAT: DATETIME_DATETIME_STORAGE_FORMAT;
  259. $value = $date->format($storage_format);
  260. }
  261. // We only provide a default value for the first item, as do all fields.
  262. // Otherwise, there is no way to clear out unwanted values on multiple value
  263. // fields.
  264. $item = array();
  265. $item[0]['value'] = $value;
  266. $item[0]['date'] = $date;
  267. return $item;
  268. }
  269. /**
  270. * Sets a consistent time on a date without time.
  271. *
  272. * The default time for a date without time can be anything, so long as it is
  273. * consistently applied. If we use noon, dates in most timezones will have the
  274. * same value for in both the local timezone and UTC.
  275. *
  276. * @param $date
  277. *
  278. */
  279. function datetime_date_default_time($date) {
  280. $date->setTime(12, 0, 0);
  281. }
  282. /**
  283. * Returns HTML for a HTML5-compatible #datetime form element.
  284. *
  285. * Wrapper around the date element type which creates a date and a time
  286. * component for a date.
  287. *
  288. * @param array $variables
  289. * An associative array containing:
  290. * - element: An associative array containing the properties of the element.
  291. * Properties used: #title, #value, #options, #description, #required,
  292. * #attributes.
  293. *
  294. * @ingroup themeable
  295. * @see form_process_datetime()
  296. */
  297. function theme_datetime_form($variables) {
  298. $element = $variables['element'];
  299. $attributes = array();
  300. if (isset($element['#id'])) {
  301. $attributes['id'] = $element['#id'];
  302. }
  303. if (!empty($element['#attributes']['class'])) {
  304. $attributes['class'] = (array) $element['#attributes']['class'];
  305. }
  306. $attributes['class'][] = 'container-inline';
  307. return '<div' . new Attribute($attributes) . '>' . drupal_render_children($element) . '</div>';
  308. }
  309. /**
  310. * Returns HTML for a date selection form element.
  311. *
  312. * @param array $variables
  313. * An associative array containing:
  314. * - element: An associative array containing the properties of the element.
  315. * Properties used: #title, #value, #options, #description, #required,
  316. * #attributes.
  317. *
  318. * @ingroup themeable
  319. */
  320. function theme_datelist_form($variables) {
  321. $element = $variables['element'];
  322. $attributes = array();
  323. if (isset($element['#id'])) {
  324. $attributes['id'] = $element['#id'];
  325. }
  326. if (!empty($element['#attributes']['class'])) {
  327. $attributes['class'] = (array) $element['#attributes']['class'];
  328. }
  329. $attributes['class'][] = 'container-inline';
  330. return '<div' . new Attribute($attributes) . '>' . drupal_render_children($element) . '</div>';
  331. }
  332. /**
  333. * Returns HTML for a datetime form element.
  334. *
  335. * @ingroup themeable
  336. */
  337. function theme_datetime_wrapper($variables) {
  338. $element = $variables['element'];
  339. $output = '';
  340. // If the element is required, a required marker is appended to the label.
  341. $required = '';
  342. if(!empty($element['#required'])) {
  343. $form_required_marker = array(
  344. '#theme' => 'form_required_marker',
  345. '#element' => $element,
  346. );
  347. $required = drupal_render($form_required_marker);
  348. }
  349. if (!empty($element['#title'])) {
  350. $output .= '<h4 class="label">' . t('!title!required', array('!title' => $element['#title'], '!required' => $required)) . '</h4>';
  351. }
  352. $output .= $element['#children'];
  353. return $output;
  354. }
  355. /**
  356. * Expands a #datetime element type into date and/or time elements.
  357. *
  358. * All form elements are designed to have sane defaults so any or all can be
  359. * omitted. Both the date and time components are configurable so they can be
  360. * output as HTML5 datetime elements or not, as desired.
  361. *
  362. * Examples of possible configurations include:
  363. * HTML5 date and time:
  364. * #date_date_element = 'date';
  365. * #date_time_element = 'time';
  366. * HTML5 datetime:
  367. * #date_date_element = 'datetime';
  368. * #date_time_element = 'none';
  369. * HTML5 time only:
  370. * #date_date_element = 'none';
  371. * #date_time_element = 'time'
  372. * Non-HTML5:
  373. * #date_date_element = 'text';
  374. * #date_time_element = 'text';
  375. *
  376. * Required settings:
  377. * - #default_value: A DrupalDateTime object, adjusted to the proper local
  378. * timezone. Converting a date stored in the database from UTC to the local
  379. * zone and converting it back to UTC before storing it is not handled here.
  380. * This element accepts a date as the default value, and then converts the
  381. * user input strings back into a new date object on submission. No timezone
  382. * adjustment is performed.
  383. * Optional properties include:
  384. * - #date_date_format: A date format string that describes the format that
  385. * should be displayed to the end user for the date. When using HTML5
  386. * elements the format MUST use the appropriate HTML5 format for that
  387. * element, no other format will work. See the format_date() function for a
  388. * list of the possible formats and HTML5 standards for the HTML5
  389. * requirements. Defaults to the right HTML5 format for the chosen element
  390. * if a HTML5 element is used, otherwise defaults to
  391. * config('system.date')->get('formats.html_date.pattern.php').
  392. * - #date_date_element: The date element. Options are:
  393. * - datetime: Use the HTML5 datetime element type.
  394. * - datetime-local: Use the HTML5 datetime-local element type.
  395. * - date: Use the HTML5 date element type.
  396. * - text: No HTML5 element, use a normal text field.
  397. * - none: Do not display a date element.
  398. * - #date_date_callbacks: Array of optional callbacks for the date element.
  399. * Can be used to add a jQuery datepicker.
  400. * - #date_time_element: The time element. Options are:
  401. * - time: Use a HTML5 time element type.
  402. * - text: No HTML5 element, use a normal text field.
  403. * - none: Do not display a time element.
  404. * - #date_time_format: A date format string that describes the format that
  405. * should be displayed to the end user for the time. When using HTML5
  406. * elements the format MUST use the appropriate HTML5 format for that
  407. * element, no other format will work. See the format_date() function for
  408. * a list of the possible formats and HTML5 standards for the HTML5
  409. * requirements. Defaults to the right HTML5 format for the chosen element
  410. * if a HTML5 element is used, otherwise defaults to
  411. * config('system.date')->get('formats.html_time.pattern.php').
  412. * - #date_time_callbacks: An array of optional callbacks for the time
  413. * element. Can be used to add a jQuery timepicker or an 'All day' checkbox.
  414. * - #date_year_range: A description of the range of years to allow, like
  415. * '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
  416. * earliest year and the second the latest year in the range. A year
  417. * in either position means that specific year. A +/- value describes a
  418. * dynamic value that is that many years earlier or later than the current
  419. * year at the time the form is displayed. Used in jQueryUI datepicker year
  420. * range and HTML5 min/max date settings. Defaults to '1900:2050'.
  421. * - #date_increment: The increment to use for minutes and seconds, i.e.
  422. * '15' would show only :00, :15, :30 and :45. Used for HTML5 step values and
  423. * jQueryUI datepicker settings. Defaults to 1 to show every minute.
  424. * - #date_timezone: The local timezone to use when creating dates. Generally
  425. * this should be left empty and it will be set correctly for the user using
  426. * the form. Useful if the default value is empty to designate a desired
  427. * timezone for dates created in form processing. If a default date is
  428. * provided, this value will be ignored, the timezone in the default date
  429. * takes precedence. Defaults to the value returned by
  430. * drupal_get_user_timezone().
  431. *
  432. * Example usage:
  433. * @code
  434. * $form = array(
  435. * '#type' => 'datetime',
  436. * '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
  437. * '#date_date_element' => 'date',
  438. * '#date_time_element' => 'none',
  439. * '#date_year_range' => '2010:+3',
  440. * );
  441. * @endcode
  442. *
  443. * @param array $element
  444. * The form element whose value is being processed.
  445. * @param array $form_state
  446. * The current state of the form.
  447. *
  448. * @return array
  449. * The form element whose value has been processed.
  450. */
  451. function datetime_datetime_form_process($element, &$form_state) {
  452. // The value callback has populated the #value array.
  453. $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
  454. // Set a fallback timezone.
  455. if ($date instanceOf DrupalDateTime) {
  456. $element['#date_timezone'] = $date->getTimezone()->getName();
  457. }
  458. elseif (!empty($element['#timezone'])) {
  459. $element['#date_timezone'] = $element['#date_timezone'];
  460. }
  461. else {
  462. $element['#date_timezone'] = drupal_get_user_timezone();
  463. }
  464. $element['#tree'] = TRUE;
  465. if ($element['#date_date_element'] != 'none') {
  466. $date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
  467. $date_value = !empty($date) ? $date->format($date_format) : $element['#value']['date'];
  468. // Creating format examples on every individual date item is messy, and
  469. // placeholders are invalid for HTML5 date and datetime, so an example
  470. // format is appended to the title to appear in tooltips.
  471. $extra_attributes = array(
  472. 'title' => t('Date (i.e. !format)', array('!format' => datetime_format_example($date_format))),
  473. 'type' => $element['#date_date_element'],
  474. );
  475. // Adds the HTML5 date attributes.
  476. if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
  477. $html5_min = clone($date);
  478. $range = datetime_range_years($element['#date_year_range'], $date);
  479. $html5_min->setDate($range[0], 1, 1)->setTime(0, 0, 0);
  480. $html5_max = clone($date);
  481. $html5_max->setDate($range[1], 12, 31)->setTime(23, 59, 59);
  482. $extra_attributes += array(
  483. 'min' => $html5_min->format($date_format),
  484. 'max' => $html5_max->format($date_format),
  485. );
  486. }
  487. $element['date'] = array(
  488. '#type' => 'date',
  489. '#title' => t('Date'),
  490. '#title_display' => 'invisible',
  491. '#value' => $date_value,
  492. '#attributes' => $element['#attributes'] + $extra_attributes,
  493. '#required' => $element['#required'],
  494. '#size' => max(12, strlen($element['#value']['date'])),
  495. );
  496. // Allows custom callbacks to alter the element.
  497. if (!empty($element['#date_date_callbacks'])) {
  498. foreach ($element['#date_date_callbacks'] as $callback) {
  499. if (function_exists($callback)) {
  500. $callback($element, $form_state, $date);
  501. }
  502. }
  503. }
  504. }
  505. if ($element['#date_time_element'] != 'none') {
  506. $time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
  507. $time_value = !empty($date) ? $date->format($time_format) : $element['#value']['time'];
  508. // Adds the HTML5 attributes.
  509. $extra_attributes = array(
  510. 'title' =>t('Time (i.e. !format)', array('!format' => datetime_format_example($time_format))),
  511. 'type' => $element['#date_time_element'],
  512. 'step' => $element['#date_increment'],
  513. );
  514. $element['time'] = array(
  515. '#type' => 'date',
  516. '#title' => t('Time'),
  517. '#title_display' => 'invisible',
  518. '#value' => $time_value,
  519. '#attributes' => $element['#attributes'] + $extra_attributes,
  520. '#required' => $element['#required'],
  521. '#size' => 12,
  522. );
  523. // Allows custom callbacks to alter the element.
  524. if (!empty($element['#date_time_callbacks'])) {
  525. foreach ($element['#date_time_callbacks'] as $callback) {
  526. if (function_exists($callback)) {
  527. $callback($element, $form_state, $date);
  528. }
  529. }
  530. }
  531. }
  532. return $element;
  533. }
  534. /**
  535. * Value callback for a datetime element.
  536. *
  537. * @param array $element
  538. * The form element whose value is being populated.
  539. * @param array $input
  540. * (optional) The incoming input to populate the form element. If this is
  541. * FALSE, the element's default value should be returned. Defaults to FALSE.
  542. *
  543. * @return array
  544. * The data that will appear in the $element_state['values'] collection for
  545. * this element. Return nothing to use the default.
  546. */
  547. function form_type_datetime_value($element, $input = FALSE) {
  548. if ($input !== FALSE) {
  549. $date_input = $element['#date_date_element'] != 'none' && !empty($input['date']) ? $input['date'] : '';
  550. $time_input = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : '';
  551. $date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
  552. $time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
  553. $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
  554. // Seconds will be omitted in a post in case there's no entry.
  555. if (!empty($time_input) && strlen($time_input) == 5) {
  556. $time_input .= ':00';
  557. }
  558. $date = new DrupalDateTime(trim($date_input . ' ' . $time_input), $timezone, trim($date_format . ' ' . $time_format));
  559. $input = array(
  560. 'date' => $date_input,
  561. 'time' => $time_input,
  562. 'object' => $date instanceOf DrupalDateTime && !$date->hasErrors() ? $date : NULL,
  563. );
  564. }
  565. else {
  566. $date = $element['#default_value'];
  567. if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
  568. $input = array(
  569. 'date' => $date->format($element['#date_date_format']),
  570. 'time' => $date->format($element['#date_time_format']),
  571. 'object' => $date,
  572. );
  573. }
  574. else {
  575. $input = array(
  576. 'date' => '',
  577. 'time' => '',
  578. 'object' => NULL,
  579. );
  580. }
  581. }
  582. return $input;
  583. }
  584. /**
  585. * Validation callback for a datetime element.
  586. *
  587. * If the date is valid, the date object created from the user input is set in
  588. * the form for use by the caller. The work of compiling the user input back
  589. * into a date object is handled by the value callback, so we can use it here.
  590. * We also have the raw input available for validation testing.
  591. *
  592. * @param array $element
  593. * The form element whose value is being validated.
  594. * @param array $form_state
  595. * The current state of the form.
  596. */
  597. function datetime_datetime_validate($element, &$form_state) {
  598. $input_exists = FALSE;
  599. $input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
  600. if ($input_exists) {
  601. $title = !empty($element['#title']) ? $element['#title'] : '';
  602. $date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
  603. $time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
  604. $format = trim($date_format . ' ' . $time_format);
  605. // If there's empty input and the field is not required, set it to empty.
  606. if (empty($input['date']) && empty($input['time']) && !$element['#required']) {
  607. form_set_value($element, NULL, $form_state);
  608. }
  609. // If there's empty input and the field is required, set an error. A
  610. // reminder of the required format in the message provides a good UX.
  611. elseif (empty($input['date']) && empty($input['time']) && $element['#required']) {
  612. form_error($element, t('The %field date is required. Please enter a date in the format %format.', array('%field' => $title, '%format' => datetime_format_example($format))));
  613. }
  614. else {
  615. // If the date is valid, set it.
  616. $date = $input['object'];
  617. if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
  618. form_set_value($element, $date, $form_state);
  619. }
  620. // If the date is invalid, set an error. A reminder of the required
  621. // format in the message provides a good UX.
  622. else {
  623. form_error($element, t('The %field date is invalid. Please enter a date in the format %format.', array('%field' => $title, '%format' => datetime_format_example($format))));
  624. }
  625. }
  626. }
  627. }
  628. /**
  629. * Retrieves the right format for a HTML5 date element.
  630. *
  631. * The format is important because these elements will not work with any other
  632. * format.
  633. *
  634. * @param string $part
  635. * The type of element format to retrieve.
  636. * @param string $element
  637. * The $element to assess.
  638. *
  639. * @return string
  640. * Returns the right format for the type of element, or the original format
  641. * if this is not a HTML5 element.
  642. */
  643. function datetime_html5_format($part, $element) {
  644. $format_type = datetime_default_format_type();
  645. switch ($part) {
  646. case 'date':
  647. switch ($element['#date_date_element']) {
  648. case 'date':
  649. return config('system.date')->get('formats.html_date.pattern.' . $format_type);
  650. case 'datetime':
  651. case 'datetime-local':
  652. return config('system.date')->get('formats.html_datetime.pattern.' . $format_type);
  653. default:
  654. return $element['#date_date_format'];
  655. }
  656. break;
  657. case 'time':
  658. switch ($element['#date_time_element']) {
  659. case 'time':
  660. return config('system.date')->get('formats.html_time.pattern.' . $format_type);
  661. default:
  662. return $element['#date_time_format'];
  663. }
  664. break;
  665. }
  666. }
  667. /**
  668. * Creates an example for a date format.
  669. *
  670. * This is centralized for a consistent method of creating these examples.
  671. *
  672. * @param string $format
  673. *
  674. *
  675. * @return string
  676. *
  677. */
  678. function datetime_format_example($format) {
  679. $date = &drupal_static(__FUNCTION__);
  680. if (empty($date)) {
  681. $date = new DrupalDateTime();
  682. }
  683. return $date->format($format);
  684. }
  685. /**
  686. * Expands a date element into an array of individual elements.
  687. *
  688. * Required settings:
  689. * - #default_value: A DrupalDateTime object, adjusted to the proper local
  690. * timezone. Converting a date stored in the database from UTC to the local
  691. * zone and converting it back to UTC before storing it is not handled here.
  692. * This element accepts a date as the default value, and then converts the
  693. * user input strings back into a new date object on submission. No timezone
  694. * adjustment is performed.
  695. * Optional properties include:
  696. * - #date_part_order: Array of date parts indicating the parts and order
  697. * that should be used in the selector, optionally including 'ampm' for
  698. * 12 hour time. Default is array('year', 'month', 'day', 'hour', 'minute').
  699. * - #date_text_parts: Array of date parts that should be presented as
  700. * text fields instead of drop-down selectors. Default is an empty array.
  701. * - #date_date_callbacks: Array of optional callbacks for the date element.
  702. * - #date_year_range: A description of the range of years to allow, like
  703. * '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
  704. * earliest year and the second the latest year in the range. A year
  705. * in either position means that specific year. A +/- value describes a
  706. * dynamic value that is that many years earlier or later than the current
  707. * year at the time the form is displayed. Defaults to '1900:2050'.
  708. * - #date_increment: The increment to use for minutes and seconds, i.e.
  709. * '15' would show only :00, :15, :30 and :45. Defaults to 1 to show every
  710. * minute.
  711. * - #date_timezone: The local timezone to use when creating dates. Generally
  712. * this should be left empty and it will be set correctly for the user using
  713. * the form. Useful if the default value is empty to designate a desired
  714. * timezone for dates created in form processing. If a default date is
  715. * provided, this value will be ignored, the timezone in the default date
  716. * takes precedence. Defaults to the value returned by
  717. * drupal_get_user_timezone().
  718. *
  719. * Example usage:
  720. * @code
  721. * $form = array(
  722. * '#type' => 'datelist',
  723. * '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
  724. * '#date_part_order' => array('month', 'day', 'year', 'hour', 'minute', 'ampm'),
  725. * '#date_text_parts' => array('year'),
  726. * '#date_year_range' => '2010:2020',
  727. * '#date_increment' => 15,
  728. * );
  729. * @endcode
  730. *
  731. * @param array $element
  732. * The form element whose value is being processed.
  733. * @param array $form_state
  734. * The current state of the form.
  735. */
  736. function datetime_datelist_form_process($element, &$form_state) {
  737. // Load translated date part labels from the appropriate calendar plugin.
  738. $date_helper = new DateHelper();
  739. // The value callback has populated the #value array.
  740. $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
  741. // Set a fallback timezone.
  742. if ($date instanceOf DrupalDateTime) {
  743. $element['#date_timezone'] = $date->getTimezone()->getName();
  744. }
  745. elseif (!empty($element['#timezone'])) {
  746. $element['#date_timezone'] = $element['#date_timezone'];
  747. }
  748. else {
  749. $element['#date_timezone'] = drupal_get_user_timezone();
  750. }
  751. $element['#tree'] = TRUE;
  752. // Determine the order of the date elements.
  753. $order = !empty($element['#date_part_order']) ? $element['#date_part_order'] : array('year', 'month', 'day');
  754. $text_parts = !empty($element['#date_text_parts']) ? $element['#date_text_parts'] : array();
  755. $has_time = FALSE;
  756. // Output multi-selector for date.
  757. foreach ($order as $part) {
  758. switch ($part) {
  759. case 'day':
  760. $options = $date_helper->days($element['#required']);
  761. $format = 'j';
  762. $title = t('Day');
  763. break;
  764. case 'month':
  765. $options = $date_helper->monthNamesAbbr($element['#required']);
  766. $format = 'n';
  767. $title = t('Month');
  768. break;
  769. case 'year':
  770. $range = datetime_range_years($element['#date_year_range'], $date);
  771. $options = $date_helper->years($range[0], $range[1], $element['#required']);
  772. $format = 'Y';
  773. $title = t('Year');
  774. break;
  775. case 'hour':
  776. $format = in_array('ampm', $element['#date_part_order']) ? 'g': 'G';
  777. $options = $date_helper->hours($format, $element['#required']);
  778. $has_time = TRUE;
  779. $title = t('Hour');
  780. break;
  781. case 'minute':
  782. $format = 'i';
  783. $options = $date_helper->minutes($format, $element['#required'], $element['#date_increment']);
  784. $has_time = TRUE;
  785. $title = t('Minute');
  786. break;
  787. case 'second':
  788. $format = 's';
  789. $options = $date_helper->seconds($format, $element['#required'], $element['#date_increment']);
  790. $has_time = TRUE;
  791. $title = t('Second');
  792. break;
  793. case 'ampm':
  794. $format = 'a';
  795. $options = $date_helper->ampm($element['#required']);
  796. $has_time = TRUE;
  797. $title = t('AM/PM');
  798. }
  799. $default = !empty($element['#value'][$part]) ? $element['#value'][$part] : '';
  800. $value = $date instanceOf DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default;
  801. if (!empty($value) && $part != 'ampm') {
  802. $value = intval($value);
  803. }
  804. $element['#attributes']['title'] = $title;
  805. $element[$part] = array(
  806. '#type' => in_array($part, $text_parts) ? 'textfield' : 'select',
  807. '#title' => $title,
  808. '#title_display' => 'invisible',
  809. '#value' => $value,
  810. '#attributes' => $element['#attributes'],
  811. '#options' => $options,
  812. '#required' => $element['#required'],
  813. );
  814. }
  815. // Allows custom callbacks to alter the element.
  816. if (!empty($element['#date_date_callbacks'])) {
  817. foreach ($element['#date_date_callbacks'] as $callback) {
  818. if (function_exists($callback)) {
  819. $callback($element, $form_state, $date);
  820. }
  821. }
  822. }
  823. return $element;
  824. }
  825. /**
  826. * Element value callback for datelist element.
  827. *
  828. * Validates the date type to adjust 12 hour time and prevent invalid dates. If
  829. * the date is valid, the date is set in the form.
  830. *
  831. * @param array $element
  832. * The element being processed.
  833. * @param array|false $input
  834. *
  835. * @param array $form_state
  836. * (optional) The current state of the form. Defaults to an empty array.
  837. *
  838. * @return array
  839. *
  840. */
  841. function form_type_datelist_value($element, $input = FALSE, &$form_state = array()) {
  842. $parts = $element['#date_part_order'];
  843. $increment = $element['#date_increment'];
  844. $date = NULL;
  845. if ($input !== FALSE) {
  846. $return = $input;
  847. if (isset($input['ampm'])) {
  848. if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
  849. $input['hour'] += 12;
  850. }
  851. elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
  852. $input['hour'] -= 12;
  853. }
  854. unset($input['ampm']);
  855. }
  856. $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
  857. $date = new DrupalDateTime($input, $timezone);
  858. if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
  859. date_increment_round($date, $increment);
  860. }
  861. }
  862. else {
  863. $return = array_fill_keys($parts, '');
  864. if (!empty($element['#default_value'])) {
  865. $date = $element['#default_value'];
  866. if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
  867. date_increment_round($date, $increment);
  868. foreach ($parts as $part) {
  869. switch ($part) {
  870. case 'day':
  871. $format = 'j';
  872. break;
  873. case 'month':
  874. $format = 'n';
  875. break;
  876. case 'year':
  877. $format = 'Y';
  878. break;
  879. case 'hour':
  880. $format = in_array('ampm', $element['#date_part_order']) ? 'g': 'G';
  881. break;
  882. case 'minute':
  883. $format = 'i';
  884. break;
  885. case 'second':
  886. $format = 's';
  887. break;
  888. case 'ampm':
  889. $format = 'a';
  890. }
  891. $return[$part] = $date->format($format);
  892. }
  893. }
  894. }
  895. }
  896. $return['object'] = $date;
  897. return $return;
  898. }
  899. /**
  900. * Validation callback for a datelist element.
  901. *
  902. * If the date is valid, the date object created from the user input is set in
  903. * the form for use by the caller. The work of compiling the user input back
  904. * into a date object is handled by the value callback, so we can use it here.
  905. * We also have the raw input available for validation testing.
  906. *
  907. * @param array $element
  908. * The element being processed.
  909. * @param array $form_state
  910. * The current state of the form.
  911. */
  912. function datetime_datelist_validate($element, &$form_state) {
  913. $input_exists = FALSE;
  914. $input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
  915. if ($input_exists) {
  916. $title = !empty($element['#title']) ? $element['#title'] : '';
  917. // If there's empty input and the field is not required, set it to empty.
  918. if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) {
  919. form_set_value($element, NULL, $form_state);
  920. }
  921. // If there's empty input and the field is required, set an error.
  922. elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
  923. form_error($element, t('The %field date is required.'));
  924. }
  925. else {
  926. // If the input is valid, set it.
  927. $date = $input['object'];
  928. if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
  929. form_set_value($element, $date, $form_state);
  930. }
  931. // If the input is invalid, set an error.
  932. else {
  933. form_error($element, t('The %field date is invalid.'));
  934. }
  935. }
  936. }
  937. }
  938. /**
  939. * Rounds minutes and seconds to nearest requested value.
  940. *
  941. * @param $date
  942. *
  943. * @param $increment
  944. *
  945. *
  946. * @return
  947. *
  948. */
  949. function date_increment_round(&$date, $increment) {
  950. // Round minutes and seconds, if necessary.
  951. if ($date instanceOf DrupalDateTime && $increment > 1) {
  952. $day = intval(date_format($date, 'j'));
  953. $hour = intval(date_format($date, 'H'));
  954. $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
  955. $minute = intval(date_format($date, 'i'));
  956. if ($second == 60) {
  957. $minute += 1;
  958. $second = 0;
  959. }
  960. $minute = intval(round($minute / $increment) * $increment);
  961. if ($minute == 60) {
  962. $hour += 1;
  963. $minute = 0;
  964. }
  965. date_time_set($date, $hour, $minute, $second);
  966. if ($hour == 24) {
  967. $day += 1;
  968. $hour = 0;
  969. $year = date_format($date, 'Y');
  970. $month = date_format($date, 'n');
  971. date_date_set($date, $year, $month, $day);
  972. }
  973. }
  974. return $date;
  975. }
  976. /**
  977. * Specifies the start and end year to use as a date range.
  978. *
  979. * Handles a string like -3:+3 or 2001:2010 to describe a dynamic range of
  980. * minimum and maximum years to use in a date selector.
  981. *
  982. * Centers the range around the current year, if any, but expands it far enough
  983. * so it will pick up the year value in the field in case the value in the field
  984. * is outside the initial range.
  985. *
  986. * @param string $string
  987. * A min and max year string like '-3:+1' or '2000:2010' or '2000:+3'.
  988. * @param object $date
  989. * (optional) A date object to test as a default value. Defaults to NULL.
  990. *
  991. * @return array
  992. * A numerically indexed array, containing the minimum and maximum year
  993. * described by this pattern.
  994. */
  995. function datetime_range_years($string, $date = NULL) {
  996. $this_year = date_format(new DrupalDateTime(), 'Y');
  997. list($min_year, $max_year) = explode(':', $string);
  998. // Valid patterns would be -5:+5, 0:+1, 2008:2010.
  999. $plus_pattern = '@[\+|\-][0-9]{1,4}@';
  1000. $year_pattern = '@^[0-9]{4}@';
  1001. if (!preg_match($year_pattern, $min_year, $matches)) {
  1002. if (preg_match($plus_pattern, $min_year, $matches)) {
  1003. $min_year = $this_year + $matches[0];
  1004. }
  1005. else {
  1006. $min_year = $this_year;
  1007. }
  1008. }
  1009. if (!preg_match($year_pattern, $max_year, $matches)) {
  1010. if (preg_match($plus_pattern, $max_year, $matches)) {
  1011. $max_year = $this_year + $matches[0];
  1012. }
  1013. else {
  1014. $max_year = $this_year;
  1015. }
  1016. }
  1017. // We expect the $min year to be less than the $max year. Some custom values
  1018. // for -99:+99 might not obey that.
  1019. if ($min_year > $max_year) {
  1020. $temp = $max_year;
  1021. $max_year = $min_year;
  1022. $min_year = $temp;
  1023. }
  1024. // If there is a current value, stretch the range to include it.
  1025. $value_year = $date instanceOf DrupalDateTime ? $date->format('Y') : '';
  1026. if (!empty($value_year)) {
  1027. $min_year = min($value_year, $min_year);
  1028. $max_year = max($value_year, $max_year);
  1029. }
  1030. return array($min_year, $max_year);
  1031. }
  1032. /**
  1033. * Implements hook_form_BASE_FORM_ID_alter() for node forms.
  1034. */
  1035. function datetime_form_node_form_alter(&$form, &$form_state, $form_id) {
  1036. // Alter the 'Authored on' date to use datetime.
  1037. $form['author']['date']['#type'] = 'datetime';
  1038. $config = Drupal::config('system.date');
  1039. $format = $config->get('formats.html_date') . ' ' . $config->get('formats.html_time');
  1040. $form['author']['date']['#description'] = t('Format: %format. Leave blank to use the time of form submission.', array('%format' => datetime_format_example($format)));
  1041. unset($form['author']['date']['#maxlength']);
  1042. }
  1043. /**
  1044. * Implements hook_node_prepare().
  1045. */
  1046. function datetime_node_prepare(NodeInterface $node) {
  1047. // Prepare the 'Authored on' date to use datetime.
  1048. $node->date = new DrupalDateTime($node->created);
  1049. }

Functions

Namesort descending Description
datetime_datelist_form_process Expands a date element into an array of individual elements.
datetime_datelist_validate Validation callback for a datelist element.
datetime_datelist_widget_validate Validation callback for the datelist widget element.
datetime_datetime_form_process Expands a #datetime element type into date and/or time elements.
datetime_datetime_validate Validation callback for a datetime element.
datetime_datetime_widget_validate Validation callback for the datetime widget element.
datetime_date_default_time Sets a consistent time on a date without time.
datetime_default_value Sets a default value for an empty date field.
datetime_element_info Implements hook_element_info().
datetime_field_info Implements hook_field_info().
datetime_field_instance_settings_form Implements hook_field_instance_settings_form().
datetime_field_is_empty Implements hook_field_is_empty().
datetime_field_load Implements hook_field_load().
datetime_field_settings_form Implements hook_field_settings_form().
datetime_format_example Creates an example for a date format.
datetime_form_node_form_alter Implements hook_form_BASE_FORM_ID_alter() for node forms.
datetime_html5_format Retrieves the right format for a HTML5 date element.
datetime_node_prepare Implements hook_node_prepare().
datetime_range_years Specifies the start and end year to use as a date range.
datetime_theme Implements hook_theme().
date_increment_round Rounds minutes and seconds to nearest requested value.
form_type_datelist_value Element value callback for datelist element.
form_type_datetime_value Value callback for a datetime element.
theme_datelist_form Returns HTML for a date selection form element.
theme_datetime_form Returns HTML for a HTML5-compatible #datetime form element.
theme_datetime_wrapper Returns HTML for a datetime form element.

Constants

Namesort descending Description
DATETIME_DATETIME_STORAGE_FORMAT Defines the format that date and time should be stored in.
DATETIME_DATE_STORAGE_FORMAT Defines the format that dates should be stored in.
DATETIME_STORAGE_TIMEZONE Defines the timezone that dates should be stored in.