Commit d59f92c6 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Nouvel élément de formulaire Select2 (https://select2.org) et son aide de vue...

Nouvel élément de formulaire Select2 (https://select2.org) et son aide de vue associée FormSelect2 (cf. doc/Formulaires.md).

Nouvel élément de formulaire Select2 et son aide de vue associée FormSelect2 (cf. https://select2.org) ; réécriture de l'élément SearchAndSelect2 et de son aide de vue associée FormSearchAndSelect2.
parent 3b313c7e
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ use UnicaenApp\Form\View\Helper\FormControlText;
use UnicaenApp\Form\View\Helper\FormElementCollection;
use UnicaenApp\Form\View\Helper\FormElementRow;
use UnicaenApp\Form\View\Helper\FormSearchAndSelect2;
use UnicaenApp\Form\View\Helper\FormSelect2;
use UnicaenApp\HostLocalization\HostLocalization;
use UnicaenApp\HostLocalization\HostLocalizationFactory;
use UnicaenApp\Message\View\Helper\MessageHelper;
@@ -398,6 +399,7 @@ return [
            'formRowDateInfSup'         => 'UnicaenApp\Form\View\Helper\FormRowDateInfSup',
            'formSearchAndSelect'       => 'UnicaenApp\Form\View\Helper\FormSearchAndSelect',
            'formSearchAndSelect2'      => FormSearchAndSelect2::class,
            'formSelect2'               => FormSelect2::class,
            'formLdapPeople'            => 'UnicaenApp\Form\View\Helper\FormLdapPeople',
            'formErrors'                => 'UnicaenApp\Form\View\Helper\FormErrors',
            'form'                      => 'UnicaenApp\Form\View\Helper\Form',
+91 −20
Original line number Diff line number Diff line
Formulaires
===========


Formulaires
-----------

MultipageForm
=============
### MultipageForm

Classe mère des formulaire multi-pages (saisie en plusieurs étapes).

Cf. documentation du plugin de contrôleur
[MultipageFormPlugin](./Plugins.md#multipageformplugin).


Éléments de formulaire
----------------------

DateInfSup
==========
### Select2

Elément de formulaire destiné à fonctionner avec le composant Javascript "Select2" (https://select2.org).

NB : charge à vous d'inclure dans votre appli les fichiers du composant javascript "Select2".

#### Côté formulaire/fieldset

Exemple :
```php
    public function init(): void
    {
        $valueOptions = array_map(fn(Etablissement $etablissement) => (object) [
            'id' => $etablissement->getId(),
            'text' => $etablissement->getStructure()->getLibelle(),
            'attributes' => [
                'data-extra' => $etablissement->getStructure()->getSigle(),
                'data-sourcecode' => $etablissement->getSourceCode(), // exploité pour filtrer les versions de diplomes
            ],
        ], $this->etablissements);

        $this->add([
            'type' => \UnicaenApp\Form\Element\Select2::class,
            'name' => $name = 'etablissement',
            'options' => [
                'label' => "Établissement d'inscription :",
                'value_options' => $valueOptions,
                'empty_option' => "Aucune sélection",

            ],
            'attributes' => [
                'id' => $name,
                'placeholder' => "Sélectionner l'établissement...",
            ],
        ]);
        
        // ...    
    }
```

#### Côté vue phtml

- Exemple basique :
```php
        <?php
        /** @var \UnicaenApp\Form\Element\Select2 $etablissementSelect */
        $etablissementSelect = $fs->get('etablissement');
        echo $this->formControlGroup($etablissementSelect);
        ?>
```

- Exemple avec customisation du template d'affichage des items :
```php
        <?php
        /** @var \UnicaenApp\Form\Element\Select2 $etablissementSelect */
        $etablissementSelect = $fs->get('etablissement');
        $formSelect2 = $this->formSelect2();
        $formSelect2->setTemplateForDataAttribute('<strong>%s</strong>', 'text');
        echo $this->formControlGroup($etablissementSelect, $formSelect2);
        ?>
```

- Exemple pour l'utilisation dans une modale (il faut spécifier le "dropdownParent") :
```php
        <div id="etablissement-select2">
            <?php
            /** @var \UnicaenApp\Form\Element\Select2 $etablissementSelect */
            $etablissementSelect = $fs->get('etablissement');
            $etablissementSelect->setDropdownParent('$("#etablissement-select2")');
            echo $this->formControlGroup($etablissementSelect);
            ?>
        </div>
```


### DateInfSup

Elément de formulaire permettant de choisir une date inférieure (date de
début) et une date supérieure éventuelle (date de fin).
@@ -88,8 +163,8 @@ code HTML complet de cet élément (labels, champs et erreurs) est
\"[FormDateInfSup](/develop/unicaen2/moduleunicaenunicaenapp/viewhelpers/FormDateInfSup)\").
\</note\>

MultipageFormNavElement
================

### MultipageFormNavElement

Élément composite de navigation au sein d\'un formulaire multipage
(formulaire en plusieurs étapes)
@@ -108,8 +183,8 @@ Cette aide de vue n\'a pas vraiment vocation à être utilisée directement
: elle est utilisée en interne par l\'aide de vue
[MultipageFormRow](/develop/unicaen2/moduleunicaenunicaenapp/viewhelpers/multipageformrow).

SearchAndSelect
===============

### SearchAndSelect

Élément de formulaire permettant de rechercher puis sélectionner quelque
chose dans une source de données.
@@ -159,11 +234,10 @@ public function rechercherAction()
}
```

AdvancedMultiCheckBox
=====================

Description
-----------
### AdvancedMultiCheckBox

#### Description

Ceci est une aide de vue pour élément de formulaire. Il accepte les
éléments de type `Laminas\Form\Element\MultiCheckbox`{.php}.
@@ -181,8 +255,7 @@ De plus, il exploite le
[WidgetInitializer](/develop/unicaen2/ModuleUnicaenUnicaenApp/js) donc
inutile de l\'initialiser, ça se fait automatiquement. \</WRAP\>

Exemple
-------
#### Exemple

Dans la vue, il s\'exploite comme suit :

@@ -210,8 +283,7 @@ $("votre_selecteur_de_div").formAdvancedMultiCheckbox("selectAll");

```

Liste des propriétés
--------------------
#### Liste des propriétés

-   `height`{.php} : définit la hauteur maximale de votre widget
-   `overflow`{.php} : gère l\'overflow du widget
@@ -219,8 +291,7 @@ Liste des propriétés
Ces propriétés sont également utilisables directement en PHP via le
ViewHelper.

Liste des méthodes
------------------
#### Liste des méthodes

-   `selectAll`{.php} : sélectionne tous les items de la liste
-   `selectNone`{.php} : désélectionne tous les items de la liste
@@ -229,11 +300,11 @@ Liste des méthodes
-   `getSelectNoneBtn`{.php} : retorune le bouton qui permet de tout
    désélectionner


Filtres d\'entrée (input filters)
---------------------------------

DateInfSupInputFilter
=====================
### DateInfSupInputFilter

Filtre d\'entrée associé à l\'élément de formulaire \"DateInfSup\" à la
saisie d\'une date inférieure (date de début) et une date supérieure
+21 −9
Original line number Diff line number Diff line
@@ -80,8 +80,8 @@ class SearchAndSelect extends Element implements InputProviderInterface
                    self::LABEL_ELEMENT_NAME => func_get_arg(1),
                );
            }
            elseif (is_string($value) && stripos($value, $delimiter = self::DEFAULT_SEP) !== false) {
                $value = array_combine(array(self::ID_ELEMENT_NAME, self::LABEL_ELEMENT_NAME), explode($delimiter, $value, 2));
            elseif (is_string($value) && stripos($value, self::DEFAULT_SEP) !== false) {
                $value = array_combine(array(self::ID_ELEMENT_NAME, self::LABEL_ELEMENT_NAME), explode(self::DEFAULT_SEP, $value, 2));
            }
            else {
                throw new InvalidArgumentException("Valeur spécifiée invalide.");
@@ -137,18 +137,14 @@ class SearchAndSelect extends Element implements InputProviderInterface
    }

    /**
     * Retourne la valeur composite de cet élément : l'id et le label séparé par 
     * le caractère spécifié.
     *
     * @param string $glue Séparateur
     * @return string
     * Retourne la valeur composite de cet élément : l'id et le label séparé par le sépararteur.
     */
    public function getValueImplode($glue = '|')
    public function getValueImplode(): ?string
    {
        if (!$this->getValueId()) {
            return null;
        }
        return implode($glue, array($this->getValueId(), $this->getValueLabel()));
        return static::createValueFromIdAndLabel($this->getValueId(), $this->getValueLabel());
    }

    /**
@@ -295,4 +291,20 @@ class SearchAndSelect extends Element implements InputProviderInterface
            ),
        );
    }


    static public function createValueFromIdAndLabel($id, string $label): string
    {
        return implode(self::DEFAULT_SEP, [$id, $label]);
    }

    static public function extractIdFromValue(string $value): string
    {
        return explode(self::DEFAULT_SEP, $value)[0];
    }

    static public function extractLabelFromValue(string $value): string
    {
        return explode(self::DEFAULT_SEP, $value)[1];
    }
}
 No newline at end of file
+66 −68
Original line number Diff line number Diff line
@@ -3,26 +3,34 @@
namespace UnicaenApp\Form\Element;

use InvalidArgumentException;
use Laminas\Form\Element\Select;
use stdClass;
use Webmozart\Assert\Assert;

/**
 * Elément de formulaire permettant de sélectionner un ou plusieurs items recherchés dans une source de
 * données distante (via ajax).
 * Elément de formulaire héritant de {@see \UnicaenApp\Form\Element\Select2} permettant de sélectionner un item
 * recherché dans une source de données distante (via une requête Ajax).
 *
 * **Attention : le mode 'multiple' n'est pas supporté.**
 *
 * 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
class SearchAndSelect2 extends Select2
{
    const SEPARATOR = '|';

    protected bool $selectionRequired = false;
    protected ?string $autocompleteSource = null;

    public function setAttribute(string $key, $value): static
    {
        if ($key === 'multiple' && ($value === true || $value === 'multiple')) {
            throw new InvalidArgumentException("Le mode 'multiple' n'est pas supporté par cet élément de formulaire");
        }

        return parent::setAttribute($key, $value);
    }

    /**
     * Spécifie la source de données dans laquelle est effectuée la recherche.
     * Spécifie l'URL à appeler pour effectuer la recherche.
     */
    public function setAutocompleteSource(string $autocompleteSource): self
    {
@@ -32,85 +40,75 @@ class SearchAndSelect2 extends Select
    }

    /**
     * Retourne la source de données dans laquelle est effectuée la recherche.
     * Retourne l'URL appelée pour effectuer la recherche.
     */
    public function getAutocompleteSource(): ?string
    {
        return $this->autocompleteSource;
    }

    public function setValue($value): self
    /**
     * Spécifie la valeur de cet élément.
     *
     * @param array $value Trois formats acceptés :
     *     - [ <value> => <label>, ... ]
     *     - [ ['id' => <value>, 'text' => <label>, 'extra' => <extra> ], [...], ... ]
     *     - [ {id: <value>, text: <label>, extra: <extra> }, {...}, ... ]
     */
    public function setValue($value): static
    {
        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);

        if ($value === null) {
            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 [];
        }
        Assert::isArray($value, "Type de valeur acceptée pour cet élément de formulaire : array");

        $valueOptions = [];
        if ($this->isMultiple()) {
            foreach ($value as $item) {
                $valueOptions[$item] = self::extractLabelFromValue($item);
            }
        } else {
            $valueOptions[$value] = self::extractLabelFromValue($value);

        if (!empty($value)) {
            // la valeur n'est pas une liste : l'élément est sensé être un select simple
            Assert::keyExists($value, 'value', "Chaque valeur doit possèder une clé %s");
            Assert::keyExists($value, 'text', "Chaque valeur doit possèder une clé %s");
            $valueOptions = [$value];
        }

        return $valueOptions;
        $this->setValueOptions($valueOptions);

        return parent::setValue($value);
    }

    static public function createValueFromIdAndLabel($id, string $label): string
    /**
     * Spécifie les <option> pré-sélectionnées.
     *
     * On détourne un peu le but initial de cette méthode car spécifier les <option> d'un élément de type
     * {@see \UnicaenApp\Form\Element\SearchAndSelect2} n'a pas de sens puisqu'elles sont le résultat
     * d'une requête Ajax.
     *
     * @param array $options Trois formats acceptés :
     *    - [ <value> => <label>, ... ]
     *    - [ ['value' => <value>, 'text' => <label>, 'extra' => <extra> ], [...], ... ]
     *    - [ {value: <value>, text: <label>, extra: <extra> }, {...}, ... ]
     */
    public function setValueOptions(array $options): static
    {
        return implode(self::SEPARATOR, [$id, $label]);
        return parent::setValueOptions($options);
    }

    static public function extractIdFromValue(string $value): string
    /**
     * Redéfinition de la méthode pour injecter `'selected' => true` dans chaque <option>,
     * cf. commentaires dans {@see self::setValueOptions()}.
     *
     * @return stdClass[]
     */
    protected function normalizeValueOptions(array $options): array
    {
        return explode(self::SEPARATOR, $value)[0];
        $normalizedValueOptions = parent::normalizeValueOptions($options);

        // marquage de l'option comme sélectionnée
        foreach ($normalizedValueOptions as $valueOption) {
            $valueOption->selected = true;
        }

    static public function extractLabelFromValue(string $value): string
    {
        return explode(self::SEPARATOR, $value)[1];
        return $normalizedValueOptions;
    }
}
 No newline at end of file
+119 −0
Original line number Diff line number Diff line
<?php

namespace UnicaenApp\Form\Element;

use InvalidArgumentException;
use Laminas\Form\Element\Select;
use stdClass;
use Webmozart\Assert\Assert;

/**
 * Select sur lequel doit être installé le composant Javascript "Select2" (cf. https://select2.org)
 * grâce à l'aide de vue {@see \UnicaenApp\Form\View\Helper\FormSelect2}.
 *
 * @author Unicaen
 */
class Select2 extends Select
{
    protected $disableInArrayValidator = true;

    protected string $dropdownParent = '$("body")';

    public function setDropdownParent(string $dropdownParent): static
    {
        $this->dropdownParent = $dropdownParent;
        return $this;
    }

    public function setOptions(iterable $options): static
    {
        parent::setOptions($options);

        if (isset($this->options['dropdown_parent'])) {
            $this->setDropdownParent($this->options['dropdown_parent']);
        }
        // Alias for 'dropdown_parent'
        if (isset($this->options['dropdownParent'])) {
            $this->setDropdownParent($this->options['dropdownParent']);
        }

        return $this;
    }

    /**
     * Fournit les 'value options' du select.
     *
     * @param array $options Trois formats acceptés :
     *   - [ <value> => <label>, ... ]
     *   - [ ['value' => <value>, 'text' => <label>, 'extra' => <extra> ], [...], ... ]
     *   - [ {value: <value>, text: <label>, extra: <extra> }, {...}, ... ]
     */
    public function setValueOptions(array $options): static
    {
        $normalizedValueOptions = $this->normalizeValueOptions($options);

        return parent::setValueOptions($normalizedValueOptions);
    }

    /**
     * Transforme si besoin au format {@see stdClass} les 'value options' spécifiées.
     *
     * @return stdClass[]
     */
    protected function normalizeValueOptions(array $options): array
    {
        $normalizedValueOptions = [];
        foreach ($options as $k => $value) {
            $normalizedValueOptions[] = $this->normalizeOption($value, $k);
        }

        return $normalizedValueOptions;
    }

    protected function normalizeOption(mixed $value, mixed $key): stdClass
    {
        if (is_array($value)) {
            $normalizedOption = $this->normalizeArrayOption($value);
        } elseif (is_string($value)) {
            $normalizedOption = $this->normalizeStringOption($value, $key);
        } elseif ($value instanceof stdClass) {
            $normalizedOption = $this->normalizeStdClassOption($value);
        } else {
            throw new InvalidArgumentException("Type de valeur attendu pour chaque option : array, stdClass ou string");
        }

        return $normalizedOption;
    }

    protected function normalizeArrayOption(array $value): stdClass
    {
        Assert::keyExists($value, 'value');
        if (!array_key_exists('id', $value)) {
            $value['id'] = $value['value'];
        }
        Assert::keyExists($value, 'text');
        if (!array_key_exists('label', $value)) {
            $value['label'] = $value['text'];
        }

        return (object)$value;
    }

    protected function normalizeStringOption(string $value, mixed $key): stdClass
    {
        return (object)['value' => $key, 'text' => $value];
    }

    protected function normalizeStdClassOption(stdClass $value): stdClass
    {
        Assert::propertyExists($value, 'value', "Propriété attendue pour chaque option : %s");
        Assert::propertyExists($value, 'text', "Propriété attendue pour chaque option : %s");

        return $value;
    }

    public function getDropdownParent(): string
    {
        return $this->dropdownParent;
    }
}
 No newline at end of file
Loading