Skip to content

Commit a1d6c73

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

11 files changed

+722
-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: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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->createDateTime($value);
98+
} elseif ($value instanceof \DateTime) {
99+
$dt = \DateTimeImmutable::createFromMutable($value);
100+
} elseif ($value instanceof \DateTimeImmutable) {
101+
$dt = $value;
102+
} else {
103+
throw new \TypeError('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+
try {
120+
parent::loadHttpData();
121+
} catch (\Throwable $e) {
122+
$this->value = null;
123+
}
124+
}
125+
126+
127+
public function getControl(): Nette\Utils\Html
128+
{
129+
return parent::getControl()->addAttributes($this->getAttributesFromRules())->addAttributes([
130+
'value' => $this->value ? $this->formatHtmlValue($this->value) : null,
131+
'type' => [self::TypeDate => 'date', self::TypeTime => 'time', self::TypeDateTime => 'datetime-local'][$this->type],
132+
]);
133+
}
134+
135+
136+
/**
137+
* Formats a date/time for HTML attributes.
138+
* @param \DateTimeInterface|string|int $value
139+
*/
140+
public function formatHtmlValue($value): string
141+
{
142+
$value = $this->normalizeValue($value);
143+
return $value->format([
144+
self::TypeDate => 'Y-m-d',
145+
self::TypeTime => $this->withSeconds ? 'H:i:s' : 'H:i',
146+
self::TypeDateTime => $this->withSeconds ? 'Y-m-d\\TH:i:s' : 'Y-m-d\\TH:i',
147+
][$this->type]);
148+
}
149+
150+
151+
/**
152+
* Formats a date/time according to the locale and formatting options.
153+
* @param \DateTimeInterface|string|int $value
154+
*/
155+
public function formatLocaleText($value): string
156+
{
157+
$value = $this->normalizeValue($value);
158+
if ($this->type === self::TypeDate) {
159+
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE]);
160+
} elseif ($this->type === self::TypeTime) {
161+
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::NONE, $this->withSeconds ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT]);
162+
} elseif ($this->type === self::TypeDateTime) {
163+
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::MEDIUM, $this->withSeconds ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT]);
164+
}
165+
}
166+
167+
168+
private function getAttributesFromRules(): array
169+
{
170+
$attrs = [];
171+
$format = function ($val) {
172+
return is_scalar($val) || $val instanceof \DateTimeInterface
173+
? $this->formatHtmlValue($val)
174+
: null;
175+
};
176+
foreach ($this->getRules() as $rule) {
177+
if ($rule->branch) {
178+
} elseif (!$rule->canExport()) {
179+
break;
180+
} elseif ($rule->validator === Form::Min) {
181+
$attrs['min'] = $format($rule->arg);
182+
} elseif ($rule->validator === Form::Max) {
183+
$attrs['max'] = $format($rule->arg);
184+
} elseif ($rule->validator === Form::Range) {
185+
$attrs['min'] = $format($rule->arg[0] ?? null);
186+
$attrs['max'] = $format($rule->arg[1] ?? null);
187+
}
188+
}
189+
return $attrs;
190+
}
191+
192+
193+
public function validateMinMax($min, $max): bool
194+
{
195+
$value = $this->normalizeValue($this->value);
196+
$min = $min === null ? null : $this->normalizeValue($min);
197+
$max = $max === null ? null : $this->normalizeValue($max);
198+
return $this->type === self::TypeTime && $min > $max
199+
? $value >= $min || $value <= $max
200+
: $value >= $min && ($max === null || $value <= $max);
201+
}
202+
203+
204+
private function createDateTime(string $value): \DateTimeImmutable
205+
{
206+
$dt = new \DateTimeImmutable($value);
207+
$errors = \DateTimeImmutable::getLastErrors();
208+
if ($errors && $errors['warnings']) {
209+
throw new Nette\InvalidArgumentException(Nette\Utils\Arrays::first($errors['warnings']) . " '$value'");
210+
}
211+
return $dt;
212+
}
213+
}

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)