diff --git a/examples/datetime/DatetimePresenter.latte b/examples/datetime/DatetimePresenter.latte
new file mode 100644
index 0000000..a58c89a
--- /dev/null
+++ b/examples/datetime/DatetimePresenter.latte
@@ -0,0 +1,10 @@
+
+
+
+
+ Datetime demo
+
+
+{control form}
+
+
diff --git a/examples/datetime/DatetimePresenter.php b/examples/datetime/DatetimePresenter.php
new file mode 100644
index 0000000..9103d2b
--- /dev/null
+++ b/examples/datetime/DatetimePresenter.php
@@ -0,0 +1,35 @@
+addSubmit('save', 'Send');
+ $form->onSuccess[] = function ($form, $values) {
+ dump($values);
+ };
+ return $form;
+ }
+
+
+ public function formatTemplateFiles(): array
+ {
+ return [__DIR__ . '/DatetimePresenter.latte'];
+ }
+}
diff --git a/examples/datetime/config.neon b/examples/datetime/config.neon
new file mode 100644
index 0000000..6700f9e
--- /dev/null
+++ b/examples/datetime/config.neon
@@ -0,0 +1,7 @@
+application:
+ scanDirs: false
+ mapping:
+ *: NextrasDemos\FormsComponents\Datetime\*Module\*Presenter
+
+services:
+ routing.router: Nette\Application\Routers\SimpleRouter('Datetime:default')
diff --git a/examples/datetime/index.php b/examples/datetime/index.php
new file mode 100644
index 0000000..83a5dd1
--- /dev/null
+++ b/examples/datetime/index.php
@@ -0,0 +1,19 @@
+enableDebugger(__DIR__ . '/log');
+$configurator->setTempDirectory(__DIR__ . '/temp');
+$configurator->createRobotLoader()->addDirectory(__DIR__)->register();
+$configurator->addConfig(__DIR__ . '/config.neon');
+
+$container = $configurator->createContainer();
+$app = $container->getByType(Application::class);
+$app->run();
diff --git a/readme.md b/readme.md
index 96e1c80..1c709e0 100644
--- a/readme.md
+++ b/readme.md
@@ -14,8 +14,8 @@ Architecture components provide Nette Forms' BaseControl in two flavors:
UI components:
- *AutocompleteControl* - text input with support for autocomplete signal handling;
-- *DatePicker* - date picker - text input returning `DateTimeImmutable` instance;
-- *DateTimePicker* - date picker - text input returning `DateTimeImmutable` instance;
+- *DateControl* - date picker - text input returning `DateTimeImmutable` instance;
+- *DateTimeControl* - date tiime picker - text input returning `DateTimeImmutable` instance;
### Installation
diff --git a/src/Controls/DateControl.php b/src/Controls/DateControl.php
new file mode 100644
index 0000000..af34cbe
--- /dev/null
+++ b/src/Controls/DateControl.php
@@ -0,0 +1,67 @@
+\d{1,2})[. -]\s*(?P\d{1,2})([. -]\s*(?P\d{4})?)?$#', $value, $matches)) {
+ return null;
+ }
+
+ $dd = $matches['dd'];
+ $mm = $matches['mm'];
+ $yyyy = $matches['yyyy'] ?? date('Y');
+
+ if (!checkdate($mm, $dd, $yyyy)) {
+ return null;
+ }
+
+ return (new DateTimeImmutable())
+ ->setDate($yyyy, $mm, $dd)
+ ->setTime(0, 0, 0);
+ };
+ }
+
+
+ /**
+ * @return DateTimeImmutable|null
+ */
+ public function getValue()
+ {
+ $val = parent::getValue();
+ // set the midnight so the limit dates (min & max) pass the :RANGE validation rule
+ if ($val !== null) {
+ return $val->setTime(0, 0, 0);
+ }
+ return $val;
+ }
+}
diff --git a/src/Controls/DateTimeControl.php b/src/Controls/DateTimeControl.php
new file mode 100644
index 0000000..27d12b5
--- /dev/null
+++ b/src/Controls/DateTimeControl.php
@@ -0,0 +1,61 @@
+\d{1,2})[. -]\s*(?P\d{1,2})(?:[. -]\s*(?P\d{4})?)?(?:\s*[ @-]\s*(?P\d{1,2})[:.](?P\d{1,2})(?:[:.](?P\d{1,2}))?)?$#', $value, $matches)) {
+ return null;
+ }
+
+ $dd = $matches['dd'];
+ $mm = $matches['mm'];
+ $yyyy = $matches['yyyy'] ?? date('Y');
+
+ if (!checkdate($mm, $dd, $yyyy)) {
+ return null;
+ }
+
+ $hh = $matches['hh'] ?? 0;
+ $ii = $matches['ii'] ?? 0;
+ $ss = $matches['ss'] ?? 0;
+
+ if (!($hh >= 0 && $hh < 24 && $ii >= 0 && $ii <= 59 && $ss >= 0 && $ss <= 59)) {
+ return null;
+ }
+
+ return (new DateTimeImmutable())
+ ->setDate($yyyy, $mm, $dd)
+ ->setTime($hh, $ii, $ss);
+ };
+ }
+}
diff --git a/src/Controls/DateTimeControlPrototype.php b/src/Controls/DateTimeControlPrototype.php
new file mode 100644
index 0000000..ba31871
--- /dev/null
+++ b/src/Controls/DateTimeControlPrototype.php
@@ -0,0 +1,146 @@
+type = $this->htmlType;
+ $control->addClass($this->htmlType);
+
+ list($min, $max) = $this->extractRangeRule($this->getRules());
+ if ($min instanceof DateTimeInterface) {
+ $control->min = $min->format($this->htmlFormat);
+ }
+ if ($max instanceof DateTimeInterface) {
+ $control->max = $max->format($this->htmlFormat);
+ }
+ $value = $this->getValue();
+ if ($value instanceof DateTimeInterface) {
+ $control->value = $value->format($this->htmlFormat);
+ }
+
+ return $control;
+ }
+
+
+ public function setValue($value)
+ {
+ return parent::setValue(
+ $value instanceof DateTimeInterface
+ ? $value->format($this->htmlFormat)
+ : $value
+ );
+ }
+
+
+ /**
+ * @return DateTimeImmutable|null
+ */
+ public function getValue()
+ {
+ if ($this->value instanceof DateTimeImmutable) {
+ return $this->value;
+
+ } elseif ($this->value instanceof DateTime) {
+ return DateTimeImmutable::createFromMutable($this->value);
+
+ } elseif (empty($this->value)) {
+ return null;
+
+ } elseif (is_string($this->value)) {
+ $parsers = $this->parsers;
+ $parsers[] = $this->getDefaultParser();
+
+ foreach ($parsers as $parser) {
+ $value = call_user_func($parser, $this->value);
+ if ($value instanceof DateTimeImmutable) {
+ return $value;
+ } elseif ($value instanceof DateTime) {
+ return DateTimeImmutable::createFromMutable($value);
+ }
+ }
+
+ // fall-through
+ }
+
+ return null;
+ }
+
+
+ public function addParser(callable $parser)
+ {
+ $this->parsers[] = $parser;
+ return $this;
+ }
+
+
+ abstract protected function getDefaultParser();
+
+
+ /**
+ * Finds minimum and maximum allowed dates.
+ *
+ * @return array 0 => DateTime|null $minDate, 1 => DateTime|null $maxDate
+ */
+ protected function extractRangeRule(Rules $rules)
+ {
+ $controlMin = $controlMax = null;
+ /** @var Nette\Forms\Rule $rule */
+ foreach ($rules as $rule) {
+ /** @var Rules|null $branch */
+ $branch = $rule->branch;
+ if ($branch === null) {
+ if ($rule->validator === Form::RANGE && !$rule->isNegative) {
+ $ruleMinMax = $rule->arg;
+ }
+
+ } else {
+ if ($rule->validator === Form::FILLED && !$rule->isNegative && $rule->control === $this) {
+ $ruleMinMax = $this->extractRangeRule($branch);
+ }
+ }
+
+ if (isset($ruleMinMax)) {
+ list($ruleMin, $ruleMax) = $ruleMinMax;
+ if ($ruleMin !== null && ($controlMin === null || $ruleMin > $controlMin)) {
+ $controlMin = $ruleMin;
+ }
+ if ($ruleMax !== null && ($controlMax === null || $ruleMax < $controlMax)) {
+ $controlMax = $ruleMax;
+ }
+ $ruleMinMax = null;
+ }
+ }
+ return [$controlMin, $controlMax];
+ }
+}
diff --git a/tests/cases/DateTimeControlTest.phpt b/tests/cases/DateTimeControlTest.phpt
new file mode 100644
index 0000000..6a82eb4
--- /dev/null
+++ b/tests/cases/DateTimeControlTest.phpt
@@ -0,0 +1,94 @@
+setRequired();
+
+ $form = new Nette\Forms\Form;
+ $form->addComponent($dateTimePicker, 'dateTimePicker');
+
+ $rule = new Nette\Forms\Rules($dateTimePicker);
+
+ Assert::notSame(
+ Nette\Forms\Helpers::exportRules($rule),
+ $dateTimePicker->getControl()->getAttribute('data-nette-rules')
+ );
+
+ $rule->setRequired();
+
+ Assert::same(
+ Nette\Forms\Helpers::exportRules($rule),
+ $dateTimePicker->getControl()->getAttribute('data-nette-rules')
+ );
+ }
+
+ public function testDateTimeReturn()
+ {
+ $dateTimePicker_emptyValue = new DateTimeControl();
+ Assert::null($dateTimePicker_emptyValue->getValue());
+
+ $dateTimePicker_stringValue = new DateTimeControl();
+ $dateTimePicker_stringValue->setValue('2018-01-01 06:00:00');
+
+ Assert::type(DateTimeImmutable::class, $dateTimePicker_stringValue->getValue());
+ Assert::equal(new DateTimeImmutable('2018-01-01 06:00:00'), $dateTimePicker_stringValue->getValue());
+
+ $dateTimePicker_dateTimeValue = new DateTimeControl();
+ $dateTimePicker_dateTimeValue->setValue(new DateTime('2018-01-01 06:00:00'));
+
+ Assert::type(DateTimeImmutable::class, $dateTimePicker_dateTimeValue->getValue());
+ Assert::equal(new DateTimeImmutable('2018-01-01 06:00:00'), $dateTimePicker_dateTimeValue->getValue());
+
+ $dateTimePicker_dateTimeImmutableValue = new DateTimeControl();
+ $dateTimePicker_dateTimeImmutableValue->setValue(new DateTimeImmutable('2018-01-01 06:00:00'));
+
+ Assert::type(DateTimeImmutable::class, $dateTimePicker_dateTimeImmutableValue->getValue());
+ Assert::equal(new DateTimeImmutable('2018-01-01 06:00:00'), $dateTimePicker_dateTimeImmutableValue->getValue());
+ }
+
+ public function testDateReturn()
+ {
+ $datePicker_emptyValue = new DateControl();
+ Assert::null($datePicker_emptyValue->getValue());
+
+ $datePicker_stringValue = new DateControl();
+ $datePicker_stringValue->setValue('2018-01-01 06:00:00');
+
+ Assert::type(DateTimeImmutable::class, $datePicker_stringValue->getValue());
+ Assert::equal(new DateTimeImmutable('2018-01-01 00:00:00'), $datePicker_stringValue->getValue());
+
+ $datePicker_dateTimeValue = new DateControl();
+ $datePicker_dateTimeValue->setValue(new DateTime('2018-01-01 06:00:00'));
+
+ Assert::type(DateTimeImmutable::class, $datePicker_dateTimeValue->getValue());
+ Assert::equal(new DateTimeImmutable('2018-01-01 00:00:00'), $datePicker_dateTimeValue->getValue());
+
+ $datePicker_dateTimeImmutableValue = new DateControl();
+ $datePicker_dateTimeImmutableValue->setValue(new DateTimeImmutable('2018-01-01 06:00:00'));
+
+ Assert::type(DateTimeImmutable::class, $datePicker_dateTimeImmutableValue->getValue());
+ Assert::equal(new DateTimeImmutable('2018-01-01 00:00:00'), $datePicker_dateTimeImmutableValue->getValue());
+ }
+}
+
+
+$test = new DateTimeControlTest();
+$test->run();