3.5 בעבוע ודלגציה פתרון
פתרון תרגול בעבוע ודלגציה¶
תרגיל 1 - הבנת בעבוע¶
-
לחיצה על הכפתור:
-
לחיצה על middle (לא על הכפתור):
-
לחיצה על outer (לא על middle):
-
אם נוסיף
event.stopPropagation()ל-handler של 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-colattribute מאפשר לזהות לפי איזו עמודה למייןlocaleCompareממיין אלפביתית בצורה נכונה- שורות חדשות מקבלות את אותם
data-colattributes ועובדות אוטומטית
תרגיל 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 בלי)
- כל הודעה מופיעה עם עיכוב כדי להדגים את הסדר
opacitytransition יוצר אפקט של הופעה הדרגתית- הלוג מוצג רק כשלוחצים על האלמנט הפנימי (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"><</button>' +
'<button class="move-right">></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-statusattribute על כל טור מאפשר לזהות את המצב הנוכחיstatusOrderarray מגדיר את סדר הטורים כדי לחשב הזזה ימינה/שמאלהclosest()מזהה את הכפתור שנלחץ ואת הכרטיס שהוא שייך אליו- משימות חדשות נוספות לטור "to do" ועובדות מיד כי ה-handler רשום על ההורה