Skip to content

Commit 9373bfc

Browse files
committed
added addDate(), addTime() & addDateTime()
1 parent 4e9b083 commit 9373bfc

11 files changed

+716
-9
lines changed

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
"conflict": {
3232
"latte/latte": ">=3.1"
3333
},
34+
"suggest": {
35+
"ext-intl": "to use date/time controls"
36+
},
3437
"autoload": {
3538
"classmap": ["src/"]
3639
},

src/Forms/Container.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,36 @@ public function addFloat(string $name, $label = null): Controls\TextInput
405405
}
406406

407407

408+
/**
409+
* Adds input for date selection.
410+
* @param string|object|null $label
411+
*/
412+
public function addDate(string $name, $label = null): Controls\DateTimeControl
413+
{
414+
return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDate);
415+
}
416+
417+
418+
/**
419+
* Adds input for time selection.
420+
* @param string|object|null $label
421+
*/
422+
public function addTime(string $name, $label = null, bool $withSeconds = false): Controls\DateTimeControl
423+
{
424+
return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeTime, $withSeconds);
425+
}
426+
427+
428+
/**
429+
* Adds input for date and time selection.
430+
* @param string|object|null $label
431+
*/
432+
public function addDateTime(string $name, $label = null, bool $withSeconds = false): Controls\DateTimeControl
433+
{
434+
return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDateTime, $withSeconds);
435+
}
436+
437+
408438
/**
409439
* Adds control that allows the user to upload files.
410440
* @param string|object|null $label
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Framework (https://nette.org)
5+
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Nette\Forms\Controls;
11+
12+
use Nette;
13+
use Nette\Forms\Form;
14+
15+
16+
/**
17+
* Selects date or time or date & time.
18+
*/
19+
class DateTimeControl extends BaseControl
20+
{
21+
public const
22+
TypeDate = 1,
23+
TypeTime = 2,
24+
TypeDateTime = 3;
25+
26+
public const
27+
FormatObject = 'object',
28+
FormatTimestamp = 'timestamp';
29+
30+
/** @var int */
31+
private $type;
32+
33+
/** @var bool */
34+
private $withSeconds;
35+
36+
/** @var string */
37+
private $format = self::FormatObject;
38+
39+
40+
public function __construct($label = null, int $type = self::TypeDate, bool $withSeconds = false)
41+
{
42+
$this->type = $type;
43+
$this->withSeconds = $withSeconds;
44+
parent::__construct($label);
45+
$this->control->step = $withSeconds ? 1 : null;
46+
$this->setOption('type', 'datetime');
47+
}
48+
49+
50+
/**
51+
* Format of returned value. Allowed values are string (ie 'Y-m-d'), DateTimeControl::FormatObject and DateTimeControl::FormatTimestamp.
52+
* @return static
53+
*/
54+
public function setFormat(string $format)
55+
{
56+
$this->format = $format;
57+
return $this;
58+
}
59+
60+
61+
/**
62+
* @param \DateTimeInterface|string|int|null $value
63+
* @return static
64+
*/
65+
public function setValue($value)
66+
{
67+
$this->value = $value === null || $value === ''
68+
? null
69+
: $this->normalizeValue($value);
70+
return $this;
71+
}
72+
73+
74+
/**
75+
* @return \DateTimeImmutable|string|int|null
76+
*/
77+
public function getValue()
78+
{
79+
if ($this->format === self::FormatObject) {
80+
return $this->value;
81+
} elseif ($this->format === self::FormatTimestamp) {
82+
return $this->value ? $this->value->getTimestamp() : null;
83+
} else {
84+
return $this->value ? $this->value->format($this->format) : null;
85+
}
86+
}
87+
88+
89+
/**
90+
* @param \DateTimeInterface|string|int $value
91+
*/
92+
private function normalizeValue($value): \DateTimeImmutable
93+
{
94+
if (is_numeric($value)) {
95+
$dt = (new \DateTimeImmutable)->setTimestamp((int) $value);
96+
} elseif (is_string($value) && $value !== '') {
97+
$dt = $this->createDateTimeFromString($value);
98+
} elseif ($value instanceof \DateTime) {
99+
$dt = \DateTimeImmutable::createFromMutable($value);
100+
} elseif ($value instanceof \DateTimeImmutable) {
101+
$dt = $value;
102+
} else {
103+
throw new Nette\InvalidArgumentException('Value must be DateTimeInterface|string|int|null, ' . gettype($value) . ' given.');
104+
}
105+
106+
[$h, $m, $s] = [(int) $dt->format('H'), (int) $dt->format('i'), $this->withSeconds ? (int) $dt->format('s') : 0];
107+
if ($this->type === self::TypeDate) {
108+
return $dt->setTime(0, 0);
109+
} elseif ($this->type === self::TypeTime) {
110+
return $dt->setDate(0, 1, 1)->setTime($h, $m, $s);
111+
} elseif ($this->type === self::TypeDateTime) {
112+
return $dt->setTime($h, $m, $s);
113+
}
114+
}
115+
116+
117+
public function loadHttpData(): void
118+
{
119+
$value = $this->getHttpData(Nette\Forms\Form::DataText);
120+
try {
121+
$this->value = is_string($value) && preg_match('~^[\dT.:-]+$~', $value)
122+
? $this->normalizeValue($value)
123+
: null;
124+
} catch (\Throwable $e) {
125+
$this->value = null;
126+
}
127+
}
128+
129+
130+
public function getControl(): Nette\Utils\Html
131+
{
132+
return parent::getControl()->addAttributes($this->getAttributesFromRules())->addAttributes([
133+
'value' => $this->value ? $this->formatHtmlValue($this->value) : null,
134+
'type' => [self::TypeDate => 'date', self::TypeTime => 'time', self::TypeDateTime => 'datetime-local'][$this->type],
135+
]);
136+
}
137+
138+
139+
/**
140+
* Formats a date/time for HTML attributes.
141+
* @param \DateTimeInterface|string|int $value
142+
*/
143+
public function formatHtmlValue($value): string
144+
{
145+
$value = $this->normalizeValue($value);
146+
return $value->format([
147+
self::TypeDate => 'Y-m-d',
148+
self::TypeTime => $this->withSeconds ? 'H:i:s' : 'H:i',
149+
self::TypeDateTime => $this->withSeconds ? 'Y-m-d\\TH:i:s' : 'Y-m-d\\TH:i',
150+
][$this->type]);
151+
}
152+
153+
154+
/**
155+
* Formats a date/time according to the locale and formatting options.
156+
* @param \DateTimeInterface|string|int $value
157+
*/
158+
public function formatLocaleText($value): string
159+
{
160+
$value = $this->normalizeValue($value);
161+
if ($this->type === self::TypeDate) {
162+
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE]);
163+
} elseif ($this->type === self::TypeTime) {
164+
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::NONE, $this->withSeconds ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT]);
165+
} elseif ($this->type === self::TypeDateTime) {
166+
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::MEDIUM, $this->withSeconds ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT]);
167+
}
168+
}
169+
170+
171+
private function getAttributesFromRules(): array
172+
{
173+
$attrs = [];
174+
$format = function ($val) {
175+
return is_scalar($val) || $val instanceof \DateTimeInterface
176+
? $this->formatHtmlValue($val)
177+
: null;
178+
};
179+
foreach ($this->getRules() as $rule) {
180+
if ($rule->branch) {
181+
} elseif (!$rule->canExport()) {
182+
break;
183+
} elseif ($rule->validator === Form::Min) {
184+
$attrs['min'] = $format($rule->arg);
185+
} elseif ($rule->validator === Form::Max) {
186+
$attrs['max'] = $format($rule->arg);
187+
} elseif ($rule->validator === Form::Range) {
188+
$attrs['min'] = $format($rule->arg[0] ?? null);
189+
$attrs['max'] = $format($rule->arg[1] ?? null);
190+
}
191+
}
192+
return $attrs;
193+
}
194+
195+
196+
public function validateMinMax($min, $max): bool
197+
{
198+
$value = $this->normalizeValue($this->value);
199+
$min = $min === null ? null : $this->normalizeValue($min);
200+
$max = $max === null ? null : $this->normalizeValue($max);
201+
return $this->type === self::TypeTime && $min > $max
202+
? $value >= $min || $value <= $max
203+
: $value >= $min && ($max === null || $value <= $max);
204+
}
205+
206+
207+
private function createDateTimeFromString(string $value): \DateTimeImmutable
208+
{
209+
$dt = new \DateTimeImmutable($value);
210+
$errors = \DateTimeImmutable::getLastErrors();
211+
if ($errors && $errors['warnings']) {
212+
throw new Nette\InvalidArgumentException(Nette\Utils\Arrays::first($errors['warnings']) . " '$value'");
213+
}
214+
return $dt;
215+
}
216+
}

