לדלג לתוכן

3.4 אירועים פתרון

פתרון - אירועים

פתרון תרגיל 1

<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>Mouse Events</title>
</head>
<body>
    <div id="box" style="width: 200px; height: 200px; background: lightblue; border: 2px solid navy;"></div>
    <p id="info">Interact with the box above</p>

    <script>
        const box = document.getElementById("box");
        const info = document.getElementById("info");

        function randomColor() {
            return "#" + Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0");
        }

        // 1. click - random color
        box.addEventListener("click", function() {
            box.style.backgroundColor = randomColor();
        });

        // 2. double click - reset color
        box.addEventListener("dblclick", function() {
            box.style.backgroundColor = "lightblue";
        });

        // 3. mouse enter - thick border
        box.addEventListener("mouseenter", function() {
            box.style.borderWidth = "5px";
        });

        // 4. mouse leave - reset border
        box.addEventListener("mouseleave", function() {
            box.style.borderWidth = "2px";
        });

        // 5. mouse move - show coordinates
        box.addEventListener("mousemove", function(event) {
            info.textContent = "Mouse: X=" + event.clientX + ", Y=" + event.clientY;
        });

        // 6. context menu - prevent and show message
        box.addEventListener("contextmenu", function(event) {
            event.preventDefault();
            info.textContent = "Right-click is disabled on this box!";
        });
    </script>
</body>
</html>

פתרון תרגיל 2

<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>Keyboard Events</title>
</head>
<body>
    <input type="text" id="text-input" placeholder="Type something..." style="width: 300px; padding: 8px;">
    <p>Characters typed: <span id="char-count">0</span></p>
    <p>Last key: <span id="last-key">none</span></p>
    <p>Key code: <span id="key-code">none</span></p>
    <p>Modifiers: <span id="modifiers">none</span></p>
    <div id="output" style="border: 1px solid #ddd; padding: 10px; min-height: 50px; margin-top: 10px;"></div>

    <script>
        const input = document.getElementById("text-input");
        const charCount = document.getElementById("char-count");
        const lastKey = document.getElementById("last-key");
        const keyCode = document.getElementById("key-code");
        const modifiers = document.getElementById("modifiers");
        const output = document.getElementById("output");

        input.addEventListener("keydown", function(event) {
            // 5. prevent numbers
            if (event.key >= "0" && event.key <= "9") {
                event.preventDefault();
                return;
            }

            // 1. update last key and key code
            lastKey.textContent = event.key;
            keyCode.textContent = event.code;

            // 2. show modifiers
            const mods = [];
            if (event.shiftKey) mods.push("Shift");
            if (event.ctrlKey) mods.push("Ctrl");
            if (event.altKey) mods.push("Alt");
            if (event.metaKey) mods.push("Meta");
            modifiers.textContent = mods.length > 0 ? mods.join(" + ") : "none";

            // 3. Enter - add text to output
            if (event.key === "Enter") {
                const text = input.value.trim();
                if (text !== "") {
                    const p = document.createElement("p");
                    p.textContent = text;
                    output.appendChild(p);
                    input.value = "";
                }
            }

            // 4. Escape - clear input
            if (event.key === "Escape") {
                input.value = "";
            }
        });

        // update character count on input event
        input.addEventListener("input", function() {
            charCount.textContent = input.value.length;
        });
    </script>
</body>
</html>

פתרון תרגיל 3

