diff options
Diffstat (limited to 'vendor/ipl/web/src/FormElement/TermInput')
5 files changed, 606 insertions, 0 deletions
diff --git a/vendor/ipl/web/src/FormElement/TermInput/RegisteredTerm.php b/vendor/ipl/web/src/FormElement/TermInput/RegisteredTerm.php new file mode 100644 index 0000000..dd79dd1 --- /dev/null +++ b/vendor/ipl/web/src/FormElement/TermInput/RegisteredTerm.php @@ -0,0 +1,144 @@ +<?php + +namespace ipl\Web\FormElement\TermInput; + +class RegisteredTerm implements Term +{ + /** @var string The search value */ + protected $value; + + /** @var ?string The label */ + protected $label; + + /** @var ?string The CSS class */ + protected $class; + + /** @var string The failure message */ + protected $message; + + /** @var string The validation constraint */ + protected $pattern; + + /** + * Create a new RegisteredTerm + * + * @param string $value The search value + */ + public function __construct(string $value) + { + $this->setSearchValue($value); + } + + public function setSearchValue(string $value): self + { + $this->value = $value; + + return $this; + } + + public function getSearchValue(): string + { + return $this->value; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setClass(string $class): self + { + $this->class = $class; + + return $this; + } + + public function getClass(): ?string + { + return $this->class; + } + + public function setMessage(string $message): self + { + $this->message = $message; + + return $this; + } + + public function getMessage(): ?string + { + return $this->message; + } + + public function setPattern(string $pattern): self + { + $this->pattern = $pattern; + + return $this; + } + + public function getPattern(): ?string + { + if ($this->message === null) { + return null; + } + + return $this->pattern ?? sprintf(Term::DEFAULT_CONSTRAINT, $this->getLabel() ?? $this->getSearchValue()); + } + + /** + * Render this term as a string + * + * Pass the separator being used to separate multiple terms. If the term's value contains it, + * the result will be automatically quoted. + * + * @param string $separator + * + * @return string + */ + public function render(string $separator): string + { + if (strpos($this->value, $separator) !== false) { + return '"' . $this->value . '"'; + } + + return $this->value; + } + + /** + * Apply the given term data to this term + * + * @param array $termData + * + * @return void + */ + public function applyTermData(array $termData): void + { + if (isset($termData['search'])) { + $this->value = $termData['search']; + } + + if (isset($termData['label'])) { + $this->setLabel($termData['label']); + } + + if (isset($termData['class'])) { + $this->setClass($termData['class']); + } + + if (isset($termData['invalidMsg'])) { + $this->setMessage($termData['invalidMsg']); + } + + if (isset($termData['pattern'])) { + $this->setPattern($termData['pattern']); + } + } +} diff --git a/vendor/ipl/web/src/FormElement/TermInput/Term.php b/vendor/ipl/web/src/FormElement/TermInput/Term.php new file mode 100644 index 0000000..be08e8a --- /dev/null +++ b/vendor/ipl/web/src/FormElement/TermInput/Term.php @@ -0,0 +1,89 @@ +<?php + +namespace ipl\Web\FormElement\TermInput; + +interface Term +{ + /** @var string The default validation constraint */ + public const DEFAULT_CONSTRAINT = '^\s*(?!%s\b).*\s*$'; + + /** + * Set the search value + * + * @param string $value + * + * @return $this + */ + public function setSearchValue(string $value); + + /** + * Get the search value + * + * @return string + */ + public function getSearchValue(): string; + + /** + * Set the label + * + * @param string $label + * + * @return $this + */ + public function setLabel(string $label); + + /** + * Get the label + * + * @return ?string + */ + public function getLabel(): ?string; + + /** + * Set the CSS class + * + * @param string $class + * + * @return $this + */ + public function setClass(string $class); + + /** + * Get the CSS class + * + * @return ?string + */ + public function getClass(): ?string; + + /** + * Set the failure message + * + * @param string $message + * + * @return $this + */ + public function setMessage(string $message); + + /** + * Get the failure message + * + * @return ?string + */ + public function getMessage(): ?string; + + /** + * Set the validation constraint + * + * @param string $pattern + * + * @return $this + */ + public function setPattern(string $pattern); + + /** + * Get the validation constraint + * + * @return ?string + */ + public function getPattern(): ?string; +} diff --git a/vendor/ipl/web/src/FormElement/TermInput/TermContainer.php b/vendor/ipl/web/src/FormElement/TermInput/TermContainer.php new file mode 100644 index 0000000..c5a614c --- /dev/null +++ b/vendor/ipl/web/src/FormElement/TermInput/TermContainer.php @@ -0,0 +1,54 @@ +<?php + +namespace ipl\Web\FormElement\TermInput; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Web\FormElement\TermInput; + +class TermContainer extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'terms']; + + /** @var TermInput */ + protected $input; + + /** + * Create a new TermContainer + * + * @param TermInput $input + */ + public function __construct(TermInput $input) + { + $this->input = $input; + } + + protected function assemble() + { + foreach ($this->input->getTerms() as $i => $term) { + $label = $term->getLabel() ?: $term->getSearchValue(); + + $this->addHtml(new HtmlElement( + 'label', + Attributes::create([ + 'class' => $term->getClass(), + 'data-search' => $term->getSearchValue(), + 'data-label' => $label, + 'data-index' => $i + ]), + new HtmlElement( + 'input', + Attributes::create([ + 'type' => 'text', + 'value' => $label, + 'pattern' => $term->getPattern(), + 'data-invalid-msg' => $term->getMessage() + ]) + ) + )); + } + } +} diff --git a/vendor/ipl/web/src/FormElement/TermInput/TermSuggestions.php b/vendor/ipl/web/src/FormElement/TermInput/TermSuggestions.php new file mode 100644 index 0000000..26b00ea --- /dev/null +++ b/vendor/ipl/web/src/FormElement/TermInput/TermSuggestions.php @@ -0,0 +1,281 @@ +<?php + +namespace ipl\Web\FormElement\TermInput; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\I18n\Translation; +use Psr\Http\Message\ServerRequestInterface; +use Traversable; + +use function ipl\Stdlib\yield_groups; + +class TermSuggestions extends BaseHtmlElement +{ + use Translation; + + protected $tag = 'ul'; + + /** @var Traversable */ + protected $provider; + + /** @var ?callable */ + protected $groupingCallback; + + /** @var ?string */ + protected $searchTerm; + + /** @var ?string */ + protected $searchPattern; + + /** @var ?string */ + protected $originalValue; + + /** @var string[] */ + protected $excludeTerms = []; + + /** + * Create new TermSuggestions + * + * The provider must deliver terms in form of arrays with the following keys: + * * (required) search: The search value + * * label: A human-readable label + * * class: A CSS class + * * title: A message shown upon hover on the term + * + * Any excess key is also transferred to the client, but currently unused. + * + * @param Traversable $provider + */ + public function __construct(Traversable $provider) + { + $this->provider = $provider; + } + + /** + * Set a callback to identify groups for terms delivered by the provider + * + * The callback must return a string which is used as label for the group. + * Its interface is: `function (array $data): string` + * + * @param callable $callback + * + * @return $this + */ + public function setGroupingCallback(callable $callback): self + { + $this->groupingCallback = $callback; + + return $this; + } + + /** + * Get the callback used to identify groups for terms delivered by the provider + * + * @return ?callable + */ + public function getGroupingCallback(): ?callable + { + return $this->groupingCallback; + } + + /** + * Set the search term (can contain `*` wildcards) + * + * @param string $term + * + * @return $this + */ + public function setSearchTerm(string $term): self + { + $this->searchTerm = $term; + $this->setSearchPattern( + '/' . str_replace( + '\\000', + '.*', + preg_quote( + str_replace( + '*', + "\0", + $term + ), + '/' + ) + ) . '/i' + ); + + return $this; + } + + /** + * Get the search term + * + * @return ?string + */ + public function getSearchTerm(): ?string + { + return $this->searchTerm; + } + + /** + * Set the search pattern used by {@see matchSearch} + * + * @param string $pattern + * + * @return $this + */ + protected function setSearchPattern(string $pattern): self + { + $this->searchPattern = $pattern; + + return $this; + } + + /** + * Set the original search value + * + * The one without automatically added wildcards. + * + * @param string $term + * + * @return $this + */ + public function setOriginalSearchValue(string $term): self + { + $this->originalValue = $term; + + return $this; + } + + /** + * Get the original search value + * + * @return ?string + */ + public function getOriginalSearchValue(): ?string + { + return $this->originalValue; + } + + /** + * Set the terms to exclude in the suggestion list + * + * @param string[] $terms + * + * @return $this + */ + public function setExcludeTerms(array $terms): self + { + $this->excludeTerms = $terms; + + return $this; + } + + /** + * Get the terms to exclude in the suggestion list + * + * @return string[] + */ + public function getExcludeTerms(): array + { + return $this->excludeTerms; + } + + /** + * Match the given search term against the users search + * + * @param string $term + * + * @return bool Whether the search matches or not + */ + public function matchSearch(string $term): bool + { + if (! $this->searchPattern || $this->searchPattern === '.*') { + return true; + } + + return (bool) preg_match($this->searchPattern, $term); + } + + /** + * Load suggestions as requested by the client + * + * @param ServerRequestInterface $request + * + * @return $this + */ + public function forRequest(ServerRequestInterface $request): self + { + if ($request->getMethod() !== 'POST') { + return $this; + } + + /** @var array<string, array<int|string, string>> $requestData */ + $requestData = json_decode($request->getBody()->read(8192), true); + if (empty($requestData)) { + return $this; + } + + $this->setSearchTerm($requestData['term']['label']); + $this->setOriginalSearchValue($requestData['term']['search']); + $this->setExcludeTerms($requestData['exclude'] ?? []); + + return $this; + } + + protected function assemble() + { + $groupingCallback = $this->getGroupingCallback(); + if ($groupingCallback) { + $provider = yield_groups($this->provider, $groupingCallback); + } else { + $provider = [null => $this->provider]; + } + + /** @var iterable<?string, array<array<string, string>>> $provider */ + foreach ($provider as $group => $suggestions) { + if ($group) { + $this->addHtml( + new HtmlElement( + 'li', + Attributes::create([ + 'class' => 'suggestion-title' + ]), + Text::create($group) + ) + ); + } + + foreach ($suggestions as $data) { + $attributes = [ + 'type' => 'button', + 'value' => $data['label'] ?? $data['search'] + ]; + foreach ($data as $name => $value) { + $attributes["data-$name"] = $value; + } + + $this->addHtml( + new HtmlElement( + 'li', + null, + new HtmlElement( + 'input', + Attributes::create($attributes) + ) + ) + ); + } + } + + if ($this->isEmpty()) { + $this->addHtml(new HtmlElement( + 'li', + Attributes::create(['class' => 'nothing-to-suggest']), + new HtmlElement('em', null, Text::create($this->translate('Nothing to suggest'))) + )); + } + } +} diff --git a/vendor/ipl/web/src/FormElement/TermInput/ValidatedTerm.php b/vendor/ipl/web/src/FormElement/TermInput/ValidatedTerm.php new file mode 100644 index 0000000..e91c203 --- /dev/null +++ b/vendor/ipl/web/src/FormElement/TermInput/ValidatedTerm.php @@ -0,0 +1,38 @@ +<?php + +namespace ipl\Web\FormElement\TermInput; + +use BadMethodCallException; + +class ValidatedTerm extends \ipl\Web\Control\SearchBar\ValidatedTerm implements Term +{ + const DEFAULT_PATTERN = Term::DEFAULT_CONSTRAINT; + + /** @var ?string The CSS class */ + protected $class; + + public function setClass(string $class): Term + { + $this->class = $class; + + return $this; + } + + public function getClass(): ?string + { + return $this->class; + } + + public function toTermData() + { + $data = parent::toTermData(); + $data['class'] = $this->getClass(); + + return $data; + } + + public function toMetaData() + { + throw new BadMethodCallException(self::class . '::toTermData() not implemented yet'); + } +} |