diff options
Diffstat (limited to 'vendor/ipl/web/src/FormElement/TermInput.php')
-rw-r--r-- | vendor/ipl/web/src/FormElement/TermInput.php | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/vendor/ipl/web/src/FormElement/TermInput.php b/vendor/ipl/web/src/FormElement/TermInput.php new file mode 100644 index 0000000..352cce4 --- /dev/null +++ b/vendor/ipl/web/src/FormElement/TermInput.php @@ -0,0 +1,450 @@ +<?php + +namespace ipl\Web\FormElement; + +use ipl\Html\Attributes; +use ipl\Html\Form; +use ipl\Html\FormElement\FieldsetElement; +use ipl\Html\FormElement\HiddenElement; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Stdlib\Events; +use ipl\Web\FormElement\TermInput\RegisteredTerm; +use ipl\Web\FormElement\TermInput\TermContainer; +use ipl\Web\FormElement\TermInput\ValidatedTerm; +use ipl\Web\Url; +use Psr\Http\Message\ServerRequestInterface; + +class TermInput extends FieldsetElement +{ + use Events; + + /** @var string Emitted in case the user added new terms */ + const ON_ADD = 'on_add'; + + /** @var string Emitted in case the user inserted new terms */ + const ON_PASTE = 'on_paste'; + + /** @var string Emitted in case the user changed existing terms */ + const ON_SAVE = 'on_save'; + + /** @var string Emitted in case the user removed terms */ + const ON_REMOVE = 'on_remove'; + + /** @var string Emitted in case terms need to be enriched */ + const ON_ENRICH = 'on_enrich'; + + /** @var Url The suggestion url */ + protected $suggestionUrl; + + /** @var bool Whether term direction is vertical */ + protected $verticalTermDirection = false; + + /** @var array Changes to transmit to the client */ + protected $changes = []; + + /** @var RegisteredTerm[] The terms */ + protected $terms = []; + + /** @var bool Whether this input has been automatically submitted */ + private $hasBeenAutoSubmitted = false; + + /** @var bool Whether the term input value has been pasted */ + private $valueHasBeenPasted; + + /** @var TermContainer The term container */ + protected $termContainer; + + /** + * Set the suggestion url + * + * @param Url $url + * + * @return $this + */ + public function setSuggestionUrl(Url $url): self + { + $this->suggestionUrl = $url; + + return $this; + } + + /** + * Get the suggestion url + * + * @return ?Url + */ + public function getSuggestionUrl(): ?Url + { + return $this->suggestionUrl; + } + + /** + * Set whether term direction should be vertical + * + * @param bool $state + * + * @return $this + */ + public function setVerticalTermDirection(bool $state = true): self + { + $this->verticalTermDirection = $state; + + return $this; + } + + /** + * Get the desired term direction + * + * @return ?string + */ + public function getTermDirection(): ?string + { + return $this->verticalTermDirection ? 'vertical' : null; + } + + /** + * Set terms + * + * @param RegisteredTerm ...$terms + * + * @return $this + */ + public function setTerms(RegisteredTerm ...$terms): self + { + $this->terms = $terms; + + return $this; + } + + /** + * Get the terms + * + * @return RegisteredTerm[] + */ + public function getTerms(): array + { + return $this->terms; + } + + public function getElements() + { + // TODO: Only a quick-fix. Remove once fieldsets are properly partially validated + $this->ensureAssembled(); + + return parent::getElements(); + } + + public function getValue($name = null, $default = null) + { + if ($name !== null) { + return parent::getValue($name, $default); + } + + $terms = []; + foreach ($this->getTerms() as $term) { + $terms[] = $term->render(','); + } + + return implode(',', $terms); + } + + public function setValue($value) + { + $recipients = $value; + if (is_array($value)) { + $recipients = $value['value'] ?? ''; + parent::setValue($value); + } + + $terms = []; + foreach ($this->parseValue($recipients) as $term) { + $terms[] = new RegisteredTerm($term); + } + + return $this->setTerms(...$terms); + } + + /** + * Parse the given separated string of terms + * + * @param string $value + * + * @return string[] + */ + public function parseValue(string $value): array + { + $terms = []; + + $term = ''; + $ignoreSeparator = false; + for ($i = 0; $i <= strlen($value); $i++) { + if (! isset($value[$i])) { + if (! empty($term)) { + $terms[] = rawurldecode($term); + } + + break; + } + + $c = $value[$i]; + if ($c === '"') { + $ignoreSeparator = ! $ignoreSeparator; + } elseif (! $ignoreSeparator && $c === ',') { + $terms[] = rawurldecode($term); + $term = ''; + } else { + $term .= $c; + } + } + + return $terms; + } + + /** + * Prepare updates to transmit for this input during multipart responses + * + * @param ServerRequestInterface $request + * + * @return array + */ + public function prepareMultipartUpdate(ServerRequestInterface $request): array + { + $updates = []; + if ($this->valueHasBeenPasted()) { + $updates[] = $this->termContainer(); + $updates[] = [ + HtmlString::create(json_encode(['#' . $this->getName() . '-search-input', []])), + 'Behavior:InputEnrichment' + ]; + } elseif (! empty($this->changes)) { + $updates[] = [ + HtmlString::create(json_encode(['#' . $this->getName() . '-search-input', $this->changes])), + 'Behavior:InputEnrichment' + ]; + } + + if (empty($updates) && $this->hasBeenAutoSubmitted()) { + $updates[] = $updates[] = [ + HtmlString::create(json_encode(['#' . $this->getName() . '-search-input', 'bogus'])), + 'Behavior:InputEnrichment' + ]; + } + + return $updates; + } + + /** + * Get whether this input has been automatically submitted + * + * @return bool + */ + private function hasBeenAutoSubmitted(): bool + { + return $this->hasBeenAutoSubmitted; + } + + /** + * Get whether the term input value has been pasted + * + * @return bool + */ + private function valueHasBeenPasted(): bool + { + if ($this->valueHasBeenPasted === null) { + $this->valueHasBeenPasted = ($this->getElement('data')->getValue()['type'] ?? null) === 'paste'; + } + + return $this->valueHasBeenPasted; + } + + public function onRegistered(Form $form) + { + $termContainerId = $this->getName() . '-terms'; + $mainInputId = $this->getName() . '-search-input'; + $autoSubmittedBy = $form->getRequest()->getHeader('X-Icinga-Autosubmittedby'); + + $this->hasBeenAutoSubmitted = in_array($mainInputId, $autoSubmittedBy, true) + || in_array($termContainerId, $autoSubmittedBy, true); + + parent::onRegistered($form); + } + + /** + * Validate the given terms + * + * @param string $type The type of change to validate + * @param array $terms The terms affected by the change + * @param array $changes Potential changes made by validators + * + * @return bool + */ + private function validateTerms(string $type, array $terms, array &$changes): bool + { + $validatedTerms = []; + foreach ($terms as $index => $data) { + $validatedTerms[$index] = ValidatedTerm::fromTermData($data); + } + + switch ($type) { + case 'submit': + case 'exchange': + $type = self::ON_ADD; + + break; + case 'paste': + $type = self::ON_PASTE; + + break; + case 'save': + $type = self::ON_SAVE; + + break; + case 'remove': + default: + return true; + } + + $this->emit($type, [$validatedTerms]); + + $invalid = false; + foreach ($validatedTerms as $index => $term) { + if (! $term->isValid()) { + $invalid = true; + } + + if (! $term->isValid() || $term->hasBeenChanged()) { + $changes[$index] = $term->toTermData(); + } + } + + return $invalid; + } + + /** + * Get the term container + * + * @return TermContainer + */ + protected function termContainer(): TermContainer + { + if ($this->termContainer === null) { + $this->termContainer = (new TermContainer($this)) + ->setAttribute('id', $this->getName() . '-terms'); + } + + return $this->termContainer; + } + + protected function assemble() + { + $myName = $this->getName(); + + $termInputId = $myName . '-term-input'; + $dataInputId = $myName . '-data-input'; + $searchInputId = $myName . '-search-input'; + $suggestionsId = $myName . '-suggestions'; + + $termContainer = $this->termContainer(); + + $suggestions = (new HtmlElement('div')) + ->setAttribute('id', $suggestionsId) + ->setAttribute('class', 'search-suggestions'); + + $termInput = $this->createElement('hidden', 'value', [ + 'id' => $termInputId, + 'disabled' => true + ]); + + $dataInput = new class ('data', [ + 'ignore' => true, + 'id' => $dataInputId, + 'validators' => ['callback' => function ($data) use ($termContainer) { + $changes = []; + $invalid = $this->validateTerms($data['type'], $data['terms'] ?? [], $changes); + $this->changes = $changes; + + $terms = $this->getTerms(); + foreach ($changes as $index => $termData) { + $terms[$index]->applyTermData($termData); + } + + return ! $invalid; + }] + ]) extends HiddenElement { + /** @var TermInput */ + private $parent; + + public function setParent(TermInput $parent): void + { + $this->parent = $parent; + } + + public function setValue($value) + { + $data = json_decode($value, true); + if (($data['type'] ?? null) === 'paste') { + array_push($data['terms'], ...array_map(function ($t) { + return ['search' => $t]; + }, $this->parent->parseValue($data['input']))); + } + + return parent::setValue($data); + } + + public function getValueAttribute() + { + return null; + } + }; + $dataInput->setParent($this); + + $label = $this->getLabel(); + $this->setLabel(null); + + // TODO: Separator customization + $mainInput = $this->createElement('text', 'value', [ + 'id' => $searchInputId, + 'label' => $label, + 'required' => $this->isRequired(), + 'placeholder' => $this->translate('Type to search. Separate multiple terms by comma.'), + 'class' => 'term-input', + 'autocomplete' => 'off', + 'data-term-separator' => ',', + 'data-enrichment-type' => 'terms', + 'data-with-multi-completion' => true, + 'data-no-auto-submit-on-remove' => true, + 'data-term-direction' => $this->getTermDirection(), + 'data-data-input' => '#' . $dataInputId, + 'data-term-input' => '#' . $termInputId, + 'data-term-container' => '#' . $termContainer->getAttribute('id')->getValue(), + 'data-term-suggestions' => '#' . $suggestionsId + ]); + $mainInput->getAttributes() + ->registerAttributeCallback('value', function () { + return null; + }); + if ($this->getSuggestionUrl() !== null) { + $mainInput->getAttributes()->registerAttributeCallback('data-suggest-url', function () { + return (string) $this->getSuggestionUrl(); + }); + } + + $this->addElement($termInput); + $this->addElement($dataInput); + $this->addElement($mainInput); + + $mainInput->prependWrapper((new HtmlElement( + 'div', + Attributes::create(['class' => ['term-input-area', $this->getTermDirection()]]), + $termContainer, + new HtmlElement('label', null, $mainInput) + ))); + + $this->addHtml($suggestions); + + if (! $this->hasBeenAutoSubmitted()) { + $this->emit(self::ON_ENRICH, [$this->getTerms()]); + } + } +} |