Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions com.woltlab.wcf/templates/shared_timeFormField.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<input {*
*}type="time" {*
*}data-native-date-picker {*
*}id="{$field->getPrefixedId()}" {*
*}name="{$field->getPrefixedId()}" {*
*}value="{$field->getValue()}"{*
*}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{if $field->isAutofocused()} autofocus{/if}{*
*}{if $field->isRequired()} required{/if}{*
*}{if $field->isImmutable()} disabled{/if}{*
*}{if $field->getEarliestTime() !== null} min="{$field->getEarliestTime()}"{/if}{*
*}{if $field->getLatestTime() !== null} max="{$field->getLatestTime()}"{/if}{*
*}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*

*}>
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
<?php

namespace wcf\system\form\builder\field;

use wcf\system\form\builder\field\validation\FormFieldValidationError;
use wcf\system\WCF;

/**
* Implementation of a form field for a time of day without a date.
*
* The value is a wall-clock time in `H:i` format that is not subject to any
* time zone conversion.
*
* @author Marcel Werk
* @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.3
*/
class TimeFormField extends AbstractFormField implements
IAttributeFormField,
IAutoFocusFormField,
ICssClassFormField,
IImmutableFormField,
INullableFormField
{
use TInputAttributeFormField {
getReservedFieldAttributes as private inputGetReservedFieldAttributes;
}
use TAutoFocusFormField;
use TCssClassFormField;
use TImmutableFormField;
use TNullableFormField;

/**
* earliest valid time in `H:i` format or `null` if no earliest valid time has been set
*/
protected ?string $earliestTime = null;

/**
* @inheritDoc
*/
protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value';

/**
* latest valid time in `H:i` format or `null` if no latest valid time has been set
*/
protected ?string $latestTime = null;

/**
* @inheritDoc
*/
protected $templateName = 'shared_timeFormField';

const TIME_FORMAT = 'H:i';

public function __construct()
{
$this->addFieldClass('small');
}

/**
* Sets the earliest valid time in `H:i` format and returns this field. If `null`
* is given, the previously set earliest valid time is unset.
*/
public function earliestTime(?string $earliestTime = null): static
{
if ($earliestTime !== null) {
$normalizedTime = $this->normalizeTime($earliestTime);
if ($normalizedTime === null) {
throw new \InvalidArgumentException(
"Earliest time '{$earliestTime}' does not have format '" . static::TIME_FORMAT . "' for field '{$this->getId()}'."
);
}

if ($this->latestTime !== null && $this->latestTime < $normalizedTime) {
throw new \InvalidArgumentException(
"Earliest time '{$normalizedTime}' cannot be later than latest time '{$this->latestTime}' for field '{$this->getId()}'."
);
}

$earliestTime = $normalizedTime;
}

$this->earliestTime = $earliestTime;

return $this;
}

/**
* Returns the earliest valid time in `H:i` format or `null` if no earliest
* valid time has been set.
*/
public function getEarliestTime(): ?string
{
return $this->earliestTime;
}

/**
* Sets the latest valid time in `H:i` format and returns this field. If `null`
* is given, the previously set latest valid time is unset.
*/
public function latestTime(?string $latestTime = null): static
{
if ($latestTime !== null) {
$normalizedTime = $this->normalizeTime($latestTime);
if ($normalizedTime === null) {
throw new \InvalidArgumentException(
"Latest time '{$latestTime}' does not have format '" . static::TIME_FORMAT . "' for field '{$this->getId()}'."
);
}

if ($this->earliestTime !== null && $normalizedTime < $this->earliestTime) {
throw new \InvalidArgumentException(
"Latest time '{$normalizedTime}' cannot be earlier than earliest time '{$this->earliestTime}' for field '{$this->getId()}'."
);
}

$latestTime = $normalizedTime;
}

$this->latestTime = $latestTime;

return $this;
}

/**
* Returns the latest valid time in `H:i` format or `null` if no latest
* valid time has been set.
*/
public function getLatestTime(): ?string
{
return $this->latestTime;
}

/**
* @return ?string
*/
#[\Override]
public function getSaveValue(): ?string
{
if ($this->getValue() === null) {
return $this->isNullable() ? null : '';
}

return $this->getValue();
}

#[\Override]
public function readValue(): static
{
if (
$this->getDocument()->hasRequestData($this->getPrefixedId())
&& \is_string($this->getDocument()->getRequestData($this->getPrefixedId()))
) {
$value = $this->getDocument()->getRequestData($this->getPrefixedId());

if ($value === '') {
$this->value = null;
} else {
// Native time inputs submit the seconds if the `step` attribute
// allows values below one minute. Invalid values are kept as is
// to be reported during validation.
$this->value = $this->normalizeTime($value) ?? $value;
}
}

return $this;
}

#[\Override]
public function validate(): void
{
if ($this->getValue() === null) {
if ($this->isRequired()) {
$this->addValidationError(new FormFieldValidationError('empty'));
}

return;
}

if ($this->normalizeTime($this->getValue()) === null) {
$this->addValidationError(new FormFieldValidationError(
'format',
'wcf.form.field.time.error.format'
));

return;
}

if ($this->earliestTime !== null && $this->getValue() < $this->earliestTime) {
$this->addValidationError(new FormFieldValidationError(
'minimum',
'wcf.form.field.time.error.earliestTime',
['earliestTime' => $this->formatTime($this->earliestTime)]
));

return;
}

if ($this->latestTime !== null && $this->getValue() > $this->latestTime) {
$this->addValidationError(new FormFieldValidationError(
'maximum',
'wcf.form.field.time.error.latestTime',
['latestTime' => $this->formatTime($this->latestTime)]
));
}
}

#[\Override]
public function value(mixed $value): static
{
if ($value === null || $value === '') {
$this->value = null;

return $this;
}

$normalizedTime = \is_string($value) ? $this->normalizeTime($value) : null;
if ($normalizedTime === null) {
throw new \InvalidArgumentException(
"Given value does not match format '" . static::TIME_FORMAT . "' for field '{$this->getId()}'."
);
}

return parent::value($normalizedTime);
}

/**
* @return string[]
*/
protected static function getReservedFieldAttributes(): array
{
return \array_merge(
// @phpstan-ignore staticClassAccess.privateMethod
static::inputGetReservedFieldAttributes(),
[
'max',
'min',
]
);
}

/**
* Returns the given time in `H:i` format or `null` if it is neither given
* in `H:i` nor in `H:i:s` format. Seconds are discarded.
*/
protected function normalizeTime(string $time): ?string
{
if (!\preg_match('~^(?<hour>[01]\d|2[0-3]):(?<minute>[0-5]\d)(?::[0-5]\d)?\z~', $time, $matches)) {
return null;
}

return $matches['hour'] . ':' . $matches['minute'];
}

/**
* Returns the given time in `H:i` format formatted in the user's locale.
*/
protected function formatTime(string $time): string
{
$dateTime = \DateTimeImmutable::createFromFormat(
'!' . static::TIME_FORMAT,
$time,
new \DateTimeZone('UTC')
);

$formatter = new \IntlDateFormatter(
WCF::getLanguage()->getLocale(),
\IntlDateFormatter::NONE,
\IntlDateFormatter::SHORT,
new \DateTimeZone('UTC')
);

return $formatter->format($dateTime);
}
}
3 changes: 3 additions & 0 deletions wcfsetup/install/lang/de.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4063,6 +4063,9 @@ Dateianhänge:
<item name="wcf.form.field.date.error.format"><![CDATA[Der angegebene Wert hat ein ungültiges Format.]]></item>
<item name="wcf.form.field.date.error.earliestDate"><![CDATA[Der angegebene Wert darf nicht früher sein als {$earliestDate}.]]></item>
<item name="wcf.form.field.date.error.latestDate"><![CDATA[Der angegebene Wert darf nicht später sein als {$latestDate}.]]></item>
<item name="wcf.form.field.time.error.format"><![CDATA[Der angegebene Wert hat ein ungültiges Format.]]></item>
<item name="wcf.form.field.time.error.earliestTime"><![CDATA[Die angegebene Uhrzeit darf nicht früher sein als {$earliestTime}.]]></item>
<item name="wcf.form.field.time.error.latestTime"><![CDATA[Die angegebene Uhrzeit darf nicht später sein als {$latestTime}.]]></item>
<item name="wcf.form.field.itemList.error.maximumMultiples"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Du darfst{else}Sie dürfen{/if} nicht mehr als {#$maximumCount} {if $maximumCount == 1}Eintrag{else}Einträge{/if} angeben, es wurde{if $count != 1}n{/if} aber {#$count} {if $count == 1}Eintrag{else}Einträge{/if} angegeben.]]></item>
<item name="wcf.form.field.itemList.error.minimumMultiples"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Du musst{else}Sie müssen{/if} zumindest {#$minimumCount} {if $minimumCount == 1}Eintrag{else}Einträge{/if} angeben, es wurde{if $count != 1}n{/if} aber nur {#$count} {if $count == 1}Eintrag{else}Einträge{/if} angegeben.]]></item>
<item name="wcf.form.field.email"><![CDATA[E-Mail-Adresse]]></item>
Expand Down
3 changes: 3 additions & 0 deletions wcfsetup/install/lang/en.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4009,6 +4009,9 @@ Attachments:
<item name="wcf.form.field.date.error.format"><![CDATA[The format of the entered value is invalid.]]></item>
<item name="wcf.form.field.date.error.earliestDate"><![CDATA[The entered value may not be earlier than {$earliestDate}.]]></item>
<item name="wcf.form.field.date.error.latestDate"><![CDATA[The entered value may not be later than {$latestDate}.]]></item>
<item name="wcf.form.field.time.error.format"><![CDATA[The format of the entered value is invalid.]]></item>
<item name="wcf.form.field.time.error.earliestTime"><![CDATA[The entered time may not be earlier than {$earliestTime}.]]></item>
<item name="wcf.form.field.time.error.latestTime"><![CDATA[The entered time may not be later than {$latestTime}.]]></item>
<item name="wcf.form.field.itemList.error.maximumMultiples"><![CDATA[You may not enter more than {#$maximumCount} {if $maximumCount == 1}entry{else}entries{/if} but {#$count} {if $count == 1}entry is{else}entries are{/if} given.]]></item>
<item name="wcf.form.field.itemList.error.minimumMultiples"><![CDATA[You have to entered at least {#$minimumCount} {if $minimumCount == 1}entry{else}entries{/if} but only {#$count} {if $count == 1}entry is{else}entries are{/if} given.]]></item>
<item name="wcf.form.field.email"><![CDATA[Email Address]]></item>
Expand Down
Loading