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