Intégration Drupal
Intégration
Intégration Drupal / Twig
Guide complet pour importer et rendre les formations VibeLearning dans Drupal avec Twig.
Vue d'ensemble
1
Import JSONRécupérer le JSON de formation via API ou fichier et le stocker dans Drupal.
2
Templates TwigCréer les templates de rendu pour chaque type de bloc (47 templates).
3
JavaScriptAjouter l'interactivité (quiz, accordéons, drag & drop).
Structure de fichiers recommandée
themes/custom/your_theme/
├── templates/
│ └── formation/
│ ├── formation.html.twig # Container principal
│ ├── chapitre.html.twig # Rendu d'un chapitre
│ ├── lesson.html.twig # Rendu d'une leçon
│ └── blocks/
│ ├── block--paragraph.html.twig
│ ├── block--heading.html.twig
│ ├── block--image.html.twig
│ ├── block--video.html.twig
│ ├── block--qcm.html.twig
│ ├── block--flashcard.html.twig
│ └── ... (47 templates)
├── css/
│ └── formation/
│ ├── base.css
│ ├── blocks.css
│ └── quiz.css
└── js/
└── formation/
├── quiz.js
├── interactive.js
└── progress.jsTemplate principal
formation.html.twig
Template racine qui orchestre le rendu complet
{# formation.html.twig #}
{% set formation = formation_json %}
<article class="formation" data-formation-id="{{ formation.source_path }}">
<header class="formation__header">
{% if formation.image_url %}
<img src="{{ formation.image_url }}" alt="{{ formation.titre }}" class="formation__cover">
{% endif %}
<h1 class="formation__title">{{ formation.titre }}</h1>
{% if formation.description %}
<div class="formation__description">{{ formation.description|raw }}</div>
{% endif %}
<div class="formation__meta">
<span class="formation__duration">{{ formation.duree_minutes }} min</span>
<span class="formation__lessons">{{ formation.nb_lecons }} leçons</span>
</div>
</header>
<nav class="formation__toc">
<h2>{{ 'Sommaire'|t }}</h2>
<ol>
{% for chapitre in formation.chapitres %}
<li>
<a href="#chapitre-{{ chapitre.id }}">{{ chapitre.titre }}</a>
<ol>
{% for lesson in chapitre.lecons %}
<li><a href="#lesson-{{ lesson.id }}">{{ lesson.titre }}</a></li>
{% endfor %}
</ol>
</li>
{% endfor %}
</ol>
</nav>
<div class="formation__content">
{% for chapitre in formation.chapitres %}
{% include 'formation/chapitre.html.twig' with {chapitre: chapitre} %}
{% endfor %}
</div>
</article>Templates Chapitre & Leçon
chapitre.html.twig
{# chapitre.html.twig #}
<section class="chapitre" id="chapitre-{{ chapitre.id }}">
<header class="chapitre__header">
<span class="chapitre__number">{{ chapitre.ordre }}</span>
<h2 class="chapitre__title">{{ chapitre.titre }}</h2>
{% if chapitre.description %}
<p class="chapitre__description">{{ chapitre.description }}</p>
{% endif %}
</header>
{% if chapitre.objectifs %}
<div class="chapitre__objectifs">
<h3>{{ 'Objectifs'|t }}</h3>
<ul>
{% for obj in chapitre.objectifs %}
<li>{{ obj }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="chapitre__lessons">
{% for lesson in chapitre.lecons %}
{% include 'formation/lesson.html.twig' %}
{% endfor %}
</div>
</section>lesson.html.twig
{# lesson.html.twig #}
<article class="lesson lesson--{{ lesson.type }}"
id="lesson-{{ lesson.id }}"
data-lesson-type="{{ lesson.type }}">
<header class="lesson__header">
<h3 class="lesson__title">{{ lesson.titre }}</h3>
{% if lesson.duree_minutes %}
<span class="lesson__duration">{{ lesson.duree_minutes }} min</span>
{% endif %}
</header>
{% if lesson.objectif %}
<div class="lesson__objectif">
<strong>{{ 'Objectif :'|t }}</strong> {{ lesson.objectif }}
</div>
{% endif %}
<div class="lesson__content">
{% for block in lesson.blocs %}
{% include 'formation/blocks/block--' ~ block.type ~ '.html.twig'
with {block: block}
ignore missing %}
{% endfor %}
</div>
</article>Dispatcher de blocs (Alternative)
block-dispatcher.html.twig
Alternative avec un switch au lieu d'includes dynamiques
{# block-dispatcher.html.twig #}
{% macro render_block(block) %}
{% import _self as self %}
<div class="block block--{{ block.type }}" id="{{ block.id }}"
data-block-type="{{ block.type }}">
{% if block.type == 'paragraph' %}
{% include 'formation/blocks/block--paragraph.html.twig' %}
{% elseif block.type == 'heading' %}
{% include 'formation/blocks/block--heading.html.twig' %}
{% elseif block.type == 'image' %}
{% include 'formation/blocks/block--image.html.twig' %}
{% elseif block.type == 'video' %}
{% include 'formation/blocks/block--video.html.twig' %}
{% elseif block.type in ['qcm', 'qcmMultiple', 'trueFalse'] %}
{% include 'formation/blocks/block--quiz.html.twig' %}
{% elseif block.type == 'flashcard' %}
{% include 'formation/blocks/block--flashcard.html.twig' %}
{% elseif block.type == 'accordion' %}
{% include 'formation/blocks/block--accordion.html.twig' %}
{# ... autres types ... #}
{% else %}
<div class="block--unknown">
{{ 'Type de bloc non reconnu :'|t }} {{ block.type }}
</div>
{% endif %}
</div>
{% endmacro %}JavaScript pour l'interactivité
quiz.js
Gestion des quiz QCM avec validation
// quiz.js
(function(Drupal) {
Drupal.behaviors.formationQuiz = {
attach: function(context, settings) {
// QCM Simple
context.querySelectorAll('.block-qcm').forEach(function(quiz) {
const choices = quiz.querySelectorAll('.block-qcm__choice input');
const explanation = quiz.querySelector('.block-qcm__explanation');
const submitBtn = quiz.querySelector('.block-qcm__submit');
submitBtn?.addEventListener('click', function() {
let correct = false;
choices.forEach(function(choice) {
if (choice.checked) {
correct = choice.dataset.correct === 'true';
choice.closest('.block-qcm__choice').classList.add(
correct ? 'is-correct' : 'is-incorrect'
);
}
});
if (explanation) {
explanation.hidden = false;
}
// Désactiver les choix après validation
choices.forEach(c => c.disabled = true);
submitBtn.disabled = true;
// Envoyer le résultat au serveur
if (typeof Drupal.behaviors.formationProgress !== 'undefined') {
Drupal.behaviors.formationProgress.saveQuizResult(quiz.dataset.questionId, correct);
}
});
});
// Flashcards
context.querySelectorAll('.block-flashcard').forEach(function(card) {
card.addEventListener('click', function() {
this.dataset.flipped = this.dataset.flipped === 'true' ? 'false' : 'true';
});
});
// Accordéons
context.querySelectorAll('.block-accordion__trigger').forEach(function(trigger) {
trigger.addEventListener('click', function() {
const content = this.nextElementSibling;
const isExpanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', !isExpanded);
content.hidden = isExpanded;
});
});
}
};
})(Drupal);CSS de base
blocks.css
Styles de base pour les blocs de formation
/* blocks.css */
/* Base */
.block {
margin-bottom: 1.5rem;
}
/* Paragraph */
.block-paragraph {
line-height: 1.7;
}
/* Heading */
.block-heading--level-1 { font-size: 2rem; }
.block-heading--level-2 { font-size: 1.5rem; }
.block-heading--level-3 { font-size: 1.25rem; }
/* Image */
.block-image {
text-align: center;
}
.block-image img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
}
.block-image figcaption {
margin-top: 0.5rem;
font-size: 0.875rem;
color: var(--color-muted);
}
/* Alerts */
.block-alert {
display: flex;
gap: 1rem;
padding: 1rem;
border-radius: 0.5rem;
border-left: 4px solid;
}
.block-alert--attention {
background: var(--color-warning-bg);
border-color: var(--color-warning);
}
.block-alert--important {
background: var(--color-error-bg);
border-color: var(--color-error);
}
.block-alert--tip {
background: var(--color-success-bg);
border-color: var(--color-success);
}
/* Quiz */
.block-qcm__choice {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
margin-bottom: 0.5rem;
cursor: pointer;
transition: background 0.2s;
}
.block-qcm__choice:hover {
background: var(--color-muted);
}
.block-qcm__choice.is-correct {
background: var(--color-success-bg);
border-color: var(--color-success);
}
.block-qcm__choice.is-incorrect {
background: var(--color-error-bg);
border-color: var(--color-error);
}
/* Flashcard */
.block-flashcard {
perspective: 1000px;
cursor: pointer;
}
.block-flashcard__inner {
position: relative;
transition: transform 0.6s;
transform-style: preserve-3d;
}
.block-flashcard[data-flipped="true"] .block-flashcard__inner {
transform: rotateY(180deg);
}
.block-flashcard__front,
.block-flashcard__back {
backface-visibility: hidden;
padding: 2rem;
border: 2px solid var(--color-primary);
border-radius: 0.75rem;
}
.block-flashcard__back {
position: absolute;
inset: 0;
transform: rotateY(180deg);
background: var(--color-primary-bg);
}Bonnes pratiques
Recommandations
- • Utiliser les attributs
data-*pour le JavaScript - • Préfixer toutes les classes CSS avec
block- - • Rendre les quiz accessibles (ARIA labels)
- • Lazy-load les images et vidéos
- • Sauvegarder la progression en local + serveur
- • Valider le JSON côté serveur avant rendu
Points d'attention
- • Échapper le HTML avec
|e('html_attr') - • Gérer les types de blocs inconnus gracieusement
- • Tester les quiz avec différents navigateurs
- • Prévoir le mode hors-ligne pour les formations
- • Limiter la taille des médias uploadés