diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:30:08 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:30:08 +0000 |
commit | 4ce65d59ca91871cfd126497158200a818720bce (patch) | |
tree | e277def01fc7eba7dbc21c4a4ae5576e8aa2cf1f /vendor/ipl/html/src/FormElement | |
parent | Initial commit. (diff) | |
download | icinga-php-library-4ce65d59ca91871cfd126497158200a818720bce.tar.xz icinga-php-library-4ce65d59ca91871cfd126497158200a818720bce.zip |
Adding upstream version 0.13.1.upstream/0.13.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/ipl/html/src/FormElement')
22 files changed, 2561 insertions, 0 deletions
diff --git a/vendor/ipl/html/src/FormElement/BaseFormElement.php b/vendor/ipl/html/src/FormElement/BaseFormElement.php new file mode 100644 index 0000000..837ac45 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/BaseFormElement.php @@ -0,0 +1,390 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attribute; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Contract\FormElement; +use ipl\Html\Contract\ValueCandidates; +use ipl\Html\Form; +use ipl\I18n\Translation; +use ipl\Stdlib\Messages; +use ipl\Validator\ValidatorChain; +use ReflectionProperty; + +abstract class BaseFormElement extends BaseHtmlElement implements FormElement, ValueCandidates +{ + use Messages; + use Translation; + + /** @var string Description of the element */ + protected $description; + + /** @var string Label of the element */ + protected $label; + + /** @var string Name of the element */ + protected $name; + + /** @var bool Whether the element is ignored */ + protected $ignored = false; + + /** @var bool Whether the element is required */ + protected $required = false; + + /** @var null|bool Whether the element is valid; null if the element has not been validated yet, bool otherwise */ + protected $valid; + + /** @var ValidatorChain Registered validators */ + protected $validators; + + /** @var mixed Value of the element */ + protected $value; + + /** @var array<int, mixed> Value candidates of the element */ + protected $valueCandidates = []; + + /** + * Create a new form element + * + * @param string $name Name of the form element + * @param mixed $attributes Attributes of the form element + */ + public function __construct($name, $attributes = null) + { + $this->setName($name); + $this->init(); + + if ($attributes !== null) { + $this->addAttributes($attributes); + } + } + + public function getDescription() + { + return $this->description; + } + + /** + * Set the description of the element + * + * @param string $description + * + * @return $this + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + public function getLabel() + { + return $this->label; + } + + /** + * Set the label of the element + * + * @param string $label + * + * @return $this + */ + public function setLabel($label) + { + $this->label = $label; + + return $this; + } + + public function getName() + { + return $this->name; + } + + /** + * Set the name for the element + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + public function isIgnored() + { + return $this->ignored; + } + + /** + * Set whether the element is ignored + * + * @param bool $ignored + * + * @return $this + */ + public function setIgnored($ignored = true) + { + $this->ignored = (bool) $ignored; + + return $this; + } + + public function isRequired() + { + return $this->required; + } + + /** + * Set whether the element is required + * + * @param bool $required + * + * @return $this + */ + public function setRequired($required = true) + { + $this->required = (bool) $required; + + return $this; + } + + public function isValid() + { + if ($this->valid === null) { + $this->validate(); + } + + return $this->valid; + } + + /** + * Get the validators + * + * @return ValidatorChain + */ + public function getValidators() + { + if ($this->validators === null) { + $chain = new ValidatorChain(); + $this->addDefaultValidators($chain); + $this->validators = $chain; + } + + return $this->validators; + } + + /** + * Set the validators + * + * @param iterable $validators + * + * @return $this + */ + public function setValidators($validators) + { + $this + ->getValidators() + ->clearValidators() + ->addValidators($validators); + + return $this; + } + + /** + * Add validators + * + * @param iterable $validators + * + * @return $this + */ + public function addValidators($validators) + { + $this->getValidators()->addValidators($validators); + + return $this; + } + + public function hasValue() + { + $value = $this->getValue(); + + return ! Form::isEmptyValue($value); + } + + public function getValue() + { + return $this->value; + } + + public function setValue($value) + { + if ($value === '') { + $this->value = null; + } else { + $this->value = $value; + } + + $this->valid = null; + + return $this; + } + + public function getValueCandidates() + { + return $this->valueCandidates; + } + + public function setValueCandidates(array $values) + { + $this->valueCandidates = $values; + + return $this; + } + + public function onRegistered(Form $form) + { + } + + /** + * Validate the element using all registered validators + * + * @return $this + */ + public function validate() + { + $this->ensureAssembled(); + + if (! $this->hasValue()) { + if ($this->isRequired()) { + $this->setMessages([$this->translate('This field is required.')]); + $this->valid = false; + } else { + $this->valid = true; + } + } else { + $this->valid = $this->getValidators()->isValid($this->getValue()); + $this->setMessages($this->getValidators()->getMessages()); + } + + return $this; + } + + public function hasBeenValidated() + { + return $this->valid !== null; + } + + /** + * Callback for the name attribute + * + * @return Attribute|string + */ + public function getNameAttribute() + { + return $this->getName(); + } + + /** + * Callback for the required attribute + * + * @return Attribute|null + */ + public function getRequiredAttribute() + { + if ($this->isRequired()) { + return new Attribute('required', true); + } + + return null; + } + + /** + * Callback for the value attribute + * + * @return mixed + */ + public function getValueAttribute() + { + return $this->getValue(); + } + + /** + * Initialize this form element + * + * If you want to initialize this element after construction, override this method + */ + protected function init(): void + { + } + + /** + * Add default validators + */ + protected function addDefaultValidators(ValidatorChain $chain): void + { + } + + protected function registerValueCallback(Attributes $attributes) + { + $attributes->registerAttributeCallback( + 'value', + [$this, 'getValueAttribute'], + [$this, 'setValue'] + ); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + $this->registerValueCallback($attributes); + + $attributes + ->registerAttributeCallback('label', null, [$this, 'setLabel']) + ->registerAttributeCallback('name', [$this, 'getNameAttribute'], [$this, 'setName']) + ->registerAttributeCallback('description', null, [$this, 'setDescription']) + ->registerAttributeCallback('validators', null, [$this, 'setValidators']) + ->registerAttributeCallback('ignore', null, [$this, 'setIgnored']) + ->registerAttributeCallback('required', [$this, 'getRequiredAttribute'], [$this, 'setRequired']); + + $this->registerCallbacks(); + } + + /** @deprecated Use {@link registerAttributeCallbacks()} instead */ + protected function registerCallbacks() + { + } + + /** + * @deprecated + * + * {@see Attributes::get()} does not respect callbacks, + * but we need the value of the callback to nest attribute names. + */ + protected function getValueOfNameAttribute() + { + $attributes = $this->getAttributes(); + + $callbacksProperty = new ReflectionProperty(get_class($attributes), 'callbacks'); + $callbacksProperty->setAccessible(true); + $callbacks = $callbacksProperty->getValue($attributes); + + if (isset($callbacks['name'])) { + $name = $callbacks['name'](); + + if ($name instanceof Attribute) { + return $name->getValue(); + } + + return $name; + } + + return $this->getName(); + } +} diff --git a/vendor/ipl/html/src/FormElement/ButtonElement.php b/vendor/ipl/html/src/FormElement/ButtonElement.php new file mode 100644 index 0000000..63ae540 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/ButtonElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class ButtonElement extends BaseFormElement +{ + protected $tag = 'button'; +} diff --git a/vendor/ipl/html/src/FormElement/CheckboxElement.php b/vendor/ipl/html/src/FormElement/CheckboxElement.php new file mode 100644 index 0000000..37e036a --- /dev/null +++ b/vendor/ipl/html/src/FormElement/CheckboxElement.php @@ -0,0 +1,124 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attributes; + +class CheckboxElement extends InputElement +{ + /** @var bool Whether the checkbox is checked */ + protected $checked = false; + + /** @var string Value of the checkbox when it is checked */ + protected $checkedValue = 'y'; + + /** @var string Value of the checkbox when it is not checked */ + protected $uncheckedValue = 'n'; + + protected $type = 'checkbox'; + + /** + * Get whether the checkbox is checked + * + * @return bool + */ + public function isChecked() + { + return $this->checked; + } + + /** + * Set whether the checkbox is checked + * + * @param bool $checked + * + * @return $this + */ + public function setChecked($checked) + { + $this->checked = (bool) $checked; + + return $this; + } + + /** + * Get the value of the checkbox when it is checked + * + * @return string + */ + public function getCheckedValue() + { + return $this->checkedValue; + } + + /** + * Set the value of the checkbox when it is checked + * + * @param string $checkedValue + * + * @return $this + */ + public function setCheckedValue($checkedValue) + { + $this->checkedValue = $checkedValue; + + return $this; + } + + /** + * Get the value of the checkbox when it is not checked + * + * @return string + */ + public function getUncheckedValue() + { + return $this->uncheckedValue; + } + + /** + * Set the value of the checkbox when it is not checked + * + * @param string $uncheckedValue + * + * @return $this + */ + public function setUncheckedValue($uncheckedValue) + { + $this->uncheckedValue = $uncheckedValue; + + return $this; + } + + public function setValue($value) + { + if (is_bool($value)) { + $value = $value ? $this->getCheckedValue() : $this->getUncheckedValue(); + } + + $this->setChecked($value === $this->getCheckedValue()); + + return parent::setValue($value); + } + + public function getValueAttribute() + { + return $this->getCheckedValue(); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes + ->registerAttributeCallback('checked', [$this, 'isChecked'], [$this, 'setChecked']) + ->registerAttributeCallback('checkedValue', null, [$this, 'setCheckedValue']) + ->registerAttributeCallback('uncheckedValue', null, [$this, 'setUncheckedValue']); + } + + public function renderUnwrapped() + { + $html = parent::renderUnwrapped(); + + return (new HiddenElement($this->getValueOfNameAttribute(), ['value' => $this->getUncheckedValue()])) . $html; + } +} diff --git a/vendor/ipl/html/src/FormElement/ColorElement.php b/vendor/ipl/html/src/FormElement/ColorElement.php new file mode 100644 index 0000000..21d6c3a --- /dev/null +++ b/vendor/ipl/html/src/FormElement/ColorElement.php @@ -0,0 +1,16 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Validator\HexColorValidator; +use ipl\Validator\ValidatorChain; + +class ColorElement extends InputElement +{ + protected $type = 'color'; + + protected function addDefaultValidators(ValidatorChain $chain): void + { + $chain->add(new HexColorValidator()); + } +} diff --git a/vendor/ipl/html/src/FormElement/DateElement.php b/vendor/ipl/html/src/FormElement/DateElement.php new file mode 100644 index 0000000..2f73b3c --- /dev/null +++ b/vendor/ipl/html/src/FormElement/DateElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class DateElement extends InputElement +{ + protected $type = 'date'; +} diff --git a/vendor/ipl/html/src/FormElement/FieldsetElement.php b/vendor/ipl/html/src/FormElement/FieldsetElement.php new file mode 100644 index 0000000..0d70ea4 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/FieldsetElement.php @@ -0,0 +1,122 @@ +<?php + +namespace ipl\Html\FormElement; + +use InvalidArgumentException; +use ipl\Html\Contract\FormElement; +use ipl\Html\Contract\FormElementDecorator; +use ipl\Html\Contract\Wrappable; +use LogicException; + +use function ipl\Stdlib\get_php_type; + +class FieldsetElement extends BaseFormElement +{ + use FormElements { + FormElements::getValue as private getElementValue; + } + + protected $tag = 'fieldset'; + + /** + * Get whether any of this set's elements has a value + * + * @return bool + */ + public function hasValue() + { + foreach ($this->getElements() as $element) { + if ($element->hasValue()) { + return true; + } + } + + return false; + } + + public function getValue($name = null, $default = null) + { + if ($name === null) { + if ($default !== null) { + throw new LogicException("Can't provide default without a name"); + } + + return $this->getValues(); + } + + return $this->getElementValue($name, $default); + } + + public function setValue($value) + { + if (! is_iterable($value)) { + throw new InvalidArgumentException( + sprintf( + '%s expects parameter $value to be an array|iterable, got %s instead', + __METHOD__, + get_php_type($value) + ) + ); + } + + // We expect an array/iterable here, + // so call populate to loop through it and apply values to the child elements of the fieldset. + $this->populate($value); + + return $this; + } + + public function validate() + { + $this->ensureAssembled(); + + $this->valid = true; + foreach ($this->getElements() as $element) { + $element->validate(); + if (! $element->isValid()) { + $this->valid = false; + } + } + + if ($this->valid) { + parent::validate(); + } + + return $this; + } + + public function getValueAttribute() + { + // Fieldsets do not have the value attribute. + return null; + } + + public function setWrapper(Wrappable $wrapper) + { + // TODO(lippserd): Revise decorator implementation to properly implement decorator propagation + if ( + ! $this->hasDefaultElementDecorator() + && $wrapper instanceof FormElementDecorator + ) { + $this->setDefaultElementDecorator(clone $wrapper); + } + + return parent::setWrapper($wrapper); + } + + protected function onElementRegistered(FormElement $element) + { + $element->getAttributes()->registerAttributeCallback('name', function () use ($element) { + /** + * We don't change the {@see BaseFormElement::$name} property of the element, + * otherwise methods like {@see FormElements::populate() and {@see FormElements::getElement()} would fail, + * but only change the name attribute to nest the names. + */ + return sprintf( + '%s[%s]', + $this->getValueOfNameAttribute(), + $element->getName() + ); + }); + } +} diff --git a/vendor/ipl/html/src/FormElement/FileElement.php b/vendor/ipl/html/src/FormElement/FileElement.php new file mode 100644 index 0000000..d9ca8fd --- /dev/null +++ b/vendor/ipl/html/src/FormElement/FileElement.php @@ -0,0 +1,414 @@ +<?php + +namespace ipl\Html\FormElement; + +use GuzzleHttp\Psr7\UploadedFile; +use InvalidArgumentException; +use ipl\Html\Attributes; +use ipl\Html\Form; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\I18n\Translation; +use ipl\Validator\FileValidator; +use ipl\Validator\ValidatorChain; +use Psr\Http\Message\UploadedFileInterface; +use ipl\Html\Common\MultipleAttribute; + +use function ipl\Stdlib\get_php_type; + +/** + * File upload element + * + * Once the file element is added to the form and the form attribute `enctype` is not set, + * it is automatically set to `multipart/form-data`. + */ +class FileElement extends InputElement +{ + use MultipleAttribute; + use Translation; + + protected $type = 'file'; + + /** @var UploadedFileInterface|UploadedFileInterface[] */ + protected $value; + + /** @var UploadedFileInterface[] Files that are stored on disk */ + protected $files = []; + + /** @var string[] Files to be removed from disk */ + protected $filesToRemove = []; + + /** @var ?string Path to store files to preserve them across requests */ + protected $destination; + + /** @var int The default maximum file size */ + protected static $defaultMaxFileSize; + + public function __construct($name, $attributes = null) + { + $this->getAttributes()->get('accept')->setSeparator(', '); + + parent::__construct($name, $attributes); + } + + /** + * Get the path to store files to preserve them across requests + * + * @return string + */ + public function getDestination(): ?string + { + return $this->destination; + } + + /** + * Set the path to store files to preserve them across requests + * + * Uploaded files are moved to the given directory to + * retain the file through automatic form submissions and failed form validations. + * + * Please note that using file persistence currently has the following drawbacks: + * + * * Works only if the file element is added to the form during {@link Form::assemble()}. + * * Persisted files are not removed automatically. + * * Files with the same name override each other. + * + * @param string $path + * + * @return $this + */ + public function setDestination(string $path): self + { + $this->destination = $path; + + return $this; + } + + public function getValueAttribute() + { + // Value attributes of file inputs are set only client-side. + return null; + } + + public function getNameAttribute() + { + $name = $this->getName(); + + return $this->isMultiple() ? ($name . '[]') : $name; + } + + public function hasValue() + { + if ($this->value === null) { + $files = $this->loadFiles(); + if (empty($files)) { + return false; + } + + if (! $this->isMultiple()) { + $files = $files[0]; + } + + $this->value = $files; + } + + return $this->value !== null; + } + + public function setValue($value) + { + if (! empty($value)) { + $fileToTest = $value; + if ($this->isMultiple()) { + $fileToTest = $value[0]; + } + + if (! $fileToTest instanceof UploadedFileInterface) { + throw new InvalidArgumentException( + sprintf('%s is not an uploaded file', get_php_type($fileToTest)) + ); + } + + if ($fileToTest->getError() === UPLOAD_ERR_NO_FILE && ! $fileToTest->getClientFilename()) { + // This is checked here as it's only about file elements for which no value has been chosen + $value = null; + } else { + $files = $value; + if (! $this->isMultiple()) { + $files = [$files]; + } + + /** @var UploadedFileInterface[] $files */ + $storedFiles = $this->storeFiles(...$files); + if (! $this->isMultiple()) { + $storedFiles = $storedFiles[0] ?? null; + } + + $value = $storedFiles; + } + } else { + $value = null; + } + + return parent::setValue($value); + } + + /** + * Get whether there are any files stored on disk + * + * @return bool + */ + protected function hasFiles(): bool + { + return $this->destination !== null && reset($this->files); + } + + /** + * Load and return all files stored on disk + * + * @return UploadedFileInterface[] + */ + protected function loadFiles(): array + { + if (empty($this->files) || $this->destination === null) { + return []; + } + + foreach ($this->files as $name => $_) { + $filePath = $this->getFilePath($name); + if (! is_readable($filePath) || ! is_file($filePath)) { + // If one file isn't accessible, none is + return []; + } + + if (in_array($name, $this->filesToRemove, true)) { + @unlink($filePath); + } else { + $this->files[$name] = new UploadedFile( + $filePath, + filesize($filePath) ?: null, + 0, + $name, + mime_content_type($filePath) ?: null + ); + } + } + + $this->files = array_diff_key($this->files, array_flip($this->filesToRemove)); + + return array_values($this->files); + } + + /** + * Store the given files on disk + * + * @param UploadedFileInterface ...$files + * + * @return UploadedFileInterface[] + */ + protected function storeFiles(UploadedFileInterface ...$files): array + { + if ($this->destination === null || ! is_writable($this->destination)) { + return $files; + } + + $storedFiles = []; + foreach ($files as $file) { + $name = $file->getClientFilename(); + $path = $this->getFilePath($name); + + if ($file->getError() !== UPLOAD_ERR_OK) { + // The file is still returned as otherwise it won't be validated + $storedFiles[] = $file; + continue; + } + + $file->moveTo($path); + + // Re-created to ensure moveTo() still works if called externally + $file = new UploadedFile( + $path, + $file->getSize(), + 0, + $name, + $file->getClientMediaType() + ); + + $this->files[$name] = $file; + $storedFiles[] = $file; + } + + return $storedFiles; + } + + /** + * Get the file path on disk of the given file + * + * @param string $name + * + * @return string + */ + protected function getFilePath(string $name): string + { + return implode(DIRECTORY_SEPARATOR, [$this->destination, sha1($name)]); + } + + public function onRegistered(Form $form) + { + if (! $form->hasAttribute('enctype')) { + $form->setAttribute('enctype', 'multipart/form-data'); + } + + $chosenFiles = (array) $form->getPopulatedValue('chosen_file_' . $this->getName(), []); + foreach ($chosenFiles as $chosenFile) { + $this->files[$chosenFile] = null; + } + + $this->filesToRemove = (array) $form->getPopulatedValue('remove_file_' . $this->getName(), []); + } + + protected function addDefaultValidators(ValidatorChain $chain): void + { + $chain->add(new FileValidator([ + 'maxSize' => $this->getDefaultMaxFileSize(), + 'mimeType' => array_filter( + (array) $this->getAttributes()->get('accept')->getValue(), + function ($type) { + // file inputs also allow file extensions in the accept attribute. These + // must not be passed as they don't resemble valid mime type definitions. + return is_string($type) && ltrim($type)[0] !== '.'; + } + ) + ])); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + $this->registerMultipleAttributeCallback($attributes); + $this->getAttributes()->registerAttributeCallback('destination', null, [$this, 'setDestination']); + } + + /** + * Get the system's default maximum file upload size + * + * @return int + */ + public function getDefaultMaxFileSize(): int + { + if (static::$defaultMaxFileSize === null) { + $ini = $this->convertIniToInteger(trim(static::getPostMaxSize())); + $max = $this->convertIniToInteger(trim(static::getUploadMaxFilesize())); + $min = max($ini, $max); + if ($ini > 0) { + $min = min($min, $ini); + } + + if ($max > 0) { + $min = min($min, $max); + } + + static::$defaultMaxFileSize = $min; + } + + return static::$defaultMaxFileSize; + } + + /** + * Converts a ini setting to a integer value + * + * @param string $setting + * + * @return int + */ + private function convertIniToInteger(string $setting): int + { + if (! is_numeric($setting)) { + $type = strtoupper(substr($setting, -1)); + $setting = (int) substr($setting, 0, -1); + + switch ($type) { + case 'K': + $setting *= 1024; + break; + + case 'M': + $setting *= 1024 * 1024; + break; + + case 'G': + $setting *= 1024 * 1024 * 1024; + break; + + default: + break; + } + } + + return (int) $setting; + } + + /** + * Get the `post_max_size` INI setting + * + * @return string + */ + protected static function getPostMaxSize(): string + { + return ini_get('post_max_size') ?: '8M'; + } + + /** + * Get the `upload_max_filesize` INI setting + * + * @return string + */ + protected static function getUploadMaxFilesize(): string + { + return ini_get('upload_max_filesize') ?: '2M'; + } + + protected function assemble() + { + $doc = new HtmlDocument(); + if ($this->hasFiles()) { + foreach ($this->files as $file) { + $doc->addHtml(new HiddenElement('chosen_file_' . $this->getValueOfNameAttribute(), [ + 'value' => $file->getClientFilename() + ])); + } + + $this->prependWrapper($doc); + } + } + + public function renderUnwrapped() + { + if (! $this->hasValue() || ! $this->hasFiles()) { + return parent::renderUnwrapped(); + } + + $uploadedFiles = new HtmlElement('ul', Attributes::create(['class' => 'uploaded-files'])); + foreach ($this->files as $file) { + $uploadedFiles->addHtml(new HtmlElement( + 'li', + null, + (new ButtonElement('remove_file_' . $this->getValueOfNameAttribute(), Attributes::create([ + 'type' => 'submit', + 'formnovalidate' => true, + 'class' => 'remove-uploaded-file', + 'value' => $file->getClientFilename(), + 'title' => sprintf($this->translate('Remove file "%s"'), $file->getClientFilename()) + ])))->addHtml(new HtmlElement( + 'span', + null, + new HtmlElement('i', Attributes::create(['class' => ['icon', 'fa', 'fa-xmark']])), + Text::create($file->getClientFilename()) + )) + )); + } + + return $uploadedFiles->render(); + } +} diff --git a/vendor/ipl/html/src/FormElement/FormElements.php b/vendor/ipl/html/src/FormElement/FormElements.php new file mode 100644 index 0000000..4a2c598 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/FormElements.php @@ -0,0 +1,509 @@ +<?php + +namespace ipl\Html\FormElement; + +use InvalidArgumentException; +use ipl\Html\Contract\FormElement; +use ipl\Html\Contract\FormElementDecorator; +use ipl\Html\Contract\ValueCandidates; +use ipl\Html\Form; +use ipl\Html\FormDecorator\DecoratorInterface; +use ipl\Html\ValidHtml; +use ipl\Stdlib\Events; +use ipl\Stdlib\Plugins; +use UnexpectedValueException; + +use function ipl\Stdlib\get_php_type; + +trait FormElements +{ + use Events; + use Plugins; + + /** @var FormElementDecorator|null */ + private $defaultElementDecorator; + + /** @var bool Whether the default element decorator loader has been registered */ + protected $defaultElementDecoratorLoaderRegistered = false; + + /** @var bool Whether the default element loader has been registered */ + protected $defaultElementLoaderRegistered = false; + + /** @var FormElement[] */ + private $elements = []; + + /** @var array<string, array<int, mixed>> */ + private $populatedValues = []; + + /** + * Get all elements + * + * @return FormElement[] + */ + public function getElements() + { + return $this->elements; + } + + /** + * Get whether the given element exists + * + * @param string|FormElement $element + * + * @return bool + */ + public function hasElement($element) + { + if (is_string($element)) { + return array_key_exists($element, $this->elements); + } + + if ($element instanceof FormElement) { + return in_array($element, $this->elements, true); + } + + return false; + } + + /** + * Get the element by the given name + * + * @param string $name + * + * @return FormElement + * + * @throws InvalidArgumentException If no element with the given name exists + */ + public function getElement($name) + { + if (! array_key_exists($name, $this->elements)) { + throw new InvalidArgumentException(sprintf( + "Can't get element '%s'. Element does not exist", + $name + )); + } + + return $this->elements[$name]; + } + + /** + * Add an element + * + * @param string|FormElement $typeOrElement Type of the element as string or an instance of FormElement + * @param string $name Name of the element + * @param mixed $options Element options as key-value pairs + * + * @return $this + * + * @throws InvalidArgumentException If $typeOrElement is neither a string nor an instance of FormElement + * or if $typeOrElement is a string and $name is not set + * or if $typeOrElement is a string but type is unknown + * or if $typeOrElement is an instance of FormElement but does not have a name + */ + public function addElement($typeOrElement, $name = null, $options = null) + { + if (is_string($typeOrElement)) { + if ($name === null) { + throw new InvalidArgumentException(sprintf( + '%s expects parameter 2 to be set if parameter 1 is a string', + __METHOD__ + )); + } + + $element = $this->createElement($typeOrElement, $name, $options); + } elseif ($typeOrElement instanceof FormElement) { + $element = $typeOrElement; + } else { + throw new InvalidArgumentException(sprintf( + '%s() expects parameter 1 to be a string or an instance of %s, %s given', + __METHOD__, + FormElement::class, + get_php_type($typeOrElement) + )); + } + + $this + ->registerElement($element) // registerElement() must be called first because of the name check + ->decorate($element) + ->addHtml($element); + + return $this; + } + + /** + * Create an element + * + * @param string $type Type of the element + * @param string $name Name of the element + * @param mixed $options Element options as key-value pairs + * + * @return FormElement + * + * @throws InvalidArgumentException If the type of the element is unknown + */ + public function createElement($type, $name, $options = null) + { + $this->ensureDefaultElementLoaderRegistered(); + + $class = $this->loadPlugin('element', $type); + + if (! $class) { + throw new InvalidArgumentException(sprintf( + "Can't create element of unknown type '%s", + $type + )); + } + + /** @var FormElement $element */ + $element = new $class($name); + + if ($options !== null) { + $element->addAttributes($options); + } + + return $element; + } + + /** + * Register an element + * + * Registers the element for value and validation handling but does not add it to the render stack. + * + * @param FormElement $element + * + * @return $this + * + * @throws InvalidArgumentException If $element does not provide a name + */ + public function registerElement(FormElement $element) + { + $name = $element->getName(); + + if ($name === null) { + throw new InvalidArgumentException(sprintf( + '%s expects the element to provide a name', + __METHOD__ + )); + } + + $this->elements[$name] = $element; + + if (array_key_exists($name, $this->populatedValues)) { + $element->setValue($this->populatedValues[$name][count($this->populatedValues[$name]) - 1]); + + if ($element instanceof ValueCandidates) { + $element->setValueCandidates($this->populatedValues[$name]); + } + } + + $this->onElementRegistered($element); + $this->emit(Form::ON_ELEMENT_REGISTERED, [$element]); + + return $this; + } + + /** + * Get whether a default element decorator exists + * + * @return bool + */ + public function hasDefaultElementDecorator() + { + return $this->defaultElementDecorator !== null; + } + + /** + * Get the default element decorator, if any + * + * @return FormElementDecorator|null + */ + public function getDefaultElementDecorator() + { + return $this->defaultElementDecorator; + } + + /** + * Set the default element decorator + * + * If $decorator is a string, the decorator will be automatically created from a registered decorator loader. + * A loader for the namespace ipl\\Html\\FormDecorator is automatically registered by default. + * See {@link addDecoratorLoader()} for registering a custom loader. + * + * @param FormElementDecorator|string $decorator + * + * @return $this + * + * @throws InvalidArgumentException If $decorator is a string and can't be loaded from registered decorator loaders + * or if a decorator loader does not return an instance of + * {@link FormElementDecorator} + */ + public function setDefaultElementDecorator($decorator) + { + if ($decorator instanceof FormElementDecorator || $decorator instanceof DecoratorInterface) { + $this->defaultElementDecorator = $decorator; + } else { + $this->ensureDefaultElementDecoratorLoaderRegistered(); + + $class = $this->loadPlugin('decorator', $decorator); + if (! $class) { + throw new InvalidArgumentException(sprintf( + "Can't create decorator of unknown type '%s", + $decorator + )); + } + + $d = new $class(); + if (! $d instanceof FormElementDecorator && ! $d instanceof DecoratorInterface) { + throw new InvalidArgumentException(sprintf( + "Expected instance of %s for decorator '%s'," + . " got %s from a decorator loader instead", + FormElementDecorator::class, + $decorator, + get_php_type($d) + )); + } + + $this->defaultElementDecorator = $d; + } + + return $this; + } + + /** + * Get the value of the element specified by name + * + * Returns $default if the element does not exist or has no value. + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getValue($name, $default = null) + { + if ($this->hasElement($name)) { + $value = $this->getElement($name)->getValue(); + if ($value !== null) { + return $value; + } + } + + return $default; + } + + /** + * Get the values for all but ignored elements + * + * @return array Values as name-value pairs + */ + public function getValues() + { + $values = []; + foreach ($this->getElements() as $element) { + if (! $element->isIgnored()) { + $values[$element->getName()] = $element->getValue(); + } + } + + return $values; + } + + /** + * Populate values of registered elements + * + * @param iterable<string, mixed> $values Values as name-value pairs + * + * @return $this + */ + public function populate($values) + { + foreach ($values as $name => $value) { + $this->populatedValues[$name][] = $value; + if ($this->hasElement($name)) { + $this->getElement($name)->setValue($value); + } + } + + return $this; + } + + /** + * Get the populated value of the element specified by name + * + * Returns $default if there is no populated value for this element. + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getPopulatedValue($name, $default = null) + { + return isset($this->populatedValues[$name]) + ? $this->populatedValues[$name][count($this->populatedValues[$name]) - 1] + : $default; + } + + /** + * Clear populated value of the given element + * + * @param string $name + * + * @return $this + */ + public function clearPopulatedValue($name) + { + if (isset($this->populatedValues[$name])) { + unset($this->populatedValues[$name]); + } + + return $this; + } + + /** + * Add all elements from the given element collection + * + * @param Form $form + * + * @return $this + */ + public function addElementsFrom($form) + { + foreach ($form->getElements() as $element) { + $this->addElement($element); + } + + return $this; + } + + /** + * Add a decorator loader + * + * @param string $namespace Namespace of the decorators + * @param string $postfix Decorator name postfix, if any + * + * @return $this + */ + public function addDecoratorLoader($namespace, $postfix = null) + { + $this->addPluginLoader('decorator', $namespace, $postfix); + + return $this; + } + + /** + * Add an element loader + * + * @param string $namespace Namespace of the elements + * @param string $postfix Element name postfix, if any + * + * @return $this + */ + public function addElementLoader($namespace, $postfix = null) + { + $this->addPluginLoader('element', $namespace, $postfix); + + return $this; + } + + /** + * Ensure that our default element decorator loader is registered + * + * @return $this + */ + protected function ensureDefaultElementDecoratorLoaderRegistered() + { + if (! $this->defaultElementDecoratorLoaderRegistered) { + $this->addDefaultPluginLoader( + 'decorator', + 'ipl\\Html\\FormDecorator', + 'Decorator' + ); + + $this->defaultElementDecoratorLoaderRegistered = true; + } + + return $this; + } + + /** + * Ensure that our default element loader is registered + * + * @return $this + */ + protected function ensureDefaultElementLoaderRegistered() + { + if (! $this->defaultElementLoaderRegistered) { + $this->addDefaultPluginLoader('element', __NAMESPACE__, 'Element'); + + $this->defaultElementLoaderRegistered = true; + } + + return $this; + } + + /** + * Decorate the given element + * + * @param FormElement $element + * + * @return $this + * + * @throws UnexpectedValueException If the default decorator is set but not an instance of + * {@link FormElementDecorator} + */ + protected function decorate(FormElement $element) + { + if ($this->hasDefaultElementDecorator()) { + $decorator = $this->getDefaultElementDecorator(); + + if (! $decorator instanceof FormElementDecorator && ! $decorator instanceof DecoratorInterface) { + throw new UnexpectedValueException(sprintf( + '%s expects the default decorator to be an instance of %s, got %s instead', + __METHOD__, + FormElementDecorator::class, + get_php_type($decorator) + )); + } + + $decorator->decorate($element); + } + + return $this; + } + + public function isValidEvent($event) + { + return in_array($event, [ + Form::ON_SUCCESS, + Form::ON_SENT, + Form::ON_ERROR, + Form::ON_REQUEST, + Form::ON_VALIDATE, + Form::ON_ELEMENT_REGISTERED, + ]); + } + + public function remove(ValidHtml $elementOrHtml) + { + if ($elementOrHtml instanceof FormElement) { + if ($this->hasElement($elementOrHtml)) { + $name = array_search($elementOrHtml, $this->elements, true); + if ($name !== false) { + unset($this->elements[$name]); + } + } + } + + return parent::remove($elementOrHtml); + } + + /** + * Handler which is called after an element has been registered + * + * @param FormElement $element + */ + protected function onElementRegistered(FormElement $element) + { + } +} diff --git a/vendor/ipl/html/src/FormElement/HiddenElement.php b/vendor/ipl/html/src/FormElement/HiddenElement.php new file mode 100644 index 0000000..bffc7eb --- /dev/null +++ b/vendor/ipl/html/src/FormElement/HiddenElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class HiddenElement extends InputElement +{ + protected $type = 'hidden'; +} diff --git a/vendor/ipl/html/src/FormElement/InputElement.php b/vendor/ipl/html/src/FormElement/InputElement.php new file mode 100644 index 0000000..d5f945d --- /dev/null +++ b/vendor/ipl/html/src/FormElement/InputElement.php @@ -0,0 +1,49 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attribute; +use ipl\Html\Attributes; + +class InputElement extends BaseFormElement +{ + /** @var string Type of the input */ + protected $type; + + protected $tag = 'input'; + + /** + * Get the type of the input + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Set the type of the input + * + * @param string $type + * + * @return $this + */ + public function setType($type) + { + $this->type = (string) $type; + + return $this; + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes->registerAttributeCallback( + 'type', + [$this, 'getType'], + [$this, 'setType'] + ); + } +} diff --git a/vendor/ipl/html/src/FormElement/LocalDateTimeElement.php b/vendor/ipl/html/src/FormElement/LocalDateTimeElement.php new file mode 100644 index 0000000..a628b57 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/LocalDateTimeElement.php @@ -0,0 +1,53 @@ +<?php + +namespace ipl\Html\FormElement; + +use DateTime; +use ipl\Validator\DateTimeValidator; +use ipl\Validator\ValidatorChain; + +class LocalDateTimeElement extends InputElement +{ + public const FORMAT = 'Y-m-d\TH:i:s'; + + protected $type = 'datetime-local'; + + protected $defaultAttributes = ['step' => '1']; + + /** @var DateTime */ + protected $value; + + public function setValue($value) + { + if (is_string($value)) { + $originalVal = $value; + $value = DateTime::createFromFormat(static::FORMAT, $value); + // In Chrome, if the seconds are set to 00, DateTime::createFromFormat() returns false. + // Create DateTime without seconds in format + if ($value === false) { + $format = substr(static::FORMAT, 0, strrpos(static::FORMAT, ':') ?: null); + $value = DateTime::createFromFormat($format, $originalVal); + } + + if ($value === false) { + $value = $originalVal; + } + } + + return parent::setValue($value); + } + + public function getValueAttribute() + { + if (! $this->value instanceof DateTime) { + return $this->value; + } + + return $this->value->format(static::FORMAT); + } + + protected function addDefaultValidators(ValidatorChain $chain): void + { + $chain->add(new DateTimeValidator()); + } +} diff --git a/vendor/ipl/html/src/FormElement/NumberElement.php b/vendor/ipl/html/src/FormElement/NumberElement.php new file mode 100644 index 0000000..b593135 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/NumberElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class NumberElement extends InputElement +{ + protected $type = 'number'; +} diff --git a/vendor/ipl/html/src/FormElement/PasswordElement.php b/vendor/ipl/html/src/FormElement/PasswordElement.php new file mode 100644 index 0000000..dfa6d8c --- /dev/null +++ b/vendor/ipl/html/src/FormElement/PasswordElement.php @@ -0,0 +1,55 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attributes; +use ipl\Html\Form; + +class PasswordElement extends InputElement +{ + /** @var string Dummy passwd of this element to be rendered */ + public const DUMMYPASSWORD = '_ipl_form_5847ed1b5b8ca'; + + protected $type = 'password'; + + /** @var bool Status of the form */ + protected $isFormValid = true; + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes->registerAttributeCallback( + 'value', + function () { + if ($this->hasValue() && count($this->getValueCandidates()) === 1 && $this->isFormValid) { + return self::DUMMYPASSWORD; + } + + if (parent::getValue() === self::DUMMYPASSWORD) { + return self::DUMMYPASSWORD; + } + + return null; + } + ); + } + + public function onRegistered(Form $form) + { + $form->on(Form::ON_VALIDATE, function ($form) { + $this->isFormValid = $form->isValid(); + }); + } + + public function getValue() + { + $value = parent::getValue(); + $candidates = $this->getValueCandidates(); + while ($value === self::DUMMYPASSWORD) { + $value = array_pop($candidates); + } + + return $value; + } +} diff --git a/vendor/ipl/html/src/FormElement/RadioElement.php b/vendor/ipl/html/src/FormElement/RadioElement.php new file mode 100644 index 0000000..831671c --- /dev/null +++ b/vendor/ipl/html/src/FormElement/RadioElement.php @@ -0,0 +1,177 @@ +<?php + +namespace ipl\Html\FormElement; + +use InvalidArgumentException; +use ipl\Html\Attributes; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\I18n\Translation; +use ipl\Validator\DeferredInArrayValidator; +use ipl\Validator\ValidatorChain; + +class RadioElement extends BaseFormElement +{ + use Translation; + + /** @var string The element type */ + protected $type = 'radio'; + + /** @var RadioOption[] Radio options */ + protected $options = []; + + /** @var array Disabled radio options */ + protected $disabledOptions = []; + + /** + * Set the options + * + * @param array $options + * + * @return $this + */ + public function setOptions(array $options): self + { + $this->options = []; + foreach ($options as $value => $label) { + $option = (new RadioOption($value, $label)) + ->setDisabled( + in_array($value, $this->disabledOptions, ! is_int($value)) + || ($value === '' && in_array(null, $this->disabledOptions, true)) + ); + + $this->options[$value] = $option; + } + + $this->disabledOptions = []; + + return $this; + } + + /** + * Get the option with specified value + * + * @param string|int $value + * + * @return RadioOption + * + * @throws InvalidArgumentException If no option with the specified value exists + */ + public function getOption($value): RadioOption + { + if (! isset($this->options[$value])) { + throw new InvalidArgumentException(sprintf('There is no such option "%s"', $value)); + } + + return $this->options[$value]; + } + + /** + * Set the specified options as disable + * + * @param array $disabledOptions + * + * @return $this + */ + public function setDisabledOptions(array $disabledOptions): self + { + if (! empty($this->options)) { + foreach ($this->options as $value => $option) { + $option->setDisabled( + in_array($value, $disabledOptions, ! is_int($value)) + || ($value === '' && in_array(null, $disabledOptions, true)) + ); + } + + $this->disabledOptions = []; + } else { + $this->disabledOptions = $disabledOptions; + } + + return $this; + } + + public function renderUnwrapped() + { + // Parent::renderUnwrapped() requires $tag and the content should be empty. However, since we are wrapping + // each button in a label, the call to parent cannot work here and must be overridden. + return HtmlDocument::renderUnwrapped(); + } + + protected function assemble() + { + foreach ($this->options as $option) { + $radio = (new InputElement($this->getValueOfNameAttribute())) + ->setType($this->type) + ->setValue($option->getValue()); + + // Only add the non-callback attributes to all options + foreach ($this->getAttributes() as $attribute) { + $radio->getAttributes()->addAttribute(clone $attribute); + } + + $radio->getAttributes() + ->merge($option->getAttributes()) + ->registerAttributeCallback( + 'checked', + function () use ($option) { + $optionValue = $option->getValue(); + + return ! is_int($optionValue) + ? $this->getValue() === $optionValue + : $this->getValue() == $optionValue; + } + ) + ->registerAttributeCallback( + 'disabled', + function () use ($option) { + return $this->getAttributes()->get('disabled')->getValue() || $option->isDisabled(); + } + ); + + $label = new HtmlElement( + 'label', + new Attributes(['class' => $option->getLabelCssClass()]), + $radio, + Text::create($option->getLabel()) + ); + + $this->addHtml($label); + } + } + + protected function addDefaultValidators(ValidatorChain $chain): void + { + $chain->add(new DeferredInArrayValidator(function (): array { + $possibleValues = []; + + foreach ($this->options as $option) { + if ($option->isDisabled()) { + continue; + } + + $possibleValues[] = $option->getValue(); + } + + return $possibleValues; + })); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $this->getAttributes()->registerAttributeCallback( + 'options', + null, + [$this, 'setOptions'] + ); + + $this->getAttributes()->registerAttributeCallback( + 'disabledOptions', + null, + [$this, 'setDisabledOptions'] + ); + } +} diff --git a/vendor/ipl/html/src/FormElement/RadioOption.php b/vendor/ipl/html/src/FormElement/RadioOption.php new file mode 100644 index 0000000..4968c35 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/RadioOption.php @@ -0,0 +1,148 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attributes; + +class RadioOption +{ + /** @var string The default label class */ + public const LABEL_CLASS = 'radio-label'; + + /** @var string|int|null Value of the option */ + protected $value; + + /** @var string Label of the option */ + protected $label; + + /** @var mixed Css class of the option's label */ + protected $labelCssClass = self::LABEL_CLASS; + + /** @var bool Whether the radio option is disabled */ + protected $disabled = false; + + /** @var Attributes */ + protected $attributes; + + /** + * RadioOption constructor. + * + * @param string|int|null $value + * @param string $label + */ + public function __construct($value, string $label) + { + $this->value = $value === '' ? null : $value; + $this->label = $label; + } + + /** + * Set the label of the option + * + * @param string $label + * + * @return $this + */ + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + /** + * Get the label of the option + * + * @return string + */ + public function getLabel(): string + { + return $this->label; + } + + /** + * Get the value of the option + * + * @return string|int|null + */ + public function getValue() + { + return $this->value; + } + + /** + * Set css class to the option label + * + * @param string|string[] $labelCssClass + * + * @return $this + */ + public function setLabelCssClass($labelCssClass): self + { + $this->labelCssClass = $labelCssClass; + + return $this; + } + + /** + * Get css class of the option label + * + * @return string|string[] + */ + public function getLabelCssClass() + { + return $this->labelCssClass; + } + + /** + * Set whether to disable the option + * + * @param bool $disabled + * + * @return $this + */ + public function setDisabled(bool $disabled = true): self + { + $this->disabled = $disabled; + + return $this; + } + + /** + * Get whether the option is disabled + * + * @return bool + */ + public function isDisabled(): bool + { + return $this->disabled; + } + + /** + * Add the attributes + * + * @param Attributes $attributes + * + * @return $this + */ + public function addAttributes(Attributes $attributes): self + { + $this->attributes = $attributes; + + return $this; + } + + /** + * Get the attributes + * + * @return Attributes + */ + public function getAttributes(): Attributes + { + if ($this->attributes === null) { + $this->attributes = new Attributes(); + } + + return $this->attributes; + } +} diff --git a/vendor/ipl/html/src/FormElement/SelectElement.php b/vendor/ipl/html/src/FormElement/SelectElement.php new file mode 100644 index 0000000..e6b4f21 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/SelectElement.php @@ -0,0 +1,238 @@ +<?php + +namespace ipl\Html\FormElement; + +use InvalidArgumentException; +use ipl\Html\Attributes; +use ipl\Html\Common\MultipleAttribute; +use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Validator\DeferredInArrayValidator; +use ipl\Validator\ValidatorChain; +use UnexpectedValueException; + +class SelectElement extends BaseFormElement +{ + use MultipleAttribute; + + protected $tag = 'select'; + + /** @var SelectOption[] */ + protected $options = []; + + /** @var SelectOption[]|HtmlElement[] */ + protected $optionContent = []; + + /** @var array Disabled select options */ + protected $disabledOptions = []; + + /** @var array|string|null */ + protected $value; + + /** + * Get the option with specified value + * + * @param string|int|null $value + * + * @return ?SelectOption + */ + public function getOption($value): ?SelectOption + { + return $this->options[$value] ?? null; + } + + /** + * Set the options from specified values + * + * @param array $options + * + * @return $this + */ + public function setOptions(array $options): self + { + $this->options = []; + $this->optionContent = []; + foreach ($options as $value => $label) { + $this->optionContent[$value] = $this->makeOption($value, $label); + } + + return $this; + } + + /** + * Set the specified options as disable + * + * @param array $disabledOptions + * + * @return $this + */ + public function setDisabledOptions(array $disabledOptions): self + { + if (! empty($this->options)) { + foreach ($this->options as $option) { + $optionValue = $option->getValue(); + + $option->setAttribute( + 'disabled', + in_array($optionValue, $disabledOptions, ! is_int($optionValue)) + || ($optionValue === null && in_array('', $disabledOptions, true)) + ); + } + + $this->disabledOptions = []; + } else { + $this->disabledOptions = $disabledOptions; + } + + return $this; + } + + /** + * Get the value of the element + * + * Returns `array` when the attribute `multiple` is set to `true`, `string` or `null` otherwise + * + * @return array|string|null + */ + public function getValue() + { + if ($this->isMultiple()) { + return parent::getValue() ?? []; + } + + return parent::getValue(); + } + + public function getValueAttribute() + { + // select elements don't have a value attribute + return null; + } + + public function getNameAttribute() + { + $name = $this->getName(); + + return $this->isMultiple() ? ($name . '[]') : $name; + } + + /** + * Make the selectOption for the specified value and the label + * + * @param string|int|null $value Value of the option + * @param string|array $label Label of the option + * + * @return SelectOption|HtmlElement + */ + protected function makeOption($value, $label) + { + if (is_array($label)) { + $grp = Html::tag('optgroup', ['label' => $value]); + foreach ($label as $option => $val) { + $grp->addHtml($this->makeOption($option, $val)); + } + + return $grp; + } + + $option = (new SelectOption($value, $label)) + ->setAttribute('disabled', in_array($value, $this->disabledOptions, ! is_int($value))); + + $option->getAttributes()->registerAttributeCallback('selected', function () use ($option) { + return $this->isSelectedOption($option->getValue()); + }); + + $this->options[$value] = $option; + + return $this->options[$value]; + } + + /** + * Get whether the given option is selected + * + * @param int|string|null $optionValue + * + * @return bool + */ + protected function isSelectedOption($optionValue): bool + { + $value = $this->getValue(); + + if ($optionValue === '') { + $optionValue = null; + } + + if ($this->isMultiple()) { + if (! is_array($value)) { + throw new UnexpectedValueException( + 'Value must be an array when the `multiple` attribute is set to `true`' + ); + } + + return in_array($optionValue, $this->getValue(), ! is_int($optionValue)) + || ($optionValue === null && in_array('', $this->getValue(), true)); + } + + if (is_array($value)) { + throw new UnexpectedValueException( + 'Value cannot be an array without setting the `multiple` attribute to `true`' + ); + } + + return is_int($optionValue) + // The loose comparison is required because PHP casts + // numeric strings to integers if used as array keys + ? $value == $optionValue + : $value === $optionValue; + } + + protected function addDefaultValidators(ValidatorChain $chain): void + { + $chain->add( + new DeferredInArrayValidator(function (): array { + $possibleValues = []; + + foreach ($this->options as $option) { + if ($option->getAttributes()->get('disabled')->getValue()) { + continue; + } + + $possibleValues[] = $option->getValue(); + } + + return $possibleValues; + }) + ); + } + + protected function assemble() + { + $this->addHtml(...array_values($this->optionContent)); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes->registerAttributeCallback( + 'options', + null, + [$this, 'setOptions'] + ); + + $attributes->registerAttributeCallback( + 'disabledOptions', + null, + [$this, 'setDisabledOptions'] + ); + + // ZF1 compatibility: + $this->getAttributes()->registerAttributeCallback( + 'multiOptions', + null, + [$this, 'setOptions'] + ); + + $this->registerMultipleAttributeCallback($attributes); + } +} diff --git a/vendor/ipl/html/src/FormElement/SelectOption.php b/vendor/ipl/html/src/FormElement/SelectOption.php new file mode 100644 index 0000000..3d799a2 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/SelectOption.php @@ -0,0 +1,79 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\BaseHtmlElement; + +class SelectOption extends BaseHtmlElement +{ + protected $tag = 'option'; + + /** @var string|int|null Value of the option */ + protected $value; + + /** @var string Label of the option */ + protected $label; + + /** + * SelectOption constructor. + * + * @param string|int|null $value + * @param string $label + */ + public function __construct($value, string $label) + { + $this->value = $value === '' ? null : $value; + $this->label = $label; + + $this->getAttributes()->registerAttributeCallback('value', [$this, 'getValueAttribute']); + } + + /** + * Set the label of the option + * + * @param string $label + * + * @return $this + */ + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + /** + * Get the label of the option + * + * @return string + */ + public function getLabel(): string + { + return $this->label; + } + + /** + * Get the value of the option + * + * @return string|int|null + */ + public function getValue() + { + return $this->value; + } + + /** + * Callback for the value attribute + * + * @return mixed + */ + public function getValueAttribute() + { + return (string) $this->getValue(); + } + + protected function assemble() + { + $this->setContent($this->getLabel()); + } +} diff --git a/vendor/ipl/html/src/FormElement/SubmitButtonElement.php b/vendor/ipl/html/src/FormElement/SubmitButtonElement.php new file mode 100644 index 0000000..b880bb5 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/SubmitButtonElement.php @@ -0,0 +1,65 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attributes; +use ipl\Html\Contract\FormSubmitElement; + +class SubmitButtonElement extends ButtonElement implements FormSubmitElement +{ + protected $defaultAttributes = ['type' => 'submit']; + + /** @var string The value that's transmitted once the button is pressed */ + protected $submitValue = 'y'; + + /** + * Get the value to transmit once the button is pressed + * + * @return string + */ + public function getSubmitValue(): string + { + return $this->submitValue; + } + + /** + * Set the value to transmit once the button is pressed + * + * @param string $value + * + * @return $this + */ + public function setSubmitValue(string $value): self + { + $this->submitValue = $value; + + return $this; + } + + public function setLabel($label) + { + return $this->setContent($label); + } + + public function hasBeenPressed() + { + return $this->getValue() === $this->getSubmitValue(); + } + + public function isIgnored() + { + return true; + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes->registerAttributeCallback('value', null, [$this, 'setSubmitValue']); + } + + public function getValueAttribute() + { + return $this->submitValue; + } +} diff --git a/vendor/ipl/html/src/FormElement/SubmitElement.php b/vendor/ipl/html/src/FormElement/SubmitElement.php new file mode 100644 index 0000000..51d4aa5 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/SubmitElement.php @@ -0,0 +1,50 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attribute; +use ipl\Html\Contract\FormSubmitElement; + +class SubmitElement extends InputElement implements FormSubmitElement +{ + protected $type = 'submit'; + + protected $buttonLabel; + + public function setLabel($label) + { + $this->buttonLabel = $label; + + return $this; + } + + /** + * @return string + */ + public function getButtonLabel() + { + if ($this->buttonLabel === null) { + return $this->getName(); + } else { + return $this->buttonLabel; + } + } + + /** + * @return mixed|static + */ + public function getValueAttribute() + { + return new Attribute('value', $this->getButtonLabel()); + } + + public function hasBeenPressed() + { + return $this->getButtonLabel() === $this->getValue(); + } + + public function isIgnored() + { + return true; + } +} diff --git a/vendor/ipl/html/src/FormElement/TextElement.php b/vendor/ipl/html/src/FormElement/TextElement.php new file mode 100644 index 0000000..0e3423d --- /dev/null +++ b/vendor/ipl/html/src/FormElement/TextElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class TextElement extends InputElement +{ + protected $type = 'text'; +} diff --git a/vendor/ipl/html/src/FormElement/TextareaElement.php b/vendor/ipl/html/src/FormElement/TextareaElement.php new file mode 100644 index 0000000..dc5c42b --- /dev/null +++ b/vendor/ipl/html/src/FormElement/TextareaElement.php @@ -0,0 +1,24 @@ +<?php + +namespace ipl\Html\FormElement; + +class TextareaElement extends BaseFormElement +{ + protected $tag = 'textarea'; + + public function setValue($value) + { + parent::setValue($value); + + // A textarea's content actually is the value + $this->setContent($value); + + return $this; + } + + public function getValueAttribute() + { + // textarea elements don't have a value attribute + return null; + } +} diff --git a/vendor/ipl/html/src/FormElement/TimeElement.php b/vendor/ipl/html/src/FormElement/TimeElement.php new file mode 100644 index 0000000..1ee0323 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/TimeElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class TimeElement extends InputElement +{ + protected $type = 'time'; +} |