לדלג לתוכן

4.8 ממשקי דפדפן הרצאה

צופה בצמתים - IntersectionObserver

IntersectionObserver הוא API שמאפשר לדעת מתי אלמנט נכנס או יוצא מאזור הנראות (viewport) או מתוך אלמנט אב אחר.

  • לא חוסם את ה-thread הראשי (בניגוד ל-scroll event)
  • הדפדפן מנהל את הבדיקה בצורה יעילה
  • שימושי במיוחד לטעינה עצלה (lazy loading) וגלילה אינסופית (infinite scroll)
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        console.log(entry.target);           // the observed element
        console.log(entry.isIntersecting);   // true if visible
        console.log(entry.intersectionRatio);// 0 to 1 (how much is visible)
    });
});

// start observing an element
const element = document.querySelector("#myElement");
observer.observe(element);

// stop observing
observer.unobserve(element);

// stop all observations
observer.disconnect();

אפשרויות - Options

const observer = new IntersectionObserver(callback, {
    root: null,         // null = viewport, or a parent element
    rootMargin: "0px",  // margin around root (like CSS margin)
    threshold: 0        // 0 to 1, or array [0, 0.25, 0.5, 0.75, 1]
});
  • root - האלמנט שמשמש כאזור ההתייחסות. null = החלון
  • rootMargin - מרווח סביב ה-root. שימושי לטעינה מראש (למשל "200px" יפעיל כשהאלמנט 200px לפני שנכנס לתצוגה)
  • threshold - באיזה אחוז חפיפה להפעיל את הקולבק. 0 = ברגע שפיקסל אחד נכנס, 1 = כשכולו גלוי

טעינה עצלה - Lazy Loading עם IntersectionObserver

טעינה עצלה פירושה לטעון תמונות רק כשהן קרובות לאזור הנראות:

<img data-src="heavy-image.jpg" class="lazy" alt="...">
<img data-src="another-image.jpg" class="lazy" alt="...">
const lazyObserver = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src; // load the real image
            img.classList.remove("lazy");
            lazyObserver.unobserve(img); // stop watching this image
        }
    });
}, {
    rootMargin: "200px" // start loading 200px before visible
});

// observe all lazy images
document.querySelectorAll("img.lazy").forEach(img => {
    lazyObserver.observe(img);
});

הערה: דפדפנים מודרניים תומכים גם ב-loading="lazy" על תגית img, אבל IntersectionObserver נותן יותר שליטה.


גלילה אינסופית - Infinite Scroll

<div id="items"></div>
<div id="sentinel"></div>
const container = document.getElementById("items");
const sentinel = document.getElementById("sentinel");
let page = 1;
let loading = false;

const scrollObserver = new IntersectionObserver(async (entries) => {
    if (entries[0].isIntersecting && !loading) {
        loading = true;

        const response = await fetch(`/api/items?page=${page}`);
        const items = await response.json();

        items.forEach(item => {
            const div = document.createElement("div");
            div.textContent = item.title;
            container.appendChild(div);
        });

        page++;
        loading = false;
    }
});

scrollObserver.observe(sentinel);
  • ה-sentinel הוא אלמנט ריק בתחתית הרשימה
  • כשהוא נכנס לתצוגה, טוענים עוד פריטים
  • הפריטים החדשים דוחפים את ה-sentinel למטה, וכשגוללים שוב - טוענים עוד

צופה בגודל - ResizeObserver

ResizeObserver מאפשר לדעת מתי אלמנט משנה גודל:

const resizeObserver = new ResizeObserver((entries) => {
    entries.forEach(entry => {
        const { width, height } = entry.contentRect;
        console.log(`Size: ${width} x ${height}`);
    });
});

const element = document.querySelector("#resizable");
resizeObserver.observe(element);

// stop observing
resizeObserver.unobserve(element);
resizeObserver.disconnect();

שימושים:
- התאמת גודל canvas לגודל האלמנט שמכיל אותו
- החלפת layout כשאלמנט מסוים (לא החלון) משנה גודל
- responsive components שלא תלויים בגודל החלון אלא בגודל ה-container שלהם

// responsive chart that adapts to container size
const chartContainer = document.querySelector("#chart");

