Cours JavaScript et TypeScript · Module 8: Événements et DOM

Projet final : panneau de contrôle interactif de Jurassic Park

11 min de lecture
Dans cette leçon4

« C'est le moment de vérité », annonce Ray Arnold, ingénieur système en chef de Jurassic Park, en pointant l'écran sombre et désactivé du panneau de contrôle central. « Notre système de surveillance principal est tombé en panne lors de la dernière tempête. Tu dois reconstruire le panneau de contrôle interactif à partir de zéro, en utilisant tout ce que tu as appris sur les événements et les interactions en JavaScript. »

Dans ce projet, tu vas créer un panneau de contrôle complet et interactif de Jurassic Park. Le panneau gérera les clics, le glisser-déposer, les événements clavier, les formulaires, la délégation d'événements, la propagation, le throttling, les événements personnalisés et une bonne gestion de la mémoire. C'est un véritable test de toutes les compétences de ce module - des écouteurs d'événements de base aux patterns architecturaux avancés.

Objectif du projet

Tu vas construire un panneau de contrôle avec les fonctionnalités suivantes :

  • Carte du parc avec des secteurs cliquables (délégation d'événements)
  • Système d'alarme réagissant aux raccourcis clavier
  • Formulaire d'enregistrement d'incidents avec validation
  • Moniteur d'activité des dinosaures avec throttling
  • Event Bus pour la communication inter-modules
  • Nettoyage correct de la mémoire à la fermeture des panneaux

Exigences fonctionnelles

1. Carte du parc avec délégation et propagation d'événements

Au lieu d'ajouter un écouteur d'événement à chaque secteur individuellement, utilise la délégation d'événements - un seul écouteur sur le conteneur de la carte gère les clics dans tous les secteurs. Souviens-toi aussi qu'un clic sur un dinosaure bouillonne à travers le secteur jusqu'à la carte : dans l'écouteur délégué, vérifie d'abord le dinosaure et termine par return pour ne pas activer aussi le secteur.

1// Structure HTML de la carte du parc
2// <div id="park-map">
3//   <div class="sector" data-sector="A" data-danger="high">
4//     <span class="dino" data-dino-id="1">T-Rex</span>
5//     <span class="dino" data-dino-id="2">Velociraptor</span>
6//   </div>
7//   <div class="sector" data-sector="B" data-danger="low">
8//     <span class="dino" data-dino-id="3">Triceratops</span>
9//   </div>
10// </div>
11
12class ParkMap {
13  constructor(mapElement) {
14    this.mapElement = mapElement;
15    this.selectedSector = null;
16    this.setupDelegation();
17  }
18
19  setupDelegation() {
20    // Un seul écouteur gère tous les clics sur la carte
21    this.mapElement.addEventListener('click', (event) => {
22      const dinoElement = event.target.closest('.dino');
23
24      if (dinoElement) {
25        // Clic sur un dinosaure - on le traite et on s'arrête pour ne pas sélectionner le secteur
26        // (stopPropagation() n'y changerait rien : l'événement est déjà passé par le secteur)
27        this.handleDinoClick(dinoElement);
28        return;
29      }
30
31      const sectorElement = event.target.closest('.sector');
32      if (sectorElement) {
33        this.handleSectorClick(sectorElement);
34      }
35    });
36  }
37
38  handleSectorClick(sectorEl) {
39    // Désélectionner le secteur précédent
40    if (this.selectedSector) {
41      this.selectedSector.classList.remove('selected');
42    }
43
44    sectorEl.classList.add('selected');
45    this.selectedSector = sectorEl;
46
47    const sectorId = sectorEl.dataset.sector;
48    const danger = sectorEl.dataset.danger;
49
50    console.log(`Secteur ${sectorId} sélectionné [Danger : ${danger}]`);
51
52    // Émettre un custom event avec les infos du secteur sélectionné
53    const sectorEvent = new CustomEvent('sector:selected', {
54      detail: { sectorId, danger },
55      bubbles: true
56    });
57    sectorEl.dispatchEvent(sectorEvent);
58  }
59
60  handleDinoClick(dinoEl) {
61    const dinoId = dinoEl.dataset.dinoId;
62    const dinoName = dinoEl.textContent;
63
64    console.log(`Dinosaure sélectionné : ${dinoName} (ID : ${dinoId})`);
65
66    // Custom event avec les données du dinosaure
67    const dinoEvent = new CustomEvent('dino:selected', {
68      detail: { dinoId, dinoName },
69      bubbles: true
70    });
71    dinoEl.dispatchEvent(dinoEvent);
72  }
73
74  // Ajouter dynamiquement de nouveaux secteurs - grâce à la délégation,
75  // pas besoin d'ajouter de nouveaux écouteurs
76  addSector(sectorId, danger) {
77    const sector = document.createElement('div');
78    sector.className = 'sector';
79    sector.dataset.sector = sectorId;
80    sector.dataset.danger = danger;
81    sector.textContent = `Secteur ${sectorId}`;
82    this.mapElement.appendChild(sector);
83    // L'écouteur d'événement fonctionne déjà automatiquement !
84  }
85}

2. Système d'alarme avec événements clavier et souris

Crée un système qui réagit aux raccourcis clavier pour lancer rapidement des procédures d'urgence. Le système doit gérer les combinaisons de touches, distinguer le contexte (l'utilisateur est-il en train de taper dans un formulaire ou de piloter le panneau), et réagir à différents types d'événements souris.

