4.8 ממשקי דפדפן פתרון
פתרון - ממשקי דפדפן
פתרון תרגיל 1
<div id="counter">Loaded: 0 / 20</div>
<div id="gallery"></div>
<style>
.lazy-img {
width: 400px;
height: 300px;
background: #ddd;
margin: 20px;
opacity: 0;
transition: opacity 0.5s ease;
}
.lazy-img.loaded {
opacity: 1;
}
</style>
const gallery = document.getElementById("gallery");
const counter = document.getElementById("counter");
const totalImages = 20;
let loadedCount = 0;
// create placeholder images
for (let i = 1; i <= totalImages; i++) {
const img = document.createElement("img");
img.className = "lazy-img";
img.dataset.src = `https://picsum.photos/400/300?random=${i}`;
img.alt = `Image ${i}`;
gallery.appendChild(img);
}
function updateCounter() {
loadedCount++;
counter.textContent = `Loaded: ${loadedCount} / ${totalImages}`;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.onload = () => {
img.classList.add("loaded");
updateCounter();
};
img.src = img.dataset.src;
observer.unobserve(img);
}
});
}, {
rootMargin: "100px"
});
document.querySelectorAll(".lazy-img").forEach(img => {
observer.observe(img);
});
פתרון תרגיל 2
<div id="posts"></div>
<div id="sentinel">
<div id="spinner" class="hidden">Loading...</div>
</div>
<button id="scrollTop" class="hidden">Back to top</button>
<style>
.hidden { display: none; }
.post-card {
padding: 16px;
margin: 8px 0;
border: 1px solid #ddd;
border-radius: 8px;
}
#scrollTop {
position: fixed;
bottom: 20px;
right: 20px;
}
</style>
const postsContainer = document.getElementById("posts");
const sentinel = document.getElementById("sentinel");
const spinner = document.getElementById("spinner");
const scrollTopBtn = document.getElementById("scrollTop");
let page = 1;
let loading = false;
let allLoaded = false;
async function loadPosts() {
if (loading || allLoaded) return;
loading = true;
spinner.classList.remove("hidden");
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`
);
const posts = await response.json();
if (posts.length === 0) {
allLoaded = true;
sentinel.textContent = "No more posts";
scrollObserver.disconnect();
return;
}
posts.forEach(post => {
const card = document.createElement("div");
card.className = "post-card";
card.innerHTML = `<h3>${post.title}</h3><p>${post.body}</p>`;
postsContainer.appendChild(card);
});
page++;
} catch (error) {
console.error("Failed to load posts:", error);
} finally {
loading = false;
spinner.classList.add("hidden");
}
}
const scrollObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadPosts();
}
});
scrollObserver.observe(sentinel);
// scroll to top button
const topObserver = new IntersectionObserver((entries) => {
scrollTopBtn.classList.toggle("hidden", entries[0].isIntersecting);
});
topObserver.observe(postsContainer);
scrollTopBtn.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});
// initial load
loadPosts();
פתרון תרגיל 3
<div id="snippets"></div>
<hr>
<div>
<textarea id="pasteArea" rows="4" cols="50" placeholder="Click paste to fill"></textarea>
<button id="pasteBtn">Paste from clipboard</button>
</div>
<h3>Copy History</h3>
<ul id="history"></ul>
const snippets = [
'console.log("Hello World");',
'const sum = (a, b) => a + b;',
'document.querySelector("#app");',
'fetch("/api/data").then(r => r.json());',
'const [first, ...rest] = array;'
];
const snippetsContainer = document.getElementById("snippets");
const pasteArea = document.getElementById("pasteArea");
const pasteBtn = document.getElementById("pasteBtn");
const historyList = document.getElementById("history");
// check clipboard support
const clipboardSupported = navigator.clipboard && window.isSecureContext;
// render snippets
snippets.forEach((code, index) => {
const div = document.createElement("div");
div.style.display = "flex";
div.style.alignItems = "center";
div.style.gap = "10px";
div.style.margin = "8px 0";
const pre = document.createElement("pre");
pre.textContent = code;
const btn = document.createElement("button");
btn.textContent = "Copy";
btn.addEventListener("click", () => copySnippet(code, btn));
div.appendChild(pre);
div.appendChild(btn);
snippetsContainer.appendChild(div);
});
async function copySnippet(text, button) {
if (!clipboardSupported) {
alert("Clipboard API not supported in this browser/context");
return;
}
try {
await navigator.clipboard.writeText(text);
// update button
const original = button.textContent;
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = original;
}, 2000);
// add to history
addToHistory(text);
} catch (error) {
console.error("Copy failed:", error);
}
}
function addToHistory(text) {
const li = document.createElement("li");
li.textContent = text;
historyList.prepend(li);
}
// paste button
pasteBtn.addEventListener("click", async () => {
if (!clipboardSupported) {
alert("Clipboard API not supported in this browser/context");
return;
}
try {
const text = await navigator.clipboard.readText();
pasteArea.value = text;
} catch (error) {
console.error("Paste failed:", error);
alert("Could not read clipboard. Permission may be denied.");
}
});
פתרון תרגיל 4
<nav>
<a href="/" data-link>Home</a>
<a href="/about" data-link>About</a>
<a href="/contact" data-link>Contact</a>
<a href="/projects" data-link>Projects</a>
</nav>
<div id="content"></div>
<style>
#content {
transition: opacity 0.3s ease;
}
#content.fade-out {
opacity: 0;
}
nav a {
margin-right: 16px;
}
nav a.active {
font-weight: bold;
}
</style>
const content = document.getElementById("content");
const pages = {
"/": "<h1>Home</h1><p>Welcome to the home page.</p>",
"/about": "<h1>About</h1><p>This is the about page.</p>",
"/contact": "<h1>Contact</h1><p>Get in touch with us.</p>",
"/projects": "<h1>Projects</h1><p>View our projects.</p>"
};
function renderPage(path) {
const html = pages[path] || "<h1>404</h1><p>Page not found.</p>";
// fade out
content.classList.add("fade-out");
setTimeout(() => {
content.innerHTML = html;
content.classList.remove("fade-out");
}, 300);
// update active link
document.querySelectorAll("nav a").forEach(link => {
link.classList.toggle("active", link.getAttribute("href") === path);
});
}
function navigate(path) {
history.pushState({ path }, "", path);
renderPage(path);
}
// handle link clicks
document.addEventListener("click", (event) => {
const link = event.target.closest("a[data-link]");
if (link) {
event.preventDefault();
const path = link.getAttribute("href");
if (path !== window.location.pathname) {
navigate(path);
}
}
});
// handle back/forward
window.addEventListener("popstate", () => {
renderPage(window.location.pathname);
});
// initial render based on current URL
renderPage(window.location.pathname);
פתרון תרגיל 5
<div id="info"></div>
<div id="app">
<button id="toggleTheme">Toggle Theme</button>
<div id="items"></div>
</div>
<style>
body { transition: background 0.3s, color 0.3s; }
body.dark { background: #1a1a2e; color: #eee; }
body.light { background: #fff; color: #222; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.list { display: flex; flex-direction: column; gap: 10px; }
.no-motion * { animation: none !important; transition: none !important; }
</style>
const info = document.getElementById("info");
const toggleBtn = document.getElementById("toggleTheme");
const itemsContainer = document.getElementById("items");
// media queries
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
const mobileQuery = window.matchMedia("(max-width: 768px)");
const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const orientationQuery = window.matchMedia("(orientation: portrait)");
const retinaQuery = window.matchMedia("(min-resolution: 2dppx)");
// theme management
function getTheme() {
const saved = localStorage.getItem("theme");
if (saved) return saved;
return darkQuery.matches ? "dark" : "light";
}
function applyTheme(theme) {
document.body.classList.remove("dark", "light");
document.body.classList.add(theme);
}
applyTheme(getTheme());
toggleBtn.addEventListener("click", () => {
const current = document.body.classList.contains("dark") ? "dark" : "light";
const next = current === "dark" ? "light" : "dark";
localStorage.setItem("theme", next);
applyTheme(next);
});
darkQuery.addEventListener("change", () => {
// only apply system preference if user hasn't overridden
if (!localStorage.getItem("theme")) {
applyTheme(darkQuery.matches ? "dark" : "light");
}
});
// layout
function updateLayout() {
itemsContainer.className = mobileQuery.matches ? "list" : "grid";
}
updateLayout();
mobileQuery.addEventListener("change", updateLayout);
// reduced motion
function updateMotion() {
document.body.classList.toggle("no-motion", motionQuery.matches);
}
updateMotion();
motionQuery.addEventListener("change", updateMotion);
// display info
function updateInfo() {
info.innerHTML = `
<p>Screen: ${window.innerWidth} x ${window.innerHeight}</p>
<p>Orientation: ${orientationQuery.matches ? "Portrait" : "Landscape"}</p>
<p>Retina: ${retinaQuery.matches ? "Yes" : "No"}</p>
<p>Dark mode (system): ${darkQuery.matches ? "Yes" : "No"}</p>
<p>Reduced motion: ${motionQuery.matches ? "Yes" : "No"}</p>
`;
}
updateInfo();
window.addEventListener("resize", updateInfo);
orientationQuery.addEventListener("change", updateInfo);
// sample items
for (let i = 0; i < 9; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i + 1}`;
div.style.padding = "20px";
div.style.border = "1px solid #666";
div.style.borderRadius = "8px";
itemsContainer.appendChild(div);
}
פתרון תרגיל 6
<div id="notifPanel">
<p>Permission: <span id="permStatus">checking...</span></p>
<button id="requestPerm">Request Permission</button>
<hr>
<h3>Custom Notification</h3>
<input id="notifTitle" placeholder="Title">
<input id="notifBody" placeholder="Body">
<button id="sendNotif">Send</button>
<hr>
<h3>Timer Notification</h3>
<input id="timerSeconds" type="number" placeholder="Seconds" value="5">
<button id="startTimer">Start Timer</button>
<hr>
<button id="geoNotif">Notify My Location</button>
<hr>
<h3>In-Page Notifications</h3>
<div id="inPageNotifs"></div>
</div>
const permStatus = document.getElementById("permStatus");
const requestPermBtn = document.getElementById("requestPerm");
const notifTitle = document.getElementById("notifTitle");
const notifBody = document.getElementById("notifBody");
const sendNotifBtn = document.getElementById("sendNotif");
const timerSeconds = document.getElementById("timerSeconds");
const startTimerBtn = document.getElementById("startTimer");
const geoNotifBtn = document.getElementById("geoNotif");
const inPageNotifs = document.getElementById("inPageNotifs");
// update permission status
function updatePermission() {
if (!("Notification" in window)) {
permStatus.textContent = "Not supported";
return;
}
permStatus.textContent = Notification.permission;
}
updatePermission();
// request permission
requestPermBtn.addEventListener("click", async () => {
if (!("Notification" in window)) {
showInPageNotification("Error", "Notifications not supported");
return;
}
const result = await Notification.requestPermission();
updatePermission();
});
// show notification (with fallback)
function notify(title, body) {
if (Notification.permission === "granted") {
const notification = new Notification(title, {
body: body
});
notification.onclick = () => {
window.focus();
notification.close();
};
setTimeout(() => notification.close(), 5000);
} else {
showInPageNotification(title, body);
}
}
// in-page fallback notification
function showInPageNotification(title, body) {
const div = document.createElement("div");
div.style.cssText =
"padding:12px;margin:8px 0;background:#333;color:#fff;border-radius:8px;";
div.innerHTML = `<strong>${title}</strong><p>${body}</p>`;
inPageNotifs.prepend(div);
setTimeout(() => {
div.style.opacity = "0";
div.style.transition = "opacity 0.5s";
setTimeout(() => div.remove(), 500);
}, 5000);
}
// send custom notification
sendNotifBtn.addEventListener("click", () => {
const title = notifTitle.value || "Notification";
const body = notifBody.value || "";
notify(title, body);
});
// timer notification
startTimerBtn.addEventListener("click", () => {
const seconds = parseInt(timerSeconds.value) || 5;
notify("Timer Started", `You will be notified in ${seconds} seconds`);
setTimeout(() => {
notify("Timer Done!", `${seconds} seconds have passed`);
}, seconds * 1000);
});
// geolocation notification
geoNotifBtn.addEventListener("click", () => {
if (!navigator.geolocation) {
notify("Error", "Geolocation not supported");
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
notify(
"Your Location",
`Lat: ${latitude.toFixed(4)}, Lon: ${longitude.toFixed(4)}`
);
},
(error) => {
notify("Location Error", error.message);
},
{ enableHighAccuracy: true, timeout: 10000 }
);
});