Event Listeners and Handlers

Making Web Pages Interactive with JavaScript Events

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.

graph LR A[User Action] --> B[Event Triggered] B --> C[Event Listener] C --> D[Event Handler] D --> E[Response/Action] style A fill:#f9f,stroke:#333,stroke-width:4px style E fill:#bfb,stroke:#333,stroke-width:4px

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

// 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

  1. Create a keyboard-controlled game (like Snake or Tetris)
  2. Build a form with real-time validation
  3. Implement drag-and-drop functionality
  4. 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:

Next up: We'll explore event propagation and delegation in detail!