Datetime.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. <?php
  2. namespace Drupal\Core\Datetime\Element;
  3. use Drupal\Component\Utility\NestedArray;
  4. use Drupal\Core\Datetime\DrupalDateTime;
  5. use Drupal\Core\Form\FormStateInterface;
  6. use Drupal\Core\Datetime\Entity\DateFormat;
  7. /**
  8. * Provides a datetime element.
  9. *
  10. * @FormElement("datetime")
  11. */
  12. class Datetime extends DateElementBase {
  13. /**
  14. * @var \DateTimeInterface
  15. */
  16. protected static $dateExample;
  17. /**
  18. * {@inheritdoc}
  19. */
  20. public function getInfo() {
  21. $date_format = '';
  22. $time_format = '';
  23. // Date formats cannot be loaded during install or update.
  24. if (!defined('MAINTENANCE_MODE')) {
  25. if ($date_format_entity = DateFormat::load('html_date')) {
  26. /** @var $date_format_entity \Drupal\Core\Datetime\DateFormatInterface */
  27. $date_format = $date_format_entity->getPattern();
  28. }
  29. if ($time_format_entity = DateFormat::load('html_time')) {
  30. /** @var $time_format_entity \Drupal\Core\Datetime\DateFormatInterface */
  31. $time_format = $time_format_entity->getPattern();
  32. }
  33. }
  34. $class = get_class($this);
  35. // Note that since this information is cached, the #date_timezone property
  36. // is not set here, as this needs to vary potentially by-user.
  37. return [
  38. '#input' => TRUE,
  39. '#element_validate' => [
  40. [$class, 'validateDatetime'],
  41. ],
  42. '#process' => [
  43. [$class, 'processDatetime'],
  44. [$class, 'processAjaxForm'],
  45. [$class, 'processGroup'],
  46. ],
  47. '#pre_render' => [
  48. [$class, 'preRenderGroup'],
  49. ],
  50. '#theme' => 'datetime_form',
  51. '#theme_wrappers' => ['datetime_wrapper'],
  52. '#date_date_format' => $date_format,
  53. '#date_date_element' => 'date',
  54. '#date_date_callbacks' => [],
  55. '#date_time_format' => $time_format,
  56. '#date_time_element' => 'time',
  57. '#date_time_callbacks' => [],
  58. '#date_year_range' => '1900:2050',
  59. '#date_increment' => 1,
  60. ];
  61. }
  62. /**
  63. * {@inheritdoc}
  64. */
  65. public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
  66. $element += ['#date_timezone' => date_default_timezone_get()];
  67. if ($input !== FALSE) {
  68. $date_input = $element['#date_date_element'] != 'none' && !empty($input['date']) ? $input['date'] : '';
  69. $time_input = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : '';
  70. $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
  71. $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
  72. // Seconds will be omitted in a post in case there's no entry.
  73. if (!empty($time_input) && strlen($time_input) == 5) {
  74. $time_input .= ':00';
  75. }
  76. try {
  77. $date_time_format = trim($date_format . ' ' . $time_format);
  78. $date_time_input = trim($date_input . ' ' . $time_input);
  79. $date = DrupalDateTime::createFromFormat($date_time_format, $date_time_input, $element['#date_timezone']);
  80. }
  81. catch (\Exception $e) {
  82. $date = NULL;
  83. }
  84. $input = [
  85. 'date' => $date_input,
  86. 'time' => $time_input,
  87. 'object' => $date,
  88. ];
  89. }
  90. else {
  91. $date = isset($element['#default_value']) ? $element['#default_value'] : NULL;
  92. if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
  93. $date->setTimezone(new \DateTimeZone($element['#date_timezone']));
  94. $input = [
  95. 'date' => $date->format($element['#date_date_format']),
  96. 'time' => $date->format($element['#date_time_format']),
  97. 'object' => $date,
  98. ];
  99. }
  100. else {
  101. $input = [
  102. 'date' => '',
  103. 'time' => '',
  104. 'object' => NULL,
  105. ];
  106. }
  107. }
  108. return $input;
  109. }
  110. /**
  111. * Expands a datetime element type into date and/or time elements.
  112. *
  113. * All form elements are designed to have sane defaults so any or all can be
  114. * omitted. Both the date and time components are configurable so they can be
  115. * output as HTML5 datetime elements or not, as desired.
  116. *
  117. * Examples of possible configurations include:
  118. * HTML5 date and time:
  119. * #date_date_element = 'date';
  120. * #date_time_element = 'time';
  121. * HTML5 datetime:
  122. * #date_date_element = 'datetime';
  123. * #date_time_element = 'none';
  124. * HTML5 time only:
  125. * #date_date_element = 'none';
  126. * #date_time_element = 'time'
  127. * Non-HTML5:
  128. * #date_date_element = 'text';
  129. * #date_time_element = 'text';
  130. *
  131. * Required settings:
  132. * - #default_value: A DrupalDateTime object, adjusted to the proper local
  133. * timezone. Converting a date stored in the database from UTC to the local
  134. * zone and converting it back to UTC before storing it is not handled here.
  135. * This element accepts a date as the default value, and then converts the
  136. * user input strings back into a new date object on submission. No timezone
  137. * adjustment is performed.
  138. * Optional properties include:
  139. * - #date_date_format: A date format string that describes the format that
  140. * should be displayed to the end user for the date. When using HTML5
  141. * elements the format MUST use the appropriate HTML5 format for that
  142. * element, no other format will work. See the
  143. * DateFormatterInterface::format() function for a list of the possible
  144. * formats and HTML5 standards for the HTML5 requirements. Defaults to the
  145. * right HTML5 format for the chosen element if a HTML5 element is used,
  146. * otherwise defaults to DateFormat::load('html_date')->getPattern().
  147. * - #date_date_element: The date element. Options are:
  148. * - datetime: Use the HTML5 datetime element type.
  149. * - datetime-local: Use the HTML5 datetime-local element type.
  150. * - date: Use the HTML5 date element type.
  151. * - text: No HTML5 element, use a normal text field.
  152. * - none: Do not display a date element.
  153. * - #date_date_callbacks: Array of optional callbacks for the date element.
  154. * Can be used to add a jQuery datepicker.
  155. * - #date_time_element: The time element. Options are:
  156. * - time: Use a HTML5 time element type.
  157. * - text: No HTML5 element, use a normal text field.
  158. * - none: Do not display a time element.
  159. * - #date_time_format: A date format string that describes the format that
  160. * should be displayed to the end user for the time. When using HTML5
  161. * elements the format MUST use the appropriate HTML5 format for that
  162. * element, no other format will work. See the
  163. * DateFormatterInterface::format() function for a list of the possible
  164. * formats and HTML5 standards for the HTML5 requirements. Defaults to the
  165. * right HTML5 format for the chosen element if a HTML5 element is used,
  166. * otherwise defaults to DateFormat::load('html_time')->getPattern().
  167. * - #date_time_callbacks: An array of optional callbacks for the time
  168. * element. Can be used to add a jQuery timepicker or an 'All day' checkbox.
  169. * - #date_year_range: A description of the range of years to allow, like
  170. * '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
  171. * earliest year and the second the latest year in the range. A year
  172. * in either position means that specific year. A +/- value describes a
  173. * dynamic value that is that many years earlier or later than the current
  174. * year at the time the form is displayed. Used in jQueryUI datepicker year
  175. * range and HTML5 min/max date settings. Defaults to '1900:2050'.
  176. * - #date_increment: The interval (step) to use when incrementing or
  177. * decrementing time, in seconds. For example, if this value is set to 30,
  178. * time increases (or decreases) in steps of 30 seconds (00:00:00,
  179. * 00:00:30, 00:01:00, and so on.) If this value is a multiple of 60, the
  180. * "seconds"-component will not be shown in the input. Used for HTML5 step
  181. * values and jQueryUI datepicker settings. Defaults to 1 to show every
  182. * second.
  183. * - #date_timezone: The Time Zone Identifier (TZID) to use when displaying
  184. * or interpreting dates, i.e: 'Asia/Kolkata'. Defaults to the value
  185. * returned by date_default_timezone_get().
  186. *
  187. * Example usage:
  188. * @code
  189. * $form = array(
  190. * '#type' => 'datetime',
  191. * '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
  192. * '#date_date_element' => 'date',
  193. * '#date_time_element' => 'none',
  194. * '#date_year_range' => '2010:+3',
  195. * '#date_timezone' => 'Asia/Kolkata',
  196. * );
  197. * @endcode
  198. *
  199. * @param array $element
  200. * The form element whose value is being processed.
  201. * @param \Drupal\Core\Form\FormStateInterface $form_state
  202. * The current state of the form.
  203. * @param array $complete_form
  204. * The complete form structure.
  205. *
  206. * @return array
  207. * The form element whose value has been processed.
  208. *
  209. * @see \Drupal\Core\Datetime\DateFormatterInterface::format()
  210. */
  211. public static function processDatetime(&$element, FormStateInterface $form_state, &$complete_form) {
  212. $format_settings = [];
  213. // The value callback has populated the #value array.
  214. $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
  215. $element['#tree'] = TRUE;
  216. if ($element['#date_date_element'] != 'none') {
  217. $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
  218. $date_value = !empty($date) ? $date->format($date_format, $format_settings) : $element['#value']['date'];
  219. // Creating format examples on every individual date item is messy, and
  220. // placeholders are invalid for HTML5 date and datetime, so an example
  221. // format is appended to the title to appear in tooltips.
  222. $extra_attributes = [
  223. 'title' => t('Date (e.g. @format)', ['@format' => static::formatExample($date_format)]),
  224. 'type' => $element['#date_date_element'],
  225. ];
  226. // Adds the HTML5 date attributes.
  227. if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
  228. $html5_min = clone($date);
  229. $range = static::datetimeRangeYears($element['#date_year_range'], $date);
  230. $html5_min->setDate($range[0], 1, 1)->setTime(0, 0, 0);
  231. $html5_max = clone($date);
  232. $html5_max->setDate($range[1], 12, 31)->setTime(23, 59, 59);
  233. $extra_attributes += [
  234. 'min' => $html5_min->format($date_format, $format_settings),
  235. 'max' => $html5_max->format($date_format, $format_settings),
  236. ];
  237. }
  238. $element['date'] = [
  239. '#type' => 'date',
  240. '#title' => t('Date'),
  241. '#title_display' => 'invisible',
  242. '#value' => $date_value,
  243. '#attributes' => $element['#attributes'] + $extra_attributes,
  244. '#required' => $element['#required'],
  245. '#size' => max(12, strlen($element['#value']['date'])),
  246. '#error_no_message' => TRUE,
  247. '#date_date_format' => $element['#date_date_format'],
  248. ];
  249. // Allows custom callbacks to alter the element.
  250. if (!empty($element['#date_date_callbacks'])) {
  251. foreach ($element['#date_date_callbacks'] as $callback) {
  252. if (is_callable($callback)) {
  253. $callback($element, $form_state, $date);
  254. }
  255. }
  256. }
  257. }
  258. if ($element['#date_time_element'] != 'none') {
  259. $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
  260. $time_value = !empty($date) ? $date->format($time_format, $format_settings) : $element['#value']['time'];
  261. // Adds the HTML5 attributes.
  262. $extra_attributes = [
  263. 'title' => t('Time (e.g. @format)', ['@format' => static::formatExample($time_format)]),
  264. 'type' => $element['#date_time_element'],
  265. 'step' => $element['#date_increment'],
  266. ];
  267. $element['time'] = [
  268. '#type' => 'date',
  269. '#title' => t('Time'),
  270. '#title_display' => 'invisible',
  271. '#value' => $time_value,
  272. '#attributes' => $element['#attributes'] + $extra_attributes,
  273. '#required' => $element['#required'],
  274. '#size' => 12,
  275. '#error_no_message' => TRUE,
  276. ];
  277. // Allows custom callbacks to alter the element.
  278. if (!empty($element['#date_time_callbacks'])) {
  279. foreach ($element['#date_time_callbacks'] as $callback) {
  280. if (function_exists($callback)) {
  281. $callback($element, $form_state, $date);
  282. }
  283. }
  284. }
  285. }
  286. return $element;
  287. }
  288. /**
  289. * {@inheritdoc}
  290. */
  291. public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
  292. $element = parent::processAjaxForm($element, $form_state, $complete_form);
  293. // Copy the #ajax settings to the child elements.
  294. if (isset($element['#ajax'])) {
  295. if (isset($element['date'])) {
  296. $element['date']['#ajax'] = $element['#ajax'];
  297. }
  298. if (isset($element['time'])) {
  299. $element['time']['#ajax'] = $element['#ajax'];
  300. }
  301. }
  302. return $element;
  303. }
  304. /**
  305. * Validation callback for a datetime element.
  306. *
  307. * If the date is valid, the date object created from the user input is set in
  308. * the form for use by the caller. The work of compiling the user input back
  309. * into a date object is handled by the value callback, so we can use it here.
  310. * We also have the raw input available for validation testing.
  311. *
  312. * @param array $element
  313. * The form element whose value is being validated.
  314. * @param \Drupal\Core\Form\FormStateInterface $form_state
  315. * The current state of the form.
  316. * @param array $complete_form
  317. * The complete form structure.
  318. */
  319. public static function validateDatetime(&$element, FormStateInterface $form_state, &$complete_form) {
  320. $input_exists = FALSE;
  321. $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
  322. if ($input_exists) {
  323. $title = !empty($element['#title']) ? $element['#title'] : '';
  324. $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
  325. $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
  326. $format = trim($date_format . ' ' . $time_format);
  327. // If there's empty input and the field is not required, set it to empty.
  328. if (empty($input['date']) && empty($input['time']) && !$element['#required']) {
  329. $form_state->setValueForElement($element, NULL);
  330. }
  331. // If there's empty input and the field is required, set an error. A
  332. // reminder of the required format in the message provides a good UX.
  333. elseif (empty($input['date']) && empty($input['time']) && $element['#required']) {
  334. $form_state->setError($element, t('The %field date is required. Please enter a date in the format %format.', ['%field' => $title, '%format' => static::formatExample($format)]));
  335. }
  336. else {
  337. // If the date is valid, set it.
  338. $date = $input['object'];
  339. if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
  340. $form_state->setValueForElement($element, $date);
  341. }
  342. // If the date is invalid, set an error. A reminder of the required
  343. // format in the message provides a good UX.
  344. else {
  345. $form_state->setError($element, t('The %field date is invalid. Please enter a date in the format %format.', ['%field' => $title, '%format' => static::formatExample($format)]));
  346. }
  347. }
  348. }
  349. }
  350. /**
  351. * Creates an example for a date format.
  352. *
  353. * This is centralized for a consistent method of creating these examples.
  354. *
  355. * @param string $format
  356. *
  357. * @return string
  358. */
  359. public static function formatExample($format) {
  360. if (!static::$dateExample) {
  361. static::$dateExample = new DrupalDateTime();
  362. }
  363. return static::$dateExample->format($format);
  364. }
  365. /**
  366. * Retrieves the right format for a HTML5 date element.
  367. *
  368. * The format is important because these elements will not work with any other
  369. * format.
  370. *
  371. * @param array $element
  372. * The $element to assess.
  373. *
  374. * @return string
  375. * Returns the right format for the date element, or the original format
  376. * if this is not a HTML5 element.
  377. */
  378. protected static function getHtml5DateFormat($element) {
  379. switch ($element['#date_date_element']) {
  380. case 'date':
  381. return DateFormat::load('html_date')->getPattern();
  382. case 'datetime':
  383. case 'datetime-local':
  384. return DateFormat::load('html_datetime')->getPattern();
  385. default:
  386. return $element['#date_date_format'];
  387. }
  388. }
  389. /**
  390. * Retrieves the right format for a HTML5 time element.
  391. *
  392. * The format is important because these elements will not work with any other
  393. * format.
  394. *
  395. * @param array $element
  396. * The $element to assess.
  397. *
  398. * @return string
  399. * Returns the right format for the time element, or the original format
  400. * if this is not a HTML5 element.
  401. */
  402. protected static function getHtml5TimeFormat($element) {
  403. switch ($element['#date_time_element']) {
  404. case 'time':
  405. return DateFormat::load('html_time')->getPattern();
  406. default:
  407. return $element['#date_time_format'];
  408. }
  409. }
  410. }