Understanding Events
Events are like notifications that something has happened in your web page. Think of them as a doorbell system - when someone (the user) rings the bell (clicks a button), your code gets notified and can respond. Events can be triggered by user actions (clicks, key presses), browser actions (page load, resize), or even programmatically by your code.
Event Basics
There are three main ways to handle events in JavaScript, but modern best practice recommends using addEventListener.
Three Ways to Add Event Handlers
// Method 1: Inline HTML event handlers (Not recommended)
// HTML: <button onclick="handleClick()">Click me</button>
// Method 2: DOM event properties
const button = document.querySelector('button');
button.onclick = function() {
console.log('Button clicked!');
};
// Method 3: addEventListener (Recommended)
const button = document.querySelector('button');
button.addEventListener('click', function() {
console.log('Button clicked!');
});
// Why addEventListener is better:
// 1. Can attach multiple handlers to same event
const button = document.querySelector('button');
button.addEventListener('click', function() {
console.log('First handler');
});
button.addEventListener('click', function() {
console.log('Second handler');
});
// 2. More control with options
button.addEventListener('click', handleClick, {
once: true, // Remove after first trigger
capture: false, // Use bubbling phase (default)
passive: true // Won't call preventDefault()
});
The Event Object
// Event handlers receive an event object
button.addEventListener('click', function(event) {
// Event properties
console.log(event.type); // 'click'
console.log(event.target); // The element that was clicked
console.log(event.currentTarget); // The element with the listener
console.log(event.timeStamp); // When the event occurred
// Mouse event specific properties
console.log(event.clientX, event.clientY); // Mouse position
console.log(event.pageX, event.pageY); // Page coordinates
console.log(event.screenX, event.screenY); // Screen coordinates
// Keyboard event specific properties
console.log(event.key); // 'a', 'Enter', 'ArrowUp', etc.
console.log(event.code); // 'KeyA', 'Enter', 'ArrowUp', etc.
console.log(event.altKey); // Was Alt pressed?
console.log(event.ctrlKey); // Was Ctrl pressed?
console.log(event.shiftKey); // Was Shift pressed?
console.log(event.metaKey); // Was Meta/Cmd pressed?
});
// Preventing default behavior
link.addEventListener('click', function(event) {
event.preventDefault(); // Prevent navigation
console.log('Link clicked but navigation prevented');
});
// Stopping event propagation
childElement.addEventListener('click', function(event) {
event.stopPropagation(); // Stop event from bubbling up
console.log('Event will not reach parent elements');
});
Common Event Types
JavaScript provides a rich set of events for different interactions and browser behaviors.
Mouse Events
const element = document.querySelector('.interactive');
// Click events
element.addEventListener('click', (e) => {
console.log('Single click at', e.clientX, e.clientY);
});
element.addEventListener('dblclick', (e) => {
console.log('Double click');
});
element.addEventListener('contextmenu', (e) => {
e.preventDefault(); // Prevent default context menu
console.log('Right click');
});
// Mouse movement events
element.addEventListener('mouseenter', (e) => {
console.log('Mouse entered element');
});
element.addEventListener('mouseleave', (e) => {
console.log('Mouse left element');
});
element.addEventListener('mouseover', (e) => {
console.log('Mouse over element or its children');
});
element.addEventListener('mouseout', (e) => {
console.log('Mouse left element or its children');
});
element.addEventListener('mousemove', (e) => {
console.log('Mouse moving at', e.clientX, e.clientY);
});
// Mouse button events
element.addEventListener('mousedown', (e) => {
console.log('Mouse button pressed');
});
element.addEventListener('mouseup', (e) => {
console.log('Mouse button released');
});
// Practical example: Draggable element
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
const draggable = document.querySelector('.draggable');
draggable.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === draggable) {
isDragging = true;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, draggable);
}
}
function dragEnd(e) {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
function setTranslate(xPos, yPos, el) {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
}
Keyboard Events
// Keyboard event types
document.addEventListener('keydown', (e) => {
console.log('Key pressed:', e.key);
// Common key checks
if (e.key === 'Enter') {
console.log('Enter pressed');
}
if (e.key === 'Escape') {
console.log('Escape pressed');
}
// Modifier keys
if (e.ctrlKey && e.key === 's') {
e.preventDefault(); // Prevent browser save dialog
console.log('Ctrl+S pressed');
}
// Arrow keys
switch(e.key) {
case 'ArrowUp':
console.log('Up arrow');
break;
case 'ArrowDown':
console.log('Down arrow');
break;
case 'ArrowLeft':
console.log('Left arrow');
break;
case 'ArrowRight':
console.log('Right arrow');
break;
}
});
document.addEventListener('keyup', (e) => {
console.log('Key released:', e.key);
});
document.addEventListener('keypress', (e) => {
console.log('Character typed:', e.key);
});
// Practical example: Keyboard navigation
const menuItems = document.querySelectorAll('.menu-item');
let currentIndex = 0;
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
currentIndex = (currentIndex + 1) % menuItems.length;
menuItems[currentIndex].focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
currentIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
menuItems[currentIndex].focus();
}
});
// Input field with real-time validation
const passwordInput = document.querySelector('#password');
const strengthIndicator = document.querySelector('#password-strength');
passwordInput.addEventListener('input', (e) => {
const password = e.target.value;
const strength = calculatePasswordStrength(password);
strengthIndicator.textContent = strength.message;
strengthIndicator.className = `strength-${strength.level}`;
});
function calculatePasswordStrength(password) {
let strength = 0;
if (password.length >= 8) strength++;
if (password.match(/[a-z]/) && password.match(/[A-Z]/)) strength++;
if (password.match(/[0-9]/)) strength++;
if (password.match(/[^a-zA-Z0-9]/)) strength++;
const levels = ['weak', 'fair', 'good', 'strong'];
const messages = ['Weak', 'Fair', 'Good', 'Strong'];
return {
level: levels[strength],
message: messages[strength]
};
}
Form Events
const form = document.querySelector('#myForm');
const input = document.querySelector('#username');
const select = document.querySelector('#country');
// Form submission
form.addEventListener('submit', (e) => {
e.preventDefault(); // Prevent page reload
// Get form data
const formData = new FormData(form);
// Process form data
for (let [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
// Manual validation
if (!validateForm()) {
return;
}
// Submit data via AJAX
submitForm(formData);
});
// Input events
input.addEventListener('input', (e) => {
console.log('Input value changed:', e.target.value);
});
input.addEventListener('change', (e) => {
console.log('Input value committed:', e.target.value);
});
input.addEventListener('focus', (e) => {
console.log('Input focused');
e.target.classList.add('focused');
});
input.addEventListener('blur', (e) => {
console.log('Input lost focus');
e.target.classList.remove('focused');
});
// Select events
select.addEventListener('change', (e) => {
console.log('Selected option:', e.target.value);
// Get selected option details
const selectedOption = e.target.options[e.target.selectedIndex];
console.log('Option text:', selectedOption.text);
console.log('Option value:', selectedOption.value);
});
// Real-time form validation
const emailInput = document.querySelector('#email');
const emailError = document.querySelector('#email-error');
emailInput.addEventListener('input', (e) => {
const email = e.target.value;
const isValid = validateEmail(email);
if (email === '') {
emailError.textContent = '';
emailInput.classList.remove('valid', 'invalid');
} else if (isValid) {
emailError.textContent = '';
emailInput.classList.remove('invalid');
emailInput.classList.add('valid');
} else {
emailError.textContent = 'Please enter a valid email';
emailInput.classList.remove('valid');
emailInput.classList.add('invalid');
}
});
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
Window and Document Events
// Page lifecycle events
window.addEventListener('load', (e) => {
console.log('Page fully loaded');
});
document.addEventListener('DOMContentLoaded', (e) => {
console.log('DOM is ready');
});
window.addEventListener('beforeunload', (e) => {
// Show confirmation if there are unsaved changes
if (hasUnsavedChanges()) {
e.preventDefault();
e.returnValue = ''; // Required for Chrome
}
});
// Window resize and scroll
window.addEventListener('resize', (e) => {
console.log('Window resized to', window.innerWidth, window.innerHeight);
});
// Debounced resize handler
let resizeTimeout;
window.addEventListener('resize', (e) => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
handleResize();
}, 250);
});
window.addEventListener('scroll', (e) => {
const scrollPosition = window.scrollY;
console.log('Scrolled to', scrollPosition);
// Sticky header example
const header = document.querySelector('header');
if (scrollPosition > 100) {
header.classList.add('sticky');
} else {
header.classList.remove('sticky');
}
});
// Visibility change
document.addEventListener('visibilitychange', (e) => {
if (document.hidden) {
console.log('Page is hidden');
pauseVideo();
} else {
console.log('Page is visible');
resumeVideo();
}
});
// Online/offline status
window.addEventListener('online', (e) => {
console.log('Back online');
showNotification('Connection restored');
});
window.addEventListener('offline', (e) => {
console.log('Gone offline');
showNotification('No internet connection');
});
Custom Events
You can create and dispatch your own custom events for component communication.
// Creating custom events
const myEvent = new CustomEvent('myCustomEvent', {
detail: {
message: 'Hello from custom event',
timestamp: Date.now()
},
bubbles: true,
cancelable: true
});
// Dispatching custom events
const element = document.querySelector('#myElement');
element.dispatchEvent(myEvent);
// Listening for custom events
element.addEventListener('myCustomEvent', (e) => {
console.log('Custom event received:', e.detail.message);
});
// Practical example: Custom component events
class NotificationComponent {
constructor(container) {
this.container = container;
}
show(message, type = 'info') {
const notification = this.createNotification(message, type);
this.container.appendChild(notification);
// Dispatch custom event
const showEvent = new CustomEvent('notificationShow', {
detail: { message, type },
bubbles: true
});
notification.dispatchEvent(showEvent);
return notification;
}
dismiss(notification) {
notification.remove();
// Dispatch custom event
const dismissEvent = new CustomEvent('notificationDismiss', {
detail: { notification },
bubbles: true
});
this.container.dispatchEvent(dismissEvent);
}
createNotification(message, type) {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
notification.addEventListener('click', () => {
this.dismiss(notification);
});
return notification;
}
}
// Using custom events for inter-component communication
const notifier = new NotificationComponent(document.body);
document.addEventListener('notificationShow', (e) => {
console.log('Notification shown:', e.detail.message);
});
document.addEventListener('notificationDismiss', (e) => {
console.log('Notification dismissed');
});
// Custom event for data changes
class DataStore {
constructor() {
this.data = {};
this.listeners = new EventTarget();
}
set(key, value) {
const oldValue = this.data[key];
this.data[key] = value;
// Dispatch change event
const changeEvent = new CustomEvent('dataChange', {
detail: {
key,
oldValue,
newValue: value
}
});
this.listeners.dispatchEvent(changeEvent);
}
get(key) {
return this.data[key];
}
onChange(callback) {
this.listeners.addEventListener('dataChange', callback);
}
}
// Usage
const store = new DataStore();
store.onChange((e) => {
console.log(`${e.detail.key} changed from ${e.detail.oldValue} to ${e.detail.newValue}`);
});
store.set('username', 'john_doe');
Practical Event Handling Examples
Interactive Menu System
class DropdownMenu {
constructor(element) {
this.element = element;
this.button = element.querySelector('.dropdown-button');
this.menu = element.querySelector('.dropdown-menu');
this.isOpen = false;
this.init();
}
init() {
// Toggle menu on button click
this.button.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.element.contains(e.target)) {
this.close();
}
});
// Close on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen) {
this.close();
this.button.focus();
}
});
// Handle keyboard navigation
this.menu.addEventListener('keydown', (e) => {
this.handleKeyboardNavigation(e);
});
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
this.isOpen = true;
this.menu.hidden = false;
this.button.setAttribute('aria-expanded', 'true');
// Focus first menu item
const firstItem = this.menu.querySelector('[role="menuitem"]');
if (firstItem) firstItem.focus();
}
close() {
this.isOpen = false;
this.menu.hidden = true;
this.button.setAttribute('aria-expanded', 'false');
}
handleKeyboardNavigation(e) {
const items = [...this.menu.querySelectorAll('[role="menuitem"]')];
const currentIndex = items.indexOf(document.activeElement);
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
const nextIndex = (currentIndex + 1) % items.length;
items[nextIndex].focus();
break;
case 'ArrowUp':
e.preventDefault();
const prevIndex = (currentIndex - 1 + items.length) % items.length;
items[prevIndex].focus();
break;
case 'Home':
e.preventDefault();
items[0].focus();
break;
case 'End':
e.preventDefault();
items[items.length - 1].focus();
break;
}
}
}
// Initialize dropdowns
document.querySelectorAll('.dropdown').forEach(dropdown => {
new DropdownMenu(dropdown);
});
Image Gallery with Keyboard Navigation
class ImageGallery {
constructor(container) {
this.container = container;
this.images = [...container.querySelectorAll('.gallery-image')];
this.currentIndex = 0;
this.modal = this.createModal();
this.init();
}
init() {
// Add click handlers to images
this.images.forEach((img, index) => {
img.addEventListener('click', () => {
this.openModal(index);
});
img.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.openModal(index);
}
});
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (!this.modal.open) return;
switch(e.key) {
case 'ArrowLeft':
e.preventDefault();
this.previous();
break;
case 'ArrowRight':
e.preventDefault();
this.next();
break;
case 'Escape':
this.closeModal();
break;
}
});
// Close on overlay click
this.modal.overlay.addEventListener('click', () => {
this.closeModal();
});
// Touch gestures
let touchStartX = 0;
this.modal.image.addEventListener('touchstart', (e) => {
touchStartX = e.touches[0].clientX;
});
this.modal.image.addEventListener('touchend', (e) => {
const touchEndX = e.changedTouches[0].clientX;
const diff = touchStartX - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) {
this.next();
} else {
this.previous();
}
}
});
}
createModal() {
const modal = document.createElement('div');
modal.className = 'gallery-modal';
modal.innerHTML = `
`;
document.body.appendChild(modal);
return {
element: modal,
overlay: modal.querySelector('.modal-overlay'),
image: modal.querySelector('.modal-image'),
caption: modal.querySelector('.modal-caption'),
closeBtn: modal.querySelector('.modal-close'),
prevBtn: modal.querySelector('.modal-prev'),
nextBtn: modal.querySelector('.modal-next'),
open: false
};
}
openModal(index) {
this.currentIndex = index;
this.updateModal();
this.modal.element.classList.add('open');
this.modal.open = true;
// Trap focus
this.modal.closeBtn.focus();
}
closeModal() {
this.modal.element.classList.remove('open');
this.modal.open = false;
// Return focus to trigger element
this.images[this.currentIndex].focus();
}
updateModal() {
const currentImage = this.images[this.currentIndex];
this.modal.image.src = currentImage.dataset.fullsize || currentImage.src;
this.modal.caption.textContent = currentImage.alt;
// Update navigation buttons
this.modal.prevBtn.disabled = this.currentIndex === 0;
this.modal.nextBtn.disabled = this.currentIndex === this.images.length - 1;
}
next() {
if (this.currentIndex < this.images.length - 1) {
this.currentIndex++;
this.updateModal();
}
}
previous() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.updateModal();
}
}
}
// Initialize gallery
const gallery = new ImageGallery(document.querySelector('.image-gallery'));
Event Performance Considerations
- Use Event Delegation: Attach listeners to parent elements
- Debounce/Throttle: For high-frequency events
- Passive Event Listeners: For scroll performance
- Remove Unused Listeners: Prevent memory leaks
// Event delegation pattern
document.querySelector('.item-list').addEventListener('click', (e) => {
if (e.target.matches('.item-button')) {
handleItemClick(e.target);
}
});
// Debounce function
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Throttle function
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Usage
const debouncedSearch = debounce((query) => {
performSearch(query);
}, 300);
const throttledScroll = throttle(() => {
updateScrollPosition();
}, 100);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
window.addEventListener('scroll', throttledScroll);
// Passive event listeners
document.addEventListener('scroll', handleScroll, { passive: true });
document.addEventListener('touchstart', handleTouch, { passive: true });
// Cleanup example
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
this.setupListeners();
}
setupListeners() {
document.addEventListener('click', this.handleClick);
}
handleClick(e) {
// Handle click
}
destroy() {
document.removeEventListener('click', this.handleClick);
}
}
Practice Exercises
- Create a keyboard-controlled game (like Snake or Tetris)
- Build a form with real-time validation
- Implement drag-and-drop functionality
- Create a custom context menu
Exercise Solutions (Try First!)
Click to see solutions
// 1. Simple Snake Game
class SnakeGame {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.gridSize = 20;
this.snake = [{x: 10, y: 10}];
this.food = this.generateFood();
this.direction = 'right';
this.gameLoop = null;
this.init();
}
init() {
document.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowUp':
if (this.direction !== 'down') this.direction = 'up';
break;
case 'ArrowDown':
if (this.direction !== 'up') this.direction = 'down';
break;
case 'ArrowLeft':
if (this.direction !== 'right') this.direction = 'left';
break;
case 'ArrowRight':
if (this.direction !== 'left') this.direction = 'right';
break;
}
});
this.start();
}
start() {
this.gameLoop = setInterval(() => this.update(), 100);
}
update() {
const head = {...this.snake[0]};
switch(this.direction) {
case 'up': head.y--; break;
case 'down': head.y++; break;
case 'left': head.x--; break;
case 'right': head.x++; break;
}
// Check collision with walls
if (head.x < 0 || head.x >= this.canvas.width / this.gridSize ||
head.y < 0 || head.y >= this.canvas.height / this.gridSize) {
this.gameOver();
return;
}
// Check collision with self
if (this.snake.some(segment => segment.x === head.x && segment.y === head.y)) {
this.gameOver();
return;
}
this.snake.unshift(head);
// Check if food is eaten
if (head.x === this.food.x && head.y === this.food.y) {
this.food = this.generateFood();
} else {
this.snake.pop();
}
this.draw();
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw snake
this.ctx.fillStyle = 'green';
this.snake.forEach(segment => {
this.ctx.fillRect(
segment.x * this.gridSize,
segment.y * this.gridSize,
this.gridSize - 1,
this.gridSize - 1
);
});
// Draw food
this.ctx.fillStyle = 'red';
this.ctx.fillRect(
this.food.x * this.gridSize,
this.food.y * this.gridSize,
this.gridSize - 1,
this.gridSize - 1
);
}
generateFood() {
return {
x: Math.floor(Math.random() * (this.canvas.width / this.gridSize)),
y: Math.floor(Math.random() * (this.canvas.height / this.gridSize))
};
}
gameOver() {
clearInterval(this.gameLoop);
alert('Game Over!');
}
}
// 2. Form with Real-time Validation
class FormValidator {
constructor(form) {
this.form = form;
this.fields = {};
this.init();
}
init() {
// Setup validation rules
this.fields.username = {
element: this.form.querySelector('#username'),
rules: {
required: true,
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]+$/
}
};
this.fields.email = {
element: this.form.querySelector('#email'),
rules: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
}
};
this.fields.password = {
element: this.form.querySelector('#password'),
rules: {
required: true,
minLength: 8,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/
}
};
// Add event listeners
Object.values(this.fields).forEach(field => {
field.element.addEventListener('input', () => this.validateField(field));
field.element.addEventListener('blur', () => this.validateField(field));
});
this.form.addEventListener('submit', (e) => {
e.preventDefault();
if (this.validateAll()) {
console.log('Form is valid!');
// Submit form
}
});
}
validateField(field) {
const value = field.element.value;
const errors = [];
if (field.rules.required && !value) {
errors.push('This field is required');
}
if (value && field.rules.minLength && value.length < field.rules.minLength) {
errors.push(`Must be at least ${field.rules.minLength} characters`);
}
if (value && field.rules.maxLength && value.length > field.rules.maxLength) {
errors.push(`Must be no more than ${field.rules.maxLength} characters`);
}
if (value && field.rules.pattern && !field.rules.pattern.test(value)) {
errors.push('Invalid format');
}
this.showErrors(field.element, errors);
return errors.length === 0;
}
validateAll() {
let isValid = true;
Object.values(this.fields).forEach(field => {
if (!this.validateField(field)) {
isValid = false;
}
});
return isValid;
}
showErrors(element, errors) {
const errorContainer = element.parentElement.querySelector('.error-message') ||
this.createErrorContainer(element);
errorContainer.textContent = errors[0] || '';
element.classList.toggle('invalid', errors.length > 0);
element.classList.toggle('valid', errors.length === 0 && element.value);
}
createErrorContainer(element) {
const container = document.createElement('div');
container.className = 'error-message';
element.parentElement.appendChild(container);
return container;
}
}
// 3. Drag and Drop
class DragDropList {
constructor(container) {
this.container = container;
this.draggingElement = null;
this.placeholder = null;
this.init();
}
init() {
this.container.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('draggable-item')) {
this.dragStart(e);
}
});
this.container.addEventListener('dragend', (e) => {
if (e.target.classList.contains('draggable-item')) {
this.dragEnd(e);
}
});
this.container.addEventListener('dragover', (e) => {
e.preventDefault();
this.dragOver(e);
});
this.container.addEventListener('drop', (e) => {
e.preventDefault();
this.drop(e);
});
}
dragStart(e) {
this.draggingElement = e.target;
this.draggingElement.classList.add('dragging');
// Create placeholder
this.placeholder = document.createElement('div');
this.placeholder.className = 'placeholder';
this.placeholder.style.height = `${this.draggingElement.offsetHeight}px`;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.draggingElement.innerHTML);
setTimeout(() => {
this.draggingElement.style.display = 'none';
this.draggingElement.parentNode.insertBefore(this.placeholder, this.draggingElement);
}, 0);
}
dragEnd(e) {
this.draggingElement.style.display = '';
this.draggingElement.classList.remove('dragging');
if (this.placeholder && this.placeholder.parentNode) {
this.placeholder.parentNode.removeChild(this.placeholder);
}
this.draggingElement = null;
this.placeholder = null;
}
dragOver(e) {
if (!this.draggingElement) return;
const target = e.target.closest('.draggable-item');
if (target && target !== this.placeholder && target !== this.draggingElement) {
const rect = target.getBoundingClientRect();
const offset = e.clientY - rect.top;
if (offset > rect.height / 2) {
target.parentNode.insertBefore(this.placeholder, target.nextSibling);
} else {
target.parentNode.insertBefore(this.placeholder, target);
}
}
}
drop(e) {
if (this.placeholder && this.draggingElement) {
this.placeholder.parentNode.insertBefore(this.draggingElement, this.placeholder);
}
}
}
// 4. Custom Context Menu
class ContextMenu {
constructor(options) {
this.menu = this.createMenu(options);
this.currentTarget = null;
this.init();
}
createMenu(options) {
const menu = document.createElement('div');
menu.className = 'context-menu';
options.items.forEach(item => {
const menuItem = document.createElement('div');
menuItem.className = 'context-menu-item';
menuItem.textContent = item.label;
menuItem.addEventListener('click', () => {
item.action(this.currentTarget);
this.hide();
});
menu.appendChild(menuItem);
});
document.body.appendChild(menu);
return menu;
}
init() {
document.addEventListener('contextmenu', (e) => {
if (e.target.matches(options.selector)) {
e.preventDefault();
this.show(e);
}
});
document.addEventListener('click', () => {
this.hide();
});
window.addEventListener('scroll', () => {
this.hide();
});
}
show(e) {
this.currentTarget = e.target;
this.menu.style.left = `${e.pageX}px`;
this.menu.style.top = `${e.pageY}px`;
this.menu.classList.add('visible');
// Adjust position if menu goes off screen
const rect = this.menu.getBoundingClientRect();
if (rect.right > window.innerWidth) {
this.menu.style.left = `${e.pageX - rect.width}px`;
}
if (rect.bottom > window.innerHeight) {
this.menu.style.top = `${e.pageY - rect.height}px`;
}
}
hide() {
this.menu.classList.remove('visible');
this.currentTarget = null;
}
}
Summary
Event handling is the foundation of interactive web applications:
- Use addEventListener for flexible event handling
- The event object provides valuable information about the event
- Common events include mouse, keyboard, form, and window events
- Custom events enable component communication
- Consider performance with event delegation and throttling
- Always clean up event listeners to prevent memory leaks
Next up: We'll explore event propagation and delegation in detail!