datetime.module

You are here

Field hooks to implement a simple datetime field.

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_element_info Implements hook_element_info().
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_form Implements hook_node_prepare_form().
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.
template_preprocess_datetime_form Prepares variables for datetime form element templates.
template_preprocess_datetime_wrapper Prepares variables for datetime form wrapper templates.

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.

File

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