javascript

exercises

exercises.js
/**
 * ============================================================
 * 10.3 Events and Event Handling - Exercises
 * ============================================================
 *
 * Practice event handling including adding listeners,
 * working with the event object, and controlling propagation.
 *
 * Instructions:
 * 1. Read each exercise description carefully
 * 2. Write your solution in the provided function
 * 3. Test with the provided HTML structure
 * 4. Check the hints and solutions if needed
 */

// =============================================================
// EXERCISE 1: Add Click Handler
// =============================================================
/**
 * Add a click handler to a button that logs a message
 * and changes the button's text
 *
 * @param {string} buttonId - The ID of the button
 * @param {string} message - Message to log on click
 * @returns {boolean} - True if successful
 *
 * Example:
 * addClickHandler("btn", "Button was clicked!")
 * → Button click logs message and changes text to "Clicked!"
 */
function addClickHandler(buttonId, message) {
  // Your code here
}

// Test
// addClickHandler("testButton", "You clicked the button!");

/*
 * HINT: Use addEventListener and modify textContent on click
 *
 * SOLUTION:
 * function addClickHandler(buttonId, message) {
 *     const button = document.getElementById(buttonId);
 *     if (!button) return false;
 *
 *     button.addEventListener("click", () => {
 *         console.log(message);
 *         button.textContent = "Clicked!";
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 2: Toggle Handler
// =============================================================
/**
 * Add a handler that toggles a class on click
 * Return function to remove the handler
 *
 * @param {string} elementId - The ID of the element
 * @param {string} className - The class to toggle
 * @returns {Function} - Function to remove the handler
 *
 * Example:
 * const remove = addToggleHandler("box", "active");
 * // Click toggles "active" class
 * remove(); // Removes the handler
 */
function addToggleHandler(elementId, className) {
  // Your code here
}

// Test
// const removeToggle = addToggleHandler("toggleBox", "highlighted");

/*
 * HINT: Store the handler function to return it for removal
 *
 * SOLUTION:
 * function addToggleHandler(elementId, className) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return () => {};
 *
 *     function handler() {
 *         element.classList.toggle(className);
 *     }
 *
 *     element.addEventListener("click", handler);
 *
 *     return function() {
 *         element.removeEventListener("click", handler);
 *     };
 * }
 */

// =============================================================
// EXERCISE 3: One-Time Handler
// =============================================================
/**
 * Add a handler that runs only once, then removes itself
 *
 * @param {string} elementId - The ID of the element
 * @param {string} eventType - Type of event to listen for
 * @param {Function} callback - Function to run once
 * @returns {boolean} - True if successful
 *
 * Example:
 * addOneTimeHandler("btn", "click", () => console.log("First click only!"));
 */
function addOneTimeHandler(elementId, eventType, callback) {
  // Your code here
}

// Test
// addOneTimeHandler("initBtn", "click", () => alert("Initialized!"));

/*
 * HINT: Use the { once: true } option
 *
 * SOLUTION:
 * function addOneTimeHandler(elementId, eventType, callback) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return false;
 *
 *     element.addEventListener(eventType, callback, { once: true });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 4: Log Event Details
// =============================================================
/**
 * Add a click handler that logs key event properties
 * Return an object with the captured event details
 *
 * @param {string} elementId - The ID of the element
 * @returns {Object|null} - Object that will contain last event details
 *
 * The returned object should track:
 * - type, target tagName, timestamp, clientX, clientY
 */
function createEventLogger(elementId) {
  // Your code here
}

// Test
// const logger = createEventLogger("logBox");
// // Click the element, then:
// console.log(logger);

/*
 * HINT: Return an object and update it inside the handler
 *
 * SOLUTION:
 * function createEventLogger(elementId) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return null;
 *
 *     const eventData = {};
 *
 *     element.addEventListener("click", (event) => {
 *         eventData.type = event.type;
 *         eventData.target = event.target.tagName;
 *         eventData.timestamp = event.timeStamp;
 *         eventData.clientX = event.clientX;
 *         eventData.clientY = event.clientY;
 *
 *         console.log("Event logged:", eventData);
 *     });
 *
 *     return eventData;
 * }
 */

