Cours JavaScript et TypeScript · Module 8: Événements et DOM
Projet final : panneau de contrôle interactif de Jurassic Park
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, unreturnsuffit 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 ?