לדלג לתוכן

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 }
    );
});