1class AlarmSystem {
2  constructor() {
3    this.alarmLevel = 0; // 0-4 (0 = aucun, 4 = critique)
4    this.isActive = true;
5    this.keyHandlers = new Map();
6    this.boundHandleKeyDown = this.handleKeyDown.bind(this);
7    this.boundHandleKeyUp = this.handleKeyUp.bind(this);
8    this.pressedKeys = new Set();
9
10    this.registerShortcuts();
11    this.attachListeners();
12  }
13
14  registerShortcuts() {
15    // Ctrl+1 à Ctrl+4 : Niveaux d'alarme
16    this.keyHandlers.set('ctrl+1', () => this.setAlarmLevel(1));
17    this.keyHandlers.set('ctrl+2', () => this.setAlarmLevel(2));
18    this.keyHandlers.set('ctrl+3', () => this.setAlarmLevel(3));
19    this.keyHandlers.set('ctrl+4', () => this.setAlarmLevel(4));
20
21    // Escape : Désactiver l'alarme
22    this.keyHandlers.set('escape', () => this.setAlarmLevel(0));
23
24    // Ctrl+Shift+E : Évacuation
25    this.keyHandlers.set('ctrl+shift+e', () => this.triggerEvacuation());
26
27    // Ctrl+Shift+L : Verrouiller tous les enclos
28    this.keyHandlers.set('ctrl+shift+l', () => this.lockAllEnclosures());
29  }
30
31  attachListeners() {
32    document.addEventListener('keydown', this.boundHandleKeyDown);
33    document.addEventListener('keyup', this.boundHandleKeyUp);
34  }
35
36  handleKeyDown(event) {
37    // Ignorer les raccourcis quand l'utilisateur tape dans un formulaire
38    if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
39      return;
40    }
41
42    this.pressedKeys.add(event.key.toLowerCase());
43
44    // Construire la combinaison de touches
45    const combo = this.buildKeyCombo(event);
46
47    const handler = this.keyHandlers.get(combo);
48    if (handler) {
49      event.preventDefault(); // Empêcher l'action par défaut du navigateur
50      handler();
51    }
52  }
53
54  handleKeyUp(event) {
55    this.pressedKeys.delete(event.key.toLowerCase());
56  }
57
58  buildKeyCombo(event) {
59    const parts = [];
60    if (event.ctrlKey || event.metaKey) parts.push('ctrl');
61    if (event.shiftKey) parts.push('shift');
62    if (event.altKey) parts.push('alt');
63
64    const key = event.key.toLowerCase();
65    if (!['control', 'shift', 'alt', 'meta'].includes(key)) {
66      parts.push(key);
67    }
68
69    return parts.join('+');
70  }
71
72  setAlarmLevel(level) {
73    const previousLevel = this.alarmLevel;
74    this.alarmLevel = level;
75
76    console.log(`Niveau d'alarme : ${previousLevel} → ${level}`);
77
78    // Émettre un custom event sur le changement de niveau d'alarme
79    document.dispatchEvent(new CustomEvent('alarm:levelChanged', {
80      detail: { previousLevel, currentLevel: level }
81    }));
82  }
83
84  triggerEvacuation() {
85    console.log('ÉVACUATION DÉCLENCHÉE !');
86    this.setAlarmLevel(4);
87    document.dispatchEvent(new CustomEvent('alarm:evacuation'));
88  }
89
90  lockAllEnclosures() {
91    console.log('Verrouillage de tous les enclos...');
92    document.dispatchEvent(new CustomEvent('alarm:lockdown'));
93  }
94
95  // Nettoyage de la mémoire - CRITIQUE
96  destroy() {
97    document.removeEventListener('keydown', this.boundHandleKeyDown);
98    document.removeEventListener('keyup', this.boundHandleKeyUp);
99    this.keyHandlers.clear();
100    this.pressedKeys.clear();
101    this.isActive = false;
102    console.log('AlarmSystem : écouteurs supprimés, mémoire nettoyée');
103  }
104}