// =============================================================
// EXERCISE 5: Keyboard Shortcut
// =============================================================
/**
 * Register a keyboard shortcut handler
 *
 * @param {Object} shortcut - Shortcut configuration
 * @param {string} shortcut.key - Key to press
 * @param {boolean} shortcut.ctrlKey - Require Ctrl?
 * @param {boolean} shortcut.shiftKey - Require Shift?
 * @param {Function} callback - Function to call when shortcut pressed
 * @returns {Function} - Function to unregister the shortcut
 *
 * Example:
 * const unregister = registerShortcut(
 *     { key: "s", ctrlKey: true },
 *     () => console.log("Save!")
 * );
 */
function registerShortcut(shortcut, callback) {
  // Your code here
}

// Test
// const unregister = registerShortcut({ key: "k", ctrlKey: true }, () => alert("Search!"));

/*
 * HINT: Check all modifier conditions before calling callback
 *
 * SOLUTION:
 * function registerShortcut(shortcut, callback) {
 *     function handler(event) {
 *         const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase();
 *         const ctrlMatch = !shortcut.ctrlKey || event.ctrlKey;
 *         const shiftMatch = !shortcut.shiftKey || event.shiftKey;
 *         const altMatch = !shortcut.altKey || event.altKey;
 *
 *         if (keyMatch && ctrlMatch && shiftMatch && altMatch) {
 *             event.preventDefault();
 *             callback(event);
 *         }
 *     }
 *
 *     document.addEventListener("keydown", handler);
 *
 *     return function() {
 *         document.removeEventListener("keydown", handler);
 *     };
 * }
 */

// =============================================================
// EXERCISE 6: Mouse Position Tracker
// =============================================================
/**
 * Track mouse position within an element
 * Display coordinates in a specified output element
 *
 * @param {string} trackerId - ID of element to track mouse in
 * @param {string} outputId - ID of element to display coordinates
 * @returns {Object} - { start, stop } to control tracking
 *
 * Example:
 * const tracker = createMouseTracker("canvas", "coords");
 * tracker.start(); // Begin tracking
 * tracker.stop();  // Stop tracking
 */
function createMouseTracker(trackerId, outputId) {
  // Your code here
}

// Test
// const tracker = createMouseTracker("trackArea", "positionDisplay");

/*
 * HINT: Store handler reference to enable start/stop
 *
 * SOLUTION:
 * function createMouseTracker(trackerId, outputId) {
 *     const tracker = document.getElementById(trackerId);
 *     const output = document.getElementById(outputId);
 *
 *     if (!tracker || !output) return null;
 *
 *     function handler(event) {
 *         const x = event.offsetX;
 *         const y = event.offsetY;
 *         output.textContent = `X: ${x}, Y: ${y}`;
 *     }
 *
 *     return {
 *         start() {
 *             tracker.addEventListener("mousemove", handler);
 *         },
 *         stop() {
 *             tracker.removeEventListener("mousemove", handler);
 *             output.textContent = "";
 *         }
 *     };
 * }
 */

// =============================================================
// EXERCISE 7: Stop Propagation
// =============================================================
/**
 * Create a click handler that stops propagation
 * Parent click handlers should not fire
 *
 * @param {string} childId - ID of child element
 * @param {Function} childCallback - Function to run on child click
 * @returns {boolean} - True if successful
 *
 * Example:
 * createIsolatedHandler("innerBtn", () => console.log("Only this runs"));
 */
function createIsolatedHandler(childId, childCallback) {
  // Your code here
}

// Test
// createIsolatedHandler("innerButton", () => console.log("Inner only!"));

