לדלג לתוכן

3.5 בעבוע ודלגציה פתרון

פתרון תרגול בעבוע ודלגציה


תרגיל 1 - הבנת בעבוע

  1. לחיצה על הכפתור:

    inner
    middle
    outer
    

  2. לחיצה על middle (לא על הכפתור):

    middle
    outer
    

  3. לחיצה על outer (לא על middle):

    outer
    

  4. אם נוסיף event.stopPropagation() ל-handler של inner, לחיצה על הכפתור תדפיס רק:

    inner
    

    כי הבעבוע נעצר ולא ממשיך ל-middle ול-outer.


תרגיל 2 - כרטיס עם כפתורים

<div id="product-card" style="padding: 20px; border: 2px solid #333; width: 300px; cursor: pointer;">
  <h3>Cool Product</h3>
  <p>A great product you should buy.</p>
  <button id="like-btn">like</button>
  <button id="share-btn">share</button>
</div>
// card click
document.getElementById("product-card").addEventListener("click", function () {
  console.log("card clicked");
});

// like button - stop propagation so card handler doesn't fire
document.getElementById("like-btn").addEventListener("click", function (event) {
  event.stopPropagation();
  console.log("liked!");
});

// share button - stop propagation
document.getElementById("share-btn").addEventListener("click", function (event) {
  event.stopPropagation();
  console.log("shared!");
});

// document handler
document.addEventListener("click", function () {
  console.log("document clicked");
});
  • לחיצה על הכרטיס: "card clicked", "document clicked"
  • לחיצה על like: "liked!" (לא card clicked, לא document clicked)
  • לחיצה על share: "shared!" (לא card clicked, לא document clicked)
  • stopPropagation עוצר את הבעבוע כדי שה-handler של הכרטיס ושל ה-document לא יופעלו

תרגיל 3 - רשימת קניות עם דלגציה

<div id="shopping-app">
  <input type="text" id="item-input" placeholder="add item...">
  <button id="add-item">add</button>
  <ul id="shopping-list"></ul>
</div>
.done {
  text-decoration: line-through;
  color: #999;
}

#shopping-list li {
  padding: 8px;
  display: flex;
  align-items: center;
  gap: 10px;
  border-bottom: 1px solid #eee;
}

#shopping-list li span {
  flex-grow: 1;
}
let itemInput = document.getElementById("item-input");
let shoppingList = document.getElementById("shopping-list");

// add item
document.getElementById("add-item").addEventListener("click", function () {
  let text = itemInput.value.trim();
  if (!text) return;

  let li = document.createElement("li");
  li.innerHTML =
    '<span class="item-text">' + text + "</span>" +
    '<button class="check-btn">V</button>' +
    '<button class="delete-btn">X</button>';
  shoppingList.appendChild(li);
  itemInput.value = "";
  itemInput.focus();
});

// delegated handler on the ul
shoppingList.addEventListener("click", function (event) {
  let li = event.target.closest("li");
  if (!li) return;

  // check button - toggle done
  if (event.target.closest(".check-btn")) {
    let textSpan = li.querySelector(".item-text");
    textSpan.classList.toggle("done");
    return;
  }

  // delete button - remove item
  if (event.target.closest(".delete-btn")) {
    li.remove();
    return;
  }
});

// add item on Enter key
itemInput.addEventListener("keydown", function (event) {
  if (event.key === "Enter") {
    document.getElementById("add-item").click();
  }
});
  • handler אחד על ה-ul מטפל בכל הלחיצות
  • closest() מזהה את הכפתור שנלחץ ואת ה-li שהוא שייך אליו
  • פריטים חדשים עובדים אוטומטית כי ה-handler רשום על ההורה

תרגיל 4 - טבלת עובדים עם דלגציה

<div id="employees-app">
  <div>
    <input type="text" id="emp-name" placeholder="name">
    <input type="text" id="emp-role" placeholder="role">
    <input type="text" id="emp-dept" placeholder="department">
    <button id="add-emp">add employee</button>
  </div>
  <table id="emp-table">
    <thead>
      <tr>
        <th data-col="name">name</th>
        <th data-col="role">role</th>
        <th data-col="dept">department</th>
        <th>actions</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td data-col="name">Dan Cohen</td>
        <td data-col="role">developer</td>
        <td data-col="dept">engineering</td>
        <td><button class="delete-btn">delete</button></td>
      </tr>
      <tr>
        <td data-col="name">Sara Levi</td>
        <td data-col="role">designer</td>
        <td data-col="dept">design</td>
        <td><button class="delete-btn">delete</button></td>
      </tr>
      <tr>
        <td data-col="name">Amit Bar</td>
        <td data-col="role">manager</td>
        <td data-col="dept">management</td>
        <td><button class="delete-btn">delete</button></td>
      </tr>
      <tr>
        <td data-col="name">Yael Oz</td>
        <td data-col="role">developer</td>
        <td data-col="dept">engineering</td>
        <td><button class="delete-btn">delete</button></td>
      </tr>
    </tbody>
  </table>