3. Formulaire d'enregistrement d'incidents avec validation

Construis un formulaire qui gère les événements submit, input, change, focus et blur. Le formulaire doit valider les données en temps réel et empêcher l'envoi de données invalides.

1class IncidentForm {
2  constructor(formElement) {
3    this.form = formElement;
4    this.errors = new Map();
5    this.boundHandlers = [];
6
7    this.setupValidation();
8    this.setupSubmit();
9  }
10
11  setupValidation() {
12    // Valider le champ « description » à chaque caractère saisi
13    const descriptionInput = this.form.querySelector('#description');
14    const descHandler = (event) => {
15      const value = event.target.value;
16      if (value.length < 10) {
17        this.setError('description', 'La description doit faire au moins 10 caractères');
18      } else if (value.length > 500) {
19        this.setError('description', 'La description ne peut pas dépasser 500 caractères');
20      } else {
21        this.clearError('description');
22      }
23    };
24    descriptionInput.addEventListener('input', descHandler);
25    this.boundHandlers.push({ element: descriptionInput, type: 'input', handler: descHandler });
26
27    // Valider le select « sector » au changement
28    const sectorSelect = this.form.querySelector('#sector');
29    const sectorHandler = (event) => {
30      if (!event.target.value) {
31        this.setError('sector', 'Sélectionne un secteur');
32      } else {
33        this.clearError('sector');
34      }
35    };
36    sectorSelect.addEventListener('change', sectorHandler);
37    this.boundHandlers.push({ element: sectorSelect, type: 'change', handler: sectorHandler });
38
39    // Effets visuels focus/blur
40    const allInputs = this.form.querySelectorAll('input, select, textarea');
41    allInputs.forEach(input => {
42      const focusHandler = () => input.parentElement.classList.add('focused');
43      const blurHandler = () => {
44        input.parentElement.classList.remove('focused');
45        // Valider le champ après l'avoir quitté
46        input.dispatchEvent(new Event('input'));
47      };
48
49      input.addEventListener('focus', focusHandler);
50      input.addEventListener('blur', blurHandler);
51      this.boundHandlers.push(
52        { element: input, type: 'focus', handler: focusHandler },
53        { element: input, type: 'blur', handler: blurHandler }
54      );
55    });
56  }
57
58  setupSubmit() {
59    const submitHandler = (event) => {
60      event.preventDefault(); // Empêcher l'envoi par défaut du formulaire
61
62      if (this.errors.size > 0) {
63        console.log('Le formulaire contient des erreurs :', [...this.errors.values()]);
64        this.highlightErrors();
65        return;
66      }
67
68      const formData = new FormData(this.form);
69      const incident = Object.fromEntries(formData.entries());
70
71      console.log('Incident signalé :', incident);
72
73      // Custom event avec les données de l'incident
74      document.dispatchEvent(new CustomEvent('incident:reported', {
75        detail: incident
76      }));
77
78      this.form.reset();
79    };
80
81    this.form.addEventListener('submit', submitHandler);
82    this.boundHandlers.push({ element: this.form, type: 'submit', handler: submitHandler });
83  }
84
85  setError(field, message) {
86    this.errors.set(field, message);
87    const errorEl = this.form.querySelector(`#${field}-error`);
88    if (errorEl) errorEl.textContent = message;
89  }
90
91  clearError(field) {
92    this.errors.delete(field);
93    const errorEl = this.form.querySelector(`#${field}-error`);
94    if (errorEl) errorEl.textContent = '';
95  }
96
97  highlightErrors() {
98    this.errors.forEach((message, field) => {
99      const input = this.form.querySelector(`#${field}`);
100      if (input) {
101        input.classList.add('error');
102        setTimeout(() => input.classList.remove('error'), 2000);
103      }
104    });
105  }
106
107  // Nettoyage de TOUS les écouteurs
108  destroy() {
109    this.boundHandlers.forEach(({ element, type, handler }) => {
110      element.removeEventListener(type, handler);
111    });
112    this.boundHandlers = [];
113    this.errors.clear();
114    console.log('IncidentForm : tous les écouteurs supprimés');
115  }
116}