/*
 * HINT: Use event.stopPropagation() inside the handler
 *
 * SOLUTION:
 * function createIsolatedHandler(childId, childCallback) {
 *     const child = document.getElementById(childId);
 *     if (!child) return false;
 *
 *     child.addEventListener("click", (event) => {
 *         event.stopPropagation();
 *         childCallback(event);
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 8: Prevent Default
// =============================================================
/**
 * Create a link handler that prevents navigation
 * and runs custom code instead
 *
 * @param {string} linkId - ID of the link element
 * @param {Function} customAction - Action to run instead of navigation
 * @returns {boolean} - True if successful
 *
 * Example:
 * createCustomLink("navLink", () => showModal());
 */
function createCustomLink(linkId, customAction) {
  // Your code here
}

// Test
// createCustomLink("myLink", () => console.log("Custom action instead of navigation"));

/*
 * HINT: Use event.preventDefault() then call the custom action
 *
 * SOLUTION:
 * function createCustomLink(linkId, customAction) {
 *     const link = document.getElementById(linkId);
 *     if (!link) return false;
 *
 *     link.addEventListener("click", (event) => {
 *         event.preventDefault();
 *         customAction(event.target.href);
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 9: Hover Effects
// =============================================================
/**
 * Add hover effects to an element using mouseenter/mouseleave
 *
 * @param {string} elementId - ID of the element
 * @param {Object} styles - { hover: {...}, normal: {...} } style objects
 * @returns {boolean} - True if successful
 *
 * Example:
 * addHoverEffect("card", {
 *     hover: { transform: "scale(1.05)", boxShadow: "0 4px 8px rgba(0,0,0,0.2)" },
 *     normal: { transform: "scale(1)", boxShadow: "none" }
 * });
 */
function addHoverEffect(elementId, styles) {
  // Your code here
}

// Test
// addHoverEffect("hoverCard", { hover: { opacity: "0.8" }, normal: { opacity: "1" } });

/*
 * HINT: Apply hover styles on mouseenter, normal styles on mouseleave
 *
 * SOLUTION:
 * function addHoverEffect(elementId, styles) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return false;
 *
 *     element.addEventListener("mouseenter", () => {
 *         Object.assign(element.style, styles.hover);
 *     });
 *
 *     element.addEventListener("mouseleave", () => {
 *         Object.assign(element.style, styles.normal);
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 10: Input Debounce
// =============================================================
/**
 * Create a debounced input handler
 * Only fires callback after user stops typing for specified delay
 *
 * @param {string} inputId - ID of the input element
 * @param {Function} callback - Function to call with input value
 * @param {number} delay - Debounce delay in milliseconds
 * @returns {boolean} - True if successful
 *
 * Example:
 * addDebouncedInput("searchBox", (value) => search(value), 300);
 */
function addDebouncedInput(inputId, callback, delay = 300) {
  // Your code here
}

// Test
// addDebouncedInput("search", (val) => console.log("Searching:", val), 500);

/*
 * HINT: Use setTimeout and clearTimeout for debouncing
 *
 * SOLUTION:
 * function addDebouncedInput(inputId, callback, delay = 300) {
 *     const input = document.getElementById(inputId);
 *     if (!input) return false;
 *
 *     let timeoutId;
 *
 *     input.addEventListener("input", (event) => {
 *         clearTimeout(timeoutId);
 *         timeoutId = setTimeout(() => {
 *             callback(event.target.value);
 *         }, delay);
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 11: Form Submission Handler
// =============================================================
/**
 * Handle form submission with validation
 * Prevent default, validate fields, call callback with data
 *
 * @param {string} formId - ID of the form element
 * @param {Function} onSubmit - Callback with form data object
 * @param {Function} onError - Callback with validation errors
 * @returns {boolean} - True if successful
 *
 * Example:
 * handleFormSubmit("loginForm",
 *     (data) => console.log("Submit:", data),
 *     (errors) => console.log("Errors:", errors)
 * );
 */
function handleFormSubmit(formId, onSubmit, onError) {
  // Your code here
}

// Test
// handleFormSubmit("myForm", console.log, console.error);