</div>
#emp-table {
  width: 100%;
  border-collapse: collapse;
}

#emp-table th, #emp-table td {
  padding: 10px;
  border: 1px solid #ddd;
  text-align: left;
}

#emp-table thead th {
  cursor: pointer;
  background: #34495e;
  color: white;
}

#emp-table thead th:hover {
  background: #2c3e50;
}

.selected {
  background: #d4edda;
}
let tbody = document.querySelector("#emp-table tbody");
let thead = document.querySelector("#emp-table thead");

// delegated handler on tbody for row actions
tbody.addEventListener("click", function (event) {
  // handle delete
  let deleteBtn = event.target.closest(".delete-btn");
  if (deleteBtn) {
    let row = deleteBtn.closest("tr");
    row.remove();
    return;
  }

  // handle row selection
  let row = event.target.closest("tr");
  if (row) {
    // remove selection from all rows
    tbody.querySelectorAll("tr").forEach(function (r) {
      r.classList.remove("selected");
    });
    row.classList.add("selected");
  }
});

// delegated handler on thead for sorting
thead.addEventListener("click", function (event) {
  let th = event.target.closest("th");
  if (!th || !th.dataset.col) return;

  let col = th.dataset.col;
  let rows = Array.from(tbody.querySelectorAll("tr"));

  rows.sort(function (a, b) {
    let textA = a.querySelector('[data-col="' + col + '"]').textContent;
    let textB = b.querySelector('[data-col="' + col + '"]').textContent;
    return textA.localeCompare(textB);
  });

  // re-append sorted rows
  rows.forEach(function (row) {
    tbody.appendChild(row);
  });
});

// add employee
document.getElementById("add-emp").addEventListener("click", function () {
  let name = document.getElementById("emp-name").value.trim();
  let role = document.getElementById("emp-role").value.trim();
  let dept = document.getElementById("emp-dept").value.trim();
  if (!name || !role || !dept) return;

  let row = document.createElement("tr");
  row.innerHTML =
    '<td data-col="name">' + name + "</td>" +
    '<td data-col="role">' + role + "</td>" +
    '<td data-col="dept">' + dept + "</td>" +
    '<td><button class="delete-btn">delete</button></td>';
  tbody.appendChild(row);

  document.getElementById("emp-name").value = "";
  document.getElementById("emp-role").value = "";
  document.getElementById("emp-dept").value = "";
});
  • handler אחד על ה-tbody מטפל בבחירת שורות ומחיקה
  • handler אחד על ה-thead מטפל במיון
  • data-col attribute מאפשר לזהות לפי איזו עמודה למיין
  • localeCompare ממיין אלפביתית בצורה נכונה
  • שורות חדשות מקבלות את אותם data-col attributes ועובדות אוטומטית

תרגיל 5 - תפריט אקורדיון

<div id="accordion">
  <div class="accordion-item">
    <div class="question">What is JavaScript?</div>
    <div class="answer"><p>JavaScript is a programming language used to make web pages interactive. It runs in the browser and can manipulate HTML and CSS.</p></div>
  </div>
  <div class="accordion-item">
    <div class="question">What is the DOM?</div>
    <div class="answer"><p>The DOM (Document Object Model) is a tree representation of the HTML document. JavaScript can read and modify this tree to change what the user sees.</p></div>
  </div>
  <div class="accordion-item">
    <div class="question">What is event bubbling?</div>
    <div class="answer"><p>Event bubbling is when an event on a child element propagates up through its parent elements in the DOM tree.</p></div>
  </div>
  <div class="accordion-item">
    <div class="question">What is event delegation?</div>
    <div class="answer"><p>Event delegation is a pattern where you attach a single event handler to a parent element instead of attaching handlers to each child individually.</p></div>
  </div>
  <div class="accordion-item">
    <div class="question">What is closest()?</div>
    <div class="answer"><p>The closest() method traverses up from the current element looking for the nearest ancestor that matches a given CSS selector.</p></div>
  </div>
</div>