<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>Form Validation</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; max-width: 400px; }
        .form-group { margin: 15px 0; }
        label { display: block; margin-bottom: 5px; }
        input { padding: 8px; width: 100%; border: 2px solid #ddd; border-radius: 4px; box-sizing: border-box; }
        input.valid { border-color: green; }
        input.invalid { border-color: red; }
        .feedback { font-size: 13px; display: block; margin-top: 3px; }
        .feedback.error { color: red; }
        .feedback.success { color: green; }
        button { padding: 10px 20px; margin-top: 10px; }
        button:disabled { opacity: 0.5; cursor: not-allowed; }
    </style>
</head>
<body>
    <h1>Registration</h1>
    <form id="signup-form">
        <div class="form-group">
            <label>Username:</label>
            <input type="text" id="username" required>
            <span class="feedback" id="username-fb"></span>
        </div>
        <div class="form-group">
            <label>Email:</label>
            <input type="email" id="email" required>
            <span class="feedback" id="email-fb"></span>
        </div>
        <div class="form-group">
            <label>Password:</label>
            <input type="password" id="password" required>
            <span class="feedback" id="password-fb"></span>
        </div>
        <div class="form-group">
            <label>Confirm Password:</label>
            <input type="password" id="confirm-password" required>
            <span class="feedback" id="confirm-fb"></span>
        </div>
        <button type="submit" id="submit-btn" disabled>Register</button>
    </form>

    <script>
        const form = document.getElementById("signup-form");
        const usernameInput = document.getElementById("username");
        const emailInput = document.getElementById("email");
        const passwordInput = document.getElementById("password");
        const confirmInput = document.getElementById("confirm-password");
        const submitBtn = document.getElementById("submit-btn");

        // track validity of each field
        const validity = {
            username: false,
            email: false,
            password: false,
            confirm: false
        };

        function updateSubmitButton() {
            const allValid = validity.username && validity.email &&
                             validity.password && validity.confirm;
            submitBtn.disabled = !allValid;
        }

        function setFeedback(id, isValid, message) {
            const fb = document.getElementById(id);
            fb.textContent = message;
            fb.className = "feedback " + (isValid ? "success" : "error");
        }

        // 1. username validation on input
        usernameInput.addEventListener("input", function() {
            const value = usernameInput.value.trim();
            if (value.length === 0) {
                usernameInput.className = "";
                document.getElementById("username-fb").textContent = "";
                validity.username = false;
            } else if (value.length < 3) {
                usernameInput.className = "invalid";
                setFeedback("username-fb", false, "At least 3 characters required");
                validity.username = false;
            } else {
                usernameInput.className = "valid";
                setFeedback("username-fb", true, "Looks good!");
                validity.username = true;
            }
            updateSubmitButton();
        });

        // 2. email validation on blur
        emailInput.addEventListener("blur", function() {
            const value = emailInput.value.trim();
            if (value === "") {
                emailInput.className = "";
                document.getElementById("email-fb").textContent = "";
                validity.email = false;
            } else if (value.includes("@") && value.includes(".")) {
                emailInput.className = "valid";
                setFeedback("email-fb", true, "Valid email!");
                validity.email = true;
            } else {
                emailInput.className = "invalid";
                setFeedback("email-fb", false, "Enter a valid email (must contain @ and .)");
                validity.email = false;
            }
            updateSubmitButton();
        });

        // 3. password validation on input
        passwordInput.addEventListener("input", function() {
            const value = passwordInput.value;
            const hasUpperCase = /[A-Z]/.test(value);
            const hasNumber = /[0-9]/.test(value);
            const hasMinLength = value.length >= 8;

            if (value.length === 0) {
                passwordInput.className = "";
                document.getElementById("password-fb").textContent = "";
                validity.password = false;
            } else if (!hasMinLength) {
                passwordInput.className = "invalid";
                setFeedback("password-fb", false, "At least 8 characters (" + value.length + "/8)");
                validity.password = false;
            } else if (!hasUpperCase) {
                passwordInput.className = "invalid";
                setFeedback("password-fb", false, "Must contain at least one uppercase letter");
                validity.password = false;
            } else if (!hasNumber) {
                passwordInput.className = "invalid";
                setFeedback("password-fb", false, "Must contain at least one number");
                validity.password = false;
            } else {
                passwordInput.className = "valid";
                setFeedback("password-fb", true, "Strong password!");
                validity.password = true;
            }

            // re-validate confirm password if it has content
            if (confirmInput.value.length > 0) {
                validateConfirmPassword();
            }
            updateSubmitButton();
        });

        // 4. confirm password validation on input
        function validateConfirmPassword() {
            const value = confirmInput.value;
            if (value.length === 0) {
                confirmInput.className = "";
                document.getElementById("confirm-fb").textContent = "";
                validity.confirm = false;
            } else if (value !== passwordInput.value) {
                confirmInput.className = "invalid";
                setFeedback("confirm-fb", false, "Passwords do not match");
                validity.confirm = false;
            } else {
                confirmInput.className = "valid";
                setFeedback("confirm-fb", true, "Passwords match!");
                validity.confirm = true;
            }
            updateSubmitButton();
        }

        confirmInput.addEventListener("input", validateConfirmPassword);

        // 6. form submission
        form.addEventListener("submit", function(event) {
            event.preventDefault();
            alert("Registration successful!\n\n" +
                  "Username: " + usernameInput.value + "\n" +
                  "Email: " + emailInput.value);
        });
    </script>
</body>
</html>

פתרון תרגיל 4

<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>Window Events</title>
    <style>
        body { height: 3000px; font-family: Arial, sans-serif; padding: 20px; }
        #info-panel { position: fixed; top: 10px; left: 10px; background: rgba(0,0,0,0.8); color: white;
                      padding: 15px; border-radius: 8px; z-index: 100; }
        #back-to-top { position: fixed; bottom: 20px; left: 20px; padding: 10px 20px;
                       background: #2196F3; color: white; border: none; border-radius: 5px;
                       cursor: pointer; display: none; z-index: 100; }
    </style>