const observer = new ResizeObserver(entries => {
    const { width, height } = entries[0].contentRect;
    redrawChart(width, height);
});

observer.observe(chartContainer);

מיקום גאוגרפי - Geolocation

ה-Geolocation API מאפשר לקבל את המיקום הנוכחי של המשתמש (דורש הרשאה):

// get current position (one time)
navigator.geolocation.getCurrentPosition(
    (position) => {
        console.log("Latitude:", position.coords.latitude);
        console.log("Longitude:", position.coords.longitude);
        console.log("Accuracy:", position.coords.accuracy, "meters");
    },
    (error) => {
        switch (error.code) {
            case error.PERMISSION_DENIED:
                console.log("User denied geolocation");
                break;
            case error.POSITION_UNAVAILABLE:
                console.log("Location unavailable");
                break;
            case error.TIMEOUT:
                console.log("Request timed out");
                break;
        }
    },
    {
        enableHighAccuracy: true, // use GPS if available
        timeout: 10000,           // max wait time in ms
        maximumAge: 60000         // cache position for 60 seconds
    }
);

מעקב רציף

// watch position continuously
const watchId = navigator.geolocation.watchPosition(
    (position) => {
        updateMap(position.coords.latitude, position.coords.longitude);
    },
    (error) => {
        console.error("Watch error:", error.message);
    }
);

// stop watching
navigator.geolocation.clearWatch(watchId);

לוח - Clipboard API

ה-Clipboard API מאפשר לקרוא ולכתוב ללוח ההעתקה:

// copy text to clipboard
async function copyToClipboard(text) {
    try {
        await navigator.clipboard.writeText(text);
        console.log("Copied!");
    } catch (error) {
        console.error("Failed to copy:", error);
    }
}

// read text from clipboard (requires permission)
async function pasteFromClipboard() {
    try {
        const text = await navigator.clipboard.readText();
        console.log("Pasted:", text);
        return text;
    } catch (error) {
        console.error("Failed to paste:", error);
    }
}

כפתור "העתק" מעשי

const copyButton = document.querySelector("#copyBtn");
const codeBlock = document.querySelector("#code");

copyButton.addEventListener("click", async () => {
    await navigator.clipboard.writeText(codeBlock.textContent);
    copyButton.textContent = "Copied!";
    setTimeout(() => {
        copyButton.textContent = "Copy";
    }, 2000);
});

חשוב:
- עובד רק ב-HTTPS (או localhost)
- writeText עובד בלי הרשאה מיוחדת (בתגובה ללחיצת משתמש)
- readText דורש הרשאה מפורשת מהמשתמש


התראות - Notifications API

ה-Notifications API מאפשר להציג התראות מחוץ לדפדפן:

// request permission
async function requestNotificationPermission() {
    const permission = await Notification.requestPermission();
    console.log("Permission:", permission); // "granted", "denied", or "default"
    return permission;
}

// show notification
function showNotification(title, body) {
    if (Notification.permission !== "granted") {
        console.log("No notification permission");
        return;
    }

    const notification = new Notification(title, {
        body: body,
        icon: "/icon.png"
    });

    notification.onclick = () => {
        window.focus();
        notification.close();
    };

    // auto close after 5 seconds
    setTimeout(() => notification.close(), 5000);
}
// practical example
async function notifyNewMessage(sender, message) {
    if (Notification.permission === "default") {
        await Notification.requestPermission();
    }

    if (Notification.permission === "granted") {
        showNotification(`New message from ${sender}`, message);
    }
}

כתובת - URL ו-URLSearchParams

ה-URL API מספק דרך מובנית לפרסר ולבנות כתובות:

const url = new URL("https://example.com:8080/path/page?q=hello&page=2#section");

console.log(url.protocol);   // "https:"
console.log(url.hostname);   // "example.com"
console.log(url.port);       // "8080"
console.log(url.pathname);   // "/path/page"
console.log(url.search);     // "?q=hello&page=2"
console.log(url.hash);       // "#section"
console.log(url.origin);     // "https://example.com:8080"

URLSearchParams

// parse existing query string
const params = new URLSearchParams("?q=hello&page=2");
console.log(params.get("q"));     // "hello"
console.log(params.get("page"));  // "2"
console.log(params.has("q"));     // true

