From a3b1cad54075228129a843b6c447a54513f6a90e Mon Sep 17 00:00:00 2001 From: Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr> Date: Mon, 28 Nov 2022 11:08:41 +0100 Subject: [PATCH] =?UTF-8?q?Nouvel=20=C3=A9l=C3=A9ment=20de=20formulaire=20?= =?UTF-8?q?SearchAndSelect2=20(bas=C3=A9=20sur=20le=20widget=20js=20Select?= =?UTF-8?q?2).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 16 +++ config/module.config.php | 5 + .../Filter/SearchAndSelect2Filter.php | 17 +++ .../Form/Element/SearchAndSelect2.php | 116 ++++++++++++++++ .../Form/View/Helper/FormControlGroup.php | 51 +++---- .../Form/View/Helper/FormSearchAndSelect2.php | 131 ++++++++++++++++++ 6 files changed, 312 insertions(+), 24 deletions(-) create mode 100644 src/UnicaenApp/Filter/SearchAndSelect2Filter.php create mode 100644 src/UnicaenApp/Form/Element/SearchAndSelect2.php create mode 100644 src/UnicaenApp/Form/View/Helper/FormSearchAndSelect2.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd5a887..894a51d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ CHANGELOG ========= +5.1.5 +----- +- Nouvel élément de formulaire SearchAndSelect2 (basé sur le widget js Select2). + +5.1.4 +----- +- + +5.1.3 +----- +- + +5.1.2 +----- +- + 5.1.1 ----- - Suppression du fix pour bootstrap-select-1.14.0-beta2 donc nécessaité de passer à bootstrap-select-1.14.0-beta3 dans vos applis. diff --git a/config/module.config.php b/config/module.config.php index 4ca44d3f..26bbbf37 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -2,12 +2,15 @@ namespace UnicaenApp; +use Laminas\ServiceManager\Factory\InvokableFactory; use UnicaenApp\Controller\CacheControllerFactory; use UnicaenApp\Controller\ConsoleController; use UnicaenApp\Controller\ConsoleControllerFactory; use UnicaenApp\Controller\InstadiaControllerFactory; +use UnicaenApp\Form\Element\SearchAndSelect2; use UnicaenApp\Form\View\Helper\FormControlGroup; use UnicaenApp\Form\View\Helper\FormControlGroupFactory; +use UnicaenApp\Form\View\Helper\FormSearchAndSelect2; use UnicaenApp\HostLocalization\HostLocalization; use UnicaenApp\HostLocalization\HostLocalizationFactory; use UnicaenApp\Message\View\Helper\MessageHelper; @@ -366,6 +369,7 @@ return [ 'form_elements' => [ 'invokables' => [ 'UploadForm' => 'UnicaenApp\Controller\Plugin\Upload\UploadForm', + SearchAndSelect2::class => InvokableFactory::class, ], 'initializers' => [ 'UnicaenApp\Service\EntityManagerAwareInitializer', @@ -426,6 +430,7 @@ return [ 'formDateInfSup' => 'UnicaenApp\Form\View\Helper\FormDateInfSup', 'formRowDateInfSup' => 'UnicaenApp\Form\View\Helper\FormRowDateInfSup', 'formSearchAndSelect' => 'UnicaenApp\Form\View\Helper\FormSearchAndSelect', + 'formSearchAndSelect2' => FormSearchAndSelect2::class, 'formLdapPeople' => 'UnicaenApp\Form\View\Helper\FormLdapPeople', 'formErrors' => 'UnicaenApp\Form\View\Helper\FormErrors', 'form' => 'UnicaenApp\Form\View\Helper\Form', diff --git a/src/UnicaenApp/Filter/SearchAndSelect2Filter.php b/src/UnicaenApp/Filter/SearchAndSelect2Filter.php new file mode 100644 index 00000000..8fafe7c1 --- /dev/null +++ b/src/UnicaenApp/Filter/SearchAndSelect2Filter.php @@ -0,0 +1,17 @@ +<?php + +namespace UnicaenApp\Filter; + +use Laminas\Filter\AbstractFilter; + +class SearchAndSelect2Filter extends AbstractFilter +{ + + /** + * @inheritDoc + */ + public function filter($value) + { + // TODO: Implement filter() method. + } +} \ No newline at end of file diff --git a/src/UnicaenApp/Form/Element/SearchAndSelect2.php b/src/UnicaenApp/Form/Element/SearchAndSelect2.php new file mode 100644 index 00000000..324e4126 --- /dev/null +++ b/src/UnicaenApp/Form/Element/SearchAndSelect2.php @@ -0,0 +1,116 @@ +<?php + +namespace UnicaenApp\Form\Element; + +use InvalidArgumentException; +use Laminas\Form\Element\Select; + +/** + * Elément de formulaire permettant de sélectionner un ou plusieurs items recherchés dans une source de + * données distante (via ajax). + * + * NB: Il faut utiliser l'aide de vue 'FormSearchAndSelect2' pour dessiner cet élément. + * + * @see \UnicaenApp\Form\View\Helper\FormSearchAndSelect + * @author Unicaen + */ +class SearchAndSelect2 extends Select +{ + const SEPARATOR = '|'; + + protected bool $selectionRequired = false; + protected ?string $autocompleteSource = null; + + /** + * Spécifie la source de données dans laquelle est effectuée la recherche. + */ + public function setAutocompleteSource(string $autocompleteSource): self + { + $this->autocompleteSource = $autocompleteSource; + + return $this; + } + + /** + * Retourne la source de données dans laquelle est effectuée la recherche. + */ + public function getAutocompleteSource(): ?string + { + return $this->autocompleteSource; + } + + public function setValue($value): self + { + if ($value) { + if (!is_string($value)) { + throw new InvalidArgumentException( + "Cet élément de formulaire n'accepte que les chaînes de caractères" + ); + } + if (!str_contains($value, $sep = self::SEPARATOR)) { + throw new InvalidArgumentException( + "Cet élément de formulaire n'accepte que les valeurs de la forme 'id{$sep}label'" + ); + } + } + + $valueOptions = $this->extractValueOptionsFromValue($value); + $this->setValueOptions($valueOptions); + + return parent::setValue($value); + } + +// public function getValue() +// { +// if ($this->isMultiple()) { +// return array_keys($this->getValueOptions()); +// } else { +// return key($this->getValueOptions()) ?: null; +// } +// } + +// /** +// * @return string|int|array +// */ +// public function getValueIds() +// { +// if ($this->isMultiple()) { +// return array_keys($this->getValueOptions()); +// } else { +// return key($this->getValueOptions()) ?: null; +// } +// } + + protected function extractValueOptionsFromValue($value): array + { + if (!$value) { + return []; + } + + $valueOptions = []; + if ($this->isMultiple()) { + foreach ($value as $item) { + $valueOptions[$item] = self::extractLabelFromValue($item); + } + } else { + $valueOptions[$value] = self::extractLabelFromValue($value); + } + + return $valueOptions; + } + + static public function createValueFromIdAndLabel($id, string $label): string + { + return implode(self::SEPARATOR, [$id, $label]); + } + + static public function extractIdFromValue(string $value): string + { + return explode(self::SEPARATOR, $value)[0]; + } + + static public function extractLabelFromValue(string $value): string + { + return explode(self::SEPARATOR, $value)[1]; + } +} \ No newline at end of file diff --git a/src/UnicaenApp/Form/View/Helper/FormControlGroup.php b/src/UnicaenApp/Form/View/Helper/FormControlGroup.php index b1c0f099..331f3401 100644 --- a/src/UnicaenApp/Form/View/Helper/FormControlGroup.php +++ b/src/UnicaenApp/Form/View/Helper/FormControlGroup.php @@ -2,19 +2,18 @@ namespace UnicaenApp\Form\View\Helper; -use UnicaenApp\Exception\LogicException; -use UnicaenApp\Form\Element\Date; -use UnicaenApp\Form\Element\DateInfSup; -use UnicaenApp\Form\Element\SearchAndSelect; use Laminas\Form\Element\Button; use Laminas\Form\Element\Checkbox; use Laminas\Form\Element\DateTime; use Laminas\Form\Element\MultiCheckbox; -use Laminas\Form\Element\Radio; -use Laminas\Form\Element\Select; use Laminas\Form\ElementInterface; use Laminas\Form\View\Helper\AbstractHelper; use Laminas\Form\View\Helper\FormElementErrors; +use UnicaenApp\Exception\LogicException; +use UnicaenApp\Form\Element\Date; +use UnicaenApp\Form\Element\DateInfSup; +use UnicaenApp\Form\Element\SearchAndSelect; +use UnicaenApp\Form\Element\SearchAndSelect2; /** * Aide de vue générant un élément de fomulaire à la mode Bootsrap 5. @@ -66,11 +65,11 @@ class FormControlGroup extends AbstractHelper * Appel de l'objet comme une fonction. * * @param \Laminas\Form\ElementInterface|null $element Élément de formulaire - * @param string|null $pluginClass Plugin + * @param string|AbstractHelper $pluginClass Plugin * * @return string|FormControlGroup */ - public function __invoke(ElementInterface $element = null, $pluginClass = 'formElement') + public function __invoke(ElementInterface $element = null, $pluginClass = null) { if (null === $element) { return $this; @@ -85,11 +84,11 @@ class FormControlGroup extends AbstractHelper * Génère le code HTML. * * @param ElementInterface $element - * @param string|null $pluginClass + * @param string|AbstractHelper $pluginClass * * @return string */ - public function render(ElementInterface $element, $pluginClass = 'formElement'): string + public function render(ElementInterface $element, $pluginClass = null): string { $this->normalizeElement($element); $this->customFromOptions($element); @@ -236,13 +235,23 @@ class FormControlGroup extends AbstractHelper return $helpContentAfter; } - private function inputHtml(ElementInterface $element, $pluginClass = 'formElement') + private function inputHtml(ElementInterface $element, $pluginClass = null) { - if (!$pluginClass) { - $pluginClass = 'formElement'; - } - - if ($element instanceof SearchAndSelect) { + if ($pluginClass) { + if (is_string($pluginClass)) { + $helper = $this->getView()->plugin($pluginClass); + $html = $helper($element); + } elseif ($pluginClass instanceof AbstractHelper) { + $html = $pluginClass->__invoke($element); + } else { + throw new LogicException('Argument $pluginClass incorrect'); + } + } elseif ($element instanceof SearchAndSelect2) { + /** @var \UnicaenApp\Form\View\Helper\FormSearchAndSelect2 $helper */ + $helper = $this->getView()->plugin('formSearchAndSelect2'); + $helper->setAutocompleteMinLength(2); + $html = $helper($element); + } elseif ($element instanceof SearchAndSelect) { /** @var FormSearchAndSelect $helper */ $helper = $this->getView()->plugin('formSearchAndSelect'); $helper->setAutocompleteMinLength(2); @@ -257,14 +266,8 @@ class FormControlGroup extends AbstractHelper $helper = $this->getView()->plugin('formDateTime'); $html = $helper($element); } else { - if (is_string($pluginClass)) { - $helper = $this->getView()->plugin($pluginClass); - $html = $helper($element); - } elseif ($pluginClass instanceof \Laminas\Form\View\Helper\AbstractHelper) { - $html = $pluginClass($element); - } else { - throw new LogicException('Argument $pluginClass incorrect'); - } + $helper = $this->getView()->plugin('formElement'); + $html = $helper($element); } if ($element instanceof MultiCheckbox) { diff --git a/src/UnicaenApp/Form/View/Helper/FormSearchAndSelect2.php b/src/UnicaenApp/Form/View/Helper/FormSearchAndSelect2.php new file mode 100644 index 00000000..c44004f3 --- /dev/null +++ b/src/UnicaenApp/Form/View/Helper/FormSearchAndSelect2.php @@ -0,0 +1,131 @@ +<?php + +namespace UnicaenApp\Form\View\Helper; + +use Laminas\Form\ElementInterface; +use Laminas\Form\Exception\InvalidElementException; +use Laminas\Form\View\Helper\FormSelect; +use UnicaenApp\Exception\LogicException; +use UnicaenApp\Form\Element\SearchAndSelect2; + +/** + * Aide de vue dédiée à l'élément {@see \UnicaenApp\Form\Element\SearchAndSelect}. + * Génère un <select> sur lequel est installé le widget "Select2" (https://select2.org). + * + * @property \Application\View\Renderer\PhpRenderer $view + * + * @author Unicaen + * @see \UnicaenApp\Form\Element\SearchAndSelect + */ +class FormSearchAndSelect2 extends FormSelect +{ + protected SearchAndSelect2 $element; + protected ?string $autocompleteSource = null; + protected int $autocompleteMinLength = 2; + + public function setAutocompleteSource(string $autocompleteSource): self + { + $this->autocompleteSource = $autocompleteSource; + + return $this; + } + + public function setAutocompleteMinLength($autocompleteMinLength): self + { + $this->autocompleteMinLength = $autocompleteMinLength; + + return $this; + } + + public function __invoke(ElementInterface $element = null) + { + if ($element && !$element instanceof SearchAndSelect2) { + throw new InvalidElementException("L'élément spécifié n'est pas du type attendu."); + } + + $this->element = $element; + + return parent::__invoke($element); + } + + /** + * @param SearchAndSelect2 $element + * @return string + */ + public function render(ElementInterface $element): string + { + if (!$element instanceof SearchAndSelect2) { + throw new InvalidElementException("L'élément spécifié n'est pas du type attendu."); + } + + $this->element = $element; + + if (!$this->element->getAttribute('id')) { + $this->element->setAttribute('id', uniqid('sas-')); + } + +// $this->element->setAttribute('class', 'sas'); + +// $element = new Select(); +// $element +// ->setAttributes($this->element->getAttributes()) +// ->setName($this->element->getName()) +// ->setAttribute('multiple', $this->element->isMultiple()) +// ->setAttribute('id', $this->element->getAttribute('id')) +// ->setAttribute('class', 'sas form-control form-control-sm'); +// +// $element->setValueOptions($this->element->getValueOptions()); +// $element->setValue($this->element->getValueIds()); + + $markup = $this->view->formSelect($this->element); + + $markup .= PHP_EOL . '<script>' . $this->getJavascript() . '</script>' . PHP_EOL; + + return $markup; + } + + public function getJavascript(): string + { + if (!$this->element) { + throw new LogicException("Aucun élément spécifié, appelez render() auparavant."); + } + + $elementDomId = $this->element->getAttribute('id'); + $autocompleteMinLength = $this->autocompleteMinLength; + $autocompleteSource = $this->autocompleteSource ?: $this->element->getAutocompleteSource(); + $placeholder = $this->element->getAttribute('placeholder'); + $separator = SearchAndSelect2::SEPARATOR; + + return <<<EOT +$(function() { + $("#$elementDomId").select2({ + allowClear: true, + minimumInputLength: $autocompleteMinLength, + placeholder: "$placeholder", + ajax: { + url: '$autocompleteSource', + processResults: function (data) { + data.forEach(function(item) { + item.id = item.id + '$separator' + item.label; // concat de l'id et du label + }); + //console.log(data); + return { results: data }; + }, + dataType: 'json', + delay: 500 + }, + templateResult: function(item) { + if (!item.id) { + return item.text; + } + if (item.extra && item.extra.trim()) { + return $('<span>' + item.text + ' <span class="badge bg-secondary">' + item.extra + '</span></span>'); + } else { + return $('<span>' + item.text + '</span>'); + } + } + }); +}); +EOT; + } +} \ No newline at end of file -- GitLab