<br>
<input type="text" id="new-question" placeholder="question">
<input type="text" id="new-answer" placeholder="answer">
<button id="add-qa">add question</button>
.question {
  padding: 15px;
  background: #2c3e50;
  color: white;
  cursor: pointer;
  border-bottom: 1px solid #34495e;
}

.question:hover {
  background: #34495e;
}

.answer {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
  background: #ecf0f1;
}

.answer p {
  padding: 15px;
  margin: 0;
}

.answer.open {
  max-height: 200px;
}
let accordion = document.getElementById("accordion");

// single delegated handler
accordion.addEventListener("click", function (event) {
  let question = event.target.closest(".question");
  if (!question) return;

  let item = question.closest(".accordion-item");
  let answer = item.querySelector(".answer");

  // close all other answers
  accordion.querySelectorAll(".answer").forEach(function (a) {
    if (a !== answer) {
      a.classList.remove("open");
    }
  });

  // toggle current answer
  answer.classList.toggle("open");
});

// add new question
document.getElementById("add-qa").addEventListener("click", function () {
  let qText = document.getElementById("new-question").value.trim();
  let aText = document.getElementById("new-answer").value.trim();
  if (!qText || !aText) return;

  let item = document.createElement("div");
  item.className = "accordion-item";
  item.innerHTML =
    '<div class="question">' + qText + "</div>" +
    '<div class="answer"><p>' + aText + "</p></div>";
  accordion.appendChild(item);

  document.getElementById("new-question").value = "";
  document.getElementById("new-answer").value = "";
});
  • handler אחד על ה-accordion מטפל בכל הלחיצות
  • max-height עם transition יוצר אנימציה חלקה של פתיחה/סגירה
  • כשפותחים שאלה, כל השאר נסגרות
  • שאלות חדשות שנוספות עובדות אוטומטית

תרגיל 6 - גלריית תמונות עם סינון

<div id="gallery-app">
  <div class="filters">
    <button data-filter="all" class="active">all</button>
    <button data-filter="nature">nature</button>
    <button data-filter="city">city</button>
    <button data-filter="food">food</button>
  </div>
  <div class="gallery">
    <div class="gallery-card" data-category="nature">
      <img src="https://picsum.photos/300/200?random=1" alt="nature 1">
      <p>nature photo 1</p>
    </div>
    <div class="gallery-card" data-category="nature">
      <img src="https://picsum.photos/300/200?random=2" alt="nature 2">
      <p>nature photo 2</p>
    </div>
    <div class="gallery-card" data-category="nature">
      <img src="https://picsum.photos/300/200?random=3" alt="nature 3">
      <p>nature photo 3</p>
    </div>
    <div class="gallery-card" data-category="city">
      <img src="https://picsum.photos/300/200?random=4" alt="city 1">
      <p>city photo 1</p>
    </div>
    <div class="gallery-card" data-category="city">
      <img src="https://picsum.photos/300/200?random=5" alt="city 2">
      <p>city photo 2</p>
    </div>
    <div class="gallery-card" data-category="city">
      <img src="https://picsum.photos/300/200?random=6" alt="city 3">
      <p>city photo 3</p>
    </div>
    <div class="gallery-card" data-category="food">
      <img src="https://picsum.photos/300/200?random=7" alt="food 1">
      <p>food photo 1</p>
    </div>
    <div class="gallery-card" data-category="food">
      <img src="https://picsum.photos/300/200?random=8" alt="food 2">
      <p>food photo 2</p>
    </div>
    <div class="gallery-card" data-category="food">
      <img src="https://picsum.photos/300/200?random=9" alt="food 3">
      <p>food photo 3</p>
    </div>
  </div>

  <!-- modal for enlarged image -->
  <div id="modal" style="display: none;">
    <div class="modal-overlay"></div>
    <div class="modal-content">
      <img id="modal-img" src="" alt="enlarged">
      <button id="modal-close">X</button>
    </div>
  </div>
</div>
.filters {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.filters button {
  padding: 8px 16px;
  border: 2px solid #333;
  background: white;
  cursor: pointer;
  border-radius: 5px;
}

.filters button.active {
  background: #333;
  color: white;
}

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 15px;
}

.gallery-card {
  border-radius: 8px;
  overflow: hidden;
  background: #f5f5f5;
  cursor: pointer;
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.gallery-card.hidden {
  display: none;
}

.gallery-card:hover {
  transform: scale(1.02);
}

.gallery-card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  display: block;
}

.gallery-card p {
  padding: 10px;
  margin: 0;
  text-align: center;
}

#modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 100;
}

.modal-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
}

.modal-content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.modal-content img {
  max-width: 80vw;
  max-height: 80vh;
  border-radius: 8px;
}

#modal-close {
  position: absolute;
  top: -15px;
  right: -15px;
  background: white;
  border: none;
  border-radius: 50%;
  width: 30px;
  height: 30px;
  cursor: pointer;
  font-size: 1rem;
}
let filtersContainer = document.querySelector(".filters");
let gallery = document.querySelector(".gallery");
let modal = document.getElementById("modal");
let modalImg = document.getElementById("modal-img");

// delegated handler for filter buttons
filtersContainer.addEventListener("click", function (event) {
  let button = event.target.closest("button");
  if (!button) return;

  let filter = button.dataset.filter;

  // update active button
  filtersContainer.querySelectorAll("button").forEach(function (btn) {
    btn.classList.remove("active");
  });
  button.classList.add("active");

  // filter gallery cards
  gallery.querySelectorAll(".gallery-card").forEach(function (card) {
    if (filter === "all" || card.dataset.category === filter) {
      card.classList.remove("hidden");
    } else {
      card.classList.add("hidden");
    }
  });
});

// delegated handler for gallery cards - open modal
gallery.addEventListener("click", function (event) {
  let card = event.target.closest(".gallery-card");
  if (!card) return;

  let img = card.querySelector("img");
  modalImg.src = img.src;
  modal.style.display = "block";
});

// close modal
modal.addEventListener("click", function (event) {
  if (event.target.closest("#modal-close") || event.target.closest(".modal-overlay")) {
    modal.style.display = "none";
  }
});
  • handler אחד על ה-filters מטפל בסינון
  • handler אחד על ה-gallery מטפל בפתיחת תמונות
  • data-filter ו-data-category מקשרים בין הכפתורים לכרטיסים
  • המודאל נסגר בלחיצה על X או על הרקע הכהה

תרגיל 7 - capture מול bubble

<div id="log-area" style="margin-bottom: 20px;">
  <button id="clear-log">clear log</button>
  <div id="event-log"></div>
</div>

<div id="box-outer" style="padding: 60px; background: #e74c3c; cursor: pointer;">
  outer
  <div id="box-middle" style="padding: 60px; background: #3498db;">
    middle
    <div id="box-inner" style="padding: 30px; background: #2ecc71;">
      inner - click me
    </div>
  </div>
</div>
#event-log {
  padding: 10px;
  background: #1a1a2e;
  color: #0f0;
  font-family: monospace;
  min-height: 100px;
  max-height: 300px;
  overflow-y: auto;
  border-radius: 8px;
  margin-top: 10px;
}

