JavaScript and TypeScript course Β· Module 8: Events and the DOM
Mouse and Keyboard Events
In this lesson5
"In the security systems of Jurassic Park," Ray Arnold shows the large control panel, "we have different types of sensors. Motion detectors react to movement, pressure sensors to touch, sound sensors to noise. Each type requires a different response. In JavaScript, we have a similar situation - different input types require different handling."
Ray points to the monitor and keyboard. "The mouse and keyboard are the two primary sources of user interaction. Each has its own unique events and behaviors."
Mouse Events - The Full Range of Interactions
The mouse generates more than a dozen different event types, from simple clicks to advanced drag gestures. A single click is three signals in a row: mousedown, mouseup, and only then click.
Basic Click Events
The observation area tells a single click from a double click.
1const dinosaurViewingArea = document.getElementById('viewing-area');
2
3// click - single click (most popular)
4dinosaurViewingArea.addEventListener('click', (event) => {
5 console.log('Single click in the viewing area');
6 console.log('Position:', event.clientX, event.clientY);
7
8 selectDinosaurAtPosition(event.clientX, event.clientY);
9});
10
11// dblclick - double click
12dinosaurViewingArea.addEventListener('dblclick', (event) => {
13 console.log('Double click - zooming in');
14
15 // Note: this does NOT block text selection, which already starts on mousedown
16 event.preventDefault();
17
18 zoomInOnDinosaur(event.clientX, event.clientY);
19});The clientX and clientY fields give the cursor position in the window. Note: preventDefault() in dblclick won't block text selection, because selection already starts on mousedown. That's what the CSS user-select: none rule is for.
1// auxclick - middle/other button click (not left)
2dinosaurViewingArea.addEventListener('auxclick', (event) => {
3 if (event.button === 1) {
4 console.log('Middle button - opening in new window');
5 openInNewWindow(event.target);
6 }
7});
8
9// contextmenu - right mouse button (context menu)
10dinosaurViewingArea.addEventListener('contextmenu', (event) => {
11 event.preventDefault(); // Block default menu
12
13 console.log('Right button - custom context menu');
14 showCustomContextMenu(event.clientX, event.clientY);
15});
16
17function selectDinosaurAtPosition(x, y) {
18 console.log(`Checking if a dinosaur is at position (${x}, ${y})`);
19 // Selection logic
20}
21
22function zoomInOnDinosaur(x, y) {
23 console.log(`Zooming in on position (${x}, ${y})`);
24 // Zoom logic
25}
26
27function showCustomContextMenu(x, y) {
28 const menu = document.getElementById('custom-menu');
29 menu.style.left = x + 'px';
30 menu.style.top = y + 'px';
31 menu.style.display = 'block';
32
33 console.log('Context menu displayed');
34}An event.button value of 1 means the middle button, and preventDefault() on contextmenu hides the browser menu, making room for your own.
Mouse Button Events
Between pressing and releasing the button, the drag is in progress, so we remember the starting point.
1const controlPanel = document.getElementById('control-panel');
2
3// mousedown - mouse button pressed (before release)
4controlPanel.addEventListener('mousedown', (event) => {
5 console.log('Mouse button pressed');
6 console.log('Which button:', event.button); // 0=left, 1=middle, 2=right
7
8 if (event.button === 0) {
9 console.log('Left button - starting drag');
10 startDragging(event);
11 }
12});
13
14// mouseup - mouse button released
15controlPanel.addEventListener('mouseup', (event) => {
16 console.log('Mouse button released');
17
18 if (isDragging) {
19 console.log('Ending drag');
20 stopDragging(event);
21 }
22});
23
24let isDragging = false;
25let dragStartX = 0;
26let dragStartY = 0;
27
28function startDragging(event) {
29 isDragging = true;
30 dragStartX = event.clientX;
31 dragStartY = event.clientY;
32
33 console.log(`Drag started from position (${dragStartX}, ${dragStartY})`);
34}
35
36function stopDragging(event) {
37 const deltaX = event.clientX - dragStartX;
38 const deltaY = event.clientY - dragStartY;
39
40 console.log(`Dragged by: X=${deltaX}px, Y=${deltaY}px`);
41
42 isDragging = false;
43}The isDragging flag ties both events into one story, and the difference in coordinates tells you how far the cursor moved.
Mouse Movement Events
Cursor movement over the park map is tracked by mousemove, a sensor that fires at every twitch of the mouse.
1const parkMap = document.getElementById('interactive-park-map');
2
3// mousemove - mouse movement (CAUTION: very frequent!)
4// Use with care - can generate hundreds of events per second
5
6let mouseMoveCount = 0;
7
8parkMap.addEventListener('mousemove', (event) => {
9 mouseMoveCount++;
10
11 // Update cursor position
12 updateCursorPosition(event.clientX, event.clientY);
13
14 // If dragging, update position
15 if (isDragging) {
16 updateDragPosition(event.clientX, event.clientY);
17 }
18
19 // Log every 10th event (to avoid flooding the console)
20 if (mouseMoveCount % 10 === 0) {
21 console.log(`Mouse move #${mouseMoveCount}: (${event.clientX}, ${event.clientY})`);
22 }
23});
24
25function updateCursorPosition(x, y) {
26 const coordsDisplay = document.getElementById('mouse-coords');
27 if (coordsDisplay) {
28 coordsDisplay.textContent = `X: ${x}, Y: ${y}`;
29 }
30}
31
32function updateDragPosition(x, y) {
33 const draggableElement = document.getElementById('draggable');
34 if (draggableElement) {
35 draggableElement.style.left = x + 'px';
36 draggableElement.style.top = y + 'px';
37 }
38}That's why the log reaches the console only every tenth event, and the throttling lesson will show you how to tame such avalanches. Now the cursor entering and leaving.
1// mouseenter - mouse enters the element's area (doesn't bubble!)
2parkMap.addEventListener('mouseenter', (event) => {
3 console.log('Mouse entered the park map');
4 parkMap.classList.add('mouse-active');
5 showMapControls();
6});
7
8// mouseleave - mouse leaves the element's area (doesn't bubble!)
9parkMap.addEventListener('mouseleave', (event) => {
10 console.log('Mouse left the park map');
11 parkMap.classList.remove('mouse-active');
12 hideMapControls();
13});
14
15// mouseover - mouse enters (bubbles - fires for children too!)
16parkMap.addEventListener('mouseover', (event) => {
17 if (event.target.classList.contains('enclosure-marker')) {
18 const enclosureName = event.target.dataset.name;
19 console.log(`Hovered over enclosure marker: ${enclosureName}`);
20 showEnclosureTooltip(event.target, enclosureName);
21 }
22});
23
24// mouseout - mouse leaves (bubbles)
25parkMap.addEventListener('mouseout', (event) => {
26 if (event.target.classList.contains('enclosure-marker')) {
27 console.log('Left enclosure marker');
28 hideEnclosureTooltip();
29 }
30});
31
32function showMapControls() {
33 const controls = document.getElementById('map-controls');
34 if (controls) {
35 controls.style.display = 'block';
36 }
37}
38
39function hideMapControls() {
40 const controls = document.getElementById('map-controls');
41 if (controls) {
42 controls.style.display = 'none';
43 }
44}The mouseenter and mouseleave pair doesn't bubble, so it reacts only to the map itself, while mouseover and mouseout bubble up from every child, hence the event.target check.
Practical Example - Drag System
"One of the most useful features," says Ray, "is drag & drop." The first part of the class starts the drag.
1class DinosaurCardDragSystem {
2 constructor() {
3 this.draggedElement = null;
4 this.dragOffsetX = 0;
5 this.dragOffsetY = 0;
6 this.initialize();
7 }
8
9 initialize() {
10 const cards = document.querySelectorAll('.dinosaur-card');
11
12 cards.forEach(card => {
13 card.addEventListener('mousedown', (e) => this.handleDragStart(e));
14 });
15
16 document.addEventListener('mousemove', (e) => this.handleDragMove(e));
17 document.addEventListener('mouseup', (e) => this.handleDragEnd(e));
18
19 console.log('Drag & drop system initialized');
20 }
21
22 handleDragStart(event) {
23 this.draggedElement = event.currentTarget;
24
25 const rect = this.draggedElement.getBoundingClientRect();
26 this.dragOffsetX = event.clientX - rect.left;
27 this.dragOffsetY = event.clientY - rect.top;
28
29 this.draggedElement.style.position = 'fixed';
30 this.draggedElement.style.zIndex = '1000';
31 this.draggedElement.classList.add('dragging');
32
33 const dinoName = this.draggedElement.dataset.dinoName;
34 console.log(`Started dragging card: ${dinoName}`);
35
36 event.preventDefault(); // Prevent text selection
37 }The mousemove and mouseup listeners sit on the document, because a quick move will take the cursor outside the card. Here preventDefault() in mousedown really does block text selection, and getBoundingClientRect() helps work out the grab point.
1 handleDragMove(event) {
2 if (!this.draggedElement) return;
3
4 const x = event.clientX - this.dragOffsetX;
5 const y = event.clientY - this.dragOffsetY;
6
7 this.draggedElement.style.left = x + 'px';
8 this.draggedElement.style.top = y + 'px';
9 }
10
11 handleDragEnd(event) {
12 if (!this.draggedElement) return;
13
14 const dinoName = this.draggedElement.dataset.dinoName;
15 console.log(`Finished dragging card: ${dinoName}`);
16
17 // Check if the card was dropped in an appropriate zone
18 const dropZone = this.findDropZone(event.clientX, event.clientY);
19
20 if (dropZone) {
21 console.log('Card dropped in zone:', dropZone.id);
22 this.handleDrop(this.draggedElement, dropZone);
23 } else {
24 console.log('Card dropped outside zone - returning to original position');
25 this.returnToOriginalPosition();
26 }
27
28 this.draggedElement.classList.remove('dragging');
29 this.draggedElement = null;
30 }The card follows the cursor, and once the button is released the class looks for the zone where the card landed.
1 findDropZone(x, y) {
2 const dropZones = document.querySelectorAll('.drop-zone');
3
4 for (const zone of dropZones) {
5 const rect = zone.getBoundingClientRect();
6
7 if (x >= rect.left && x <= rect.right &&
8 y >= rect.top && y <= rect.bottom) {
9 return zone;
10 }
11 }
12
13 return null;
14 }
15
16 handleDrop(card, dropZone) {
17 console.log('Adding card to drop zone');
18 dropZone.appendChild(card);
19
20 // Reset styles
21 card.style.position = '';
22 card.style.left = '';
23 card.style.top = '';
24 card.style.zIndex = '';
25 }
26
27 returnToOriginalPosition() {
28 // Back to the original place - no animation, because position changes cannot be animated.
29 // No setTimeout: handleDragEnd sets this.draggedElement to null right after this
30 this.draggedElement.style.position = '';
31 this.draggedElement.style.left = '';
32 this.draggedElement.style.top = '';
33 this.draggedElement.style.zIndex = '';
34 }
35}
36
37// Initialization
38const dragSystem = new DinosaurCardDragSystem();A card dropped inside a zone moves to the new enclosure, and outside of it, it goes back to its place.
Drag and Drop Built into the Browser
The browser also has its own Drag and Drop API. An element with the draggable="true" attribute sends dragstart, and the target zone receives dragover and drop.
1// HTML: <div class="dino-item" draggable="true">T-Rex</div>
2const dino = document.querySelector('.dino-item');
3const zone = document.getElementById('zone-c');
4
5dino.addEventListener('dragstart', (e) => {
6 e.dataTransfer.setData('text/plain', dino.textContent);
7 dino.classList.add('dragging');
8});
9dino.addEventListener('dragend', () => dino.classList.remove('dragging'));
10
11zone.addEventListener('dragover', (e) => {
12 e.preventDefault(); // without this the zone won't accept the drop
13});
14
15zone.addEventListener('drop', (e) => {
16 e.preventDefault(); // blocks the default action, e.g. opening a link
17 zone.appendChild(dino);
18});The key part is preventDefault() in dragover, because without it dropping is not allowed, and dragend cleans up after every drag.
Keyboard Events - Keyboard Control
"The keyboard," Ray points to the control panel, "is the second key way of interaction. Keyboard shortcuts, navigation, data input."
Basic Keyboard Events
Every key press leaves a full report in the event object.
1// keydown - key pressed (repeats when held)
2document.addEventListener('keydown', (event) => {
3 console.log('=== KEYDOWN ===');
4 console.log('Key:', event.key); // 'a', 'Enter', 'ArrowUp', etc.
5 console.log('Code:', event.code); // 'KeyA', 'Enter', 'ArrowUp', etc.
6 console.log('KeyCode (deprecated):', event.keyCode);
7 console.log('Is it a repeat:', event.repeat);
8 console.log('Ctrl:', event.ctrlKey);
9 console.log('Shift:', event.shiftKey);
10 console.log('Alt:', event.altKey);
11 console.log('Meta (Cmd/Win):', event.metaKey);
12});
13
14// keyup - key released
15document.addEventListener('keyup', (event) => {
16 console.log(`Key released: ${event.key}`);
17});
18
19// keypress - DEPRECATED! Do not use (use keydown instead)
20// keypress is deprecated and does not fire e.g. for arrow keys or Escapeevent.key is the character or the name of the key, and event.code is the physical key, independent of the keyboard layout. A key that is held down sends more keydown events with event.repeat set to true, and keyup arrives once.
Practical Keyboard Shortcuts
A single listener on the document handles the shortcuts. First saving, searching, and Escape.
1// Keyboard shortcut system for the park
2document.addEventListener('keydown', (event) => {
3 // Ctrl/Cmd + S - Save
4 if ((event.ctrlKey || event.metaKey) && event.key === 's') {
5 event.preventDefault();
6 console.log('Saving park configuration...');
7 saveParkConfiguration();
8 return;
9 }
10
11 // Ctrl/Cmd + F - Search
12 if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
13 event.preventDefault();
14 console.log('Opening dinosaur search');
15 openSearchDialog();
16 return;
17 }
18
19 // Escape - Close all windows
20 if (event.key === 'Escape') {
21 console.log('Closing all dialog windows');
22 closeAllModals();
23 return;
24 }We check ctrlKey or metaKey so that the shortcut also works with Cmd on a Mac, and preventDefault() stops the browser from saving the page. Next come the arrows, the space bar, F1, and the digits.
1 // Arrows - Map navigation
2 const arrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
3 if (arrowKeys.includes(event.key)) {
4 event.preventDefault();
5
6 const direction = {
7 'ArrowUp': 'north',
8 'ArrowDown': 'south',
9 'ArrowLeft': 'west',
10 'ArrowRight': 'east'
11 }[event.key];
12
13 console.log(`Moving map in direction: ${direction}`);
14 moveMap(direction);
15 return;
16 }
17
18 // Space - Pause/Resume monitoring
19 if (event.key === ' ') {
20 event.preventDefault();
21 console.log('Toggle monitoring pause');
22 toggleMonitoringPause();
23 return;
24 }
25
26 // F1 - Help
27 if (event.key === 'F1') {
28 event.preventDefault();
29 console.log('Opening help');
30 openHelp();
31 return;
32 }
33
34 // Numbers 1-9 - Quick access to enclosures
35 if (event.key >= '1' && event.key <= '9') {
36 const enclosureNumber = parseInt(event.key);
37 console.log(`Switching to enclosure #${enclosureNumber}`);
38 jumpToEnclosure(enclosureNumber);
39 return;
40 }
41});The space bar is named ' ', and the direction map turns an arrow into a compass direction. Then some simple worker functions.
1function saveParkConfiguration() {
2 console.log('Configuration saved');
3}
4
5function openSearchDialog() {
6 console.log('Search dialog opened');
7}
8
9function closeAllModals() {
10 document.querySelectorAll('.modal').forEach(modal => {
11 modal.style.display = 'none';
12 });
13 console.log('All windows closed');
14}
15
16function moveMap(direction) {
17 console.log(`Map moved in direction: ${direction}`);
18}
19
20function toggleMonitoringPause() {
21 console.log('Monitoring paused/resumed');
22}
23
24function openHelp() {
25 console.log('Help opened');
26}
27
28function jumpToEnclosure(number) {
29 console.log(`Jumping to enclosure #${number}`);
30}The listener recognizes the shortcut, and the logic lives in the functions.
Keyboard Input Validation
A keydown listener can reject a character before it reaches the field: the security code accepts only digits, the phone number also spaces, parentheses, and hyphens, and the name gets a capital letter.
1const securityCodeInput = document.getElementById('security-code');
2const phoneInput = document.getElementById('phone-number');
3const nameInput = document.getElementById('visitor-name');
4
5// Only digits - security code
6securityCodeInput.addEventListener('keydown', (event) => {
7 // Allow function keys
8 const allowedKeys = [
9 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
10 'ArrowLeft', 'ArrowRight', 'Home', 'End'
11 ];
12
13 if (allowedKeys.includes(event.key)) {
14 return; // Allow
15 }
16
17 // Only digits
18 if (event.key >= '0' && event.key <= '9') {
19 // Maximum 6 digits
20 if (securityCodeInput.value.length >= 6 && !event.ctrlKey && !event.metaKey) {
21 event.preventDefault();
22 console.log('Maximum 6 digits');
23 }
24 return;
25 }
26
27 // Everything else - block
28 event.preventDefault();
29 console.log('Only digits are allowed');
30});
31
32// Phone number formatting
33phoneInput.addEventListener('keydown', (event) => {
34 const allowedKeys = [
35 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
36 'ArrowLeft', 'ArrowRight', 'Home', 'End'
37 ];
38
39 if (allowedKeys.includes(event.key)) return;
40
41 // Only digits, spaces, hyphens, parentheses
42 const phoneChars = /^[0-9 ()-]$/;
43
44 if (!phoneChars.test(event.key)) {
45 event.preventDefault();
46 console.log('Invalid character in phone number');
47 }
48});
49
50// Auto-capitalize first letter
51nameInput.addEventListener('keyup', (event) => {
52 const value = nameInput.value;
53
54 if (value.length === 1) {
55 nameInput.value = value.toUpperCase();
56 }
57});Such a filter is only a convenience, because text can be pasted from the context menu, so check the value on input or submit as well.
Combining Mouse and Keyboard
"The best interfaces," says Ray, "combine both types of input." Modifier keys change the meaning of a click.
1const enclosureCards = document.querySelectorAll('.enclosure-card');
2let selectedCards = new Set();
3
4enclosureCards.forEach(card => {
5 card.addEventListener('click', (event) => {
6 const enclosureId = card.dataset.enclosureId;
7
8 // Different behavior depending on modifier keys
9 if (event.ctrlKey || event.metaKey) {
10 // Ctrl + Click = Toggle selection
11 console.log('Ctrl+Click: Toggle selection');
12
13 if (selectedCards.has(enclosureId)) {
14 selectedCards.delete(enclosureId);
15 card.classList.remove('selected');
16 } else {
17 selectedCards.add(enclosureId);
18 card.classList.add('selected');
19 }
20 } else if (event.shiftKey) {
21 // Shift + Click = Range selection
22 console.log('Shift+Click: Range selection');
23 selectRange(lastSelected, enclosureId);
24 } else {
25 // Normal click = Only this one card
26 console.log('Normal click: Single selection');
27
28 selectedCards.clear();
29 document.querySelectorAll('.enclosure-card.selected').forEach(c => {
30 c.classList.remove('selected');
31 });
32
33 selectedCards.add(enclosureId);
34 card.classList.add('selected');
35 }
36
37 lastSelected = enclosureId;
38 updateSelectionInfo();
39 });
40});
41
42let lastSelected = null;
43
44function selectRange(startId, endId) {
45 console.log(`Selecting range from ${startId} to ${endId}`);
46 // Range selection logic
47}
48
49function updateSelectionInfo() {
50 console.log(`Selected ${selectedCards.size} enclosures`);
51}Ctrl or Cmd toggles the selection, Shift selects a range, and a plain click leaves a single card. Next, the arrow keys.
1// Keyboard navigation through cards
2let focusedCardIndex = 0;
3const cards = Array.from(enclosureCards);
4
5document.addEventListener('keydown', (event) => {
6 // Only when no input field is active
7 if (document.activeElement.tagName === 'INPUT') return;
8
9 switch(event.key) {
10 case 'ArrowRight':
11 case 'ArrowDown':
12 event.preventDefault();
13 focusedCardIndex = Math.min(focusedCardIndex + 1, cards.length - 1);
14 focusCard(focusedCardIndex);
15 break;
16
17 case 'ArrowLeft':
18 case 'ArrowUp':
19 event.preventDefault();
20 focusedCardIndex = Math.max(focusedCardIndex - 1, 0);
21 focusCard(focusedCardIndex);
22 break;
23
24 case 'Enter':
25 case ' ':
26 event.preventDefault();
27 cards[focusedCardIndex].click();
28 break;
29 }
30});
31
32function focusCard(index) {
33 cards.forEach(card => card.classList.remove('focused'));
34 cards[index].classList.add('focused');
35 cards[index].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
36
37 console.log(`Focus on card #${index}: ${cards[index].dataset.enclosureName}`);
38}The activeElement condition doesn't get in the way of typing in fields, and click() on a card runs the same listener as the mouse.
Window and Touch Events
DOMContentLoaded fires when the HTML has been loaded, even though images may still be loading. load on window waits for the whole page with its resources. Scrolling reports scroll, and window.scrollY gives the number of scrolled pixels, so a value larger than before means moving down. On touch screens a finger sends touchstart, touchmove, and touchend in turn, which we'll come back to with gestures.
Summary
"Mouse and keyboard events," Ray summarizes, "are the foundation of interactivity. Just as different sensors in Jurassic Park detect different types of threats, different events allow for diverse user interactions. The key is using the right events for the right tasks."
Key mouse events:
click,dblclick,contextmenu- clicksmousedown,mouseup- button controlmousemove- position tracking (use carefully!)mouseenter,mouseleave- hovering (don't bubble)mouseover,mouseout- hovering (bubble)
Key keyboard events:
keydown- key pressed (use this one)keyup- key releasedevent.key- character/key name ('a', 'Enter', 'ArrowUp')event.code- physical key ('KeyA', 'Enter', 'ArrowUp')- Modifier keys:
ctrlKey,shiftKey,altKey,metaKey
My advice: base shortcuts on keydown, and moving items between zones on the built-in Drag and Drop. Next lesson: forms.
Remember: every sensor in the park reacts to a different signal, so match the event to the task.
Code for this lesson: index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Mouse and Keyboard Events - Camera Control</title>
7 <style>
8 body {
9 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10 background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
11 margin: 0;
12 padding: 20px;
13 color: white;
14 }
15
16 .container {
17 max-width: 1000px;
18 margin: 0 auto;
19 }
20
21 h1 {
22 text-align: center;
23 margin-bottom: 20px;
24 }
25
26 .instructions {
27 background: rgba(52, 152, 219, 0.2);
28 border: 1px solid #3498db;
29 border-radius: 10px;
30 padding: 15px;
31 margin-bottom: 20px;
32 text-align: center;
33 }
34
35 .instructions kbd {
36 background: rgba(255, 255, 255, 0.2);
37 padding: 2px 8px;
38 border-radius: 4px;
39 font-family: monospace;
40 margin: 0 3px;
41 }
42
43 .camera-viewport {
44 position: relative;
45 width: 100%;
46 height: 400px;
47 background: linear-gradient(180deg, #34495e 0%, #2c3e50 100%);
48 border-radius: 15px;
49 overflow: hidden;
50 cursor: move;
51 margin-bottom: 20px;
52 border: 3px solid #7f8c8d;
53 }
54
55 .camera-content {
56 position: absolute;
57 width: 200%;
58 height: 200%;
59 transition: transform 0.3s ease;
60 }
61
62 .dinosaur {
63 position: absolute;
64 font-size: 60px;
65 cursor: pointer;
66 transition: transform 0.2s ease;
67 user-select: none;
68 }
69
70 .dinosaur:hover {
71 transform: scale(1.2);
72 filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.5));
73 }
74
75 .dinosaur.clicked {
76 animation: bounce 0.5s ease;
77 }
78
79 @keyframes bounce {
80 0%, 100% { transform: scale(1); }
81 50% { transform: scale(1.3); }
82 }
83
84 .controls-panel {
85 display: grid;
86 grid-template-columns: 1fr 1fr;
87 gap: 20px;
88 margin-bottom: 20px;
89 }
90
91 .control-box {
92 background: rgba(0, 0, 0, 0.6);
93 border-radius: 10px;
94 padding: 20px;
95 border: 1px solid #34495e;
96 }
97
98 .control-box h3 {
99 margin-top: 0;
100 color: #3498db;
101 }
102
103 .stats {
104 display: grid;
105 grid-template-columns: repeat(2, 1fr);
106 gap: 10px;
107 }
108
109 .stat-item {
110 background: rgba(255, 255, 255, 0.1);
111 padding: 10px;
112 border-radius: 5px;
113 }
114
115 .stat-label {
116 color: #bdc3c7;
117 font-size: 12px;
118 }
119
120 .stat-value {
121 color: #ecf0f1;
122 font-size: 18px;
123 font-weight: bold;
124 }
125
126 .arrow-buttons {
127 display: grid;
128 grid-template-areas:
129 ". up ."
130 "left . right"
131 ". down .";
132 gap: 5px;
133 justify-content: center;
134 align-items: center;
135 }
136
137 .arrow-btn {
138 width: 60px;
139 height: 60px;
140 background: linear-gradient(45deg, #3498db, #2980b9);
141 border: none;
142 border-radius: 10px;
143 color: white;
144 font-size: 24px;
145 cursor: pointer;
146 transition: all 0.2s ease;
147 }
148
149 .arrow-btn:hover {
150 background: linear-gradient(45deg, #2980b9, #21618c);
151 transform: scale(1.05);
152 }
153
154 .arrow-btn:active {
155 transform: scale(0.95);
156 }
157
158 .arrow-btn.up { grid-area: up; }
159 .arrow-btn.down { grid-area: down; }
160 .arrow-btn.left { grid-area: left; }
161 .arrow-btn.right { grid-area: right; }
162
163 .event-log {
164 background: rgba(0, 0, 0, 0.8);
165 border-radius: 10px;
166 padding: 15px;
167 max-height: 200px;
168 overflow-y: auto;
169 font-family: 'Courier New', monospace;
170 font-size: 12px;
171 }
172
173 .log-entry {
174 padding: 5px;
175 border-bottom: 1px solid rgba(255, 255, 255, 0.1);
176 color: #bdc3c7;
177 }
178
179 .log-entry.mouse {
180 color: #3498db;
181 }
182
183 .log-entry.keyboard {
184 color: #e74c3c;
185 }
186
187 .log-entry.click {
188 color: #f39c12;
189 }
190
191 @media (max-width: 768px) {
192 .controls-panel {
193 grid-template-columns: 1fr;
194 }
195 }
196 </style>
197</head>
198<body>
199 <div class="container">
200 <h1>Camera Control in Jurassic Park</h1>
201
202 <div class="instructions">
203 <strong>Controls:</strong> Arrows <kbd>β</kbd> <kbd>β</kbd> <kbd>β</kbd> <kbd>β</kbd> |
204 <strong>Pause:</strong> <kbd>Space</kbd> |
205 <strong>Zoom:</strong> Double click |
206 <strong>Close:</strong> <kbd>Escape</kbd>
207 </div>
208
209 <div class="camera-viewport" id="viewport">
210 <div class="camera-content" id="camera">
211 <div class="dinosaur" style="top: 50px; left: 100px;" data-name="T-Rex">T</div>
212 <div class="dinosaur" style="top: 150px; left: 400px;" data-name="Velociraptor">V</div>
213 <div class="dinosaur" style="top: 300px; left: 200px;" data-name="Triceratops">Tr</div>
214 <div class="dinosaur" style="top: 100px; left: 600px;" data-name="Brachiosaurus">B</div>
215 <div class="dinosaur" style="top: 250px; left: 700px;" data-name="Stegosaurus">S</div>
216 </div>
217 </div>
218
219 <div class="controls-panel">
220 <div class="control-box">
221 <h3>Statistics</h3>
222 <div class="stats">
223 <div class="stat-item">
224 <div class="stat-label">Mouse position</div>
225 <div class="stat-value" id="mouse-pos">0, 0</div>
226 </div>
227 <div class="stat-item">
228 <div class="stat-label">Clicks</div>
229 <div class="stat-value" id="click-count">0</div>
230 </div>
231 <div class="stat-item">
232 <div class="stat-label">Camera position</div>
233 <div class="stat-value" id="camera-pos">0, 0</div>
234 </div>
235 <div class="stat-item">
236 <div class="stat-label">Last key</div>
237 <div class="stat-value" id="last-key">-</div>
238 </div>
239 </div>
240 </div>
241
242 <div class="control-box">
243 <h3>Camera control</h3>
244 <div class="arrow-buttons">
245 <button class="arrow-btn up" data-direction="up">β</button>
246 <button class="arrow-btn left" data-direction="left">β</button>
247 <button class="arrow-btn right" data-direction="right">β</button>
248 <button class="arrow-btn down" data-direction="down">β</button>
249 </div>
250 </div>
251 </div>
252
253 <div class="control-box">
254 <h3>Event log</h3>
255 <div class="event-log" id="event-log"></div>
256 </div>
257 </div>
258
259 <script>
260 // References to elements
261 const camera = document.getElementById('camera');
262 const viewport = document.getElementById('viewport');
263 const mousePosDisplay = document.getElementById('mouse-pos');
264 const clickCountDisplay = document.getElementById('click-count');
265 const cameraPosDisplay = document.getElementById('camera-pos');
266 const lastKeyDisplay = document.getElementById('last-key');
267 const eventLog = document.getElementById('event-log');
268
269 // Camera state
270 let cameraX = 0;
271 let cameraY = 0;
272 let clickCount = 0;
273 let isPaused = false;
274 const moveSpeed = 50;
275
276 // Event logging function
277 function logEvent(message, type = 'mouse') {
278 const logEntry = document.createElement('div');
279 logEntry.className = `log-entry ${type}`;
280 logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
281 eventLog.insertBefore(logEntry, eventLog.firstChild);
282
283 // Limit number of entries
284 if (eventLog.children.length > 20) {
285 eventLog.removeChild(eventLog.lastChild);
286 }
287 }
288
289 // Function that moves the camera
290 function moveCamera(direction) {
291 if (isPaused) {
292 logEvent('Camera paused! Press Space to resume', 'keyboard');
293 return;
294 }
295
296 switch(direction) {
297 case 'up':
298 cameraY = Math.max(cameraY - moveSpeed, -200);
299 break;
300 case 'down':
301 cameraY = Math.min(cameraY + moveSpeed, 200);
302 break;
303 case 'left':
304 cameraX = Math.max(cameraX - moveSpeed, -200);
305 break;
306 case 'right':
307 cameraX = Math.min(cameraX + moveSpeed, 200);
308 break;
309 }
310
311 camera.style.transform = `translate(${cameraX}px, ${cameraY}px)`;
312 cameraPosDisplay.textContent = `${cameraX}, ${cameraY}`;
313 logEvent(`Camera movement: ${direction}`, 'keyboard');
314 }
315
316 // MOUSE HANDLING
317
318 // Mousemove - tracking mouse position
319 viewport.addEventListener('mousemove', function(e) {
320 const rect = viewport.getBoundingClientRect();
321 const x = Math.floor(e.clientX - rect.left);
322 const y = Math.floor(e.clientY - rect.top);
323 mousePosDisplay.textContent = `${x}, ${y}`;
324 });
325
326 // Click - single click
327 viewport.addEventListener('click', function(e) {
328 clickCount++;
329 clickCountDisplay.textContent = clickCount;
330
331 const rect = viewport.getBoundingClientRect();
332 const x = Math.floor(e.clientX - rect.left);
333 const y = Math.floor(e.clientY - rect.top);
334
335 logEvent(`Click at position: (${x}, ${y})`, 'click');
336 });
337
338 // Double click - double click (zoom)
339 viewport.addEventListener('dblclick', function(e) {
340 const currentScale = camera.style.transform.includes('scale') ? 1 : 1.5;
341 const newScale = currentScale === 1 ? 1.5 : 1;
342
343 camera.style.transform = `translate(${cameraX}px, ${cameraY}px) scale(${newScale})`;
344 logEvent(`Zoom: ${newScale === 1 ? 'Off' : 'On'}`, 'click');
345 });
346
347 // Mouseenter/mouseleave - hovering over the viewport
348 viewport.addEventListener('mouseenter', function() {
349 viewport.style.borderColor = '#3498db';
350 logEvent('Mouse entered camera area', 'mouse');
351 });
352
353 viewport.addEventListener('mouseleave', function() {
354 viewport.style.borderColor = '#7f8c8d';
355 logEvent('Mouse left camera area', 'mouse');
356 });
357
358 // Click on dinosaur
359 document.querySelectorAll('.dinosaur').forEach(dino => {
360 dino.addEventListener('click', function(e) {
361 e.stopPropagation(); // Stop propagation to the viewport
362
363 const name = this.dataset.name;
364 this.classList.add('clicked');
365
366 setTimeout(() => {
367 this.classList.remove('clicked');
368 }, 500);
369
370 logEvent(`Clicked dinosaur: ${name}`, 'click');
371 console.log(`Observing: ${name}`);
372 });
373
374 dino.addEventListener('mouseenter', function() {
375 const name = this.dataset.name;
376 logEvent(`Hovered over: ${name}`, 'mouse');
377 });
378 });
379
380 // KEYBOARD HANDLING
381
382 // Keydown - key press
383 document.addEventListener('keydown', function(e) {
384 lastKeyDisplay.textContent = e.key;
385
386 switch(e.key) {
387 case 'ArrowUp':
388 e.preventDefault();
389 moveCamera('up');
390 break;
391 case 'ArrowDown':
392 e.preventDefault();
393 moveCamera('down');
394 break;
395 case 'ArrowLeft':
396 e.preventDefault();
397 moveCamera('left');
398 break;
399 case 'ArrowRight':
400 e.preventDefault();
401 moveCamera('right');
402 break;
403 case ' ':
404 e.preventDefault();
405 isPaused = !isPaused;
406 logEvent(`Pause: ${isPaused ? 'ON' : 'OFF'}`, 'keyboard');
407 viewport.style.opacity = isPaused ? '0.5' : '1';
408 break;
409 case 'Escape':
410 logEvent('Escape pressed - closing view', 'keyboard');
411 alert('Camera view will be closed');
412 break;
413 case 'r':
414 case 'R':
415 // Camera reset
416 cameraX = 0;
417 cameraY = 0;
418 camera.style.transform = 'translate(0px, 0px)';
419 cameraPosDisplay.textContent = '0, 0';
420 logEvent('Camera position reset', 'keyboard');
421 break;
422 default:
423 logEvent(`Key pressed: ${e.key}`, 'keyboard');
424 }
425 });
426
427 // Handle arrow buttons
428 document.querySelectorAll('.arrow-btn').forEach(btn => {
429 btn.addEventListener('click', function() {
430 const direction = this.dataset.direction;
431 moveCamera(direction);
432 });
433 });
434
435 // Initialization
436 logEvent('Camera system started', 'keyboard');
437 console.log('Jurassic Park monitoring system - ONLINE');
438 console.log('Use arrows to move the camera');
439 console.log('Click on dinosaurs to observe them');
440 </script>
441</body>
442</html>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which keyboard event does the browser send when any key is pressed (including Shift, Ctrl or the arrow keys) and repeat while the key is held down?
2. Which event is triggered after the page has fully loaded?
Hands-on tasks in the game
- Code editor
The index.html file has two paddocks (.enclosure) and dinosaur cards with the draggable="true" attribute. On dragstart each card stores its id in dataTransfer. Fill in the blanks: ___BLANK1___ is the event a paddock receives while a dragged card is over it (preventDefault() there allows the drop), and ___BLANK2___ is the dataTransfer method that reads the stored id in drop. A dropped card should end up in the paddock it was dropped on.
- Vertical ordering
Arrange mouse events in the order they occur during a click:
- Click in order
Arrange the code for checking the Ctrl+S combination.
- Vertical ordering
Arrange touch events in the order they occur: