DateTimeWidget.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\View\Widget;
  16. use Cake\View\Form\ContextInterface;
  17. use Cake\View\StringTemplate;
  18. use Cake\View\Widget\SelectBoxWidget;
  19. use Cake\View\Widget\WidgetInterface;
  20. /**
  21. * Input widget class for generating a date time input widget.
  22. *
  23. * This class is intended as an internal implementation detail
  24. * of Cake\View\Helper\FormHelper and is not intended for direct use.
  25. */
  26. class DateTimeWidget implements WidgetInterface
  27. {
  28. /**
  29. * Select box widget.
  30. *
  31. * @var \Cake\View\Widget\SelectBoxWidget
  32. */
  33. protected $_select;
  34. /**
  35. * List of inputs that can be rendered
  36. *
  37. * @var array
  38. */
  39. protected $_selects = [
  40. 'year',
  41. 'month',
  42. 'day',
  43. 'hour',
  44. 'minute',
  45. 'second',
  46. 'meridian',
  47. ];
  48. /**
  49. * Template instance.
  50. *
  51. * @var \Cake\View\StringTemplate
  52. */
  53. protected $_templates;
  54. /**
  55. * Constructor
  56. *
  57. * @param \Cake\View\StringTemplate $templates Templates list.
  58. * @param \Cake\View\Widget\SelectBoxWidget $selectBox Selectbox widget instance.
  59. */
  60. public function __construct(StringTemplate $templates, SelectBoxWidget $selectBox)
  61. {
  62. $this->_select = $selectBox;
  63. $this->_templates = $templates;
  64. }
  65. /**
  66. * Renders a date time widget
  67. *
  68. * - `name` - Set the input name.
  69. * - `disabled` - Either true or an array of options to disable.
  70. * - `val` - A date time string, integer or DateTime object
  71. * - `empty` - Set to true to add an empty option at the top of the
  72. * option elements. Set to a string to define the display value of the
  73. * empty option.
  74. *
  75. * In addition to the above options, the following options allow you to control
  76. * which input elements are generated. By setting any option to false you can disable
  77. * that input picker. In addition each picker allows you to set additional options
  78. * that are set as HTML properties on the picker.
  79. *
  80. * - `year` - Array of options for the year select box.
  81. * - `month` - Array of options for the month select box.
  82. * - `day` - Array of options for the day select box.
  83. * - `hour` - Array of options for the hour select box.
  84. * - `minute` - Array of options for the minute select box.
  85. * - `second` - Set to true to enable the seconds input. Defaults to false.
  86. * - `meridian` - Set to true to enable the meridian input. Defaults to false.
  87. * The meridian will be enabled automatically if you choose a 12 hour format.
  88. *
  89. * The `year` option accepts the `start` and `end` options. These let you control
  90. * the year range that is generated. It defaults to +-5 years from today.
  91. *
  92. * The `month` option accepts the `name` option which allows you to get month
  93. * names instead of month numbers.
  94. *
  95. * The `hour` option allows you to set the following options:
  96. *
  97. * - `format` option which accepts 12 or 24, allowing
  98. * you to indicate which hour format you want.
  99. * - `start` The hour to start the options at.
  100. * - `end` The hour to stop the options at.
  101. *
  102. * The start and end options are dependent on the format used. If the
  103. * value is out of the start/end range it will not be included.
  104. *
  105. * The `minute` option allows you to define the following options:
  106. *
  107. * - `interval` The interval to round options to.
  108. * - `round` Accepts `up` or `down`. Defines which direction the current value
  109. * should be rounded to match the select options.
  110. *
  111. * @param array $data Data to render with.
  112. * @param \Cake\View\Form\ContextInterface $context The current form context.
  113. * @return string A generated select box.
  114. * @throws \RuntimeException When option data is invalid.
  115. */
  116. public function render(array $data, ContextInterface $context)
  117. {
  118. $data += [
  119. 'name' => '',
  120. 'empty' => false,
  121. 'disabled' => null,
  122. 'val' => null,
  123. 'year' => [],
  124. 'month' => [],
  125. 'day' => [],
  126. 'hour' => [],
  127. 'minute' => [],
  128. 'second' => [],
  129. 'meridian' => null,
  130. ];
  131. $selected = $this->_deconstructDate($data['val'], $data);
  132. $timeFormat = isset($data['hour']['format']) ? $data['hour']['format'] : null;
  133. if ($timeFormat === 12 && !isset($data['meridian'])) {
  134. $data['meridian'] = [];
  135. }
  136. if ($timeFormat === 24) {
  137. $data['meridian'] = false;
  138. }
  139. $templateOptions = [];
  140. foreach ($this->_selects as $select) {
  141. if ($data[$select] === false || $data[$select] === null) {
  142. $templateOptions[$select] = '';
  143. unset($data[$select]);
  144. continue;
  145. }
  146. if (!is_array($data[$select])) {
  147. throw new \RuntimeException(sprintf(
  148. 'Options for "%s" must be an array|false|null',
  149. $select
  150. ));
  151. }
  152. $method = "_{$select}Select";
  153. $data[$select]['name'] = $data['name'] . "[" . $select . "]";
  154. $data[$select]['val'] = $selected[$select];
  155. if (!isset($data[$select]['empty'])) {
  156. $data[$select]['empty'] = $data['empty'];
  157. }
  158. if (!isset($data[$select]['disabled'])) {
  159. $data[$select]['disabled'] = $data['disabled'];
  160. }
  161. $templateOptions[$select] = $this->{$method}($data[$select], $context);
  162. unset($data[$select]);
  163. }
  164. unset($data['name'], $data['empty'], $data['disabled'], $data['val']);
  165. $templateOptions['attrs'] = $this->_templates->formatAttributes($data);
  166. return $this->_templates->format('dateWidget', $templateOptions);
  167. }
  168. /**
  169. * Deconstructs the passed date value into all time units
  170. *
  171. * @param string|int|array|\DateTime|null $value Value to deconstruct.
  172. * @param array $options Options for conversion.
  173. * @return array
  174. */
  175. protected function _deconstructDate($value, $options)
  176. {
  177. if ($value === '' || $value === null) {
  178. return [
  179. 'year' => '', 'month' => '', 'day' => '',
  180. 'hour' => '', 'minute' => '', 'second' => '',
  181. 'meridian' => '',
  182. ];
  183. }
  184. try {
  185. if (is_string($value)) {
  186. $date = new \DateTime($value);
  187. } elseif (is_bool($value)) {
  188. $date = new \DateTime();
  189. } elseif (is_int($value)) {
  190. $date = new \DateTime('@' . $value);
  191. } elseif (is_array($value)) {
  192. $dateArray = [
  193. 'year' => '', 'month' => '', 'day' => '',
  194. 'hour' => '', 'minute' => '', 'second' => '',
  195. 'meridian' => 'pm',
  196. ];
  197. $validDate = false;
  198. foreach ($dateArray as $key => $dateValue) {
  199. $exists = isset($value[$key]);
  200. if ($exists) {
  201. $validDate = true;
  202. }
  203. if ($exists && $value[$key] !== '') {
  204. $dateArray[$key] = str_pad($value[$key], 2, '0', STR_PAD_LEFT);
  205. }
  206. }
  207. if ($validDate) {
  208. if (!isset($dateArray['second'])) {
  209. $dateArray['second'] = 0;
  210. }
  211. if (isset($value['meridian'])) {
  212. $isAm = strtolower($dateArray['meridian']) === 'am';
  213. $dateArray['hour'] = $isAm ? $dateArray['hour'] : $dateArray['hour'] + 12;
  214. }
  215. if (!empty($dateArray['minute']) && isset($options['minute']['interval'])) {
  216. $dateArray['minute'] += $this->_adjustValue($dateArray['minute'], $options['minute']);
  217. }
  218. return $dateArray;
  219. }
  220. $date = new \DateTime();
  221. } else {
  222. $date = clone $value;
  223. }
  224. } catch (\Exception $e) {
  225. $date = new \DateTime();
  226. }
  227. if (isset($options['minute']['interval'])) {
  228. $change = $this->_adjustValue($date->format('i'), $options['minute']);
  229. $date->modify($change > 0 ? "+$change minutes" : "$change minutes");
  230. }
  231. return [
  232. 'year' => $date->format('Y'),
  233. 'month' => $date->format('m'),
  234. 'day' => $date->format('d'),
  235. 'hour' => $date->format('H'),
  236. 'minute' => $date->format('i'),
  237. 'second' => $date->format('s'),
  238. 'meridian' => $date->format('a'),
  239. ];
  240. }
  241. /**
  242. * Adjust $value based on rounding settings.
  243. *
  244. * @param int $value The value to adjust.
  245. * @param array $options The options containing interval and possibly round.
  246. * @return int The amount to adjust $value by.
  247. */
  248. protected function _adjustValue($value, $options)
  249. {
  250. $options += ['interval' => 1, 'round' => null];
  251. $changeValue = $value * (1 / $options['interval']);
  252. switch ($options['round']) {
  253. case 'up':
  254. $changeValue = ceil($changeValue);
  255. break;
  256. case 'down':
  257. $changeValue = floor($changeValue);
  258. break;
  259. default:
  260. $changeValue = round($changeValue);
  261. }
  262. return ($changeValue * $options['interval']) - $value;
  263. }
  264. /**
  265. * Generates a year select
  266. *
  267. * @param array $options Options list.
  268. * @param \Cake\View\Form\ContextInterface $context The current form context.
  269. * @return string
  270. */
  271. protected function _yearSelect($options, $context)
  272. {
  273. $options += [
  274. 'name' => '',
  275. 'val' => null,
  276. 'start' => date('Y', strtotime('-5 years')),
  277. 'end' => date('Y', strtotime('+5 years')),
  278. 'order' => 'desc',
  279. 'options' => []
  280. ];
  281. if (!empty($options['val'])) {
  282. $options['start'] = min($options['val'], $options['start']);
  283. $options['end'] = max($options['val'], $options['end']);
  284. }
  285. if (empty($options['options'])) {
  286. $options['options'] = $this->_generateNumbers($options['start'], $options['end']);
  287. }
  288. if ($options['order'] === 'desc') {
  289. $options['options'] = array_reverse($options['options'], true);
  290. }
  291. unset($options['start'], $options['end'], $options['order']);
  292. return $this->_select->render($options, $context);
  293. }
  294. /**
  295. * Generates a month select
  296. *
  297. * @param array $options The options to build the month select with
  298. * @param \Cake\View\Form\ContextInterface $context The current form context.
  299. * @return string
  300. */
  301. protected function _monthSelect($options, $context)
  302. {
  303. $options += [
  304. 'name' => '',
  305. 'names' => false,
  306. 'val' => null,
  307. 'leadingZeroKey' => true,
  308. 'leadingZeroValue' => false
  309. ];
  310. if (empty($options['options'])) {
  311. if ($options['names'] === true) {
  312. $options['options'] = $this->_getMonthNames($options['leadingZeroKey']);
  313. } elseif (is_array($options['names'])) {
  314. $options['options'] = $options['names'];
  315. } else {
  316. $options['options'] = $this->_generateNumbers(1, 12, $options);
  317. }
  318. }
  319. unset($options['leadingZeroKey'], $options['leadingZeroValue'], $options['names']);
  320. return $this->_select->render($options, $context);
  321. }
  322. /**
  323. * Generates a day select
  324. *
  325. * @param array $options The options to generate a day select with.
  326. * @param \Cake\View\Form\ContextInterface $context The current form context.
  327. * @return string
  328. */
  329. protected function _daySelect($options, $context)
  330. {
  331. $options += [
  332. 'name' => '',
  333. 'val' => null,
  334. 'leadingZeroKey' => true,
  335. 'leadingZeroValue' => false,
  336. ];
  337. $options['options'] = $this->_generateNumbers(1, 31, $options);
  338. unset($options['names'], $options['leadingZeroKey'], $options['leadingZeroValue']);
  339. return $this->_select->render($options, $context);
  340. }
  341. /**
  342. * Generates a hour select
  343. *
  344. * @param array $options The options to generate an hour select with
  345. * @param \Cake\View\Form\ContextInterface $context The current form context.
  346. * @return string
  347. */
  348. protected function _hourSelect($options, $context)
  349. {
  350. $options += [
  351. 'name' => '',
  352. 'val' => null,
  353. 'format' => 24,
  354. 'start' => null,
  355. 'end' => null,
  356. 'leadingZeroKey' => true,
  357. 'leadingZeroValue' => false,
  358. ];
  359. $is24 = $options['format'] == 24;
  360. $defaultStart = $is24 ? 0 : 1;
  361. $defaultEnd = $is24 ? 23 : 12;
  362. $options['start'] = max($defaultStart, $options['start']);
  363. $options['end'] = min($defaultEnd, $options['end']);
  364. if ($options['end'] === null) {
  365. $options['end'] = $defaultEnd;
  366. }
  367. if (!$is24 && $options['val'] > 12) {
  368. $options['val'] = sprintf('%02d', $options['val'] - 12);
  369. }
  370. if (!$is24 && in_array($options['val'], ['00', '0', 0], true)) {
  371. $options['val'] = 12;
  372. }
  373. if (empty($options['options'])) {
  374. $options['options'] = $this->_generateNumbers(
  375. $options['start'],
  376. $options['end'],
  377. $options
  378. );
  379. }
  380. unset(
  381. $options['end'], $options['start'],
  382. $options['format'], $options['leadingZeroKey'],
  383. $options['leadingZeroValue']
  384. );
  385. return $this->_select->render($options, $context);
  386. }
  387. /**
  388. * Generates a minute select
  389. *
  390. * @param array $options The options to generate a minute select with.
  391. * @param \Cake\View\Form\ContextInterface $context The current form context.
  392. * @return string
  393. */
  394. protected function _minuteSelect($options, $context)
  395. {
  396. $options += [
  397. 'name' => '',
  398. 'val' => null,
  399. 'interval' => 1,
  400. 'round' => 'up',
  401. 'leadingZeroKey' => true,
  402. 'leadingZeroValue' => true,
  403. ];
  404. $options['interval'] = max($options['interval'], 1);
  405. if (empty($options['options'])) {
  406. $options['options'] = $this->_generateNumbers(0, 59, $options);
  407. }
  408. unset(
  409. $options['leadingZeroKey'],
  410. $options['leadingZeroValue'],
  411. $options['interval'],
  412. $options['round']
  413. );
  414. return $this->_select->render($options, $context);
  415. }
  416. /**
  417. * Generates a second select
  418. *
  419. * @param array $options The options to generate a second select with
  420. * @param \Cake\View\Form\ContextInterface $context The current form context.
  421. * @return string
  422. */
  423. protected function _secondSelect($options, $context)
  424. {
  425. $options += [
  426. 'name' => '',
  427. 'val' => null,
  428. 'leadingZeroKey' => true,
  429. 'leadingZeroValue' => true,
  430. 'options' => $this->_generateNumbers(1, 60)
  431. ];
  432. unset($options['leadingZeroKey'], $options['leadingZeroValue']);
  433. return $this->_select->render($options, $context);
  434. }
  435. /**
  436. * Generates a meridian select
  437. *
  438. * @param array $options The options to generate a meridian select with.
  439. * @param \Cake\View\Form\ContextInterface $context The current form context.
  440. * @return string
  441. */
  442. protected function _meridianSelect($options, $context)
  443. {
  444. $options += [
  445. 'name' => '',
  446. 'val' => null,
  447. 'options' => ['am' => 'am', 'pm' => 'pm']
  448. ];
  449. return $this->_select->render($options, $context);
  450. }
  451. /**
  452. * Returns a translated list of month names
  453. *
  454. * @param bool $leadingZero Whether to generate month keys with leading zero.
  455. * @return array
  456. */
  457. protected function _getMonthNames($leadingZero = false)
  458. {
  459. $months = [
  460. '01' => __d('cake', 'January'),
  461. '02' => __d('cake', 'February'),
  462. '03' => __d('cake', 'March'),
  463. '04' => __d('cake', 'April'),
  464. '05' => __d('cake', 'May'),
  465. '06' => __d('cake', 'June'),
  466. '07' => __d('cake', 'July'),
  467. '08' => __d('cake', 'August'),
  468. '09' => __d('cake', 'September'),
  469. '10' => __d('cake', 'October'),
  470. '11' => __d('cake', 'November'),
  471. '12' => __d('cake', 'December'),
  472. ];
  473. if ($leadingZero === false) {
  474. $i = 1;
  475. foreach ($months as $key => $name) {
  476. $months[$i++] = $name;
  477. unset($months[$key]);
  478. }
  479. }
  480. return $months;
  481. }
  482. /**
  483. * Generates a range of numbers
  484. *
  485. * ### Options
  486. *
  487. * - leadingZeroKey - Set to true to add a leading 0 to single digit keys.
  488. * - leadingZeroValue - Set to true to add a leading 0 to single digit values.
  489. * - interval - The interval to generate numbers for. Defaults to 1.
  490. *
  491. * @param int $start Start of the range of numbers to generate
  492. * @param int $end End of the range of numbers to generate
  493. * @param array $options Options list.
  494. * @return array
  495. */
  496. protected function _generateNumbers($start, $end, $options = [])
  497. {
  498. $options += [
  499. 'leadingZeroKey' => true,
  500. 'leadingZeroValue' => true,
  501. 'interval' => 1
  502. ];
  503. $numbers = [];
  504. $i = $start;
  505. while ($i <= $end) {
  506. $key = (string)$i;
  507. $value = (string)$i;
  508. if ($options['leadingZeroKey'] === true) {
  509. $key = sprintf('%02d', $key);
  510. }
  511. if ($options['leadingZeroValue'] === true) {
  512. $value = sprintf('%02d', $value);
  513. }
  514. $numbers[$key] = $value;
  515. $i += $options['interval'];
  516. }
  517. return $numbers;
  518. }
  519. /**
  520. * Returns a list of fields that need to be secured for this widget.
  521. *
  522. * When the hour picker is in 24hr mode (null or format=24) the meridian
  523. * picker will be omitted.
  524. *
  525. * @param array $data The data to render.
  526. * @return array Array of fields to secure.
  527. */
  528. public function secureFields(array $data)
  529. {
  530. $fields = [];
  531. $hourFormat = isset($data['hour']['format']) ? $data['hour']['format'] : null;
  532. foreach ($this->_selects as $type) {
  533. if ($type === 'meridian' && ($hourFormat === null || $hourFormat === 24)) {
  534. continue;
  535. }
  536. if (!isset($data[$type]) || $data[$type] !== false) {
  537. $fields[] = $data['name'] . '[' . $type . ']';
  538. }
  539. }
  540. return $fields;
  541. }
  542. }