diff --git a/CHANGELOG.md b/CHANGELOG.md index 212e84f57d24212aa27aae791a91c9da0b230ebc..b05ea21855cb43f71a4446b76710fb7dbff0ce5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +5.1.7 +----- +- Nouvel élément de formulaire Collection et aide de vue associée FormElementCollection (adaptés d'Octopus). + 5.1.6 ----- - Nouveau collecteur dans la barre LaminasDevTools : messages produits par \UnicaenApp\Service\MessageCollector. diff --git a/config/module.config.php b/config/module.config.php index a9158c46cff787724b9ce6dbb8bd44588eb8fd62..6a46951b0254e1e1056a9803cd0b9c159712bb1d 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -13,6 +13,9 @@ use UnicaenApp\DeveloperTools\MessageCollectorServiceFactory; use UnicaenApp\Form\Element\SearchAndSelect2; use UnicaenApp\Form\View\Helper\FormControlGroup; use UnicaenApp\Form\View\Helper\FormControlGroupFactory; +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\HostLocalization\HostLocalization; use UnicaenApp\HostLocalization\HostLocalizationFactory; @@ -432,6 +435,7 @@ return [ 'formDate' => 'UnicaenApp\Form\View\Helper\FormDate', 'formDateTime' => Form\View\Helper\FormDateTime::class, 'formDateInfSup' => 'UnicaenApp\Form\View\Helper\FormDateInfSup', + 'formElementCollection' => FormElementCollection::class, 'formRowDateInfSup' => 'UnicaenApp\Form\View\Helper\FormRowDateInfSup', 'formSearchAndSelect' => 'UnicaenApp\Form\View\Helper\FormSearchAndSelect', 'formSearchAndSelect2' => FormSearchAndSelect2::class, diff --git a/src/UnicaenApp/Form/Element/Collection.php b/src/UnicaenApp/Form/Element/Collection.php new file mode 100644 index 0000000000000000000000000000000000000000..12f4e12e270ce205b934075ead73c59990bb544d --- /dev/null +++ b/src/UnicaenApp/Form/Element/Collection.php @@ -0,0 +1,153 @@ +<?php + +namespace UnicaenApp\Form\Element; + +use Laminas\Form\Element\Collection as ZendCollection; +use Laminas\Form\Element\Text; +use Laminas\Form\ElementInterface; +use Laminas\Form\FieldsetInterface; + +/** + * Collection d'éléments : + * - possibilité d'ajouter un autocomplete sur un (et un seul) élément + * - possibilité de limiter le nombre d'éléments + * + * @author David Surville <david.surville at unicaen.fr> + */ +class Collection extends ZendCollection +{ + const AUTOCOMPLETE_MIN_LENGTH = 2; + const AUTOCOMPLETE_DELAY = 750; + + + /** + * Liste des autocomplete + * Format : ['nom_element' => ['source' => , 'min-length' => , 'delay' => ]] + * + * @var array + */ + protected $autocomplete = []; + + /** + * Nombre d'éléments minimum de la collection + * 0 = pas de limitation + * + * @var int + */ + protected $minElements = 0; + + /** + * Nombre d'éléments maximum de la collection + * 0 = pas de limitation + * + * @var int + */ + protected $maxElements = 0; + + + /** + * @param null|int|string $name Optional name for the element + * @param array $options Optional options for the element + */ + public function __construct($name = null, $options = array()) + { + parent::__construct($name, $options); + $this->setAttribute('id', uniqid('collection')); + } + + + /** + * @return array + */ + public function getAutocomplete() + { + return $this->autocomplete; + } + + + /** + * Ajoute un autocomplete sur un élément + * + * @param string $elementName + * @param string $source + * @param int $minLength + * @param int $delay + */ + public function addAutocomplete($elementName, $source, $minLength = null, $delay = null) + { + $targetElement = $this->getTargetElement(); + + if ($targetElement instanceof FieldsetInterface) { + $elements = array_keys($targetElement->getElements()); + if (!in_array($elementName, $elements)) { + throw new \RuntimeException(sprintf("L'élément '%s' associé à l'autocomplete n'existe pas.", $elementName)); + } + + $element = $targetElement->get($elementName); + } elseif ($targetElement instanceof ElementInterface) { + if ($targetElement->getName() !== $elementName) { + throw new \RuntimeException(sprintf("L'élément '%s' associé à l'autocomplete n'existe pas.", $elementName)); + } + + $element = $targetElement; + } + + if (!$element instanceof Text) { + throw new \RuntimeException("L'autocomplete doit être associé à un élément de type 'text'."); + } + + $element->setAttribute('class', sprintf('%s-autocomplete', $this->getAttribute('id'))); + + $this->autocomplete[$elementName] = [ + 'source' => $source, + 'min-length' => $minLength ?: self::AUTOCOMPLETE_MIN_LENGTH, + 'delay' => $delay ?: self::AUTOCOMPLETE_DELAY + ]; + } + + /** + * + * @return int + */ + function getMinElements() + { + return $this->minElements; + } + + /** + * + * @param int $minElements + * + * @return self + */ + function setMinElements($minElements) + { + $this->minElements = $minElements; + + return $this; + } + + /** + * + * @return int + */ + function getMaxElements() + { + return $this->maxElements; + } + + /** + * + * @param int $maxElements + * + * @return self + */ + function setMaxElements($maxElements) + { + $this->maxElements = $maxElements; + + return $this; + } + + +} diff --git a/src/UnicaenApp/Form/View/Helper/FormElementCollection.php b/src/UnicaenApp/Form/View/Helper/FormElementCollection.php new file mode 100644 index 0000000000000000000000000000000000000000..f4a66d8ccd6f49bed84b0f525b0ff960054a0eaf --- /dev/null +++ b/src/UnicaenApp/Form/View/Helper/FormElementCollection.php @@ -0,0 +1,300 @@ +<?php +namespace UnicaenApp\Form\View\Helper; + +use UnicaenApp\Form\Element\Collection; +use Laminas\Form\View\Helper\FormCollection; +use Laminas\Form\ElementInterface; +use Laminas\Form\Element\Collection as CollectionElement; +use Laminas\Form\FieldsetInterface; +use Laminas\Form\LabelAwareInterface; +use Laminas\Form\Element\Button; + +/** + * Aide de vue dessinant une collection d'éléments. + * + * Ajout par rapport à celle de base : + * - boutons d'ajout et de suppression d'élément à la collection + * - respect du nombre mini et maxi d'éléments dans la collection + * - styles inline :-/ + * + * @see \UnicaenApp\Form\Element\Collection + * @author Unicaen + */ +class FormElementCollection extends FormCollection +{ + /** + * This is the default wrapper that the collection is wrapped into + * + * @var string + */ + protected $wrapper = '<fieldset%4$s>%2$s%1$s%3$s%5$s</fieldset>%6$s%7$s'; + + /** + * The name of the default view helper that is used to render sub elements. + * + * @var string + */ + protected $defaultElementHelper = 'formControlGroup'; + + /** + * Nombre d'éléments à afficher + * + * @var int + */ + protected $countElements; + + + /** + * Render a collection by iterating through all fieldsets and elements + * + * @param ElementInterface $element + * @return string + */ + public function render(ElementInterface $element) + { + $renderer = $this->getView(); + if (!method_exists($renderer, 'plugin')) { + // Bail early if renderer is not pluggable + return ''; + } + + $index=0; + $markup = ''; + $linkMarkup = ''; + $templateMarkup = ''; + $elementHelper = $this->getElementHelper(); + $fieldsetHelper = $this->getFieldsetHelper(); + + if ($element instanceof CollectionElement && $element->shouldCreateTemplate()) { + $this->countElements = $element->getCount(); + $linkMarkup = $this->getAddLink(); + $javascript = $this->getJavascript($element); + $css = $this->getCss($element); + $element->getTemplateElement()->setAttribute('data-index', '__index__'); + $templateMarkup = $this->renderTemplate($element); + } + elseif ($element instanceof FieldsetInterface) { + $linkMarkup = $this->getDelLink(); + } + + foreach ($element->getIterator() as $elementOrFieldset) { + if ($elementOrFieldset instanceof FieldsetInterface) { + $elementOrFieldset->setAttribute('data-index', $index); + $markup .= $fieldsetHelper($elementOrFieldset, $this->shouldWrap()); + $index++; + } elseif ($elementOrFieldset instanceof ElementInterface) { + $markup .= $elementHelper($elementOrFieldset); + } + } + + // Every collection is wrapped by a fieldset if needed + if ($this->shouldWrap) { + $attributes = $element->getAttributes(); + unset($attributes['name']); + $attributesString = $attributes ? ' ' . $this->createAttributesString($attributes) : ''; + + $label = $element->getLabel(); + $legend = ''; + + if (!empty($label)) { + if (null !== ($translator = $this->getTranslator())) { + $label = $translator->translate( + $label, + $this->getTranslatorTextDomain() + ); + } + + if (!$element instanceof LabelAwareInterface || !$element->getLabelOption('disable_html_escape')) { + $escapeHtmlHelper = $this->getEscapeHtmlHelper(); + $label = $escapeHtmlHelper($label); + } + + $legend = sprintf( + $this->labelWrapper, + $label + ); + } + + $markup = sprintf( + $this->wrapper, + $markup, + $legend, + $templateMarkup, + $attributesString, + $linkMarkup, + $css, + $javascript + ); + } else { + $markup .= $templateMarkup . $linkMarkup; + } + + return $markup; + } + + protected function getDelLink() + { + return '<div class="collection-delete-link">' . + '<a class="btn btn-link" title="Supprimer cet élément de la collection">' . + '<i class="fas fa-times" aria-hidden="true"></i>' . + '</a>' . + '</div>'; + } + + /** + * Get add link + * + * @return string + */ + protected function getAddLink() + { + return '<div class="collection-add-link">' . + '<a class="btn btn-link" title="Ajouter un élément à la collection">' . + '<i class="fas fa-plus" aria-hidden="true"></i> Ajouter' . + '</a>' . + '</div>'; + } + + /** + * @param Collection $container + * @return string + */ + protected function getCss($container) + { + $fieldsetId = $container->getAttribute('id'); + + $css = <<<EOT +<style> + #{$fieldsetId} { + margin-bottom: 15px; + } + + #{$fieldsetId} div.input-group { + position: relative; + float: left; + margin: 0 10px 0 0; + } + + #{$fieldsetId} div.collection-delete-link { + position: relative; + float: right; + } + + #{$fieldsetId} div.collection-delete-link > .btn { + padding: 6px 0; + } + + #{$fieldsetId} div.collection-add-link { + margin-bottom: 15px; + } +</style> +EOT; + return $css; + } + + /** + * @param Collection $container + * @return string + */ + protected function getJavascript($container) + { + $maxEnabled = ($container->getMaxElements()) ? 'true' : 'false'; + $minEnabled = ($container->getMinElements()) ? 'true' : 'false'; + + $javascript = <<<EOT +$('#{$container->getAttribute('id')}').on('click', '.collection-add-link > a', function() { + var container = $('#{$container->getAttribute('id')}'); + var currentCount = $(container).children('fieldset').length; + var lastFieldset = $(container).children('fieldset').last(); + var index = currentCount === 0 ? currentCount : parseInt(lastFieldset.attr('data-index')) + 1; + + var template = $(container).children('span').data('template'); + template = template.replace(/__index__/g, index); + + if(currentCount === 0) { + $(container).prepend(template); + } + else { + lastFieldset.after(template); + } + + if({$maxEnabled}) { + if(currentCount+1 >= {$container->getMaxElements()}) { + $('.collection-add-link').remove(); + } + } +}); + +$('#{$container->getAttribute('id')}').on('click', '.collection-delete-link' , function() { + var container = $('#{$container->getAttribute('id')}'); + var item = $(this).closest('fieldset'); + var curPos = item.index(); + var curInd = item.attr('data-index'); + + // suppression du fieldset lié à l'élément + item.remove(); + + var currentCount = $(container).children('fieldset').length; + if(currentCount > 0) { + var index = (curInd < {$this->countElements}) ? {$this->countElements} : curInd; + $(container).children('fieldset').each(function(i, el) { + if(i >= curPos) { + $(el) + .attr('data-index', '' + index) + .find('select,input').attr('name', function () { + return this.name.replace(/\[\d+\](\[.*\])$/, '['+index+']$1'); + }); + index++; + } + }); + } + + if({$maxEnabled}) { + if(currentCount+1 == {$container->getMaxElements()}) { + $(container).append('{$this->getAddLink()}'); + } + } +}); + +$().ready(function updateButtons() +{ + var container = $('#{$container->getAttribute('id')}'); + var buttons = $(container).find('.collection-delete-link'); + var currentCount = $(container).children('fieldset').length; + + if({$maxEnabled}) { + if(currentCount >= {$container->getMaxElements()}) { + $('.collection-add-link').remove(); + } + } + + if($minEnabled) { + buttons.each(function(i, el) { + if(i<={$container->getMinElements()}-1) { + $(el).remove(); + } + }); + } +}); + + +EOT; + + foreach($container->getAutocomplete() as $name => $options) { + $javascript .= <<<EOT +$('#{$container->getAttribute('id')}').on('keyup', '.{$container->getAttribute('id')}-autocomplete', function() { + $(this).autocompleteOctopus({ + source: '{$options['source']}', + minLength: {$options['min-length']}, + delay: {$options['delay']} + }); +}); + + +EOT; + + } + + return '<script type="text/javascript">' . $javascript . '</script>'; + } +}