4. Moniteur d'activité avec throttling et Event Bus

Crée un système de surveillance de l'activité des dinosaures qui utilise le throttling pour limiter la fréquence des mises à jour et un Event Bus pour la communication entre tous les modules du panneau.

1// Event Bus - système de communication central
2class EventBus {
3  constructor() {
4    this.listeners = new Map();
5  }
6
7  on(event, callback) {
8    if (!this.listeners.has(event)) {
9      this.listeners.set(event, new Set());
10    }
11    this.listeners.get(event).add(callback);
12
13    // Retourner une fonction pour se désabonner
14    return () => this.off(event, callback);
15  }
16
17  off(event, callback) {
18    const callbacks = this.listeners.get(event);
19    if (callbacks) {
20      callbacks.delete(callback);
21      if (callbacks.size === 0) {
22        this.listeners.delete(event);
23      }
24    }
25  }
26
27  emit(event, data) {
28    const callbacks = this.listeners.get(event);
29    if (callbacks) {
30      callbacks.forEach(cb => {
31        try {
32          cb(data);
33        } catch (error) {
34          console.error(`Erreur dans l'écouteur "${event}" :`, error);
35        }
36      });
37    }
38  }
39
40  // Effacer tous les abonnements
41  clear() {
42    this.listeners.clear();
43    console.log('EventBus : tous les abonnements effacés');
44  }
45}
46
47// Utilitaire throttle
48function throttle(fn, delay) {
49  let lastCall = 0;
50  let timeoutId = null;
51
52  const throttled = function(...args) {
53    const now = Date.now();
54    const remaining = delay - (now - lastCall);
55
56    if (remaining <= 0) {
57      if (timeoutId) {
58        clearTimeout(timeoutId);
59        timeoutId = null;
60      }
61      lastCall = now;
62      fn.apply(this, args);
63    } else if (!timeoutId) {
64      timeoutId = setTimeout(() => {
65        lastCall = Date.now();
66        timeoutId = null;
67        fn.apply(this, args);
68      }, remaining);
69    }
70  };
71
72  // Méthode pour le nettoyage
73  throttled.cancel = () => {
74    if (timeoutId) {
75      clearTimeout(timeoutId);
76      timeoutId = null;
77    }
78  };
79
80  return throttled;
81}
82
83// Moniteur d'activité avec throttling
84class ActivityMonitor {
85  constructor(eventBus, displayElement) {
86    this.eventBus = eventBus;
87    this.display = displayElement;
88    this.unsubscribers = [];
89    this.throttledFunctions = [];
90
91    this.setupMonitoring();
92  }
93
94  setupMonitoring() {
95    // Mises à jour throttlées de la position de la souris sur la carte
96    const updatePosition = throttle((data) => {
97      this.display.querySelector('.position').textContent =
98        `Curseur : (${data.x}, ${data.y})`;
99    }, 100); // Maximum 10 mises à jour par seconde
100    this.throttledFunctions.push(updatePosition);
101
102    // Écouter les événements de l'EventBus
103    const unsub1 = this.eventBus.on('dino:moved', (data) => {
104      this.logActivity(`Le dinosaure ${data.name} s'est déplacé vers (${data.x}, ${data.y})`);
105    });
106
107    const unsub2 = this.eventBus.on('alarm:levelChanged', (data) => {
108      this.logActivity(`Alarme : niveau ${data.previousLevel} → ${data.currentLevel}`);
109    });
110
111    const unsub3 = this.eventBus.on('incident:reported', (data) => {
112      this.logActivity(`Nouvel incident dans le secteur ${data.sector} : ${data.description}`);
113    });
114
115    this.unsubscribers.push(unsub1, unsub2, unsub3);
116
117    // Mousemove sur la carte avec throttling
118    const mapElement = document.getElementById('park-map');
119    if (mapElement) {
120      const mouseMoveHandler = (event) => {
121        const rect = mapElement.getBoundingClientRect();
122        updatePosition({
123          x: Math.round(event.clientX - rect.left),
124          y: Math.round(event.clientY - rect.top)
125        });
126      };
127      mapElement.addEventListener('mousemove', mouseMoveHandler);
128      this.unsubscribers.push(() => {
129        mapElement.removeEventListener('mousemove', mouseMoveHandler);
130      });
131    }
132  }
133
134  logActivity(message) {
135    const entry = document.createElement('div');
136    entry.className = 'log-entry';
137    const time = new Date().toLocaleTimeString();
138    entry.textContent = `[${time}] ${message}`;
139
140    this.display.querySelector('.log').prepend(entry);
141
142    // Limiter le nombre d'entrées du journal
143    const entries = this.display.querySelectorAll('.log-entry');
144    if (entries.length > 50) {
145      entries[entries.length - 1].remove();
146    }
147  }
148
149  // Tout nettoyer
150  destroy() {
151    // Se désabonner de l'EventBus
152    this.unsubscribers.forEach(unsub => unsub());
153    this.unsubscribers = [];
154
155    // Annuler les fonctions throttlées
156    this.throttledFunctions.forEach(fn => fn.cancel());
157    this.throttledFunctions = [];
158
159    console.log('ActivityMonitor : nettoyage complet terminé');
160  }
161}

