Datelist.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <?php
  2. namespace Drupal\Core\Datetime\Element;
  3. use Drupal\Component\Utility\NestedArray;
  4. use Drupal\Core\Datetime\DateHelper;
  5. use Drupal\Core\Datetime\DrupalDateTime;
  6. use Drupal\Core\Form\FormStateInterface;
  7. /**
  8. * Provides a datelist element.
  9. *
  10. * @FormElement("datelist")
  11. */
  12. class Datelist extends DateElementBase {
  13. /**
  14. * {@inheritdoc}
  15. */
  16. public function getInfo() {
  17. $class = get_class($this);
  18. return [
  19. '#input' => TRUE,
  20. '#element_validate' => [
  21. [$class, 'validateDatelist'],
  22. ],
  23. '#process' => [
  24. [$class, 'processDatelist'],
  25. ],
  26. '#theme' => 'datetime_form',
  27. '#theme_wrappers' => ['datetime_wrapper'],
  28. '#date_part_order' => ['year', 'month', 'day', 'hour', 'minute'],
  29. '#date_year_range' => '1900:2050',
  30. '#date_increment' => 1,
  31. '#date_date_callbacks' => [],
  32. '#date_timezone' => '',
  33. ];
  34. }
  35. /**
  36. * {@inheritdoc}
  37. *
  38. * Validates the date type to adjust 12 hour time and prevent invalid dates.
  39. * If the date is valid, the date is set in the form.
  40. */
  41. public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
  42. $parts = $element['#date_part_order'];
  43. $increment = $element['#date_increment'];
  44. $date = NULL;
  45. if ($input !== FALSE) {
  46. $return = $input;
  47. if (empty(static::checkEmptyInputs($input, $parts))) {
  48. if (isset($input['ampm'])) {
  49. if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
  50. $input['hour'] += 12;
  51. }
  52. elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
  53. $input['hour'] -= 12;
  54. }
  55. unset($input['ampm']);
  56. }
  57. $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
  58. try {
  59. $date = DrupalDateTime::createFromArray($input, $timezone);
  60. }
  61. catch (\Exception $e) {
  62. $form_state->setError($element, t('Selected combination of day and month is not valid.'));
  63. }
  64. if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
  65. static::incrementRound($date, $increment);
  66. }
  67. }
  68. }
  69. else {
  70. $return = array_fill_keys($parts, '');
  71. if (!empty($element['#default_value'])) {
  72. $date = $element['#default_value'];
  73. if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
  74. static::incrementRound($date, $increment);
  75. foreach ($parts as $part) {
  76. switch ($part) {
  77. case 'day':
  78. $format = 'j';
  79. break;
  80. case 'month':
  81. $format = 'n';
  82. break;
  83. case 'year':
  84. $format = 'Y';
  85. break;
  86. case 'hour':
  87. $format = in_array('ampm', $element['#date_part_order']) ? 'g' : 'G';
  88. break;
  89. case 'minute':
  90. $format = 'i';
  91. break;
  92. case 'second':
  93. $format = 's';
  94. break;
  95. case 'ampm':
  96. $format = 'a';
  97. break;
  98. default:
  99. $format = '';
  100. }
  101. $return[$part] = $date->format($format);
  102. }
  103. }
  104. }
  105. }
  106. $return['object'] = $date;
  107. return $return;
  108. }
  109. /**
  110. * Expands a date element into an array of individual elements.
  111. *
  112. * Required settings:
  113. * - #default_value: A DrupalDateTime object, adjusted to the proper local
  114. * timezone. Converting a date stored in the database from UTC to the local
  115. * zone and converting it back to UTC before storing it is not handled here.
  116. * This element accepts a date as the default value, and then converts the
  117. * user input strings back into a new date object on submission. No timezone
  118. * adjustment is performed.
  119. * Optional properties include:
  120. * - #date_part_order: Array of date parts indicating the parts and order
  121. * that should be used in the selector, optionally including 'ampm' for
  122. * 12 hour time. Default is array('year', 'month', 'day', 'hour', 'minute').
  123. * - #date_text_parts: Array of date parts that should be presented as
  124. * text fields instead of drop-down selectors. Default is an empty array.
  125. * - #date_date_callbacks: Array of optional callbacks for the date element.
  126. * - #date_year_range: A description of the range of years to allow, like
  127. * '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
  128. * earliest year and the second the latest year in the range. A year
  129. * in either position means that specific year. A +/- value describes a
  130. * dynamic value that is that many years earlier or later than the current
  131. * year at the time the form is displayed. Defaults to '1900:2050'.
  132. * - #date_increment: The increment to use for minutes and seconds, i.e.
  133. * '15' would show only :00, :15, :30 and :45. Defaults to 1 to show every
  134. * minute.
  135. * - #date_timezone: The local timezone to use when creating dates. Generally
  136. * this should be left empty and it will be set correctly for the user using
  137. * the form. Useful if the default value is empty to designate a desired
  138. * timezone for dates created in form processing. If a default date is
  139. * provided, this value will be ignored, the timezone in the default date
  140. * takes precedence. Defaults to the value returned by
  141. * drupal_get_user_timezone().
  142. *
  143. * Example usage:
  144. * @code
  145. * $form = array(
  146. * '#type' => 'datelist',
  147. * '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
  148. * '#date_part_order' => array('month', 'day', 'year', 'hour', 'minute', 'ampm'),
  149. * '#date_text_parts' => array('year'),
  150. * '#date_year_range' => '2010:2020',
  151. * '#date_increment' => 15,
  152. * );
  153. * @endcode
  154. *
  155. * @param array $element
  156. * The form element whose value is being processed.
  157. * @param \Drupal\Core\Form\FormStateInterface $form_state
  158. * The current state of the form.
  159. * @param array $complete_form
  160. * The complete form structure.
  161. *
  162. * @return array
  163. */
  164. public static function processDatelist(&$element, FormStateInterface $form_state, &$complete_form) {
  165. // Load translated date part labels from the appropriate calendar plugin.
  166. $date_helper = new DateHelper();
  167. // The value callback has populated the #value array.
  168. $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
  169. // Set a fallback timezone.
  170. if ($date instanceof DrupalDateTime) {
  171. $element['#date_timezone'] = $date->getTimezone()->getName();
  172. }
  173. elseif (!empty($element['#timezone'])) {
  174. $element['#date_timezone'] = $element['#date_timezone'];
  175. }
  176. else {
  177. $element['#date_timezone'] = drupal_get_user_timezone();
  178. }
  179. $element['#tree'] = TRUE;
  180. // Determine the order of the date elements.
  181. $order = !empty($element['#date_part_order']) ? $element['#date_part_order'] : ['year', 'month', 'day'];
  182. $text_parts = !empty($element['#date_text_parts']) ? $element['#date_text_parts'] : [];
  183. // Output multi-selector for date.
  184. foreach ($order as $part) {
  185. switch ($part) {
  186. case 'day':
  187. $options = $date_helper->days($element['#required']);
  188. $format = 'j';
  189. $title = t('Day');
  190. break;
  191. case 'month':
  192. $options = $date_helper->monthNamesAbbr($element['#required']);
  193. $format = 'n';
  194. $title = t('Month');
  195. break;
  196. case 'year':
  197. $range = static::datetimeRangeYears($element['#date_year_range'], $date);
  198. $options = $date_helper->years($range[0], $range[1], $element['#required']);
  199. $format = 'Y';
  200. $title = t('Year');
  201. break;
  202. case 'hour':
  203. $format = in_array('ampm', $element['#date_part_order']) ? 'g' : 'G';
  204. $options = $date_helper->hours($format, $element['#required']);
  205. $title = t('Hour');
  206. break;
  207. case 'minute':
  208. $format = 'i';
  209. $options = $date_helper->minutes($format, $element['#required'], $element['#date_increment']);
  210. $title = t('Minute');
  211. break;
  212. case 'second':
  213. $format = 's';
  214. $options = $date_helper->seconds($format, $element['#required'], $element['#date_increment']);
  215. $title = t('Second');
  216. break;
  217. case 'ampm':
  218. $format = 'a';
  219. $options = $date_helper->ampm($element['#required']);
  220. $title = t('AM/PM');
  221. break;
  222. default:
  223. $format = '';
  224. $options = [];
  225. $title = '';
  226. }
  227. $default = isset($element['#value'][$part]) && trim($element['#value'][$part]) != '' ? $element['#value'][$part] : '';
  228. $value = $date instanceof DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default;
  229. if (!empty($value) && $part != 'ampm') {
  230. $value = intval($value);
  231. }
  232. $element['#attributes']['title'] = $title;
  233. $element[$part] = [
  234. '#type' => in_array($part, $text_parts) ? 'textfield' : 'select',
  235. '#title' => $title,
  236. '#title_display' => 'invisible',
  237. '#value' => $value,
  238. '#attributes' => $element['#attributes'],
  239. '#options' => $options,
  240. '#required' => $element['#required'],
  241. '#error_no_message' => FALSE,
  242. '#empty_option' => $title,
  243. ];
  244. }
  245. // Allows custom callbacks to alter the element.
  246. if (!empty($element['#date_date_callbacks'])) {
  247. foreach ($element['#date_date_callbacks'] as $callback) {
  248. if (function_exists($callback)) {
  249. $callback($element, $form_state, $date);
  250. }
  251. }
  252. }
  253. return $element;
  254. }
  255. /**
  256. * Validation callback for a datelist element.
  257. *
  258. * If the date is valid, the date object created from the user input is set in
  259. * the form for use by the caller. The work of compiling the user input back
  260. * into a date object is handled by the value callback, so we can use it here.
  261. * We also have the raw input available for validation testing.
  262. *
  263. * @param array $element
  264. * The element being processed.
  265. * @param \Drupal\Core\Form\FormStateInterface $form_state
  266. * The current state of the form.
  267. * @param array $complete_form
  268. * The complete form structure.
  269. */
  270. public static function validateDatelist(&$element, FormStateInterface $form_state, &$complete_form) {
  271. $input_exists = FALSE;
  272. $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
  273. $title = static::getElementTitle($element, $complete_form);
  274. if ($input_exists) {
  275. $all_empty = static::checkEmptyInputs($input, $element['#date_part_order']);
  276. // If there's empty input and the field is not required, set it to empty.
  277. if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) {
  278. $form_state->setValueForElement($element, NULL);
  279. }
  280. // If there's empty input and the field is required, set an error.
  281. elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
  282. $form_state->setError($element, t('The %field date is required.', ['%field' => $title]));
  283. }
  284. elseif (!empty($all_empty)) {
  285. foreach ($all_empty as $value) {
  286. $form_state->setError($element, t('The %field date is incomplete.', ['%field' => $title]));
  287. $form_state->setError($element[$value], t('A value must be selected for %part.', ['%part' => $value]));
  288. }
  289. }
  290. else {
  291. // If the input is valid, set it.
  292. $date = $input['object'];
  293. if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
  294. $form_state->setValueForElement($element, $date);
  295. }
  296. // If the input is invalid and an error doesn't exist, set one.
  297. elseif ($form_state->getError($element) === NULL) {
  298. $form_state->setError($element, t('The %field date is invalid.', ['%field' => $title]));
  299. }
  300. }
  301. }
  302. }
  303. /**
  304. * Checks the input array for empty values.
  305. *
  306. * Input array keys are checked against values in the parts array. Elements
  307. * not in the parts array are ignored. Returns an array representing elements
  308. * from the input array that have no value. If no empty values are found,
  309. * returned array is empty.
  310. *
  311. * @param array $input
  312. * Array of individual inputs to check for value.
  313. * @param array $parts
  314. * Array to check input against, ignoring elements not in this array.
  315. *
  316. * @return array
  317. * Array of keys from the input array that have no value, may be empty.
  318. */
  319. protected static function checkEmptyInputs($input, $parts) {
  320. // Filters out empty array values, any valid value would have a string length.
  321. $filtered_input = array_filter($input, 'strlen');
  322. return array_diff($parts, array_keys($filtered_input));
  323. }
  324. /**
  325. * Rounds minutes and seconds to nearest requested value.
  326. *
  327. * @param $date
  328. * @param $increment
  329. *
  330. * @return
  331. */
  332. protected static function incrementRound(&$date, $increment) {
  333. // Round minutes and seconds, if necessary.
  334. if ($date instanceof DrupalDateTime && $increment > 1) {
  335. $day = intval($date->format('j'));
  336. $hour = intval($date->format('H'));
  337. $second = intval(round(intval($date->format('s')) / $increment) * $increment);
  338. $minute = intval($date->format('i'));
  339. if ($second == 60) {
  340. $minute += 1;
  341. $second = 0;
  342. }
  343. $minute = intval(round($minute / $increment) * $increment);
  344. if ($minute == 60) {
  345. $hour += 1;
  346. $minute = 0;
  347. }
  348. $date->setTime($hour, $minute, $second);
  349. if ($hour == 24) {
  350. $day += 1;
  351. $year = $date->format('Y');
  352. $month = $date->format('n');
  353. $date->setDate($year, $month, $day);
  354. }
  355. }
  356. return $date;
  357. }
  358. }