diff --git a/src/Forms/Container.php b/src/Forms/Container.php index 2392a7b28..10e4323f5 100644 --- a/src/Forms/Container.php +++ b/src/Forms/Container.php @@ -405,6 +405,36 @@ public function addFloat(string $name, $label = null): Controls\TextInput } + /** + * Adds input for date selection. + * @param string|object|null $label + */ + public function addDate(string $name, $label = null): Controls\DateTimeControl + { + return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::Date); + } + + + /** + * Adds input for time selection. + * @param string|object|null $label + */ + public function addTime(string $name, $label = null, bool $withSeconds = false): Controls\DateTimeControl + { + return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::Time, $withSeconds); + } + + + /** + * Adds input for date and time selection. + * @param string|object|null $label + */ + public function addDateTime(string $name, $label = null, bool $withSeconds = false): Controls\DateTimeControl + { + return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::DateTime, $withSeconds); + } + + /** * Adds control that allows the user to upload files. * @param string|object|null $label diff --git a/src/Forms/Controls/DateTimeControl.php b/src/Forms/Controls/DateTimeControl.php new file mode 100644 index 000000000..b533a0ce5 --- /dev/null +++ b/src/Forms/Controls/DateTimeControl.php @@ -0,0 +1,164 @@ +mode = $mode; + $this->withSeconds = $withSeconds; + parent::__construct($label); + $this->control->step = $withSeconds ? 1 : null; + } + + + /** + * Format of returned value. Allowed values are string (ie 'Y-m-d'), self::FormatObject and self::FormatTimestamp. + * @return static + */ + public function setFormat(string $format) + { + $this->format = $format; + return $this; + } + + + /** + * @param \DateTimeInterface|string|int|null $value + * @return static + */ + public function setValue($value) + { + $this->value = $value === null ? null : $this->normalize($value); + return $this; + } + + + /** + * @return \DateTimeImmutable|string|int|null + */ + public function getValue() + { + if ($this->format === self::FormatObject) { + return $this->value; + } elseif ($this->format === self::FormatTimestamp) { + return $this->value ? $this->value->getTimestamp() : null; + } else { + return $this->value ? $this->value->format($this->format) : null; + } + } + + + /** + * @param \DateTimeInterface|string|int $value + */ + private function normalize($value): \DateTimeImmutable + { + if (is_numeric($value)) { + $dt = (new \DateTimeImmutable)->setTimestamp((int) $value); + } elseif (is_string($value)) { + $dt = new \DateTimeImmutable($value); // createFromFormat() must not be used because it allows invalid values + } elseif ($value instanceof \DateTime) { + $dt = \DateTimeImmutable::createFromMutable($value); + } elseif ($value instanceof \DateTimeImmutable) { + $dt = $value; + } elseif (!$value instanceof \DateTimeInterface) { + throw new Nette\InvalidArgumentException('Value must be DateTimeInterface or string or null, ' . gettype($value) . ' given.'); + } + + [$h, $m, $s] = [(int) $dt->format('H'), (int) $dt->format('i'), $this->withSeconds ? (int) $dt->format('s') : 0]; + if ($this->mode === self::Date) { + return $dt->setTime(0, 0); + } elseif ($this->mode === self::Time) { + return $dt->setDate(0, 1, 1)->setTime($h, $m, $s); + } elseif ($this->mode === self::DateTime) { + return $dt->setTime($h, $m, $s); + } + } + + + public function loadHttpData(): void + { + $value = $this->getHttpData(Nette\Forms\Form::DataText); + try { + $this->value = is_string($value) && preg_match('~^(\d{4}-\d{2}-\d{2})?T?(\d{2}:\d{2}(:\d{2}(\.\d+)?)?)?$~', $value) + ? $this->normalize($value) + : null; + } catch (\Throwable $e) { + $this->value = null; + } + } + + + public function getControl(): Nette\Utils\Html + { + return parent::getControl()->addAttributes([ + 'value' => $this->value ? $this->formatHtmlValue($this->value) : null, + 'type' => [self::Date => 'date', self::Time => 'time', self::DateTime => 'datetime-local'][$this->mode], + ]); + } + + + private function formatHtmlValue(\DateTimeInterface $dt): string + { + return $dt->format([ + self::Date => 'Y-m-d', + self::Time => $this->withSeconds ? 'H:i:s' : 'H:i', + self::DateTime => $this->withSeconds ? 'Y-m-d\\TH:i:s' : 'Y-m-d\\TH:i', + ][$this->mode]); + } + + + /** @return static */ + public function addRule($validator, $errorMessage = null, $arg = null) + { + if ($validator === Form::Min) { + $this->control->min = $arg = $this->formatHtmlValue($this->normalize($arg)); + } elseif ($validator === Form::Max) { + $this->control->max = $arg = $this->formatHtmlValue($this->normalize($arg)); + } elseif ($validator === Form::Range) { + $this->control->min = isset($arg[0]) + ? $arg[0] = $this->formatHtmlValue($this->normalize($arg[0])) + : null; + $this->control->max = isset($arg[1]) + ? $arg[1] = $this->formatHtmlValue($this->normalize($arg[1])) + : null; + } + + return parent::addRule($validator, $errorMessage, $arg); + } +} diff --git a/tests/Forms/Controls.DateTimeControl.format.phpt b/tests/Forms/Controls.DateTimeControl.format.phpt new file mode 100644 index 000000000..c596814ee --- /dev/null +++ b/tests/Forms/Controls.DateTimeControl.format.phpt @@ -0,0 +1,44 @@ +addDate('date') + ->setValue('2023-10-22 10:30') + ->setFormat('j.n.Y'); + + Assert::same('22.10.2023', $input->getValue()); +}); + + +test('timestamp format', function () { + $form = new Form; + $input = $form->addDate('date') + ->setValue('2023-10-22 10:30') + ->setFormat(DateTimeControl::FormatTimestamp); + + Assert::same(1697925600, $input->getValue()); +}); + + +test('object format', function () { + $form = new Form; + $input = $form->addDate('date') + ->setValue('2023-10-22 10:30') + ->setFormat(DateTimeControl::FormatObject); + + Assert::equal(new DateTimeImmutable('2023-10-22 00:00'), $input->getValue()); +}); diff --git a/tests/Forms/Controls.DateTimeControl.loadData.phpt b/tests/Forms/Controls.DateTimeControl.loadData.phpt new file mode 100644 index 000000000..26eb3c936 --- /dev/null +++ b/tests/Forms/Controls.DateTimeControl.loadData.phpt @@ -0,0 +1,110 @@ +addDate('unknown'); + Assert::null($input->getValue()); + Assert::false($input->isFilled()); +}); + + +test('invalid data', function () { + $_POST = ['malformed' => ['']]; + $form = new Form; + $input = $form->addDate('malformed'); + Assert::null($input->getValue()); + Assert::false($input->isFilled()); +}); + + +test('invalid format', function () { + $_POST = ['text' => 'invalid']; + $form = new Form; + $input = $form->addDate('date'); + Assert::null($input->getValue()); + Assert::false($input->isFilled()); +}); + + +test('invalid date', function () { + $_POST = ['date' => '2023-13-22']; + $form = new Form; + $input = $form->addDate('date'); + Assert::null($input->getValue()); + Assert::false($input->isFilled()); +}); + + +test('invalid time', function () { + $_POST = ['time' => '10:60']; + $form = new Form; + $input = $form->addTime('time'); + Assert::null($input->getValue()); + Assert::false($input->isFilled()); +}); + + +test('valid date', function () { + $_POST = ['date' => '2023-10-22']; + $form = new Form; + $input = $form->addDate('date'); + Assert::equal(new DateTimeImmutable('2023-10-22 00:00'), $input->getValue()); + Assert::true($input->isFilled()); +}); + + +test('valid time', function () { + $_POST = ['time' => '10:22:33.44']; + $form = new Form; + $input = $form->addTime('time'); + Assert::equal(new DateTimeImmutable('0000-01-01 10:22'), $input->getValue()); + Assert::true($input->isFilled()); +}); + + +test('valid time with seconds', function () { + $_POST = ['time' => '10:22:33.44']; + $form = new Form; + $input = $form->addTime('time', null, true); + Assert::equal(new DateTimeImmutable('0000-01-01 10:22:33'), $input->getValue()); + Assert::true($input->isFilled()); +}); + + +test('valid date-time', function () { + $_POST = ['date' => '2023-10-22T10:23:11.123']; + $form = new Form; + $input = $form->addDateTime('date'); + Assert::equal(new DateTimeImmutable('2023-10-22 10:23:00'), $input->getValue()); + Assert::true($input->isFilled()); +}); + + +test('valid date-time with seconds', function () { + $_POST = ['date' => '2023-10-22T10:23:11.123']; + $form = new Form; + $input = $form->addDateTime('date', null, true); + Assert::equal(new DateTimeImmutable('2023-10-22 10:23:11'), $input->getValue()); + Assert::true($input->isFilled()); +}); diff --git a/tests/Forms/Controls.DateTimeControl.render.phpt b/tests/Forms/Controls.DateTimeControl.render.phpt new file mode 100644 index 000000000..e268283b1 --- /dev/null +++ b/tests/Forms/Controls.DateTimeControl.render.phpt @@ -0,0 +1,95 @@ +addDate('date', 'label'); + + Assert::type(Html::class, $input->getControl()); + Assert::same('', (string) $input->getControl()); +}); + + +test('required date', function () { + $form = new Form; + $input = $form->addDate('date')->setRequired('required'); + + Assert::same('', (string) $input->getControl()); +}); + + +test('date: min & max validator', function () { + $form = new Form; + $input = $form->addDate('date') + ->addRule($form::Min, null, new DateTime('2020-01-01 11:22:33')) + ->addRule($form::Max, null, new DateTime('2040-01-01 11:22:33')); + + Assert::same('', (string) $input->getControl()); +}); + + +test('date: range validator', function () { + $form = new Form; + $input = $form->addDate('date') + ->addRule($form::Range, null, [new DateTime('2020-01-01 11:22:33'), new DateTime('2040-01-01 22:33:44')]); + + Assert::same('', (string) $input->getControl()); +}); + + +test('time: range validator', function () { + $form = new Form; + $input = $form->addTime('time') + ->addRule($form::Range, null, [new DateTime('2020-01-01 11:22:33'), new DateTime('2040-01-01 22:33:44')]); + + Assert::same('', (string) $input->getControl()); +}); + + +test('time with seconds: range validator', function () { + $form = new Form; + $input = $form->addTime('time', null, true) + ->addRule($form::Range, null, [new DateTime('2020-01-01 11:22:33'), new DateTime('2040-01-01 22:33:44')]); + + Assert::same('', (string) $input->getControl()); +}); + + +test('date with value', function () { + $form = new Form; + $input = $form->addDate('date') + ->setValue(new Nette\Utils\DateTime('2023-10-22')); + + Assert::same('', (string) $input->getControl()); +}); + + +test('time with value', function () { + $form = new Form; + $input = $form->addTime('time') + ->setValue(new Nette\Utils\DateTime('07:05')); + + Assert::same('', (string) $input->getControl()); +}); + + +test('date-time with value', function () { + $form = new Form; + $input = $form->addDateTime('time') + ->setValue(new Nette\Utils\DateTime('2023-10-22 07:05')); + + Assert::same('', (string) $input->getControl()); +}); diff --git a/tests/Forms/Controls.DateTimeControl.value.phpt b/tests/Forms/Controls.DateTimeControl.value.phpt new file mode 100644 index 000000000..257b16142 --- /dev/null +++ b/tests/Forms/Controls.DateTimeControl.value.phpt @@ -0,0 +1,86 @@ +addDate('date'); + + Assert::exception(function () use ($input) { + $input->setValue([]); + }, Nette\InvalidArgumentException::class, 'Value must be DateTimeInterface or string or null, array given.'); +}); + + +test('date as string', function () { + $form = new Form; + $input = $form->addDate('date') + ->setValue('2013-07-05 10:30'); + + Assert::equal(new DateTimeImmutable('2013-07-05 00:00'), $input->getValue()); +}); + + +test('date as string timestamp', function () { + $form = new Form; + $input = $form->addDate('date') + ->setValue('254400000'); + + Assert::equal(new DateTimeImmutable('1978-01-23 00:00'), $input->getValue()); +}); + + +test('date as int timestamp', function () { + $form = new Form; + $input = $form->addDate('date') + ->setValue(254_400_000); + + Assert::equal(new DateTimeImmutable('1978-01-23 00:00'), $input->getValue()); +}); + + +test('date as DateTime object', function () { + $form = new Form; + $input = $form->addDate('date') + ->setValue(new Nette\Utils\DateTime('2023-10-05 11:22:33.44')); + + Assert::equal(new DateTimeImmutable('2023-10-05 00:00'), $input->getValue()); +}); + + +test('time as DateTime object', function () { + $form = new Form; + $input = $form->addTime('time') + ->setValue(new Nette\Utils\DateTime('2023-10-05 11:22:33.44')); + + Assert::equal(new DateTimeImmutable('0000-01-01 11:22'), $input->getValue()); +}); + + +test('date-time as DateTime object', function () { + $form = new Form; + $input = $form->addDateTime('time') + ->setValue(new Nette\Utils\DateTime('2023-10-05 11:22:33.44')); + + Assert::equal(new DateTimeImmutable('2023-10-05 11:22'), $input->getValue()); +}); + + +test('date-time with seconds as DateTime object', function () { + $form = new Form; + $input = $form->addDateTime('time', null, true) + ->setValue(new Nette\Utils\DateTime('2023-10-05 11:22:33.44')); + + Assert::equal(new DateTimeImmutable('2023-10-05 11:22:33'), $input->getValue()); +}); diff --git a/tests/Forms/Controls.HiddenField.render.phpt b/tests/Forms/Controls.HiddenField.render.phpt index 46ebfe265..bf6d5fec3 100644 --- a/tests/Forms/Controls.HiddenField.render.phpt +++ b/tests/Forms/Controls.HiddenField.render.phpt @@ -14,15 +14,6 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -class Translator implements Nette\Localization\ITranslator -{ - public function translate($s, ...$parameters): string - { - return strtoupper($s); - } -} - - test('', function () { $form = new Form; $input = $form->addHidden('hidden', 'value');