.log-entry {
  padding: 3px 0;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.log-entry.visible {
  opacity: 1;
}
let eventLog = document.getElementById("event-log");
let logEntries = [];

function addLog(message, delay) {
  logEntries.push({ message: message, delay: delay });
}

function displayLogs() {
  eventLog.innerHTML = "";
  logEntries.forEach(function (entry, index) {
    setTimeout(function () {
      let div = document.createElement("div");
      div.className = "log-entry";
      div.textContent = (index + 1) + ". " + entry.message;
      eventLog.appendChild(div);
      // trigger animation
      setTimeout(function () {
        div.classList.add("visible");
      }, 10);
    }, index * 400);
  });
}

let elements = ["box-outer", "box-middle", "box-inner"];
let names = ["outer", "middle", "inner"];

// register capture and bubble handlers on all elements
elements.forEach(function (id, index) {
  let el = document.getElementById(id);
  let name = names[index];

  // capture phase
  el.addEventListener("click", function (event) {
    if (event.target.id === "box-inner") {
      let phase = (name === "inner") ? "target" : "capture";
      addLog(name + " - " + phase);
    }
  }, true);

  // bubble phase (skip inner to avoid duplicate)
  if (name !== "inner") {
    el.addEventListener("click", function (event) {
      if (event.target.id === "box-inner") {
        addLog(name + " - bubble");
      }
    });
  }
});

// when clicking inner, collect all logs and display them
document.getElementById("box-inner").addEventListener("click", function () {
  displayLogs();
});

// listen on document to know when all handlers have fired
document.addEventListener("click", function (event) {
  if (event.target.id === "box-inner") {
    // reset for next click
    setTimeout(function () {
      logEntries = [];
    }, elements.length * 400 + 500);
  }
});

// clear log
document.getElementById("clear-log").addEventListener("click", function (event) {
  event.stopPropagation();
  eventLog.innerHTML = "";
  logEntries = [];
});
  • handlers רשומים בשני השלבים (capture עם true, bubble בלי)
  • כל הודעה מופיעה עם עיכוב כדי להדגים את הסדר
  • opacity transition יוצר אפקט של הופעה הדרגתית
  • הלוג מוצג רק כשלוחצים על האלמנט הפנימי (inner)

תרגיל 8 - מנהל משימות מתקדם

<div id="kanban">
  <div>
    <input type="text" id="task-title" placeholder="task title">
    <input type="text" id="task-desc" placeholder="description">
    <button id="add-task-btn">add task</button>
  </div>
  <div class="kanban-board">
    <div class="kanban-col" data-status="todo">
      <h3>to do</h3>
      <div class="col-tasks"></div>
    </div>
    <div class="kanban-col" data-status="progress">
      <h3>in progress</h3>
      <div class="col-tasks"></div>
    </div>
    <div class="kanban-col" data-status="done">
      <h3>done</h3>
      <div class="col-tasks"></div>
    </div>
  </div>
</div>
.kanban-board {
  display: flex;
  gap: 15px;
  margin-top: 15px;
}

.kanban-col {
  flex: 1;
  background: #ecf0f1;
  border-radius: 8px;
  padding: 15px;
  min-height: 300px;
}

.kanban-col h3 {
  margin-top: 0;
  text-align: center;
  padding-bottom: 10px;
  border-bottom: 2px solid #bdc3c7;
}

.task-card {
  background: white;
  border-radius: 6px;
  padding: 12px;
  margin-bottom: 10px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.task-card h4 {
  margin: 0 0 5px;
}

.task-card p {
  margin: 0 0 10px;
  color: #666;
  font-size: 0.9rem;
}

.task-actions {
  display: flex;
  gap: 5px;
}

.task-actions button {
  padding: 4px 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
  background: white;
}

.task-actions button:hover {
  background: #f0f0f0;
}

.task-actions .delete-task {
  color: #e74c3c;
  margin-left: auto;
}
let board = document.querySelector(".kanban-board");
let statusOrder = ["todo", "progress", "done"];

// single delegated handler on the entire board
board.addEventListener("click", function (event) {
  let card = event.target.closest(".task-card");
  if (!card) return;

  let currentCol = card.closest(".kanban-col");
  let currentStatus = currentCol.dataset.status;
  let currentIndex = statusOrder.indexOf(currentStatus);

  // move right (next status)
  if (event.target.closest(".move-right")) {
    if (currentIndex < statusOrder.length - 1) {
      let nextStatus = statusOrder[currentIndex + 1];
      let nextCol = board.querySelector('[data-status="' + nextStatus + '"] .col-tasks');
      nextCol.appendChild(card);
    }
    return;
  }

  // move left (previous status)
  if (event.target.closest(".move-left")) {
    if (currentIndex > 0) {
      let prevStatus = statusOrder[currentIndex - 1];
      let prevCol = board.querySelector('[data-status="' + prevStatus + '"] .col-tasks');
      prevCol.appendChild(card);
    }
    return;
  }

  // delete task
  if (event.target.closest(".delete-task")) {
    card.remove();
    return;
  }
});

// add new task
document.getElementById("add-task-btn").addEventListener("click", function () {
  let title = document.getElementById("task-title").value.trim();
  let desc = document.getElementById("task-desc").value.trim();
  if (!title) return;

  let card = document.createElement("div");
  card.className = "task-card";
  card.innerHTML =
    "<h4>" + title + "</h4>" +
    "<p>" + (desc || "no description") + "</p>" +
    '<div class="task-actions">' +
      '<button class="move-left">&lt;</button>' +
      '<button class="move-right">&gt;</button>' +
      '<button class="delete-task">X</button>' +
    "</div>";

  let todoCol = board.querySelector('[data-status="todo"] .col-tasks');
  todoCol.appendChild(card);

  document.getElementById("task-title").value = "";
  document.getElementById("task-desc").value = "";
});
  • handler אחד על ה-board כולו מטפל בכל הפעולות (הזזה ומחיקה)
  • data-status attribute על כל טור מאפשר לזהות את המצב הנוכחי
  • statusOrder array מגדיר את סדר הטורים כדי לחשב הזזה ימינה/שמאלה
  • closest() מזהה את הכפתור שנלחץ ואת הכרטיס שהוא שייך אליו
  • משימות חדשות נוספות לטור "to do" ועובדות מיד כי ה-handler רשום על ההורה