"This is the moment of truth," announces Ray Arnold, chief systems engineer of Jurassic Park, pointing at the dark, disabled screen of the central control panel. "Our main monitoring system failed during the last storm. You need to rebuild the interactive control panel from scratch, using everything you've learned about events and interactions in JavaScript."
In this project, you'll create a complete, interactive Jurassic Park control panel. The panel will handle clicking, dragging, keyboard events, forms, event delegation, propagation, throttling, custom events, and proper memory management. This is a real test of all skills from this module - from basic event listeners to advanced architectural patterns.
You'll build a control panel with the following features:
Instead of adding an event listener to each sector individually, use event delegation - one listener on the map container handles clicks in all sectors. Also remember to control propagation so that clicking a dinosaur doesn't simultaneously activate the sector.
1// HTML structure of the park map
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 // One listener handles all clicks on the map
21 this.mapElement.addEventListener('click', (event) => {
22 const dinoElement = event.target.closest('.dino');
23
24 if (dinoElement) {
25 // Clicked on a dinosaur - stop propagation to sector
26 event.stopPropagation();
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 // Deselect previous sector
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(`Sector ${sectorId} selected [Danger: ${danger}]`);
51
52 // Emit custom event with selected sector info
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(`Selected dinosaur: ${dinoName} (ID: ${dinoId})`);
65
66 // Custom event with dinosaur data
67 const dinoEvent = new CustomEvent('dino:selected', {
68 detail: { dinoId, dinoName },
69 bubbles: true
70 });
71 dinoEl.dispatchEvent(dinoEvent);
72 }
73
74 // Dynamically adding new sectors - thanks to delegation
75 // no need to add new listeners
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 = `Sector ${sectorId}`;
82 this.mapElement.appendChild(sector);
83 // Event listener already works automatically!
84 }
85}Create a system that responds to keyboard shortcuts for quickly launching emergency procedures. The system must handle key combinations, distinguish context (is the user typing in a form or controlling the panel), and respond to different types of mouse events.
1class AlarmSystem {
2 constructor() {
3 this.alarmLevel = 0; // 0-4 (0 = none, 4 = critical)
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 to Ctrl+4: Alarm levels
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: Disable alarm
22 this.keyHandlers.set('escape', () => this.setAlarmLevel(0));
23
24 // Ctrl+Shift+E: Evacuation
25 this.keyHandlers.set('ctrl+shift+e', () => this.triggerEvacuation());
26
27 // Ctrl+Shift+L: Lock all enclosures
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 // Ignore shortcuts when user is typing in a form
38 if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
39 return;
40 }
41
42 this.pressedKeys.add(event.key.toLowerCase());
43
44 // Build key combination
45 const combo = this.buildKeyCombo(event);
46
47 const handler = this.keyHandlers.get(combo);
48 if (handler) {
49 event.preventDefault(); // Prevent default browser action
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(`⚠️ Alarm level: ${previousLevel} → ${level}`);
77
78 // Emit custom event about alarm level change
79 document.dispatchEvent(new CustomEvent('alarm:levelChanged', {
80 detail: { previousLevel, currentLevel: level }
81 }));
82 }
83
84 triggerEvacuation() {
85 console.log('🚨 EVACUATION TRIGGERED!');
86 this.setAlarmLevel(4);
87 document.dispatchEvent(new CustomEvent('alarm:evacuation'));
88 }
89
90 lockAllEnclosures() {
91 console.log('🔒 Locking all enclosures...');
92 document.dispatchEvent(new CustomEvent('alarm:lockdown'));
93 }
94
95 // Memory cleanup - CRITICAL
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: Listeners removed, memory cleaned');
103 }
104}Build a form handling
submit, input, change, focus, and blur events. The form should validate data in real time and prevent submission of invalid data.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 // Validate "description" field on every typed character
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', 'Description must be at least 10 characters');
18 } else if (value.length > 500) {
19 this.setError('description', 'Description cannot exceed 500 characters');
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 // Validate "sector" select on change
28 const sectorSelect = this.form.querySelector('#sector');
29 const sectorHandler = (event) => {
30 if (!event.target.value) {
31 this.setError('sector', 'Select a sector');
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 // Focus/blur visual effects
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 // Validate field after leaving
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(); // Prevent default form submission
61
62 if (this.errors.size > 0) {
63 console.log('Form contains errors:', [...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 reported:', incident);
72
73 // Custom event with incident data
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 // Cleanup of ALL listeners
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: All listeners removed');
115 }
116}Create a dinosaur activity monitoring system that uses throttling to limit update frequency and Event Bus for communication between all panel modules.
1// Event Bus - central communication system
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 // Return a function to unsubscribe
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(`Error in listener "${event}":`, error);
35 }
36 });
37 }
38 }
39
40 // Clear all subscriptions
41 clear() {
42 this.listeners.clear();
43 console.log('EventBus: All subscriptions cleared');
44 }
45}
46
47// Throttle utility
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 // Method for cleanup
73 throttled.cancel = () => {
74 if (timeoutId) {
75 clearTimeout(timeoutId);
76 timeoutId = null;
77 }
78 };
79
80 return throttled;
81}
82
83// Activity monitor with 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 // Throttled mouse position updates on map
96 const updatePosition = throttle((data) => {
97 this.display.querySelector('.position').textContent =
98 `Cursor: (${data.x}, ${data.y})`;
99 }, 100); // Maximum 10 updates per second
100 this.throttledFunctions.push(updatePosition);
101
102 // Listen for events from EventBus
103 const unsub1 = this.eventBus.on('dino:moved', (data) => {
104 this.logActivity(`Dinosaur ${data.name} moved to (${data.x}, ${data.y})`);
105 });
106
107 const unsub2 = this.eventBus.on('alarm:levelChanged', (data) => {
108 this.logActivity(`Alarm: level ${data.previousLevel} → ${data.currentLevel}`);
109 });
110
111 const unsub3 = this.eventBus.on('incident:reported', (data) => {
112 this.logActivity(`New incident in sector ${data.sector}: ${data.description}`);
113 });
114
115 this.unsubscribers.push(unsub1, unsub2, unsub3);
116
117 // Mousemove on map with 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 // Limit the number of log entries
143 const entries = this.display.querySelectorAll('.log-entry');
144 if (entries.length > 50) {
145 entries[entries.length - 1].remove();
146 }
147 }
148
149 // Cleanup EVERYTHING
150 destroy() {
151 // Unsubscribe from EventBus
152 this.unsubscribers.forEach(unsub => unsub());
153 this.unsubscribers = [];
154
155 // Cancel throttled functions
156 this.throttledFunctions.forEach(fn => fn.cancel());
157 this.throttledFunctions = [];
158
159 console.log('ActivityMonitor: Full cleanup completed');
160 }
161}Start by creating a central
EventBus that will connect all modules. Then build individual components in order:1// 1. Initialize Event Bus
2const eventBus = new EventBus();
3
4// 2. Park map with delegation
5const parkMap = new ParkMap(document.getElementById('park-map'));
6
7// 3. Alarm system
8const alarmSystem = new AlarmSystem();
9
10// 4. Incident form
11const incidentForm = new IncidentForm(document.getElementById('incident-form'));
12
13// 5. Activity monitor
14const activityMonitor = new ActivityMonitor(
15 eventBus,
16 document.getElementById('activity-panel')
17);
18
19// 6. Connect modules via EventBus
20// Listen for custom DOM events and redirect to 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. Cleanup on close (e.g., when changing views in SPA)
34function destroyPanel() {
35 parkMap.destroy?.();
36 alarmSystem.destroy();
37 incidentForm.destroy();
38 activityMonitor.destroy();
39 eventBus.clear();
40 console.log('Control panel closed - all resources released');
41}Remember the key principles:
event.stopPropagation() when a click shouldn't "bubble" higherevent.preventDefault() for forms and keyboard shortcutsdestroy() method in every module for memory cleanup"Jurassic Park systems don't forgive mistakes," says Ray Arnold, lighting another cigarette. "A memory leak in the enclosure monitoring means the system will crash after a few hours. An unremoved event listener can block the garbage collector and consume more and more RAM. And poorly handled event propagation can cause a click on the evacuation button to open the T-Rex enclosure gate instead of closing it."
"Build this panel as if people's lives in the park depend on it. Because in a sense - they do."
Open the editor and create your interactive control panel!