-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add class
SimpleSearchField
and simpleSuggestions
- Loading branch information
1 parent
20a7a22
commit 5ce928a
Showing
2 changed files
with
366 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
<?php | ||
|
||
namespace ipl\Web\Control; | ||
|
||
use ipl\Html\Attributes; | ||
use ipl\Html\Form; | ||
use ipl\Html\FormElement\InputElement; | ||
use ipl\Html\FormElement\SubmitElement; | ||
use ipl\Html\HtmlElement; | ||
use ipl\Web\Url; | ||
use ipl\Web\Widget\Icon; | ||
|
||
use function ipl\I18n\t; | ||
|
||
/** | ||
* Form for simple value based search | ||
*/ | ||
class SimpleSearchField extends Form | ||
{ | ||
protected $defaultAttributes = [ | ||
'class' => 'search-field', | ||
'name' => 'search-field', | ||
'role' => 'search' | ||
]; | ||
|
||
/** @var string The term separator */ | ||
public const TERM_SEPARATOR = ','; | ||
|
||
/** @var string The default search parameter */ | ||
public const DEFAULT_SEARCH_PARAM = 'q'; | ||
|
||
/** @var string The search parameter */ | ||
protected $searchParameter; | ||
|
||
/** @var Url The suggestion url */ | ||
protected $suggestionUrl; | ||
|
||
/** @var string Submit label */ | ||
protected $submitLabel; | ||
|
||
/** | ||
* Set the search parameter | ||
* | ||
* @param string $name | ||
* | ||
* @return $this | ||
*/ | ||
public function setSearchParameter(string $name): self | ||
{ | ||
$this->searchParameter = $name; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Get the search parameter | ||
* | ||
* @return string | ||
*/ | ||
public function getSearchParameter(): string | ||
{ | ||
return $this->searchParameter ?: self::DEFAULT_SEARCH_PARAM; | ||
} | ||
|
||
/** | ||
* 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 submit label | ||
* | ||
* @param string $label | ||
* | ||
* @return $this | ||
*/ | ||
public function setSubmitLabel(string $label): self | ||
{ | ||
$this->submitLabel = $label; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Get submit label | ||
* | ||
* @return string | ||
*/ | ||
public function getSubmitLabel(): string | ||
{ | ||
return $this->submitLabel ?? t('Submit'); | ||
} | ||
|
||
public function assemble() | ||
{ | ||
$filterInput = new InputElement(null, [ | ||
'type' => 'text', | ||
'placeholder' => t('Type to search'), | ||
'class' => 'search-field', | ||
'id' => 'search-filed', | ||
'autocomplete' => 'off', | ||
'required' => true, | ||
'data-no-auto-submit' => true, | ||
'data-no-js-placeholder' => true, | ||
'data-enrichment-type' => 'terms', | ||
'data-term-separator' => self::TERM_SEPARATOR, | ||
'data-term-mode' => 'read-only', | ||
'data-term-direction' => 'vertical', | ||
'data-data-input' => '#data-input', | ||
'data-term-input' => '#term-input', | ||
'data-term-container' => '#term-container', | ||
'data-term-suggestions' => '#term-suggestions', | ||
'data-suggest-url' => $this->getSuggestionUrl() | ||
]); | ||
|
||
$dataInput = new InputElement('data', ['type' => 'hidden', 'id' => 'data-input']); | ||
|
||
$termInput = new InputElement($this->getSearchParameter(), ['type' => 'hidden', 'id' => 'term-input']); | ||
$this->registerElement($termInput); | ||
|
||
$termContainer = new HtmlElement( | ||
'div', | ||
Attributes::create(['id' => 'term-container', 'class' => 'term-container']) | ||
); | ||
|
||
$termSuggestions = new HtmlElement( | ||
'div', | ||
Attributes::create(['id' => 'term-suggestions', 'class' => 'search-suggestions']) | ||
); | ||
|
||
$submitButton = new SubmitElement('submit', ['label' => $this->getSubmitLabel()]); | ||
|
||
$this->registerElement($submitButton); | ||
|
||
$this->add([ | ||
HtmlElement::create( | ||
'div', | ||
null, | ||
[ | ||
new Icon('search', ['class' => 'search-icon']), | ||
$filterInput, | ||
$termSuggestions, | ||
$dataInput, | ||
$termInput, | ||
$submitButton | ||
] | ||
), | ||
$termContainer | ||
]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
<?php | ||
|
||
namespace ipl\Web\Control; | ||
|
||
use ArrayIterator; | ||
use ipl\Html\Attributes; | ||
use ipl\Html\BaseHtmlElement; | ||
use ipl\Html\FormattedString; | ||
use ipl\Html\FormElement\ButtonElement; | ||
use ipl\Html\FormElement\InputElement; | ||
use ipl\Html\HtmlElement; | ||
use ipl\Html\Text; | ||
use IteratorIterator; | ||
use LimitIterator; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
|
||
abstract class SimpleSuggestions extends BaseHtmlElement | ||
{ | ||
/** @var int Suggestions limit */ | ||
public const DEFAULT_LIMIT = 10; | ||
|
||
/** @var string Class name for suggestion title */ | ||
public const SUGGESTION_TITLE_CLASS = 'suggestion-title'; | ||
|
||
protected $tag = 'ul'; | ||
|
||
/** @var string The given input for search */ | ||
protected $searchTerm; | ||
|
||
/** @var mixed Fetched data for given input */ | ||
protected $data; | ||
|
||
/** @var string Default first suggestion in the suggestion list */ | ||
protected $default; | ||
|
||
/** | ||
* Set the search term | ||
* | ||
* @param string $searchTerm | ||
* | ||
* @return $this | ||
*/ | ||
public function setSearchTerm(string $searchTerm): self | ||
{ | ||
$this->searchTerm = $searchTerm; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Set the fetched data | ||
* | ||
* @param mixed $data | ||
* | ||
* @return $this | ||
*/ | ||
public function setData($data): self | ||
{ | ||
$this->data = $data; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Set the default suggestion | ||
* | ||
* @param string $default | ||
* | ||
* @return $this | ||
*/ | ||
public function setDefault(string $default): self | ||
{ | ||
$this->default = trim($default, '"\''); | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Fetch suggestions according to the input in the search field | ||
* | ||
* @param string $searchTerm The given input in the search field | ||
* @param array $exclude Already added terms to be excluded from the suggestion list | ||
* | ||
* @return mixed | ||
*/ | ||
abstract protected function fetchSuggestions(string $searchTerm, array $exclude = []); | ||
|
||
protected function assembleDefault(): void | ||
{ | ||
if ($this->default === null) { | ||
return; | ||
} | ||
|
||
$attributes = [ | ||
'type' => 'button', | ||
'tabindex' => -1, | ||
'data-label' => $this->default, | ||
'value' => $this->default, | ||
]; | ||
|
||
$button = new ButtonElement(null, $attributes); | ||
$button->addHtml(FormattedString::create( | ||
t('Add %s'), | ||
new HtmlElement('em', null, Text::create($this->default)) | ||
)); | ||
|
||
$this->prependHtml(new HtmlElement('li', Attributes::create(['class' => 'default']), $button)); | ||
} | ||
|
||
protected function assemble() | ||
{ | ||
if ($this->data === null) { | ||
$data = []; | ||
} else { | ||
$data = $this->data; | ||
if (is_array($data)) { | ||
$data = new ArrayIterator($data); | ||
} | ||
|
||
$data = new LimitIterator(new IteratorIterator($data), 0, self::DEFAULT_LIMIT); | ||
} | ||
|
||
foreach ($data as $term => $label) { | ||
if (is_int($term)) { | ||
$term = $label; | ||
} | ||
|
||
$attributes = [ | ||
'type' => 'button', | ||
'tabindex' => -1, | ||
'data-search' => $term | ||
]; | ||
|
||
$attributes['value'] = $label; | ||
$attributes['data-label'] = $label; | ||
|
||
$this->addHtml(new HtmlElement('li', null, new InputElement(null, $attributes))); | ||
} | ||
|
||
$showDefault = true; | ||
if ($this->searchTerm && $this->count() === 1) { | ||
// The default option is not shown if the user's input result in an exact match | ||
$input = $this->getFirst('li')->getFirst('input'); | ||
$showDefault = $input->getValue() != $this->searchTerm | ||
&& $input->getAttributes()->get('data-search')->getValue() != $this->searchTerm; | ||
} | ||
|
||
if ($showDefault) { | ||
$this->assembleDefault(); | ||
} | ||
} | ||
|
||
/** | ||
* Load suggestions as requested by the client | ||
* | ||
* @param ServerRequestInterface $request | ||
* | ||
* @return $this | ||
*/ | ||
public function forRequest(ServerRequestInterface $request): self | ||
{ | ||
if ($request->getMethod() !== 'POST') { | ||
return $this; | ||
} | ||
|
||
$requestData = json_decode($request->getBody()->read(8192), true); | ||
if (empty($requestData)) { | ||
return $this; | ||
} | ||
|
||
$search = $requestData['term']['search']; | ||
$label = $requestData['term']['label']; | ||
$exclude = $requestData['exclude']; | ||
|
||
$this->setSearchTerm($search); | ||
|
||
$this->setData($this->fetchSuggestions($label, $exclude)); | ||
|
||
if (! empty($search)) { | ||
$this->setDefault($search); | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
public function renderUnwrapped() | ||
{ | ||
$this->ensureAssembled(); | ||
|
||
if ($this->isEmpty()) { | ||
return ''; | ||
} | ||
|
||
return parent::renderUnwrapped(); | ||
} | ||
} |