/*
 * HINT: Use FormData API and check required fields
 *
 * SOLUTION:
 * function handleFormSubmit(formId, onSubmit, onError) {
 *     const form = document.getElementById(formId);
 *     if (!form) return false;
 *
 *     form.addEventListener("submit", (event) => {
 *         event.preventDefault();
 *
 *         const formData = new FormData(form);
 *         const data = Object.fromEntries(formData);
 *         const errors = [];
 *
 *         // Check required fields
 *         const requiredFields = form.querySelectorAll("[required]");
 *         requiredFields.forEach(field => {
 *             if (!field.value.trim()) {
 *                 errors.push(`${field.name || field.id} is required`);
 *             }
 *         });
 *
 *         if (errors.length > 0) {
 *             onError(errors);
 *         } else {
 *             onSubmit(data);
 *         }
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 12: Click Outside Handler
// =============================================================
/**
 * Add handler that fires when clicking outside an element
 * Useful for closing modals/dropdowns
 *
 * @param {string} elementId - ID of the element
 * @param {Function} callback - Function to call on outside click
 * @returns {Function} - Function to remove the handler
 *
 * Example:
 * const remove = onClickOutside("dropdown", () => closeDropdown());
 */
function onClickOutside(elementId, callback) {
  // Your code here
}

// Test
// const removeOutsideHandler = onClickOutside("modal", () => modal.close());

/*
 * HINT: Check if event.target is inside the element using contains()
 *
 * SOLUTION:
 * function onClickOutside(elementId, callback) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return () => {};
 *
 *     function handler(event) {
 *         if (!element.contains(event.target)) {
 *             callback(event);
 *         }
 *     }
 *
 *     // Use setTimeout to avoid triggering on the same click that opened it
 *     setTimeout(() => {
 *         document.addEventListener("click", handler);
 *     }, 0);
 *
 *     return function() {
 *         document.removeEventListener("click", handler);
 *     };
 * }
 */

// =============================================================
// EXERCISE 13: Double Click Prevention
// =============================================================
/**
 * Prevent double-click issues on buttons
 * Disable button after click, re-enable after action completes
 *
 * @param {string} buttonId - ID of the button
 * @param {Function} asyncAction - Async function to run on click
 * @returns {boolean} - True if successful
 *
 * Example:
 * preventDoubleClick("submitBtn", async () => {
 *     await saveData();
 * });
 */
function preventDoubleClick(buttonId, asyncAction) {
  // Your code here
}

// Test
// preventDoubleClick("saveBtn", async () => { await delay(1000); console.log("Saved!"); });

/*
 * HINT: Set disabled attribute, run action, then remove disabled
 *
 * SOLUTION:
 * function preventDoubleClick(buttonId, asyncAction) {
 *     const button = document.getElementById(buttonId);
 *     if (!button) return false;
 *
 *     button.addEventListener("click", async () => {
 *         if (button.disabled) return;
 *
 *         button.disabled = true;
 *         const originalText = button.textContent;
 *         button.textContent = "Processing...";
 *
 *         try {
 *             await asyncAction();
 *         } finally {
 *             button.disabled = false;
 *             button.textContent = originalText;
 *         }
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 14: Custom Event Emitter
// =============================================================
/**
 * Create a custom event system for an element
 * Support emit, on, and off methods
 *
 * @param {string} elementId - ID of the element
 * @returns {Object} - { emit, on, off } methods
 *
 * Example:
 * const events = createEventEmitter("widget");
 * events.on("update", (data) => console.log(data));
 * events.emit("update", { value: 42 });
 */
function createEventEmitter(elementId) {
  // Your code here
}

// Test
// const emitter = createEventEmitter("myWidget");
// emitter.on("change", (data) => console.log("Changed:", data));
// emitter.emit("change", { newValue: 10 });

