Skip to content

Commit 953708f

Browse files
committed
added addDate(), addTime() & addDateTime()
1 parent 62b039e commit 953708f

11 files changed

+635
-10
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: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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+
}
47+
48+
49+
public function getType(): int
50+
{
51+
return $this->type;
52+
}
53+
54+
55+
/**
56+
* Format of returned value. Allowed values are string (ie 'Y-m-d'), DateTimeControl::FormatObject and DateTimeControl::FormatTimestamp.
57+
* @return static
58+
*/
59+
public function setFormat(string $format)
60+
{
61+
$this->format = $format;
62+
return $this;
63+
}
64+
65+
66+
/**
67+
* @param \DateTimeInterface|string|int|null $value
68+
* @return static
69+
*/
70+
public function setValue($value)
71+
{
72+
$this->value = $value === null ? null : $this->normalizeValue($value);
73+
return $this;
74+
}
75+
76+
77+
/**
78+
* @return \DateTimeImmutable|string|int|null
79+
*/
80+
public function getValue()
81+
{
82+
if ($this->format === self::FormatObject) {
83+
return $this->value;
84+
} elseif ($this->format === self::FormatTimestamp) {
85+
return $this->value ? $this->value->getTimestamp() : null;
86+
} else {
87+
return $this->value ? $this->value->format($this->format) : null;
88+
}
89+
}
90+
91+
92+
/**
93+
* @param \DateTimeInterface|string|int $value
94+
*/
95+
private function normalizeValue($value): \DateTimeImmutable
96+
{
97+
if (is_numeric($value)) {
98+
$dt = (new \DateTimeImmutable)->setTimestamp((int) $value);
99+
} elseif (is_string($value)) {
100+
$dt = new \DateTimeImmutable($value); // createFromFormat() must not be used because it allows invalid values
101+
} elseif ($value instanceof \DateTime) {
102+
$dt = \DateTimeImmutable::createFromMutable($value);
103+
} elseif ($value instanceof \DateTimeImmutable) {
104+
$dt = $value;
105+
} elseif (!$value instanceof \DateTimeInterface) {
106+
throw new Nette\InvalidArgumentException('Value must be DateTimeInterface or string or null, ' . gettype($value) . ' given.');
107+
}
108+
109+
[$h, $m, $s] = [(int) $dt->format('H'), (int) $dt->format('i'), $this->withSeconds ? (int) $dt->format('s') : 0];
110+
if ($this->type === self::TypeDate) {
111+
return $dt->setTime(0, 0);
112+
} elseif ($this->type === self::TypeTime) {
113+
return $dt->setDate(0, 1, 1)->setTime($h, $m, $s);
114+
} elseif ($this->type === self::TypeDateTime) {
115+
return $dt->setTime($h, $m, $s);
116+
}
117+
}
118+
119+
120+
public function loadHttpData(): void
121+
{
122+
$value = $this->getHttpData(Nette\Forms\Form::DataText);
123+
try {
124+
$this->value = is_string($value) && preg_match('~^(\d{4}-\d{2}-\d{2})?T?(\d{2}:\d{2}(:\d{2}(\.\d+)?)?)?$~', $value)
125+
? $this->normalizeValue($value)
126+
: null;
127+
} catch (\Throwable $e) {
128+
$this->value = null;
129+
}
130+
}
131+
132+
133+
public function getControl(): Nette\Utils\Html
134+
{
135+
return parent::getControl()->addAttributes([
136+
'value' => $this->value ? $this->formatHtmlValue($this->value) : null,
137+
'type' => [self::TypeDate => 'date', self::TypeTime => 'time', self::TypeDateTime => 'datetime-local'][$this->type],
138+
]);
139+
}
140+
141+
142+
/**
143+
* Formats a date/time for HTML attributes.
144+
* @param \DateTimeInterface|string|int $value
145+
*/
146+
public function formatHtmlValue($value): string
147+
{
148+
$value = $this->normalizeValue($value);
149+
return $value->format([
150+
self::TypeDate => 'Y-m-d',
151+
self::TypeTime => $this->withSeconds ? 'H:i:s' : 'H:i',
152+
self::TypeDateTime => $this->withSeconds ? 'Y-m-d\\TH:i:s' : 'Y-m-d\\TH:i',
153+
][$this->type]);
154+
}
155+
156+
157+
/**
158+
* Formats a date/time according to the locale and formatting options.
159+
* @param \DateTimeInterface|string|int $value
160+
*/
161+
public function formatLocaleText($value): string
162+
{
163+
$value = $this->normalizeValue($value);
164+
if ($this->type === self::TypeDate) {
165+
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE]);
166+
} elseif ($this->type === self::TypeTime) {
167+
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::NONE, $this->withSeconds ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT]);
168+
} elseif ($this->type === self::TypeDateTime) {
169+
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::MEDIUM, $this->withSeconds ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT]);
170+
}
171+
}
172+
173+
174+
/** @return static */
175+
public function addRule($validator, $errorMessage = null, $arg = null)
176+
{
177+
if ($validator === Form::Min) {
178+
$this->control->min = $arg = $this->formatHtmlValue($arg);
179+
} elseif ($validator === Form::Max) {
180+
$this->control->max = $arg = $this->formatHtmlValue($arg);
181+
} elseif ($validator === Form::Range) {
182+
$this->control->min = isset($arg[0])
183+
? $arg[0] = $this->formatHtmlValue($arg[0])
184+
: null;
185+
$this->control->max = isset($arg[1])
186+
? $arg[1] = $this->formatHtmlValue($arg[1])
187+
: null;
188+
}
189+
190+
return parent::addRule($validator, $errorMessage, $arg);
191+
}
192+
}

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: 26 additions & 4 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,10 +188,25 @@ public static function validateValid(Controls\BaseControl $control): bool
181188
*/
182189
public static function validateRange(Control $control, array $range): bool
183190
{
191+
$value = $control->getValue();
192+
if ($control instanceof Controls\DateTimeControl) {
193+
$value = $control->formatHtmlValue($value);
194+
$range = array_map(function ($v) use ($control) {
195+
return $v === null ? null : $control->formatHtmlValue($v);
196+
}, $range);
197+
if (
198+
$control->getType() === Controls\DateTimeControl::TypeTime
199+
&& isset($range[0], $range[1])
200+
&& $range[0] > $range[1]
201+
) {
202+
return $value >= $range[0] || $value <= $range[1];
203+
}
204+
}
205+
184206
$range = array_map(function ($v) {
185207
return $v === '' ? null : $v;
186208
}, $range);
187-
return Validators::isInRange($control->getValue(), $range);
209+
return Validators::isInRange($value, $range);
188210
}
189211

190212

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)