PageRenderTime 45ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/concrete/src/Form/Service/Widget/DateTime.php

http://github.com/concrete5/concrete5
PHP | 455 lines | 367 code | 24 blank | 64 comment | 71 complexity | ad11adf1d8f5179c501473c25905305f MD5 | raw file
Possible License(s): MIT, LGPL-2.1, MPL-2.0-no-copyleft-exception, BSD-3-Clause
  1. <?php
  2. namespace Concrete\Core\Form\Service\Widget;
  3. use Concrete\Core\Http\ResponseAssetGroup;
  4. use Concrete\Core\Support\Facade\Application;
  5. use DateTime as PHPDateTime;
  6. use Exception;
  7. class DateTime
  8. {
  9. /**
  10. * Takes a "field" and grabs all the corresponding disparate fields from $_POST and translates into a timestamp.
  11. * If $field has both date and time, the resulting value will be converted from the user timezone to the system timezone.
  12. * If $field has only date and not time, no timezone conversion will occur.
  13. *
  14. * @param string $field The name of the field to translate
  15. * @param array $arr The array containing the value. If null (default) we'll use $_POST
  16. * @param bool $asDateTime Set to true to get a DateTime object, false (default) for a string representation
  17. *
  18. * @return \DateTime|string|null In case of success returns the timestamp (in the format 'Y-m-d H:i:s' or 'Y-m-d' if $asDateTime is false) or the DateTime instance (if $asDateTime is true); if the date/time was not received we'll return null (if $field value is empty)
  19. */
  20. public function translate($field, $arr = null, $asDateTime = false)
  21. {
  22. $app = Application::getFacadeApplication();
  23. $dh = $app->make('helper/date');
  24. /* @var \Concrete\Core\Localization\Service\Date $dh */
  25. if ($arr === null) {
  26. $arr = $_POST;
  27. }
  28. // Example of $field: akID[5][value]
  29. if (preg_match('/^([^\[\]]+)\[([^\[\]]+)(?:\]\[([^\[\]]+))*\]$/', $field, $matches)) {
  30. // Example: $matches === ['akID[5][test]', 'akID', '5', 'value']
  31. array_shift($matches);
  32. while (isset($matches[1]) && is_array($arr)) {
  33. $key = array_shift($matches);
  34. $arr = isset($arr[$key]) ? $arr[$key] : null;
  35. }
  36. $field = $matches[0];
  37. }
  38. $datetime = null;
  39. if (is_array($arr)) {
  40. $systemTimezone = $dh->getTimezone('system');
  41. if (isset($arr[$field . '_dt'])) {
  42. $value = $arr[$field . '_dt'];
  43. if (is_string($value) && trim($value) !== '') {
  44. $h = isset($arr[$field . '_h']) ? (int) $arr[$field . '_h'] : 0;
  45. $m = isset($arr[$field . '_m']) ? (int) $arr[$field . '_m'] : 0;
  46. $s = isset($arr[$field . '_s']) ? (int) $arr[$field . '_s'] : 0;
  47. if (isset($arr[$field . '_a'])) {
  48. if ($arr[$field . '_a'] === 'AM' && $h === 12) {
  49. $h = 0;
  50. } elseif ($arr[$field . '_a'] === 'PM' && $h < 12) {
  51. $h += 12;
  52. }
  53. }
  54. $value .= ' ' . substr("0$h", -2) . ':' . substr("0$m", -2) . ':' . substr("0$s", -2);
  55. try {
  56. $datetime = new PHPDateTime($value, $dh->getTimezone('user'));
  57. } catch (Exception $foo) {
  58. }
  59. $datetime->setTimezone($systemTimezone);
  60. }
  61. } elseif (isset($arr[$field])) {
  62. $value = $arr[$field];
  63. if (is_string($value) && trim($value) !== '') {
  64. try {
  65. $datetime = new PHPDateTime($value, $systemTimezone);
  66. } catch (Exception $foo) {
  67. }
  68. }
  69. }
  70. }
  71. if ($datetime === null || $asDateTime) {
  72. $result = $datetime;
  73. } else {
  74. $result = $datetime->format('Y-m-d H:i:s');
  75. }
  76. return $result;
  77. }
  78. /**
  79. * Creates form fields and JavaScript calendar includes for a particular item (date/time string representations will be converted from the user system-zone to the time-zone).
  80. *
  81. * @param string $field The field prefix (will be used as $field parameter in the translate method)
  82. * @param \DateTime|string $value The initial value
  83. * @param bool $includeActivation Set to true to include a checkbox to enable/disable the date/time fields
  84. * @param bool $calendarAutoStart Set to false to avoid initializing the Javascript calendar
  85. * @param string $classes A list of space-separated classes to add to the ui-datepicker-div container
  86. * @param int $timeResolution The time resolution in seconds (60 means we won't ask seconds)
  87. * @param array $datePickerOptions datepicker properties, see jquery-ui datepicker docs
  88. *
  89. * @return string
  90. */
  91. public function datetime($field, $value = null, $includeActivation = false, $calendarAutoStart = true, $classes = null, $timeResolution = 60, array $datePickerOptions = [])
  92. {
  93. $app = Application::getFacadeApplication();
  94. $dh = $app->make('helper/date');
  95. /* @var \Concrete\Core\Localization\Service\Date $dh */
  96. $timeResolution = (int) $timeResolution;
  97. if ($timeResolution < 1) {
  98. $timeResolution = 60;
  99. }
  100. // Calculate the field names
  101. if (substr($field, -1) === ']') {
  102. $prefix = substr($field, 0, -1);
  103. $fieldActivate = $prefix . '_activate]';
  104. $fieldDate = $prefix . '_dt]';
  105. $fieldHours = $prefix . '_h]';
  106. $fieldMinutes = $prefix . '_m]';
  107. $fieldSeconds = $prefix . '_s]';
  108. $fieldAMPM = $prefix . '_a]';
  109. } else {
  110. $checkPostField = $field;
  111. $checkPostData = $_POST;
  112. $fieldActivate = $field . '_activate';
  113. $fieldDate = $field . '_dt';
  114. $fieldHours = $field . '_h';
  115. $fieldMinutes = $field . '_m';
  116. $fieldSeconds = $field . '_s';
  117. $fieldAMPM = $field . '_a';
  118. }
  119. $id = trim(preg_replace('/[^0-9A-Za-z-]+/', '_', $field), '_');
  120. // Set the initial date/time value
  121. $dateTime = null;
  122. $requestValue = $this->translate($field, null, true);
  123. if ($requestValue !== null) {
  124. $dateTime = $requestValue;
  125. $dateTime->setTimezone($dh->getTimezone('user'));
  126. } else {
  127. if ($value) {
  128. if ($value instanceof PHPDateTime) {
  129. $dateTime = clone $value;
  130. $dateTime->setTimezone($dh->getTimezone('user'));
  131. } else {
  132. try {
  133. $dateTime = $dh->toDateTime($value, 'user', 'system');
  134. } catch (Exception $x) {
  135. }
  136. }
  137. }
  138. }
  139. // Determine the date/time parts
  140. $timeFormat = $dh->getTimeFormat();
  141. if ($dateTime === null) {
  142. $now = new PHPDateTime('now', $dh->getTimezone('user'));
  143. $timeHour = (int) $now->format('G');
  144. $timeMinute = (int) $now->format('i');
  145. $timeSecond = (int) $now->format('s');
  146. } else {
  147. $timeHour = (int) $dateTime->format('G');
  148. $timeMinute = (int) $dateTime->format('i');
  149. $timeSecond = (int) $dateTime->format('s');
  150. }
  151. if ($timeFormat === 12) {
  152. $timeAMPM = ($timeHour < 12) ? 'AM' : 'PM';
  153. $timeHour = ($timeHour % 12);
  154. if ($timeHour === 0) {
  155. $timeHour = 12;
  156. }
  157. }
  158. // Split the time resolution
  159. $tr = $timeResolution;
  160. $stepSeconds = $tr % 60;
  161. $tr = (int) (($tr - $stepSeconds) / 60);
  162. $stepMinutes = $tr % 60;
  163. $tr = (int) (($tr - $stepMinutes) / 60);
  164. $stepHours = $tr;
  165. if ($stepSeconds !== 0 && $stepMinutes === 0) {
  166. $stepMinutes = 1;
  167. }
  168. if ($stepHours === 0) {
  169. $stepHours = 1;
  170. }
  171. // Build HTML
  172. $datePickerOptionsAsJSON = json_encode($datePickerOptions, JSON_FORCE_OBJECT);
  173. $shownDateFormat = $dh->getPHPDatePattern();
  174. $disabled = '';
  175. $html = '<div class="form-inline">';
  176. if ($includeActivation) {
  177. $html .= '<input type="checkbox" id="' . $id . '_activate" class="ccm-activate-date-time" ccm-date-time-id="' . $id . '" name="' . $fieldActivate . '"';
  178. if ($dateTime === null) {
  179. $disabled = ' disabled="disabled"';
  180. } else {
  181. $html .= ' checked="checked"';
  182. }
  183. $html .= ' />';
  184. }
  185. $html .= '<span class="ccm-input-date-wrapper" id="' . $id . '_dw">';
  186. $html .= '<input type="text" id="' . $id . '_dt_pub" class="form-control ccm-input-date"' . $disabled;
  187. if (!$calendarAutoStart && $dateTime !== null) {
  188. $html .= ' value="' . h($dateTime->format($shownDateFormat)) . '"';
  189. }
  190. $html .= ' />';
  191. $html .= '<input type="hidden" id="' . $id . '_dt" name="' . $fieldDate . '"' . $disabled;
  192. if (!$calendarAutoStart && $dateTime !== null) {
  193. $html .= ' value="' . h($dateTime->format('Y-m-d')) . '"';
  194. }
  195. $html .= ' />';
  196. $html .= '</span>';
  197. $html .= '<span class="ccm-input-time-wrapper form-inline" id="' . $id . '_tw">';
  198. $html .= '<select class="form-control" id="' . $id . '_h" name="' . $fieldHours . '"' . $disabled . '>';
  199. $hourStart = ($timeFormat === 12) ? 1 : 0;
  200. $hourEnd = ($timeFormat === 12) ? 12 : 23;
  201. $hourList = [];
  202. for ($i = $hourStart; $i <= $hourEnd; $i += $stepHours) {
  203. $hoursList[] = $i;
  204. }
  205. $timeHour = $this->selectNearestValue($hoursList, $timeHour);
  206. foreach ($hoursList as $i) {
  207. $html .= '<option value="' . $i . '"';
  208. if ($i === $timeHour) {
  209. $html .= ' selected="selected"';
  210. }
  211. $html .= '>' . $i . '</option>';
  212. }
  213. $html .= '</select>';
  214. if ($stepMinutes !== 0) {
  215. $html .= '<span class="separator">:</span>';
  216. $html .= '<select class="form-control" id="' . $id . '_m" name="' . $fieldMinutes . '"' . $disabled . '>';
  217. $minutesList = [];
  218. for ($i = 0; $i < 60; $i += $stepMinutes) {
  219. $minutesList[] = $i;
  220. }
  221. $timeMinute = $this->selectNearestValue($minutesList, $timeMinute);
  222. foreach ($minutesList as $i) {
  223. $html .= '<option value="' . sprintf('%02d', $i) . '"';
  224. if ($i === $timeMinute) {
  225. $html .= ' selected="selected"';
  226. }
  227. $html .= '>' . sprintf('%02d', $i) . '</option>';
  228. }
  229. $html .= '</select>';
  230. if ($timeFormat === 12) {
  231. $html .= '<select class="form-control" id="' . $id . '_a" name="' . $fieldAMPM . '"' . $disabled . '>';
  232. $html .= '<option value="AM"';
  233. if ($timeAMPM === 'AM') {
  234. $html .= ' selected="selected"';
  235. }
  236. $html .= '>';
  237. // This prints out the translation of "AM" in the current language
  238. $html .= $dh->date('A', mktime(1), 'system');
  239. $html .= '</option>';
  240. $html .= '<option value="PM"';
  241. if ($timeAMPM === 'PM') {
  242. $html .= ' selected="selected"';
  243. }
  244. $html .= '>';
  245. // This prints out the translation of "PM" in the current language
  246. $html .= $dh->date('A', mktime(13), 'system');
  247. $html .= '</option>';
  248. $html .= '</select>';
  249. }
  250. if ($stepSeconds !== 0) {
  251. $html .= '<span class="separator">:</span>';
  252. $html .= '<select class="form-control" id="' . $id . '_s" name="' . $fieldSeconds . '"' . $disabled . '>';
  253. $secondsList = [];
  254. for ($i = 0; $i < 60; $i += $stepSeconds) {
  255. $secondsList[] = $i;
  256. }
  257. $timeSecond = $this->selectNearestValue($secondsList, $timeSecond);
  258. foreach ($secondsList as $i) {
  259. $html .= '<option value="' . sprintf('%02d', $i) . '"';
  260. if ($i === $timeSecond) {
  261. $html .= ' selected="selected"';
  262. }
  263. $html .= '>' . sprintf('%02d', $i) . '</option>';
  264. }
  265. $html .= '</select>';
  266. }
  267. }
  268. $html .= '</span>';
  269. $html .= '</div>';
  270. // Create the Javascript for the calendar
  271. if ($calendarAutoStart) {
  272. $assetList = ResponseAssetGroup::get();
  273. $assetList->requireAsset('jquery/ui');
  274. $dateFormat = json_encode($dh->getJQueryUIDatePickerFormat($shownDateFormat));
  275. if ($classes) {
  276. $beforeShow = 'beforeShow: function() { $(\'#ui-datepicker-div\').addClass(' . json_encode((string) $classes) . '); },';
  277. } else {
  278. $beforeShow = '';
  279. }
  280. if ($dateTime === null) {
  281. $defaultDateJs = "''";
  282. } else {
  283. $defaultDateJs = 'new Date(' . implode(', ', [$dateTime->format('Y'), $dateTime->format('n') - 1, (int) $dateTime->format('j')]) . ')';
  284. }
  285. $html .= <<<EOT
  286. <script type="text/javascript">
  287. $(function() {
  288. $('#{$id}_dt_pub').datepicker($.extend({
  289. dateFormat: $dateFormat,
  290. altFormat: 'yy-mm-dd',
  291. altField: '#{$id}_dt',
  292. changeYear: true,
  293. showAnim: 'fadeIn',
  294. yearRange: 'c-100:c+10',
  295. $beforeShow
  296. onClose: function(dateText, inst) {
  297. if(!dateText) {
  298. $(inst.settings.altField).val('');
  299. }
  300. }
  301. },{$datePickerOptionsAsJSON})).datepicker('setDate', $defaultDateJs).attr('autocomplete', 'off');
  302. });
  303. </script>
  304. EOT;
  305. }
  306. // Add the Javascript to handle the activation
  307. if ($includeActivation) {
  308. $html .= <<<EOT
  309. <script type="text/javascript">
  310. $(function() {
  311. $('#{$id}_activate').click(function() {
  312. if ($(this).is(':checked')) {
  313. $('#{$id}_dw input,#{$id}_tw select').removeAttr('disabled');
  314. } else {
  315. $('#{$id}_dw input,#{$id}_tw select').attr('disabled', 'disabled');
  316. }
  317. });
  318. });
  319. </script>
  320. EOT;
  321. }
  322. return $html;
  323. }
  324. /**
  325. * Creates form fields and JavaScript calendar includes for a particular item but includes only calendar controls (no time, so no time-zone conversions will be applied).
  326. *
  327. * @param string $field The field name (will be used as $field parameter in the translate method)
  328. * @param \DateTime|string $value The initial value
  329. * @param bool $calendarAutoStart Set to false to avoid initializing the Javascript calendar
  330. * @param array $datePickerOptions datepicker properties, see jquery-ui datepicker docs
  331. *
  332. * @return string
  333. */
  334. public function date($field, $value = null, $calendarAutoStart = true, array $datePickerOptions = [])
  335. {
  336. $app = Application::getFacadeApplication();
  337. $dh = $app->make('helper/date');
  338. /* @var \Concrete\Core\Localization\Service\Date $dh */
  339. $fh = $app->make('helper/form');
  340. /* @var \Concrete\Core\Form\Service\Form $fh */
  341. // Calculate the field names
  342. $id = trim(preg_replace('/[^0-9A-Za-z-]+/', '_', $field), '_');
  343. // Set the initial date/time value
  344. $dateTime = null;
  345. $requestValue = $this->translate($field, null, true);
  346. if ($requestValue !== null) {
  347. $dateTime = $requestValue;
  348. } elseif ($value) {
  349. if ($value instanceof PHPDateTime) {
  350. $dateTime = $value;
  351. } else {
  352. try {
  353. $dateTime = $dh->toDateTime($value);
  354. } catch (Exception $x) {
  355. }
  356. }
  357. }
  358. // Build HTML
  359. $datePickerOptionsAsJSON = json_encode($datePickerOptions, JSON_FORCE_OBJECT);
  360. $shownDateFormat = $dh->getPHPDatePattern();
  361. $html = '<div>';
  362. $html .= '<span class="ccm-input-date-wrapper" id="' . $id . '_dw">';
  363. $html .= '<input type="text" id="' . $id . '_pub" class="form-control ccm-input-date"';
  364. if (!$calendarAutoStart && $dateTime !== null) {
  365. $html .= ' value="' . h($dateTime->format($shownDateFormat)) . '"';
  366. }
  367. $html .= '/>';
  368. $html .= '<input type="hidden" id="' . $id . '" name="' . $field . '" />';
  369. $html .= '</span>';
  370. $html .= '</div>';
  371. // Create the Javascript for the calendar
  372. if ($calendarAutoStart) {
  373. $assetList = ResponseAssetGroup::get();
  374. $assetList->requireAsset('jquery/ui');
  375. $dateFormat = json_encode($dh->getJQueryUIDatePickerFormat($shownDateFormat));
  376. if ($dateTime === null) {
  377. $defaultDateJs = "''";
  378. } else {
  379. $defaultDateJs = 'new Date(' . implode(', ', [$dateTime->format('Y'), $dateTime->format('n') - 1, (int) $dateTime->format('j')]) . ')';
  380. }
  381. $html .= <<<EOT
  382. <script type="text/javascript">
  383. $(function() {
  384. $('#{$id}_pub').datepicker($.extend({
  385. dateFormat: $dateFormat,
  386. altFormat: 'yy-mm-dd',
  387. altField: '#{$id}',
  388. changeYear: true,
  389. showAnim: 'fadeIn',
  390. yearRange: 'c-100:c+10',
  391. onClose: function(dateText, inst) {
  392. if(!dateText) {
  393. $(inst.settings.altField).val('');
  394. }
  395. }
  396. },{$datePickerOptionsAsJSON})).datepicker('setDate', $defaultDateJs).attr('autocomplete', 'off');
  397. });
  398. </script>
  399. EOT;
  400. }
  401. return $html;
  402. }
  403. /**
  404. * Choose an array value nearest to a specified value.
  405. * Useful when we work with time resolutions.
  406. *
  407. * @param int $values
  408. * @param int $wantedValue
  409. *
  410. * @return int
  411. *
  412. * @example If the current time is 10 and the time resolution is 15, we have an array of values of [0, 15, 30, 45]: the closest value is 15.
  413. */
  414. protected function selectNearestValue(array $values, $wantedValue)
  415. {
  416. if (in_array($wantedValue, $values)) {
  417. $result = $wantedValue;
  418. } else {
  419. $result = null;
  420. $minDelta = PHP_INT_MAX;
  421. foreach ($values as $value) {
  422. $delta = abs($value - $wantedValue);
  423. if ($delta < $minDelta) {
  424. $minDelta = $delta;
  425. $result = $value;
  426. }
  427. }
  428. }
  429. return $result;
  430. }
  431. }