</head>
<body>
    <div id="info-panel">
        <p>Window: <span id="window-size">-</span></p>
        <p>Scroll: <span id="scroll-pos">0</span>px</p>
    </div>
    <button id="back-to-top">Back to Top</button>

    <h1>Scroll Down to Test</h1>
    <p>There is a lot of space below...</p>

    <script>
        const windowSize = document.getElementById("window-size");
        const scrollPos = document.getElementById("scroll-pos");
        const backToTop = document.getElementById("back-to-top");

        // 1. show and update window size
        function updateWindowSize() {
            windowSize.textContent = window.innerWidth + " x " + window.innerHeight;
        }
        updateWindowSize();
        window.addEventListener("resize", updateWindowSize);

        // 2. update scroll position
        window.addEventListener("scroll", function() {
            scrollPos.textContent = Math.round(window.scrollY);

            // 3. show/hide back to top button
            if (window.scrollY > 300) {
                backToTop.style.display = "block";
            } else {
                backToTop.style.display = "none";
            }
        });

        // back to top button
        backToTop.addEventListener("click", function() {
            window.scrollTo({ top: 0, behavior: "smooth" });
        });

        // 4. warn before leaving
        window.addEventListener("beforeunload", function(event) {
            event.preventDefault();
            event.returnValue = "";
        });
    </script>
</body>
</html>

פתרון תרגיל 5

<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>Click Speed Test</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; text-align: center; }
        button { padding: 15px 30px; font-size: 18px; margin: 10px; cursor: pointer; }
        #click-btn { background: #4CAF50; color: white; border: none; border-radius: 8px; }
        #click-btn:disabled { background: #ccc; cursor: not-allowed; }
        #result { font-size: 24px; font-weight: bold; color: #2196F3; }
    </style>
</head>
<body>
    <h1>Click Speed Test</h1>
    <p>Click the button as fast as you can in 10 seconds!</p>
    <button id="click-btn" disabled>Click Me!</button>
    <button id="start-btn">Start Game</button>
    <p>Clicks: <span id="click-count">0</span></p>
    <p>Time left: <span id="timer">10</span> seconds</p>
    <p id="result"></p>

    <script>
        const clickBtn = document.getElementById("click-btn");
        const startBtn = document.getElementById("start-btn");
        const clickCountEl = document.getElementById("click-count");
        const timerEl = document.getElementById("timer");
        const resultEl = document.getElementById("result");

        let clickCount = 0;
        let timeLeft = 10;
        let timerInterval = null;

        function resetGame() {
            clickCount = 0;
            timeLeft = 10;
            clickCountEl.textContent = "0";
            timerEl.textContent = "10";
            resultEl.textContent = "";
            clickBtn.disabled = true;
            startBtn.disabled = false;
        }

        // count clicks
        clickBtn.addEventListener("click", function() {
            clickCount++;
            clickCountEl.textContent = clickCount;
        });

        // start game
        startBtn.addEventListener("click", function() {
            resetGame();
            clickBtn.disabled = false;
            startBtn.disabled = true;
            clickBtn.focus();

            timerInterval = setInterval(function() {
                timeLeft--;
                timerEl.textContent = timeLeft;

                if (timeLeft <= 0) {
                    clearInterval(timerInterval);
                    clickBtn.disabled = true;

                    const cps = (clickCount / 10).toFixed(1);
                    resultEl.textContent = "Result: " + clickCount + " clicks (" + cps + " clicks/sec)";

                    startBtn.textContent = "Play Again";
                    startBtn.disabled = false;
                }
            }, 1000);
        });
    </script>
</body>
</html>

פתרון תרגיל 6