/*
 * HINT: Use CustomEvent with detail property for data
 *
 * SOLUTION:
 * function createEventEmitter(elementId) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return null;
 *
 *     const handlers = new Map();
 *
 *     return {
 *         emit(eventName, data) {
 *             const event = new CustomEvent(eventName, {
 *                 bubbles: true,
 *                 detail: data
 *             });
 *             element.dispatchEvent(event);
 *         },
 *
 *         on(eventName, handler) {
 *             const wrappedHandler = (e) => handler(e.detail);
 *             handlers.set(handler, wrappedHandler);
 *             element.addEventListener(eventName, wrappedHandler);
 *         },
 *
 *         off(eventName, handler) {
 *             const wrappedHandler = handlers.get(handler);
 *             if (wrappedHandler) {
 *                 element.removeEventListener(eventName, wrappedHandler);
 *                 handlers.delete(handler);
 *             }
 *         }
 *     };
 * }
 */

// =============================================================
// EXERCISE 15: Event Bus
// =============================================================
/**
 * Create a global event bus for component communication
 *
 * @returns {Object} - { subscribe, publish, unsubscribe } methods
 *
 * Example:
 * const bus = createEventBus();
 * const id = bus.subscribe("user:login", (user) => console.log(user));
 * bus.publish("user:login", { name: "John" });
 * bus.unsubscribe(id);
 */
function createEventBus() {
  // Your code here
}

// Test
// const bus = createEventBus();
// bus.subscribe("test", (data) => console.log("Received:", data));
// bus.publish("test", { message: "Hello!" });

/*
 * HINT: Use a Map to store subscribers with unique IDs
 *
 * SOLUTION:
 * function createEventBus() {
 *     const subscribers = new Map();
 *     let nextId = 1;
 *
 *     return {
 *         subscribe(event, callback) {
 *             const id = nextId++;
 *
 *             if (!subscribers.has(event)) {
 *                 subscribers.set(event, new Map());
 *             }
 *
 *             subscribers.get(event).set(id, callback);
 *             return id;
 *         },
 *
 *         publish(event, data) {
 *             const eventSubscribers = subscribers.get(event);
 *             if (eventSubscribers) {
 *                 eventSubscribers.forEach(callback => {
 *                     callback(data);
 *                 });
 *             }
 *         },
 *
 *         unsubscribe(id) {
 *             subscribers.forEach(eventSubscribers => {
 *                 eventSubscribers.delete(id);
 *             });
 *         }
 *     };
 * }
 */

// =============================================================
// BONUS CHALLENGE: Gesture Detector
// =============================================================
/**
 * Detect basic gestures (swipe, long press, double tap)
 *
 * @param {string} elementId - ID of the element
 * @param {Object} handlers - { onSwipe, onLongPress, onDoubleTap }
 * @returns {Object} - { destroy } to clean up listeners
 *
 * Example:
 * const gesture = createGestureDetector("touchArea", {
 *     onSwipe: (direction) => console.log("Swiped:", direction),
 *     onLongPress: () => console.log("Long pressed!"),
 *     onDoubleTap: () => console.log("Double tapped!")
 * });
 */
function createGestureDetector(elementId, handlers) {
  // Your code here
}