Conseils d'implémentation

Commence par créer un EventBus central qui reliera tous les modules. Puis construis les composants individuels dans l'ordre :

1// 1. Initialiser l'Event Bus
2const eventBus = new EventBus();
3
4// 2. Carte du parc avec délégation
5const parkMap = new ParkMap(document.getElementById('park-map'));
6
7// 3. Système d'alarme
8const alarmSystem = new AlarmSystem();
9
10// 4. Formulaire d'incidents
11const incidentForm = new IncidentForm(document.getElementById('incident-form'));
12
13// 5. Moniteur d'activité
14const activityMonitor = new ActivityMonitor(
15  eventBus,
16  document.getElementById('activity-panel')
17);
18
19// 6. Connecter les modules via l'EventBus
20// Écouter les custom events DOM et les rediriger vers l'EventBus
21document.addEventListener('sector:selected', (e) => {
22  eventBus.emit('sector:selected', e.detail);
23});
24
25document.addEventListener('alarm:levelChanged', (e) => {
26  eventBus.emit('alarm:levelChanged', e.detail);
27});
28
29document.addEventListener('incident:reported', (e) => {
30  eventBus.emit('incident:reported', e.detail);
31});
32
33// 7. Nettoyage à la fermeture (par ex. lors d'un changement de vue dans une SPA)
34function destroyPanel() {
35  parkMap.destroy?.();
36  alarmSystem.destroy();
37  incidentForm.destroy();
38  activityMonitor.destroy();
39  eventBus.clear();
40  console.log('Panneau de contrôle fermé - toutes les ressources libérées');
41}