src/Forms/Helpers.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,10 @@ public static function exportRules(Rules $rules): array
141141
if (is_array($rule->arg)) {
142142
$item['arg'] = [];
143143
foreach ($rule->arg as $key => $value) {
144-
$item['arg'][$key] = $value instanceof Control
145-
? ['control' => $value->getHtmlName()]
146-
: $value;
144+
$item['arg'][$key] = self::exportArgument($value, $rule->control);
147145
}
148146
} elseif ($rule->arg !== null) {
149-
$item['arg'] = $rule->arg instanceof Control
150-
? ['control' => $rule->arg->getHtmlName()]
151-
: $rule->arg;
147+
$item['arg'] = self::exportArgument($rule->arg, $rule->control);
152148
}
153149

154150
$payload[] = $item;
@@ -158,6 +154,18 @@ public static function exportRules(Rules $rules): array
158154
}
159155

160156

157+
private static function exportArgument($value, Control $control)
158+
{
159+
if ($value instanceof Control) {
160+
return ['control' => $value->getHtmlName()];
161+
} elseif ($control instanceof Controls\DateTimeControl) {
162+
return $control->formatHtmlValue($value);
163+
} else {
164+
return $value;
165+
}
166+
}
167+
168+
161169
public static function createInputList(
162170
array $items,
163171
?array $inputAttrs = null,

src/Forms/Validator.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,16 @@ public static function formatMessage(Rule $rule, bool $withValue = true)
9292
default:
9393
$args = is_array($rule->arg) ? $rule->arg : [$rule->arg];
9494
$i = (int) $m[1] ? (int) $m[1] - 1 : $i + 1;
95-
return isset($args[$i])
96-
? ($args[$i] instanceof Control ? ($withValue ? $args[$i]->getValue() : "%$i") : $args[$i])
97-
: '';
95+
$arg = $args[$i] ?? null;
96+
if ($arg === null) {
97+
return '';
98+
} elseif ($arg instanceof Control) {
99+
return $withValue ? $args[$i]->getValue() : "%$i";
100+
} elseif ($rule->control instanceof Controls\DateTimeControl) {
101+
return $rule->control->formatLocaleText($arg);
102+
} else {
103+
return $arg;
104+
}
98105
}
99106
}, $message);
100107
return $message;
@@ -181,6 +188,9 @@ public static function validateValid(Controls\BaseControl $control): bool
181188
*/
182189
public static function validateRange(Control $control, array $range): bool
183190
{
191+
if ($control instanceof Controls\DateTimeControl) {
192+
return $control->validateMinMax($range[0] ?? null, $range[1] ?? null);
193+
}
184194
$range = array_map(function ($v) {
185195
return $v === '' ? null : $v;
186196
}, $range);

src/assets/netteForms.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,8 @@
508508
range: function(elem, arg, val) {
509509
if (!Array.isArray(arg)) {
510510
return null;
511+
} else if (elem.type === 'time' && arg[0] > arg[1]) {
512+
return val >= arg[0] || val <= arg[1];
511513
}
512514
return (arg[0] === null || Nette.validators.min(elem, arg[0], val))
513515
&& (arg[1] === null || Nette.validators.max(elem, arg[1], val));
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/**
4+
* Test: Nette\Forms\Controls\DateTimeControl.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
use Nette\Forms\Controls\DateTimeControl;
10+
use Nette\Forms\Form;
11+
use Tester\Assert;
12+
13+
14+
require __DIR__ . '/../bootstrap.php';
15+
16+
17+
test('string format', function () {
18+
$form = new Form;
19+
$input = $form->addDate('date')
20+
->setValue('2023-10-22 10:30')
21+
->setFormat('j.n.Y');
22+
23+
Assert::same('22.10.2023', $input->getValue());
24+
});
25+
26+
27+
test('timestamp format', function () {
28+
$form = new Form;
29+
$input = $form->addDate('date')
30+
->setValue('2023-10-22 10:30')
31+
->setFormat(DateTimeControl::FormatTimestamp);
32+
33+
Assert::same(1697925600, $input->getValue());
34+
});
35+
36+
37+
test('object format', function () {
38+
$form = new Form;
39+
$input = $form->addDate('date')
40+
->setValue('2023-10-22 10:30')
41+
->setFormat(DateTimeControl::FormatObject);
42+
43+
Assert::equal(new DateTimeImmutable('2023-10-22 00:00'), $input->getValue());
44+
});

0 commit comments

Comments
 (0)