VLC

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 JSON
Récupérer le JSON de formation via API ou fichier et le stocker dans Drupal.
2
Templates Twig
Créer les templates de rendu pour chaque type de bloc (47 templates).
3
JavaScript
Ajouter 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.js

Template 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

Ressources

    Vibe Learning Club