Je bosse sur plusieurs modules assez conséquents depuis un bon bout de temps. Entre autre chose, un module qui permet de créer des pages avec différents types de contenus, que j'avais nommé à l'époque "single-page". L'idée était de faire des sites simples et d'avoir un outil adapté pour ça. Au fil des évolutions, l'outil s'est étoffé de pas mal de fonctionnalités et il commence à être mature.
Le principe est le suivant :
- Une entité "Single page" est créée. Cette entité est assez basique... On lui attribue un titre, 2-3 configurations, et c'est tout.
- Une fois créée, on a accès à un onglet "Éléments" qui permet d'ajouter de nouvelles sections (entité "single-page-item").
- Pour chaque section créée, on peut ajouter diverses infos : texte d'en-tête et de pied de page, choix du type d'affichage (normal, accordéon, swiper, etc...), choix du layout d'affichage (basé sur le système de colonnes bootstrap), et surtout, un champ "paragraphes" qui permet d'embarquer des paragraphes de différents types. C'est du custom, ce n'est pas géré avec le module de contribution "paragraphs" de la communauté Drupal.
Bref... En gros, on crée une page, puis on y ajoute des sections, et chaque section peut contenir un ou plusieurs éléments qui seront affichés selon les options choisies par l'utilisateur.
Donc ça nous fait une belle page avec plein de sections. Et là, je voulais faire en sorte que, plutôt que de passer par l'onglet "Éléments" puis devoir cliquer sur "Modifier" dans la liste des sections, l'utilisateur puisse y accéder directement via un menu contextuel. Bref... Rien de sorcier, mais c'est toujours bon de le noter.
On a besoin de quelques éléments pour ça :
Routage
Pour permettre de rediriger l'utilisateur vers une page ou un formulaire spécifique, on doit tout d'abord avoir les routes adéquates. Dans mon cas, j'ai créé 2 routes spécifiques. La première pour aller à l'édition d'un SPI (single page item), la seconde pour accéder à la page de suppression.
entity.single_page_item.edit_form:
path: '/single_page_item/{single_page_item}/edit'
defaults:
_entity_form: 'single_page_item.edit'
_title: 'Edit single page item'
options:
_admin_route: TRUE
parameters:
single_page_item:
type: entity:single_page_item
requirements:
_entity_access: 'single_page_item.update'
entity.single_page_item.delete_form:
path: '/single_page_item/{single_page_item}/delete'
defaults:
_entity_form: 'single_page_item.delete'
_title: 'Delete single page item'
options:
_admin_route: TRUE
parameters:
single_page_item:
type: entity:single_page_item
requirements:
_permission: 'single_page_item.update'Je passe par dessus toute la config, ce billet n'est pas là pour expliquer le système de routage de Drupal / Symfony. Mais en gros, j'ai mes 2 routes avec leur chemin, correspondant également à ce qui est dans l'annotation de l'entité. Dans mon cas, il y a une petite spécificité mais qu'on ne voit pas ici. Je passe dans ces routes l'identifiant du SPI (au niveau parameters) mais en plus, je dois également passer l'identifiant du parent, à savoir l'entité "single_page". Mais c'est vraiment spécifique à mon système, et ce n'est pas ici que ça se passe.
Fichier {module}.links.contextual.yml
# single_page.links.contextual.yml
single_page_item.edit:
title: 'Edit element'
route_name: entity.single_page_item.edit_form
group: 'single_page_item'
single_page_item.remove:
title: 'Remove element'
route_name: entity.single_page_item.delete_form
group: 'single_page_item'Pour proposer des menus contextuels, on doit ensuite les déclarer à l'aide d'un fichier {module}.links.contextual.yml. On crée les entrées nécessaires (dans mon cas, je les ai nommés single_page_item.edit et single_page_item.remove)
J'ai indiqué les routes que j'ai créées auparavant. Ce qui fait que chaque élément de mon menu contextuel va rediriger vers la route correspondante, à savoir le formulaire d'édition, respectivement de suppression.
Et là, on voit un "group". C'est en réalité d'une importance capitale. Car ce "group" va être utilisé pour construire les liens contextuels. Je vais illustrer ça avec un petit exemple. Admettons qu'on veuille ajouter un menu contextuel pour les SPI (single_page_item) mais également un menu contextuel pour les SP (single_page). Il faudrait alors créer les entrées pour chaque lien de menu, et indiquer des groupes différents, par exemple :
- groupe "single_page_item" correspond aux entrées de menu contextuel pour les SPI
- groupe "single_page" correspond aux entrées de menu contextuel pour les SP
Vous allez comprendre avec la suite...
Ajout des menus contextuels
C'est maintenant que ça va se jouer. Pour mon entité SPI, j'ai une classe SinglePageItemViewBuilder qui me permet d'implémenter son rendu final. Dans cette classe, je vais surdéfinir la méthode viewMultiple(...) pour faire en sorte de rajouter la partie qui concerne les menus contextuels :
public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) {
$build = parent::viewMultiple($entities, $view_mode, $langcode);
foreach ($entities as $delta => $entity) {
if (!isset($build[$delta])) {
continue;
}
$build[$delta]['#contextual_links'] = [
'single_page_item' => [
'route_parameters' => $entity->toUrl('edit-form')->getRouteParameters(),
],
];
}
return $build;
}L'idée, c'est que pour chaque entité dans le render array "$build", je vais lui rajouter une clé "#contextual_links" pour laquelle je vais indiquer le groupe (c'est ici que le groupe est utilisé), et je vais lui indiquer les paramètres de route adéquats.
Donc premièrement, l'iimportance du "group" dans le fichier {module}.links.contextual.yml... En réalité, lorsqu'on désire ajouter des menus contextuels, on n'ajoute pas tous les menus via le viewBuilder. On déclare les entrées de menu dans le fichier YAML, puis on attache le groupe complet. Bien entendu, ce groupe sera conditionné aux permissions d'accès. Il est donc possible qu'un utilisateur avec des permissions restreintes ne voit pas l'ensemble des liens. Mais l'important à comprendre réside dans l'utilisation du "group" pour dire : embarque dans cette entité tous les liens contextuels du groupe "xyz".
Si je devais maintenant ajouter un menu contextuel à mes entités "single_page", j'aurais simplement à faire de même en spécifiant le bon groupe.
Enfin, pour récupérer les paramètres de route et ne pas trop m'embêter, j'ai directement fait appel à la méthode toUrl() en lui passant le formulaire, puis en appelant sa méthode getRouteParameters(), ce qui me permet d'être certain que les paramètres seront toujours ok, même si ils devaient évoluer dans le futur.
Twig
Pour que les menus contextuels s'affichent, il est impératif que l'entité embarque {{ title_prefix }} et les attributs du div parent :
<div{{attributes}}>
{{ title_prefix }}Personnalisation
Pour aller plus loin, il est encore possible d'attribuer une classe spécifique pour customiser les entrées du menu contextuel :
# single_page.links.contextual.yml
single_page_item.edit:
title: 'Edit element'
route_name: entity.single_page_item.edit_form
group: 'single_page_item'
class: '\Drupal\single_page\Plugin\Menu\ContextualLink\SinglePageItemContextualLink'Cette classe devra étendre la classe \Drupal\Core\Menu\ContextualLinkDefault et permettra des modifications mineures sur le lien et ses propriétés.
Conclusion
Il est toujours bon de rappeler les petites subtilités de Drupal. Les menus contextuels n'ont l'air de rien mais sont d'une grande aide dans l'expérience utilisateur, ça serait dommage de ne pas les exploiter à leur plein potentiel. Ce petit billet permet de se rendre compte qu'il ne suffit pas de grand chose pour faciliter la vie des utilisateurs.