<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>Interactive List</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; max-width: 500px; }
        li { padding: 8px; margin: 5px 0; border: 1px solid #eee; border-radius: 4px; display: flex;
             align-items: center; justify-content: space-between; }
        .item-text { flex-grow: 1; }
        .done .item-text { text-decoration: line-through; color: #999; }
        button { margin-left: 5px; padding: 4px 8px; cursor: pointer; }
        .delete-btn { background: #f44336; color: white; border: none; border-radius: 3px; }
        .edit-btn { background: #2196F3; color: white; border: none; border-radius: 3px; }
        #filter { padding: 8px; width: 100%; margin-top: 10px; box-sizing: border-box; }
    </style>
</head>
<body>
    <h2>Interactive List</h2>
    <input type="text" id="new-item" placeholder="New item...">
    <button id="add-btn">Add</button>
    <input type="text" id="filter" placeholder="Filter items...">

    <ul id="item-list"></ul>

    <script>
        const newItemInput = document.getElementById("new-item");
        const addBtn = document.getElementById("add-btn");
        const list = document.getElementById("item-list");
        const filterInput = document.getElementById("filter");

        function createListItem(text) {
            const li = document.createElement("li");

            const span = document.createElement("span");
            span.classList.add("item-text");
            span.textContent = text;

            // 4. double click to toggle done
            span.addEventListener("dblclick", function() {
                li.classList.toggle("done");
            });

            const editBtn = document.createElement("button");
            editBtn.textContent = "Edit";
            editBtn.classList.add("edit-btn");

            // 3. edit button
            editBtn.addEventListener("click", function() {
                const newText = prompt("Edit item:", span.textContent);
                if (newText !== null && newText.trim() !== "") {
                    span.textContent = newText.trim();
                }
            });

            const deleteBtn = document.createElement("button");
            deleteBtn.textContent = "Delete";
            deleteBtn.classList.add("delete-btn");

            // 2. delete button with confirm
            deleteBtn.addEventListener("click", function() {
                if (confirm("Delete '" + span.textContent + "'?")) {
                    li.remove();
                }
            });

            const buttonsDiv = document.createElement("div");
            buttonsDiv.append(editBtn, deleteBtn);
            li.append(span, buttonsDiv);

            return li;
        }

        // 1. add item
        function addItem() {
            const text = newItemInput.value.trim();
            if (text === "") return;

            const li = createListItem(text);
            list.appendChild(li);

            newItemInput.value = "";
            newItemInput.focus();
        }

        addBtn.addEventListener("click", addItem);
        newItemInput.addEventListener("keydown", function(event) {
            if (event.key === "Enter") addItem();
        });

        // add initial items
        list.appendChild(createListItem("Learn JavaScript"));
        list.appendChild(createListItem("Build a project"));

        // 5. filter items
        filterInput.addEventListener("input", function() {
            const filterText = filterInput.value.toLowerCase();
            const items = list.querySelectorAll("li");

            items.forEach(function(item) {
                const text = item.querySelector(".item-text").textContent.toLowerCase();
                if (text.includes(filterText)) {
                    item.style.display = "";
                } else {
                    item.style.display = "none";
                }
            });
        });
    </script>
</body>
</html>

פתרון תרגיל 7

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Drag Element</title>
    <style>
        body { margin: 0; height: 100vh; }
    </style>
</head>
<body>
    <div id="draggable" style="width: 100px; height: 100px; background: coral; position: absolute;
         top: 100px; left: 100px; cursor: grab; display: flex; align-items: center;
         justify-content: center; border-radius: 10px; user-select: none; font-family: Arial;">
        Drag me!
    </div>

    <script>
        const element = document.getElementById("draggable");

        element.addEventListener("mousedown", function(event) {
            // prevent text selection while dragging
            event.preventDefault();

            // calculate offset between mouse and element position
            const offsetX = event.clientX - element.offsetLeft;
            const offsetY = event.clientY - element.offsetTop;

            // change cursor
            element.style.cursor = "grabbing";

            function onMouseMove(event) {
                element.style.left = (event.clientX - offsetX) + "px";
                element.style.top = (event.clientY - offsetY) + "px";
            }

            function onMouseUp() {
                element.style.cursor = "grab";
                document.removeEventListener("mousemove", onMouseMove);
                document.removeEventListener("mouseup", onMouseUp);
            }

            document.addEventListener("mousemove", onMouseMove);
            document.addEventListener("mouseup", onMouseUp);
        });
    </script>
</body>
</html>

נקודות חשובות:
- ה-mousemove ו-mouseup נרשמים על ה-document, לא על האלמנט, כדי שהגרירה תמשיך גם אם העכבר יוצא מגבולות האלמנט
- event.preventDefault() ב-mousedown מונע בחירת טקסט בזמן הגרירה
- שמירת reference לפונקציות מאפשרת להסיר אותן ב-mouseup

פתרון תרגיל 8

<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>Event Logger</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; }
        #test-area { width: 300px; height: 200px; border: 2px solid #333; padding: 20px; margin-bottom: 10px; }
        #test-area input { padding: 5px; margin: 5px; }
        #event-log { height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; font-family: monospace; font-size: 13px; }
        .log-click { color: #e91e63; }
        .log-dblclick { color: #9c27b0; }
        .log-mouseenter { color: #4CAF50; }
        .log-mouseleave { color: #FF9800; }
        .log-keydown { color: #2196F3; }
        .log-focus { color: #009688; }
        .log-blur { color: #795548; }
        .log-input { color: #607d8b; }
    </style>
</head>
<body>
    <h2>Event Logger</h2>
    <div id="test-area">
        <input type="text" placeholder="Type here...">
        <button>Click me</button>
        <a href="#" id="test-link">Test link</a>
    </div>
    <div id="event-log"></div>
    <button id="clear-log">Clear Log</button>

    <script>
        const testArea = document.getElementById("test-area");
        const eventLog = document.getElementById("event-log");
        const clearBtn = document.getElementById("clear-log");
        const testLink = document.getElementById("test-link");

        function logEvent(event) {
            const time = new Date().toLocaleTimeString();
            const type = event.type;
            const target = event.target.tagName;

            let extra = "";
            if (type === "keydown") {
                extra = " | key: " + event.key;
            }
            if (type === "input") {
                extra = " | value: " + event.target.value;
            }

            const entry = document.createElement("div");
            entry.className = "log-" + type;
            entry.textContent = time + " | " + type + " | " + target + extra;

            eventLog.prepend(entry);
        }

        // listen for all event types
        const eventTypes = ["click", "dblclick", "mouseenter", "mouseleave",
                            "keydown", "focus", "blur", "input"];

        eventTypes.forEach(function(type) {
            // focus and blur don't bubble, so use capture or focusin/focusout
            const useCapture = (type === "focus" || type === "blur");
            testArea.addEventListener(type, logEvent, useCapture);
        });

        // 5. prevent link navigation
        testLink.addEventListener("click", function(event) {
            event.preventDefault();
        });

        // 4. clear log
        clearBtn.addEventListener("click", function() {
            eventLog.innerHTML = "";
        });
    </script>
</body>
</html>

שימו לב: אירועי focus ו-blur לא מבעבעים (don't bubble), אז כדי לתפוס אותם על ה-container צריך להשתמש ב-capture phase (פרמטר שלישי true) או להשתמש ב-focusin/focusout שכן מבעבעים.

תשובות לשאלות

  1. event.target מול event.currentTarget - event.target הוא האלמנט שבאמת הפעיל את האירוע (מה שהמשתמש לחץ עליו). event.currentTarget הוא האלמנט שה-listener רשום עליו. הם שונים כשהאירוע בעבע מאלמנט ילד.

  2. event.preventDefault() מבטל את ההתנהגות הרגילה של הדפדפן. למשל: מניעת שליחת טופס (ורענון דף), מניעת ניווט בלחיצה על קישור, מניעת תפריט קליק ימני. משתמשים בו כשרוצים לטפל באירוע בעצמנו במקום לתת לדפדפן לעשות את ברירת המחדל.

  3. addEventListener עדיף על onclick attribute כי: אפשר להוסיף כמה handlers לאותו אירוע, יש הפרדה בין HTML ל-JS, אין בעיות אבטחה של CSP, ואפשר להסיר listeners ספציפיים. onclick attribute מאפשר רק handler אחד ומערבב HTML עם JS.

  4. input מול change - input נורה על כל שינוי בזמן אמת (כל הקלדה, הדבקה, מחיקה). change נורה כשהערך שונה והאלמנט מאבד פוקוס (בשדות טקסט). ב-select, checkbox ו-radio, change נורה מיד בשינוי.

  5. event.key מול event.code - event.key הוא שם התו שנוצר (מושפע מ-Shift ומשפת המקלדת). למשל: "a", "A", "Enter". event.code הוא קוד המקש הפיזי (תמיד אותו דבר, לא משנה שפה). למשל: "KeyA", "Enter". כדאי להשתמש ב-key לרוב ו-code כשרוצים מקש ספציפי בלי קשר לשפה.

  6. once: true גורם ל-listener לרוץ פעם אחת בלבד. אחרי שהאירוע מתרחש ו-handler רץ, הוא מוסר אוטומטית. שימושי לאירועים חד-פעמיים כמו אנימציית כניסה, לחיצה ראשונה, או טעינה ראשונית.