Souviens-toi des principes clés :

  • Délégation d'événements au lieu de nombreux écouteurs sur des éléments individuels
  • event.stopPropagation() seulement quand un clic ne doit vraiment pas atteindre les ancêtres (en délégation, un return suffit en général)
  • event.preventDefault() pour les formulaires et les raccourcis clavier
  • Throttling pour les événements à haute fréquence (mousemove, scroll)
  • Stocker les références des gestionnaires pour pouvoir les supprimer plus tard
  • Méthode destroy() dans chaque module pour le nettoyage de la mémoire

Bonne chance !

« Les systèmes de Jurassic Park ne pardonnent pas les erreurs », dit Ray Arnold en allumant une autre cigarette. « Une fuite mémoire dans la surveillance des enclos signifie que le système plantera au bout de quelques heures. Un écouteur d'événement non supprimé peut bloquer le ramasse-miettes et consommer de plus en plus de RAM. Et une propagation d'événement mal gérée peut faire qu'un clic sur le bouton d'évacuation ouvre le portail de l'enclos du T-Rex au lieu de le fermer. »

« Construis ce panneau comme si la vie des gens du parc en dépendait. Parce qu'en un sens - c'est le cas. »

Ouvre l'éditeur et crée ton panneau de contrôle interactif !

Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <title>Panneau de contrôle - Parc Jurassique</title>
6    <style>
7        * { margin: 0; padding: 0; box-sizing: border-box; }
8        body { font-family: Arial, sans-serif; background: #1a1a2e; color: #eee; padding: 20px; }
9        h1 { text-align: center; color: #e94560; margin-bottom: 20px; }
10        .panel { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; max-width: 900px; margin: 0 auto; }
11        .section { background: rgba(255,255,255,0.05); padding: 20px; border-radius: 10px; border: 1px solid #333; }
12        .section h2 { color: #0f3460; font-size: 16px; margin-bottom: 10px; color: #e94560; }
13        .sector { padding: 10px; margin: 5px 0; border-radius: 6px; cursor: pointer; background: rgba(255,255,255,0.05); border: 1px solid #444; }
14        .sector:hover { background: rgba(233,69,96,0.2); }
15        .sector .dino { display: inline-block; background: #0f3460; padding: 3px 8px; border-radius: 4px; margin: 2px; cursor: pointer; font-size: 12px; }
16        .dino:hover { background: #e94560; }
17        .log { background: #000; padding: 10px; border-radius: 6px; height: 150px; overflow-y: auto; font-family: monospace; font-size: 12px; }
18        .log-entry { padding: 2px 0; border-bottom: 1px solid #222; }
19        input, select, textarea { padding: 8px; border-radius: 4px; border: 1px solid #444; background: #16213e; color: #eee; width: 100%; margin: 5px 0; }
20        button { padding: 8px 16px; border-radius: 4px; border: none; cursor: pointer; background: #e94560; color: white; margin: 5px 2px; }
21        button:hover { background: #c0392b; }
22        .alert { padding: 8px; margin: 5px 0; border-radius: 4px; font-size: 13px; }
23        .alert-high { background: rgba(231,76,60,0.3); border-left: 3px solid #e74c3c; }
24        .alert-low { background: rgba(46,204,113,0.3); border-left: 3px solid #2ecc71; }
25    </style>
26</head>
27<body>
28    <h1>Panneau de contrôle du Parc Jurassique</h1>
29    <div class="panel">
30        <!-- SECTION 1 : Carte du parc avec délégation d'événements -->
31        <div class="section">
32            <h2>Carte du parc</h2>
33            <div id="park-map">
34                <!-- TODO: Ajoute un event listener sur #park-map (délégation) -->
35                <!-- Clic sur .sector -> affiche les infos du secteur -->
36                <!-- Clic sur .dino -> affiche les infos du dinosaure et termine par return pour ne pas sélectionner le secteur -->
37                <div class="sector" data-sector="A" data-danger="high">
38                    Sector A (Highe danger)
39                    <div><span class="dino" data-dino-id="1">T-Rex</span> <span class="dino" data-dino-id="2">Velociraptor</span></div>
40                </div>
41                <div class="sector" data-sector="B" data-danger="low">
42                    Sector B (Lowe danger)
43                    <div><span class="dino" data-dino-id="3">Triceratops</span> <span class="dino" data-dino-id="4">Brachio</span></div>
44                </div>
45                <div class="sector" data-sector="C" data-danger="medium">
46                    Sector C (Moyen danger)
47                    <div><span class="dino" data-dino-id="5">Stegosaurus</span></div>
48                </div>
49            </div>
50        </div>
51
52        <!-- SECTION 2 : Formulaire d'incidents -->
53        <div class="section">
54            <h2>Enregistrement d'un incident</h2>
55            <form id="incident-form">
56                <!-- TODO: Ajoute un event listener sur submit avec preventDefault -->
57                <!-- TODO: Validation des champs avant l'envoi -->
58                <input type="text" id="incident-title" name="title" placeholder="Titre de l'incident" required>
59                <textarea id="description" name="description" placeholder="Description de l'incident (au moins 10 caractères)"></textarea>
60                <div class="error" id="description-error"></div>
61                <select id="sector" name="sector">
62                    <option value="">Choisis un secteur</option>
63                    <option value="A">Secteur A</option>
64                    <option value="B">Secteur B</option>
65                    <option value="C">Secteur C</option>
66                </select>
67                <div class="error" id="sector-error"></div>
68                <select id="incident-severity" name="severity">
69                    <option value="">Priorité</option>
70                    <option value="high">Élevée</option>
71                    <option value="medium">Moyenne</option>
72                    <option value="low">Faible</option>
73                </select>
74                <button type="submit">Signaler l'incident</button>
75            </form>
76            <div id="alerts"></div>
77        </div>
78
79        <!-- SECTION 3 : Journal des événements -->
80        <div class="section" style="grid-column: 1 / -1;">
81            <h2>Journal des événements (raccourcis : Ctrl+1-4 = niveau d'alarme, Échap = alarme coupée, Ctrl+Shift+E = évacuation, Ctrl+Shift+L = verrouillage)</h2>
82            <div class="log" id="event-log"></div>
83        </div>
84
85        <!-- SECTION 4 : Moniteur d'activité (ActivityMonitor de la leçon) -->
86        <div class="section" id="activity-panel" style="grid-column: 1 / -1;">
87            <h2>Moniteur d'activité</h2>
88            <div class="position">Curseur : (0, 0)</div>
89            <div class="log"></div>
90        </div>
91    </div>
92
93    <script>
94        // === PROJET FINAL : Panneau de contrôle interactif ===
95
96        // TODO 1 : DÉLÉGATION D'ÉVÉNEMENTS sur la carte du parc
97        // Ajoute UN SEUL event listener sur #park-map
98        // Utilise event.target.closest() pour identifier l'élément cliqué
99
100        // TODO 2 : FORMULAIRE avec validation
101        // Ajoute un listener sur le submit du formulaire
102        // Utilise preventDefault()
103        // Valide les champs et ajoute une alerte dans #alerts
104
105        // TODO 3 : RACCOURCIS CLAVIER
106        // Ajoute un listener sur keydown
107        // Ctrl+1-4 = niveau d'alarme, Échap = alarme coupée, Ctrl+Shift+E = évacuation, Ctrl+Shift+L = verrouillage
108
109        // TODO 4 : FONCTION DE LOG
110        // Crée une fonction logEvent(message) qui ajoute une entrée dans #event-log
111
112        function logEvent(message) {
113            // TODO: Ajoute une entrée dans le log avec un timestamp
114        }
115
116        // TODO 5 : NETTOYAGE MÉMOIRE
117        // Crée une fonction cleanup() qui supprime tous les listeners
118        // Appelle-la par ex. à la fermeture du panneau
119
120        console.log("Panneau de contrôle prêt pour la configuration !");
121    </script>
122</body>
123</html>

Tu as repéré une erreur dans cette leçon ?

Articles utiles