// build query string
const newParams = new URLSearchParams();
newParams.set("search", "javascript");
newParams.set("sort", "date");
newParams.append("tag", "frontend");
newParams.append("tag", "web");

console.log(newParams.toString()); // "search=javascript&sort=date&tag=frontend&tag=web"

// get all values for a key
console.log(newParams.getAll("tag")); // ["frontend", "web"]

// iterate
for (const [key, value] of newParams) {
    console.log(`${key}: ${value}`);
}

// delete
newParams.delete("sort");

שילוב URL ו-URLSearchParams

const url = new URL("https://api.example.com/search");
url.searchParams.set("q", "hello world");
url.searchParams.set("page", "1");

console.log(url.toString());
// "https://api.example.com/search?q=hello+world&page=1"

// modify existing URL
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set("view", "grid");
window.history.pushState({}, "", currentUrl);

היסטוריה - History API

ה-History API מאפשר לנהל את היסטוריית הניווט בלי לטעון דף מחדש:

// add entry to history (URL changes, page doesn't reload)
history.pushState({ page: 1 }, "", "/page/1");

// replace current entry (doesn't add to history)
history.replaceState({ page: 2 }, "", "/page/2");

// navigate back/forward
history.back();
history.forward();
history.go(-2); // go back 2 pages

// listen for back/forward navigation
window.addEventListener("popstate", (event) => {
    console.log("Navigation:", event.state);
    // update UI based on event.state
});

ניווט - SPA Router פשוט

function navigate(path) {
    history.pushState({ path }, "", path);
    renderPage(path);
}

function renderPage(path) {
    const content = document.getElementById("content");
    switch (path) {
        case "/":
            content.innerHTML = "<h1>Home</h1>";
            break;
        case "/about":
            content.innerHTML = "<h1>About</h1>";
            break;
        case "/contact":
            content.innerHTML = "<h1>Contact</h1>";
            break;
        default:
            content.innerHTML = "<h1>404</h1>";
    }
}

// handle back/forward buttons
window.addEventListener("popstate", (event) => {
    renderPage(window.location.pathname);
});

// handle link clicks
document.addEventListener("click", (event) => {
    if (event.target.matches("a[data-link]")) {
        event.preventDefault();
        navigate(event.target.getAttribute("href"));
    }
});

בדיקת מדיה - matchMedia

matchMedia מאפשר לבדוק media queries מ-JS ולהגיב לשינויים:

// check if screen is narrow
const mobileQuery = window.matchMedia("(max-width: 768px)");

console.log(mobileQuery.matches); // true or false

// listen for changes
mobileQuery.addEventListener("change", (event) => {
    if (event.matches) {
        console.log("Switched to mobile layout");
    } else {
        console.log("Switched to desktop layout");
    }
});

בדיקת מצב כהה - Dark Mode

const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");

function applyTheme(isDark) {
    document.body.classList.toggle("dark", isDark);
}

// apply on load
applyTheme(darkModeQuery.matches);

// react to system changes
darkModeQuery.addEventListener("change", (event) => {
    applyTheme(event.matches);
});

בדיקות נוספות

// reduced motion preference
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
if (reducedMotion.matches) {
    // disable animations
}

// screen orientation
const portrait = window.matchMedia("(orientation: portrait)");
portrait.addEventListener("change", (event) => {
    console.log(event.matches ? "Portrait" : "Landscape");
});

// high resolution display
const retina = window.matchMedia("(min-resolution: 2dppx)");
if (retina.matches) {
    // load higher resolution images
}

סיכום

  • IntersectionObserver - מעקב אחרי נראות אלמנטים (lazy loading, infinite scroll)
  • ResizeObserver - מעקב אחרי שינויי גודל אלמנטים
  • Geolocation - קבלת מיקום המשתמש (דורש הרשאה)
  • Clipboard - קריאה וכתיבה ללוח ההעתקה
  • Notifications - הצגת התראות מחוץ לדפדפן (דורש הרשאה)
  • URL / URLSearchParams - פירסור ובניית כתובות
  • History API - ניהול היסטוריית ניווט (בסיס ל-SPA routing)
  • matchMedia - בדיקת media queries מ-JS (responsive, dark mode)