JavaScript and TypeScript course Β· Module 8: Events and the DOM

Mouse and Keyboard Events

13 min read
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 Escape

event.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 - clicks
  • mousedown, mouseup - button control
  • mousemove - 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 released
  • event.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. 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. 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:

Useful articles