Skip to content

Commit

Permalink
added addDate(), addTime() & addDateTime()
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Nov 1, 2023
1 parent a32d0da commit 87fcb76
Show file tree
Hide file tree
Showing 11 changed files with 693 additions and 9 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"conflict": {
"latte/latte": ">=3.1"
},
"suggest": {
"ext-intl": "to use date/time controls"
},
"autoload": {
"classmap": ["src/"]
},
Expand Down
30 changes: 30 additions & 0 deletions src/Forms/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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::TypeDate);
}


/**
* 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::TypeTime, $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::TypeDateTime, $withSeconds);
}


/**
* Adds control that allows the user to upload files.
* @param string|object|null $label
Expand Down
206 changes: 206 additions & 0 deletions src/Forms/Controls/DateTimeControl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\Forms\Controls;

use Nette;
use Nette\Forms\Form;


/**
* Selects date or time or date & time.
*/
class DateTimeControl extends BaseControl
{
public const
TypeDate = 1,
TypeTime = 2,
TypeDateTime = 3;

public const
FormatObject = 'object',
FormatTimestamp = 'timestamp';

/** @var int */
private $type;

/** @var bool */
private $withSeconds;

/** @var string */
private $format = self::FormatObject;


public function __construct($label = null, int $type = self::TypeDate, bool $withSeconds = false)
{
$this->type = $type;
$this->withSeconds = $withSeconds;
parent::__construct($label);
$this->control->step = $withSeconds ? 1 : null;
$this->setOption('type', 'datetime');
}


/**
* Format of returned value. Allowed values are string (ie 'Y-m-d'), DateTimeControl::FormatObject and DateTimeControl::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 || $value === ''
? null
: $this->normalizeValue($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 normalizeValue($value): \DateTimeImmutable
{
if (is_numeric($value)) {
$dt = (new \DateTimeImmutable)->setTimestamp((int) $value);
} elseif (is_string($value) && $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->type === self::TypeDate) {
return $dt->setTime(0, 0);
} elseif ($this->type === self::TypeTime) {
return $dt->setDate(0, 1, 1)->setTime($h, $m, $s);
} elseif ($this->type === self::TypeDateTime) {
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('~^[\dT.:-]+$~', $value)
? $this->normalizeValue($value)
: null;
} catch (\Throwable $e) {
$this->value = null;
}
}


public function getControl(): Nette\Utils\Html
{
return parent::getControl()->addAttributes([
...$this->getAttributesFromRules(),
'value' => $this->value ? $this->formatHtmlValue($this->value) : null,
'type' => [self::TypeDate => 'date', self::TypeTime => 'time', self::TypeDateTime => 'datetime-local'][$this->type],
]);
}


/**
* Formats a date/time for HTML attributes.
* @param \DateTimeInterface|string|int $value
*/
public function formatHtmlValue($value): string
{
$value = $this->normalizeValue($value);
return $value->format([
self::TypeDate => 'Y-m-d',
self::TypeTime => $this->withSeconds ? 'H:i:s' : 'H:i',
self::TypeDateTime => $this->withSeconds ? 'Y-m-d\\TH:i:s' : 'Y-m-d\\TH:i',
][$this->type]);
}


/**
* Formats a date/time according to the locale and formatting options.
* @param \DateTimeInterface|string|int $value
*/
public function formatLocaleText($value): string
{
$value = $this->normalizeValue($value);
if ($this->type === self::TypeDate) {
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE]);
} elseif ($this->type === self::TypeTime) {
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::NONE, $this->withSeconds ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT]);
} elseif ($this->type === self::TypeDateTime) {
return \IntlDateFormatter::formatObject($value, [\IntlDateFormatter::MEDIUM, $this->withSeconds ? \IntlDateFormatter::MEDIUM : \IntlDateFormatter::SHORT]);
}
}


private function getAttributesFromRules(): array
{
$attrs = [];
$format = function ($val) {
return is_scalar($val) || $val instanceof \DateTimeInterface
? $this->formatHtmlValue($val)
: null;
};
foreach ($this->getRules() as $rule) {
if ($rule->branch) {
} elseif (!$rule->canExport()) {
break;
} elseif ($rule->validator === Form::Min) {
$attrs['min'] = $format($rule->arg);
} elseif ($rule->validator === Form::Max) {
$attrs['max'] = $format($rule->arg);
} elseif ($rule->validator === Form::Range) {
$attrs['min'] = $format($rule->arg[0] ?? null);
$attrs['max'] = $format($rule->arg[1] ?? null);
}
}
return $attrs;
}


public function validateMinMax($min, $max): bool
{
$value = $this->formatHtmlValue($this->value);
$min = $min === null ? '00:00' : $this->formatHtmlValue($min);
$max = $max === null ? '23:59' : $this->formatHtmlValue($max);
return $this->type === self::TypeTime && $min > $max
? $value >= $min || $value <= $max
: $value >= $min && $value <= $max;
}
}
20 changes: 14 additions & 6 deletions src/Forms/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,10 @@ public static function exportRules(Rules $rules): array
if (is_array($rule->arg)) {
$item['arg'] = [];
foreach ($rule->arg as $key => $value) {
$item['arg'][$key] = $value instanceof Control
? ['control' => $value->getHtmlName()]
: $value;
$item['arg'][$key] = self::exportArgument($value, $rule->control);
}
} elseif ($rule->arg !== null) {
$item['arg'] = $rule->arg instanceof Control
? ['control' => $rule->arg->getHtmlName()]
: $rule->arg;
$item['arg'] = self::exportArgument($rule->arg, $rule->control);
}

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


private static function exportArgument($value, Control $control)
{
if ($value instanceof Control) {
return ['control' => $value->getHtmlName()];
} elseif ($control instanceof Controls\DateTimeControl) {
return $control->formatHtmlValue($value);
} else {
return $value;
}
}


public static function createInputList(
array $items,
?array $inputAttrs = null,
Expand Down
16 changes: 13 additions & 3 deletions src/Forms/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,16 @@ public static function formatMessage(Rule $rule, bool $withValue = true)
default:
$args = is_array($rule->arg) ? $rule->arg : [$rule->arg];
$i = (int) $m[1] ? (int) $m[1] - 1 : $i + 1;
return isset($args[$i])
? ($args[$i] instanceof Control ? ($withValue ? $args[$i]->getValue() : "%$i") : $args[$i])
: '';
$arg = $args[$i] ?? null;
if ($arg === null) {
return '';
} elseif ($arg instanceof Control) {
return $withValue ? $args[$i]->getValue() : "%$i";
} elseif ($rule->control instanceof Controls\DateTimeControl) {
return $rule->control->formatLocaleText($arg);
} else {
return $arg;
}
}
}, $message);
return $message;
Expand Down Expand Up @@ -181,6 +188,9 @@ public static function validateValid(Controls\BaseControl $control): bool
*/
public static function validateRange(Control $control, array $range): bool
{
if ($control instanceof Controls\DateTimeControl) {
return $control->validateMinMax($range[0] ?? null, $range[1] ?? null);
}
$range = array_map(function ($v) {
return $v === '' ? null : $v;
}, $range);
Expand Down
2 changes: 2 additions & 0 deletions src/assets/netteForms.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@
range: function(elem, arg, val) {
if (!Array.isArray(arg)) {
return null;
} else if (elem.type === 'time' && arg[0] > arg[1]) {
return val >= arg[0] || val <= arg[1];
}
return (arg[0] === null || Nette.validators.min(elem, arg[0], val))
&& (arg[1] === null || Nette.validators.max(elem, arg[1], val));
Expand Down
44 changes: 44 additions & 0 deletions tests/Forms/Controls.DateTimeControl.format.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* Test: Nette\Forms\Controls\DateTimeControl.
*/

declare(strict_types=1);

use Nette\Forms\Controls\DateTimeControl;
use Nette\Forms\Form;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


test('string format', function () {
$form = new Form;
$input = $form->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());
});
Loading

0 comments on commit 87fcb76

Please sign in to comment.