/*
 * SOLUTION:
 * function createGestureDetector(elementId, handlers) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return null;
 *
 *     let startX, startY, startTime;
 *     let longPressTimer;
 *     let lastTap = 0;
 *
 *     const SWIPE_THRESHOLD = 50;
 *     const LONG_PRESS_DURATION = 500;
 *     const DOUBLE_TAP_DELAY = 300;
 *
 *     function handleStart(event) {
 *         const touch = event.touches?.[0] || event;
 *         startX = touch.clientX;
 *         startY = touch.clientY;
 *         startTime = Date.now();
 *
 *         longPressTimer = setTimeout(() => {
 *             if (handlers.onLongPress) {
 *                 handlers.onLongPress();
 *             }
 *         }, LONG_PRESS_DURATION);
 *     }
 *
 *     function handleEnd(event) {
 *         clearTimeout(longPressTimer);
 *
 *         const touch = event.changedTouches?.[0] || event;
 *         const deltaX = touch.clientX - startX;
 *         const deltaY = touch.clientY - startY;
 *         const duration = Date.now() - startTime;
 *
 *         // Swipe detection
 *         if (duration < 300) {
 *             if (Math.abs(deltaX) > SWIPE_THRESHOLD && handlers.onSwipe) {
 *                 handlers.onSwipe(deltaX > 0 ? "right" : "left");
 *             } else if (Math.abs(deltaY) > SWIPE_THRESHOLD && handlers.onSwipe) {
 *                 handlers.onSwipe(deltaY > 0 ? "down" : "up");
 *             }
 *         }
 *
 *         // Double tap detection
 *         const now = Date.now();
 *         if (now - lastTap < DOUBLE_TAP_DELAY && handlers.onDoubleTap) {
 *             handlers.onDoubleTap();
 *             lastTap = 0;
 *         } else {
 *             lastTap = now;
 *         }
 *     }
 *
 *     function handleMove() {
 *         clearTimeout(longPressTimer);
 *     }
 *
 *     element.addEventListener("mousedown", handleStart);
 *     element.addEventListener("mouseup", handleEnd);
 *     element.addEventListener("mousemove", handleMove);
 *     element.addEventListener("touchstart", handleStart);
 *     element.addEventListener("touchend", handleEnd);
 *     element.addEventListener("touchmove", handleMove);
 *
 *     return {
 *         destroy() {
 *             element.removeEventListener("mousedown", handleStart);
 *             element.removeEventListener("mouseup", handleEnd);
 *             element.removeEventListener("mousemove", handleMove);
 *             element.removeEventListener("touchstart", handleStart);
 *             element.removeEventListener("touchend", handleEnd);
 *             element.removeEventListener("touchmove", handleMove);
 *         }
 *     };
 * }
 */

// =============================================================
// Test HTML Template
// =============================================================
/*
Create an HTML file with this structure to test the exercises:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Event Handling Exercises</title>
    <style>
        .box { width: 100px; height: 100px; background: #3498db; margin: 10px; cursor: pointer; }
        .highlighted { background: #e74c3c; transform: scale(1.1); }
        .error { border: 2px solid red; }
        #trackArea { width: 300px; height: 200px; background: #ecf0f1; }
        #positionDisplay { font-family: monospace; }
        .modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); }
        .modal.open { display: flex; align-items: center; justify-content: center; }
    </style>
</head>
<body>
    <button id="testButton">Click Me</button>
    <div id="toggleBox" class="box"></div>
    <a id="myLink" href="https://example.com">Custom Link</a>
    
    <div id="trackArea">Move mouse here</div>
    <div id="positionDisplay"></div>
    
    <form id="myForm">
        <input name="email" type="email" required>
        <input name="password" type="password" required>
        <button type="submit">Submit</button>
    </form>
    
    <input id="search" type="text" placeholder="Search...">
    
    <div id="dropdown">
        <button>Toggle Dropdown</button>
        <div class="dropdown-content">Dropdown content</div>
    </div>
    
    <button id="saveBtn">Save</button>
    
    <div id="touchArea" style="width: 200px; height: 200px; background: #9b59b6; color: white;">
        Gesture Area
    </div>

    <script src="exercises.js"></script>
</body>
</html>
*/

// =============================================================
// Summary
// =============================================================
console.log('='.repeat(60));
console.log('10.3 Events and Event Handling - Exercises Loaded');
console.log('='.repeat(60));
console.log('Exercises 1-3:   Basic event listeners');
console.log('Exercises 4-6:   Event object and properties');
console.log('Exercises 7-8:   Propagation control');
console.log('Exercises 9-10:  Mouse and input events');
console.log('Exercises 11-13: Form and button handling');
console.log('Exercises 14-15: Custom event systems');
console.log('Bonus:           Gesture detection');
console.log('='.repeat(60));
Exercises - JavaScript Tutorial | DeepML